on ab2f849 help sign in
unity8/qml/Greeter/Greeter.qml
627 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 return LightDMService.greeter.defaultSession;
149 }
151 QtObject {
152 id: d
154 readonly property bool multiUser: LightDMService.users.count > 1
155 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
156 property int currentIndex: Math.max(selectUserIndex, 0)
157 property bool waiting
158 property bool isLockscreen // true when we are locking an active session, rather than first user login
159 readonly property bool secureFingerprint: isLockscreen &&
160 AccountsService.failedFingerprintLogins <
161 root.failedFingerprintLoginsDisableAttempts
162 readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
164 // We want 'launcherOffset' to animate down to zero. But not to animate
165 // while being dragged. So ideally we change this only when the user
166 // lets go and launcherOffset drops to zero. But we need to wait for
167 // the behavior to be enabled first. So we cache the last known good
168 // launcherOffset value to cover us during that brief gap between
169 // release and the behavior turning on.
170 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
171 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
172 Behavior on launcherOffsetProxy {
173 id: launcherOffsetProxyBehavior
174 enabled: launcherOffset === 0
175 UbuntuNumberAnimation {}
176 }
178 function getUserIndex(username) {
179 if (username === "")
180 return -1;
182 // Find index for requested user, if it exists
183 for (var i = 0; i < LightDMService.users.count; i++) {
184 if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
185 return i;
186 }
187 }
189 return -1;
190 }
192 function selectUser(index, reset) {
193 if (index < 0 || index >= LightDMService.users.count)
194 return;
195 d.waiting = true;
196 if (reset) {
197 loader.item.reset(false /* forceShow */);
198 }
199 currentIndex = index;
200 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
201 AccountsService.user = user;
202 LauncherModel.setUser(user);
203 LightDMService.greeter.authenticate(user); // always resets auth state
204 }
206 function hideView() {
207 if (loader.item) {
208 loader.item.enabled = false; // drop OSK and prevent interaction
209 loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
210 loader.item.hide();
211 }
212 }
214 function login() {
215 d.waiting = true;
216 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
217 sessionStarted();
218 hideView();
219 } else if (loader.item) {
220 loader.item.notifyAuthenticationFailed();
221 }
222 d.waiting = false;
223 }
225 function startUnlock(toTheRight) {
226 if (loader.item) {
227 return loader.item.tryToUnlock(toTheRight);
228 } else {
229 return false;
230 }
231 }
233 function checkForcedUnlock(hideNow) {
234 if (forcedUnlock && shown) {
235 hideView();
236 if (hideNow) {
237 root.hideNow(); // skip hide animation
238 }
239 }
240 }
242 function showPromptMessage(text, isError) {
243 // inefficient, but we only rarely deal with messages
244 var html = text.replace(/&/g, "&amp;")
245 .replace(/</g, "&lt;")
246 .replace(/>/g, "&gt;")
247 .replace(/\n/g, "<br>");
248 if (isError) {
249 html = "<font color=\"#df382c\">" + html + "</font>";
250 }
252 if (loader.item) {
253 loader.item.showMessage(html);
254 }
255 }
257 function showFingerprintMessage(msg) {
258 if (loader.item) {
259 loader.item.reset(false /* forceShow */);
260 loader.item.showErrorMessage(msg);
261 }
262 showPromptMessage(msg, true);
263 }
264 }
266 onLauncherOffsetChanged: {
267 if (launcherOffset > 0) {
268 d.lastKnownPositiveOffset = launcherOffset;
269 }
270 }
272 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
273 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
275 onLockedChanged: {
276 if (!locked) {
277 AccountsService.failedLogins = 0;
278 AccountsService.failedFingerprintLogins = 0;
280 // Stop delay timer if they logged in with fingerprint
281 forcedDelayTimer.stop();
282 forcedDelayTimer.delayMinutes = 0;
283 }
284 }
286 onRequiredChanged: {
287 if (required) {
288 d.waiting = true;
289 lockedApp = "";
290 }
291 }
293 GSettings {
294 id: greeterSettings
295 schema.id: "com.canonical.Unity8.Greeter"
296 }
298 Timer {
299 id: forcedDelayTimer
301 // We use a short interval and check against the system wall clock
302 // because we have to consider the case that the system is suspended
303 // for a few minutes. When we wake up, we want to quickly be correct.
304 interval: 500
306 property var delayTarget
307 property int delayMinutes
309 function forceDelay() {
310 // Store the beginning time for a lockout in GSettings, so that
311 // we still lock the user out if they reboot. And we store
312 // starting time rather than end-time or how-long because:
313 // - If storing end-time and on boot we have a problem with NTP,
314 // we might get locked out for a lot longer than we thought.
315 // - If storing how-long, and user turns their phone off for an
316 // hour rather than wait, they wouldn't expect to still be locked
317 // out.
318 // - A malicious actor could manipulate either of the above
319 // settings to keep the user out longer. But by storing
320 // start-time, we never make the user wait longer than the full
321 // lock out time.
322 greeterSettings.lockedOutTime = new Date().getTime();
323 checkForForcedDelay();
324 }
326 onTriggered: {
327 var diff = delayTarget - new Date();
328 if (diff > 0) {
329 delayMinutes = Math.ceil(diff / 60000);
330 start(); // go again
331 } else {
332 delayMinutes = 0;
333 }
334 }
336 function checkForForcedDelay() {
337 if (greeterSettings.lockedOutTime === 0) {
338 return;
339 }
341 var now = new Date();
342 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
344 // If tooEarly is true, something went very wrong. Bug or NTP
345 // misconfiguration maybe?
346 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
347 var tooLate = now >= delayTarget;
349 // Compare stored time to system time. If a malicious actor is
350 // able to manipulate time to avoid our lockout, they already have
351 // enough access to cause damage. So we choose to trust this check.
352 if (tooEarly || tooLate) {
353 stop();
354 delayMinutes = 0;
355 } else {
356 triggered();
357 }
358 }
360 Component.onCompleted: checkForForcedDelay()
361 }
363 // event eater
364 // Nothing should leak to items behind the greeter
365 MouseArea { anchors.fill: parent; hoverEnabled: true }
367 Loader {
368 id: loader
369 objectName: "loader"
371 anchors.fill: parent
373 active: root.required
374 source: root.viewSource.toString() ? root.viewSource :
375 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
377 onLoaded: {
378 root.lockedApp = "";
379 item.forceActiveFocus();
380 d.selectUser(d.currentIndex, true);
381 LightDMService.infographic.readyForDataChange();
382 }
384 Connections {
385 target: loader.item
386 onSelected: {
387 d.selectUser(index, true);
388 }
389 onResponded: {
390 if (root.locked) {
391 LightDMService.greeter.respond(response);
392 } else {
393 d.login();
394 }
395 }
396 onTease: root.tease()
397 onEmergencyCall: root.emergencyCall()
398 onRequiredChanged: {
399 if (!loader.item.required) {
400 root.hide();
401 }
402 }
403 }
405 Binding {
406 target: loader.item
407 property: "backgroundTopMargin"
408 value: -root.y
409 }
411 Binding {
412 target: loader.item
413 property: "launcherOffset"
414 value: d.launcherOffsetProxy
415 }
417 Binding {
418 target: loader.item
419 property: "dragHandleLeftMargin"
420 value: root.dragHandleLeftMargin
421 }
423 Binding {
424 target: loader.item
425 property: "delayMinutes"
426 value: forcedDelayTimer.delayMinutes
427 }
429 Binding {
430 target: loader.item
431 property: "background"
432 value: root.background
433 }
435 Binding {
436 target: loader.item
437 property: "hasCustomBackground"
438 value: root.hasCustomBackground
439 }
441 Binding {
442 target: loader.item
443 property: "locked"
444 value: root.locked
445 }
447 Binding {
448 target: loader.item
449 property: "waiting"
450 value: d.waiting
451 }
453 Binding {
454 target: loader.item
455 property: "alphanumeric"
456 value: d.alphanumeric
457 }
459 Binding {
460 target: loader.item
461 property: "currentIndex"
462 value: d.currentIndex
463 }
465 Binding {
466 target: loader.item
467 property: "userModel"
468 value: LightDMService.users
469 }
471 Binding {
472 target: loader.item
473 property: "infographicModel"
474 value: LightDMService.infographic
475 }
476 }
478 Connections {
479 target: LightDMService.greeter
481 onShowGreeter: root.forceShow()
482 onHideGreeter: root.forcedUnlock = true
484 onShowMessage: d.showPromptMessage(text, isError)
486 onShowPrompt: {
487 if (loader.item) {
488 loader.item.showPrompt(text, isSecret, isDefaultPrompt);
489 }
491 d.waiting = false;
492 }
494 onAuthenticationComplete: {
495 d.waiting = false;
497 if (LightDMService.greeter.authenticated) {
498 if (!LightDMService.greeter.promptless) {
499 d.login();
500 }
501 } else {
502 if (!LightDMService.greeter.promptless) {
503 AccountsService.failedLogins++;
504 }
506 // Check if we should initiate a factory reset
507 if (maxFailedLogins >= 2) { // require at least a warning
508 if (AccountsService.failedLogins === maxFailedLogins - 1) {
509 loader.item.showLastChance();
510 } else if (AccountsService.failedLogins >= maxFailedLogins) {
511 SystemImage.factoryReset(); // Ouch!
512 }
513 }
515 // Check if we should initiate a forced login delay
516 if (failedLoginsDelayAttempts > 0
517 && AccountsService.failedLogins > 0
518 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
519 forcedDelayTimer.forceDelay();
520 }
522 loader.item.notifyAuthenticationFailed();
523 if (!LightDMService.greeter.promptless) {
524 d.selectUser(d.currentIndex, false);
525 }
526 }
527 }
529 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
530 }
532 Connections {
533 target: DBusUnitySessionService
534 onLockRequested: root.forceShow()
535 onUnlocked: {
536 root.forcedUnlock = true;
537 root.hideNow();
538 }
539 }
541 Binding {
542 target: LightDMService.greeter
543 property: "active"
544 value: root.active
545 }
547 Binding {
548 target: LightDMService.infographic
549 property: "username"
550 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
551 }
553 Connections {
554 target: i18n
555 onLanguageChanged: LightDMService.infographic.readyForDataChange()
556 }
558 Observer {
559 id: biometryd
560 objectName: "biometryd"
562 property var operation: null
563 readonly property bool idEnabled: root.active &&
564 root.allowFingerprint &&
565 Powerd.status === Powerd.On &&
566 Biometryd.available &&
567 AccountsService.enableFingerprintIdentification
569 function cancelOperation() {
570 if (operation) {
571 operation.cancel();
572 operation = null;
573 }
574 }
576 function restartOperation() {
577 cancelOperation();
579 if (idEnabled) {
580 var identifier = Biometryd.defaultDevice.identifier;
581 operation = identifier.identifyUser();
582 operation.start(biometryd);
583 }
584 }
586 function failOperation(reason) {
587 console.log("Failed to identify user by fingerprint:", reason);
588 restartOperation();
589 if (!d.secureFingerprint) {
590 d.startUnlock(false /* toTheRight */); // use normal login instead
591 }
592 var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
593 d.showFingerprintMessage(msg);
594 }
596 Component.onCompleted: restartOperation()
597 Component.onDestruction: cancelOperation()
598 onIdEnabledChanged: restartOperation()
600 onSucceeded: {
601 if (!d.secureFingerprint) {
602 failOperation("fingerprint reader is locked");
603 return;
604 }
605 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
606 AccountsService.failedFingerprintLogins++;
607 failOperation("not the selected user");
608 return;
609 }
610 console.log("Identified user by fingerprint:", result);
611 if (loader.item) {
612 loader.item.enabled = false;
613 loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
614 }
615 if (root.active)
616 root.forcedUnlock = true;
617 }
618 onFailed: {
619 if (!d.secureFingerprint) {
620 failOperation("fingerprint reader is locked");
621 } else {
622 AccountsService.failedFingerprintLogins++;
623 failOperation(reason);
624 }
625 }
626 }