on 89ff34e help sign in
unity8/qml/Greeter/Greeter.qml
631 lines 21 kB history raw
1/*
2 * Copyright (C) 2013-2016 Canonical, Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
17import QtQuick 2.4
18import AccountsService 0.1
19import Biometryd 0.0
20import GSettings 1.0
21import Powerd 0.1
22import Ubuntu.Components 1.3
23import Ubuntu.SystemImage 0.1
24import Unity.Launcher 0.1
25import Unity.Session 0.1
27import "." 0.1
28import "../Components"
30Showable {
31 id: root
32 created: loader.status == Loader.Ready
34 property real dragHandleLeftMargin: 0
36 property url background
37 property bool hasCustomBackground
39 // How far to offset the top greeter layer during a launcher left-drag
40 property real launcherOffset
42 readonly property bool active: required || hasLockedApp
43 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
45 property bool allowFingerprint: true
47 // True when the greeter is waiting for PAM or other setup process
48 readonly property alias waiting: d.waiting
50 property string lockedApp: ""
51 readonly property bool hasLockedApp: lockedApp !== ""
53 property bool forcedUnlock
54 readonly property bool locked: LightDMService.greeter.active && !LightDMService.greeter.authenticated && !forcedUnlock
56 property bool tabletMode
57 property url viewSource // only used for testing
59 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
60 property int failedLoginsDelayAttempts: 7 // number of failed logins
61 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
62 property int failedFingerprintLoginsDisableAttempts: 3 // number of failed fingerprint logins
64 readonly property bool animating: loader.item ? loader.item.animating : false
66 signal tease()
67 signal sessionStarted()
68 signal emergencyCall()
70 function forceShow() {
71 if (!active) {
72 d.isLockscreen = true;
73 }
74 forcedUnlock = false;
75 if (required) {
76 if (loader.item) {
77 loader.item.reset(true /* forceShow */);
78 }
79 // Normally loader.onLoaded will select a user, but if we're
80 // already shown, do it manually.
81 d.selectUser(d.currentIndex, false);
82 }
84 // Even though we may already be shown, we want to call show() for its
85 // possible side effects, like hiding indicators and such.
86 //
87 // We re-check forcedUnlock here, because selectUser above might
88 // process events during authentication, and a request to unlock could
89 // have come in in the meantime.
90 if (!forcedUnlock) {
91 showNow();
92 }
93 }
95 function notifyAppFocusRequested(appId) {
96 if (!active) {
97 return;
98 }
100 if (hasLockedApp) {
101 if (appId === lockedApp) {
102 hide(); // show locked app
103 } else {
104 show();
105 d.startUnlock(false /* toTheRight */);
106 }
107 } else if (appId !== "unity8-dash") { // dash isn't started by user
108 d.startUnlock(false /* toTheRight */);
109 }
110 }
112 // Notify that the user has explicitly requested an app
113 function notifyUserRequestedApp() {
114 if (!active) {
115 return;
116 }
118 // A hint that we're about to focus an app. This way we can look
119 // a little more responsive, rather than waiting for the above
120 // notifyAppFocusRequested call. We also need this in case we have a locked
121 // app, in order to show lockscreen instead of new app.
122 d.startUnlock(false /* toTheRight */);
123 }
125 // This is a just a glorified notifyUserRequestedApp(), but it does one
126 // other thing: it hides any cover pages to the RIGHT, because the user
127 // just came from a launcher drag starting on the left.
128 // It also returns a boolean value, indicating whether there was a visual
129 // change or not (the shell only wants to hide the launcher if there was
130 // a change).
131 function notifyShowingDashFromDrag() {
132 if (!active) {
133 return false;
134 }
136 return d.startUnlock(true /* toTheRight */);
137 }
139 function sessionToStart() {
140 for (var i = 0; i < LightDMService.sessions.count; i++) {
141 var session = LightDMService.sessions.data(i,
142 LightDMService.sessionRoles.KeyRole);
143 if (loader.item.sessionToStart === session) {
144 return session;
145 }
146 }
148 if (loader.item.sessionToStart === LightDMService.greeter.defaultSession) {
149 return LightDMService.greeter.defaultSession;
150 } else {
151 return "ubuntu"; // The default / fallback
152 }
153 }
155 QtObject {
156 id: d
158 readonly property bool multiUser: LightDMService.users.count > 1
159 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
160 property int currentIndex: Math.max(selectUserIndex, 0)
161 property bool waiting
162 property bool isLockscreen // true when we are locking an active session, rather than first user login
163 readonly property bool secureFingerprint: isLockscreen &&
164 AccountsService.failedFingerprintLogins <
165 root.failedFingerprintLoginsDisableAttempts
166 readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
168 // We want 'launcherOffset' to animate down to zero. But not to animate
169 // while being dragged. So ideally we change this only when the user
170 // lets go and launcherOffset drops to zero. But we need to wait for
171 // the behavior to be enabled first. So we cache the last known good
172 // launcherOffset value to cover us during that brief gap between
173 // release and the behavior turning on.
174 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
175 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
176 Behavior on launcherOffsetProxy {
177 id: launcherOffsetProxyBehavior
178 enabled: launcherOffset === 0
179 UbuntuNumberAnimation {}
180 }
182 function getUserIndex(username) {
183 if (username === "")
184 return -1;
186 // Find index for requested user, if it exists
187 for (var i = 0; i < LightDMService.users.count; i++) {
188 if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
189 return i;
190 }
191 }
193 return -1;
194 }
196 function selectUser(index, reset) {
197 if (index < 0 || index >= LightDMService.users.count)
198 return;
199 d.waiting = true;
200 if (reset) {
201 loader.item.reset(false /* forceShow */);
202 }
203 currentIndex = index;
204 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
205 AccountsService.user = user;
206 LauncherModel.setUser(user);
207 LightDMService.greeter.authenticate(user); // always resets auth state
208 }
210 function hideView() {
211 if (loader.item) {
212 loader.item.enabled = false; // drop OSK and prevent interaction
213 loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
214 loader.item.hide();
215 }
216 }
218 function login() {
219 d.waiting = true;
220 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
221 sessionStarted();
222 hideView();
223 } else if (loader.item) {
224 loader.item.notifyAuthenticationFailed();
225 }
226 d.waiting = false;
227 }
229 function startUnlock(toTheRight) {
230 if (loader.item) {
231 return loader.item.tryToUnlock(toTheRight);
232 } else {
233 return false;
234 }
235 }
237 function checkForcedUnlock(hideNow) {
238 if (forcedUnlock && shown) {
239 hideView();
240 if (hideNow) {
241 root.hideNow(); // skip hide animation
242 }
243 }
244 }
246 function showPromptMessage(text, isError) {
247 // inefficient, but we only rarely deal with messages
248 var html = text.replace(/&/g, "&amp;")
249 .replace(/</g, "&lt;")
250 .replace(/>/g, "&gt;")
251 .replace(/\n/g, "<br>");
252 if (isError) {
253 html = "<font color=\"#df382c\">" + html + "</font>";
254 }
256 if (loader.item) {
257 loader.item.showMessage(html);
258 }
259 }
261 function showFingerprintMessage(msg) {
262 if (loader.item) {
263 loader.item.reset(false /* forceShow */);
264 loader.item.showErrorMessage(msg);
265 }
266 showPromptMessage(msg, true);
267 }
268 }
270 onLauncherOffsetChanged: {
271 if (launcherOffset > 0) {
272 d.lastKnownPositiveOffset = launcherOffset;
273 }
274 }
276 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
277 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
279 onLockedChanged: {
280 if (!locked) {
281 AccountsService.failedLogins = 0;
282 AccountsService.failedFingerprintLogins = 0;
284 // Stop delay timer if they logged in with fingerprint
285 forcedDelayTimer.stop();
286 forcedDelayTimer.delayMinutes = 0;
287 }
288 }
290 onRequiredChanged: {
291 if (required) {
292 d.waiting = true;
293 lockedApp = "";
294 }
295 }
297 GSettings {
298 id: greeterSettings
299 schema.id: "com.canonical.Unity8.Greeter"
300 }
302 Timer {
303 id: forcedDelayTimer
305 // We use a short interval and check against the system wall clock
306 // because we have to consider the case that the system is suspended
307 // for a few minutes. When we wake up, we want to quickly be correct.
308 interval: 500
310 property var delayTarget
311 property int delayMinutes
313 function forceDelay() {
314 // Store the beginning time for a lockout in GSettings, so that
315 // we still lock the user out if they reboot. And we store
316 // starting time rather than end-time or how-long because:
317 // - If storing end-time and on boot we have a problem with NTP,
318 // we might get locked out for a lot longer than we thought.
319 // - If storing how-long, and user turns their phone off for an
320 // hour rather than wait, they wouldn't expect to still be locked
321 // out.
322 // - A malicious actor could manipulate either of the above
323 // settings to keep the user out longer. But by storing
324 // start-time, we never make the user wait longer than the full
325 // lock out time.
326 greeterSettings.lockedOutTime = new Date().getTime();
327 checkForForcedDelay();
328 }
330 onTriggered: {
331 var diff = delayTarget - new Date();
332 if (diff > 0) {
333 delayMinutes = Math.ceil(diff / 60000);
334 start(); // go again
335 } else {
336 delayMinutes = 0;
337 }
338 }
340 function checkForForcedDelay() {
341 if (greeterSettings.lockedOutTime === 0) {
342 return;
343 }
345 var now = new Date();
346 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
348 // If tooEarly is true, something went very wrong. Bug or NTP
349 // misconfiguration maybe?
350 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
351 var tooLate = now >= delayTarget;
353 // Compare stored time to system time. If a malicious actor is
354 // able to manipulate time to avoid our lockout, they already have
355 // enough access to cause damage. So we choose to trust this check.
356 if (tooEarly || tooLate) {
357 stop();
358 delayMinutes = 0;
359 } else {
360 triggered();
361 }
362 }
364 Component.onCompleted: checkForForcedDelay()
365 }
367 // event eater
368 // Nothing should leak to items behind the greeter
369 MouseArea { anchors.fill: parent; hoverEnabled: true }
371 Loader {
372 id: loader
373 objectName: "loader"
375 anchors.fill: parent
377 active: root.required
378 source: root.viewSource.toString() ? root.viewSource :
379 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
381 onLoaded: {
382 root.lockedApp = "";
383 item.forceActiveFocus();
384 d.selectUser(d.currentIndex, true);
385 LightDMService.infographic.readyForDataChange();
386 }
388 Connections {
389 target: loader.item
390 onSelected: {
391 d.selectUser(index, true);
392 }
393 onResponded: {
394 if (root.locked) {
395 LightDMService.greeter.respond(response);
396 } else {
397 d.login();
398 }
399 }
400 onTease: root.tease()
401 onEmergencyCall: root.emergencyCall()
402 onRequiredChanged: {
403 if (!loader.item.required) {
404 root.hide();
405 }
406 }
407 }
409 Binding {
410 target: loader.item
411 property: "backgroundTopMargin"
412 value: -root.y
413 }
415 Binding {
416 target: loader.item
417 property: "launcherOffset"
418 value: d.launcherOffsetProxy
419 }
421 Binding {
422 target: loader.item
423 property: "dragHandleLeftMargin"
424 value: root.dragHandleLeftMargin
425 }
427 Binding {
428 target: loader.item
429 property: "delayMinutes"
430 value: forcedDelayTimer.delayMinutes
431 }
433 Binding {
434 target: loader.item
435 property: "background"
436 value: root.background
437 }
439 Binding {
440 target: loader.item
441 property: "hasCustomBackground"
442 value: root.hasCustomBackground
443 }
445 Binding {
446 target: loader.item
447 property: "locked"
448 value: root.locked
449 }
451 Binding {
452 target: loader.item
453 property: "waiting"
454 value: d.waiting
455 }
457 Binding {
458 target: loader.item
459 property: "alphanumeric"
460 value: d.alphanumeric
461 }
463 Binding {
464 target: loader.item
465 property: "currentIndex"
466 value: d.currentIndex
467 }
469 Binding {
470 target: loader.item
471 property: "userModel"
472 value: LightDMService.users
473 }
475 Binding {
476 target: loader.item
477 property: "infographicModel"
478 value: LightDMService.infographic
479 }
480 }
482 Connections {
483 target: LightDMService.greeter
485 onShowGreeter: root.forceShow()
486 onHideGreeter: root.forcedUnlock = true
488 onShowMessage: d.showPromptMessage(text, isError)
490 onShowPrompt: {
491 if (loader.item) {
492 loader.item.showPrompt(text, isSecret, isDefaultPrompt);
493 }
495 d.waiting = false;
496 }
498 onAuthenticationComplete: {
499 d.waiting = false;
501 if (LightDMService.greeter.authenticated) {
502 if (!LightDMService.greeter.promptless) {
503 d.login();
504 }
505 } else {
506 if (!LightDMService.greeter.promptless) {
507 AccountsService.failedLogins++;
508 }
510 // Check if we should initiate a factory reset
511 if (maxFailedLogins >= 2) { // require at least a warning
512 if (AccountsService.failedLogins === maxFailedLogins - 1) {
513 loader.item.showLastChance();
514 } else if (AccountsService.failedLogins >= maxFailedLogins) {
515 SystemImage.factoryReset(); // Ouch!
516 }
517 }
519 // Check if we should initiate a forced login delay
520 if (failedLoginsDelayAttempts > 0
521 && AccountsService.failedLogins > 0
522 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
523 forcedDelayTimer.forceDelay();
524 }
526 loader.item.notifyAuthenticationFailed();
527 if (!LightDMService.greeter.promptless) {
528 d.selectUser(d.currentIndex, false);
529 }
530 }
531 }
533 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
534 }
536 Connections {
537 target: DBusUnitySessionService
538 onLockRequested: root.forceShow()
539 onUnlocked: {
540 root.forcedUnlock = true;
541 root.hideNow();
542 }
543 }
545 Binding {
546 target: LightDMService.greeter
547 property: "active"
548 value: root.active
549 }
551 Binding {
552 target: LightDMService.infographic
553 property: "username"
554 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
555 }
557 Connections {
558 target: i18n
559 onLanguageChanged: LightDMService.infographic.readyForDataChange()
560 }
562 Observer {
563 id: biometryd
564 objectName: "biometryd"
566 property var operation: null
567 readonly property bool idEnabled: root.active &&
568 root.allowFingerprint &&
569 Powerd.status === Powerd.On &&
570 Biometryd.available &&
571 AccountsService.enableFingerprintIdentification
573 function cancelOperation() {
574 if (operation) {
575 operation.cancel();
576 operation = null;
577 }
578 }
580 function restartOperation() {
581 cancelOperation();
583 if (idEnabled) {
584 var identifier = Biometryd.defaultDevice.identifier;
585 operation = identifier.identifyUser();
586 operation.start(biometryd);
587 }
588 }
590 function failOperation(reason) {
591 console.log("Failed to identify user by fingerprint:", reason);
592 restartOperation();
593 if (!d.secureFingerprint) {
594 d.startUnlock(false /* toTheRight */); // use normal login instead
595 }
596 var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
597 d.showFingerprintMessage(msg);
598 }
600 Component.onCompleted: restartOperation()
601 Component.onDestruction: cancelOperation()
602 onIdEnabledChanged: restartOperation()
604 onSucceeded: {
605 if (!d.secureFingerprint) {
606 failOperation("fingerprint reader is locked");
607 return;
608 }
609 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
610 AccountsService.failedFingerprintLogins++;
611 failOperation("not the selected user");
612 return;
613 }
614 console.log("Identified user by fingerprint:", result);
615 if (loader.item) {
616 loader.item.enabled = false;
617 loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
618 }
619 if (root.active)
620 root.forcedUnlock = true;
621 }
622 onFailed: {
623 if (!d.secureFingerprint) {
624 failOperation("fingerprint reader is locked");
625 } else {
626 AccountsService.failedFingerprintLogins++;
627 failOperation(reason);
628 }
629 }
630 }