on bb09f86 help sign in
unity8/qml/Greeter/Greeter.qml
597 lines 20 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.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);
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 readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
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) {
193 if (index < 0 || index >= LightDMService.users.count)
194 return;
195 currentIndex = index;
196 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
197 AccountsService.user = user;
198 LauncherModel.setUser(user);
199 LightDMService.greeter.authenticate(user); // always resets auth state
200 }
202 function hideView() {
203 if (loader.item) {
204 loader.item.enabled = false; // drop OSK and prevent interaction
205 loader.item.hide();
206 }
207 }
209 function login() {
210 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
211 sessionStarted();
212 hideView();
213 } else if (loader.item) {
214 loader.item.notifyAuthenticationFailed();
215 }
216 }
218 function startUnlock(toTheRight) {
219 if (loader.item) {
220 return loader.item.tryToUnlock(toTheRight);
221 } else {
222 return false;
223 }
224 }
226 function checkForcedUnlock(hideNow) {
227 if (forcedUnlock && shown) {
228 hideView();
229 if (hideNow) {
230 root.hideNow(); // skip hide animation
231 }
232 }
233 }
235 function showFingerprintMessage(msg) {
236 d.selectUser(d.currentIndex);
237 LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
238 if (loader.item) {
239 loader.item.showErrorMessage(msg);
240 loader.item.notifyAuthenticationFailed();
241 }
242 }
243 }
245 onLauncherOffsetChanged: {
246 if (launcherOffset > 0) {
247 d.lastKnownPositiveOffset = launcherOffset;
248 }
249 }
251 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
252 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
254 onLockedChanged: {
255 if (!locked) {
256 AccountsService.failedLogins = 0;
257 AccountsService.failedFingerprintLogins = 0;
259 // Stop delay timer if they logged in with fingerprint
260 forcedDelayTimer.stop();
261 forcedDelayTimer.delayMinutes = 0;
262 }
263 }
265 onRequiredChanged: {
266 if (required) {
267 lockedApp = "";
268 }
269 }
271 GSettings {
272 id: greeterSettings
273 schema.id: "com.canonical.Unity8.Greeter"
274 }
276 Timer {
277 id: forcedDelayTimer
279 // We use a short interval and check against the system wall clock
280 // because we have to consider the case that the system is suspended
281 // for a few minutes. When we wake up, we want to quickly be correct.
282 interval: 500
284 property var delayTarget
285 property int delayMinutes
287 function forceDelay() {
288 // Store the beginning time for a lockout in GSettings, so that
289 // we still lock the user out if they reboot. And we store
290 // starting time rather than end-time or how-long because:
291 // - If storing end-time and on boot we have a problem with NTP,
292 // we might get locked out for a lot longer than we thought.
293 // - If storing how-long, and user turns their phone off for an
294 // hour rather than wait, they wouldn't expect to still be locked
295 // out.
296 // - A malicious actor could manipulate either of the above
297 // settings to keep the user out longer. But by storing
298 // start-time, we never make the user wait longer than the full
299 // lock out time.
300 greeterSettings.lockedOutTime = new Date().getTime();
301 checkForForcedDelay();
302 }
304 onTriggered: {
305 var diff = delayTarget - new Date();
306 if (diff > 0) {
307 delayMinutes = Math.ceil(diff / 60000);
308 start(); // go again
309 } else {
310 delayMinutes = 0;
311 }
312 }
314 function checkForForcedDelay() {
315 if (greeterSettings.lockedOutTime === 0) {
316 return;
317 }
319 var now = new Date();
320 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
322 // If tooEarly is true, something went very wrong. Bug or NTP
323 // misconfiguration maybe?
324 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
325 var tooLate = now >= delayTarget;
327 // Compare stored time to system time. If a malicious actor is
328 // able to manipulate time to avoid our lockout, they already have
329 // enough access to cause damage. So we choose to trust this check.
330 if (tooEarly || tooLate) {
331 stop();
332 delayMinutes = 0;
333 } else {
334 triggered();
335 }
336 }
338 Component.onCompleted: checkForForcedDelay()
339 }
341 // event eater
342 // Nothing should leak to items behind the greeter
343 MouseArea { anchors.fill: parent; hoverEnabled: true }
345 Loader {
346 id: loader
347 objectName: "loader"
349 anchors.fill: parent
351 active: root.required
352 source: root.viewSource.toString() ? root.viewSource :
353 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
355 onLoaded: {
356 root.lockedApp = "";
357 item.forceActiveFocus();
358 d.selectUser(d.currentIndex);
359 LightDMService.infographic.readyForDataChange();
360 }
362 Connections {
363 target: loader.item
364 onSelected: {
365 d.selectUser(index);
366 }
367 onResponded: {
368 if (root.locked) {
369 LightDMService.greeter.respond(response);
370 } else {
371 d.login();
372 }
373 }
374 onTease: root.tease()
375 onEmergencyCall: root.emergencyCall()
376 onRequiredChanged: {
377 if (!loader.item.required) {
378 root.hide();
379 }
380 }
381 }
383 Binding {
384 target: loader.item
385 property: "backgroundTopMargin"
386 value: -root.y
387 }
389 Binding {
390 target: loader.item
391 property: "launcherOffset"
392 value: d.launcherOffsetProxy
393 }
395 Binding {
396 target: loader.item
397 property: "dragHandleLeftMargin"
398 value: root.dragHandleLeftMargin
399 }
401 Binding {
402 target: loader.item
403 property: "delayMinutes"
404 value: forcedDelayTimer.delayMinutes
405 }
407 Binding {
408 target: loader.item
409 property: "background"
410 value: root.background
411 }
413 Binding {
414 target: loader.item
415 property: "hasCustomBackground"
416 value: root.hasCustomBackground
417 }
419 Binding {
420 target: loader.item
421 property: "locked"
422 value: root.locked
423 }
425 Binding {
426 target: loader.item
427 property: "waiting"
428 value: d.waiting
429 }
431 Binding {
432 target: loader.item
433 property: "alphanumeric"
434 value: d.alphanumeric
435 }
437 Binding {
438 target: loader.item
439 property: "currentIndex"
440 value: d.currentIndex
441 }
443 Binding {
444 target: loader.item
445 property: "userModel"
446 value: LightDMService.users
447 }
449 Binding {
450 target: loader.item
451 property: "infographicModel"
452 value: LightDMService.infographic
453 }
454 }
456 Connections {
457 target: LightDMService.greeter
459 onShowGreeter: root.forceShow()
460 onHideGreeter: root.forcedUnlock = true
462 onLoginError: {
463 if (!loader.item) {
464 return;
465 }
467 loader.item.notifyAuthenticationFailed();
469 if (!automatic) {
470 AccountsService.failedLogins++;
472 // Check if we should initiate a factory reset
473 if (maxFailedLogins >= 2) { // require at least a warning
474 if (AccountsService.failedLogins === maxFailedLogins - 1) {
475 loader.item.showLastChance();
476 } else if (AccountsService.failedLogins >= maxFailedLogins) {
477 SystemImage.factoryReset(); // Ouch!
478 }
479 }
481 // Check if we should initiate a forced login delay
482 if (failedLoginsDelayAttempts > 0
483 && AccountsService.failedLogins > 0
484 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
485 forcedDelayTimer.forceDelay();
486 }
488 d.selectUser(d.currentIndex);
489 }
490 }
492 onLoginSuccess: {
493 if (!automatic) {
494 d.login();
495 }
496 }
498 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
499 }
501 Connections {
502 target: DBusUnitySessionService
503 onLockRequested: root.forceShow()
504 onUnlocked: {
505 root.forcedUnlock = true;
506 root.hideNow();
507 }
508 }
510 Binding {
511 target: LightDMService.greeter
512 property: "active"
513 value: root.active
514 }
516 Binding {
517 target: LightDMService.infographic
518 property: "username"
519 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
520 }
522 Connections {
523 target: i18n
524 onLanguageChanged: LightDMService.infographic.readyForDataChange()
525 }
527 Observer {
528 id: biometryd
529 objectName: "biometryd"
531 property var operation: null
532 readonly property bool idEnabled: root.active &&
533 root.allowFingerprint &&
534 Powerd.status === Powerd.On &&
535 Biometryd.available &&
536 AccountsService.enableFingerprintIdentification
538 function cancelOperation() {
539 if (operation) {
540 operation.cancel();
541 operation = null;
542 }
543 }
545 function restartOperation() {
546 cancelOperation();
548 if (idEnabled) {
549 var identifier = Biometryd.defaultDevice.identifier;
550 operation = identifier.identifyUser();
551 operation.start(biometryd);
552 }
553 }
555 function failOperation(reason) {
556 console.log("Failed to identify user by fingerprint:", reason);
557 restartOperation();
558 if (!d.secureFingerprint) {
559 d.startUnlock(false /* toTheRight */); // use normal login instead
560 }
561 var msg = d.secureFingerprint ? i18n.tr("Try again") :
562 d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
563 i18n.tr("Enter passcode to unlock");
564 d.showFingerprintMessage(msg);
565 }
567 Component.onCompleted: restartOperation()
568 Component.onDestruction: cancelOperation()
569 onIdEnabledChanged: restartOperation()
571 onSucceeded: {
572 if (!d.secureFingerprint) {
573 failOperation("fingerprint reader is locked");
574 return;
575 }
576 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
577 AccountsService.failedFingerprintLogins++;
578 failOperation("not the selected user");
579 return;
580 }
581 console.log("Identified user by fingerprint:", result);
582 if (loader.item) {
583 loader.item.showFakePassword();
584 }
585 if (root.active)
586 root.forcedUnlock = true;
587 }
588 onFailed: {
589 if (!d.secureFingerprint) {
590 failOperation("fingerprint reader is locked");
591 } else {
592 AccountsService.failedFingerprintLogins++;
593 failOperation(reason);
594 }
595 }
596 }