on 5ea3c69 help sign in
unity8/qml/Greeter/Greeter.qml
586 lines 19 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 Unity.Launcher 0.1
24import Unity.Session 0.1
26import "." 0.1
27import "../Components"
29Showable {
30 id: root
31 created: loader.status == Loader.Ready
33 property real dragHandleLeftMargin: 0
35 property url background
36 property bool hasCustomBackground
38 // How far to offset the top greeter layer during a launcher left-drag
39 property real launcherOffset
41 readonly property bool active: required || hasLockedApp
42 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
44 property bool allowFingerprint: true
46 // True when the greeter is waiting for PAM or other setup process
47 readonly property alias waiting: d.waiting
49 property string lockedApp: ""
50 readonly property bool hasLockedApp: lockedApp !== ""
52 property bool forcedUnlock
53 readonly property bool locked: LightDMService.greeter.active && !LightDMService.greeter.authenticated && !forcedUnlock
55 property bool tabletMode
56 property url viewSource // only used for testing
58 property int failedLoginsDelayAttempts: 7 // number of failed logins
59 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
60 property int failedFingerprintLoginsDisableAttempts: 3 // number of failed fingerprint logins
62 readonly property bool animating: loader.item ? loader.item.animating : false
64 signal tease()
65 signal sessionStarted()
66 signal emergencyCall()
68 function forceShow() {
69 if (!active) {
70 d.isLockscreen = true;
71 }
72 forcedUnlock = false;
73 if (required) {
74 if (loader.item) {
75 loader.item.forceShow();
76 }
77 // Normally loader.onLoaded will select a user, but if we're
78 // already shown, do it manually.
79 d.selectUser(d.currentIndex);
80 }
82 // Even though we may already be shown, we want to call show() for its
83 // possible side effects, like hiding indicators and such.
84 //
85 // We re-check forcedUnlock here, because selectUser above might
86 // process events during authentication, and a request to unlock could
87 // have come in in the meantime.
88 if (!forcedUnlock) {
89 showNow();
90 }
91 }
93 function notifyAppFocusRequested(appId) {
94 if (!active) {
95 return;
96 }
98 if (hasLockedApp) {
99 if (appId === lockedApp) {
100 hide(); // show locked app
101 } else {
102 show();
103 d.startUnlock(false /* toTheRight */);
104 }
105 } else {
106 d.startUnlock(false /* toTheRight */);
107 }
108 }
110 // Notify that the user has explicitly requested an app
111 function notifyUserRequestedApp() {
112 if (!active) {
113 return;
114 }
116 // A hint that we're about to focus an app. This way we can look
117 // a little more responsive, rather than waiting for the above
118 // notifyAppFocusRequested call. We also need this in case we have a locked
119 // app, in order to show lockscreen instead of new app.
120 d.startUnlock(false /* toTheRight */);
121 }
123 // This is a just a glorified notifyUserRequestedApp(), but it does one
124 // other thing: it hides any cover pages to the RIGHT, because the user
125 // just came from a launcher drag starting on the left.
126 // It also returns a boolean value, indicating whether there was a visual
127 // change or not (the shell only wants to hide the launcher if there was
128 // a change).
129 function notifyShowingDashFromDrag() {
130 if (!active) {
131 return false;
132 }
134 return d.startUnlock(true /* toTheRight */);
135 }
137 function sessionToStart() {
138 for (var i = 0; i < LightDMService.sessions.count; i++) {
139 var session = LightDMService.sessions.data(i,
140 LightDMService.sessionRoles.KeyRole);
141 if (loader.item.sessionToStart === session) {
142 return session;
143 }
144 }
146 return LightDMService.greeter.defaultSession;
147 }
149 QtObject {
150 id: d
152 readonly property bool multiUser: LightDMService.users.count > 1
153 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
154 property int currentIndex: Math.max(selectUserIndex, 0)
155 readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
156 property bool isLockscreen // true when we are locking an active session, rather than first user login
157 readonly property bool secureFingerprint: isLockscreen &&
158 AccountsService.failedFingerprintLogins <
159 root.failedFingerprintLoginsDisableAttempts
160 readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
162 // We want 'launcherOffset' to animate down to zero. But not to animate
163 // while being dragged. So ideally we change this only when the user
164 // lets go and launcherOffset drops to zero. But we need to wait for
165 // the behavior to be enabled first. So we cache the last known good
166 // launcherOffset value to cover us during that brief gap between
167 // release and the behavior turning on.
168 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
169 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
170 Behavior on launcherOffsetProxy {
171 id: launcherOffsetProxyBehavior
172 enabled: launcherOffset === 0
173 UbuntuNumberAnimation {}
174 }
176 function getUserIndex(username) {
177 if (username === "")
178 return -1;
180 // Find index for requested user, if it exists
181 for (var i = 0; i < LightDMService.users.count; i++) {
182 if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
183 return i;
184 }
185 }
187 return -1;
188 }
190 function selectUser(index) {
191 if (index < 0 || index >= LightDMService.users.count)
192 return;
193 currentIndex = index;
194 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
195 AccountsService.user = user;
196 LauncherModel.setUser(user);
197 LightDMService.greeter.authenticate(user); // always resets auth state
198 }
200 function hideView() {
201 if (loader.item) {
202 loader.item.enabled = false; // drop OSK and prevent interaction
203 loader.item.hide();
204 }
205 }
207 function login() {
208 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
209 sessionStarted();
210 hideView();
211 } else if (loader.item) {
212 loader.item.notifyAuthenticationFailed();
213 }
214 }
216 function startUnlock(toTheRight) {
217 if (loader.item) {
218 return loader.item.tryToUnlock(toTheRight);
219 } else {
220 return false;
221 }
222 }
224 function checkForcedUnlock(hideNow) {
225 if (forcedUnlock && shown) {
226 hideView();
227 if (hideNow) {
228 root.hideNow(); // skip hide animation
229 }
230 }
231 }
233 function showFingerprintMessage(msg) {
234 d.selectUser(d.currentIndex);
235 LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
236 if (loader.item) {
237 loader.item.showErrorMessage(msg);
238 loader.item.notifyAuthenticationFailed();
239 }
240 }
241 }
243 onLauncherOffsetChanged: {
244 if (launcherOffset > 0) {
245 d.lastKnownPositiveOffset = launcherOffset;
246 }
247 }
249 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
250 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
252 onLockedChanged: {
253 if (!locked) {
254 AccountsService.failedLogins = 0;
255 AccountsService.failedFingerprintLogins = 0;
257 // Stop delay timer if they logged in with fingerprint
258 forcedDelayTimer.stop();
259 forcedDelayTimer.delayMinutes = 0;
260 }
261 }
263 onRequiredChanged: {
264 if (required) {
265 lockedApp = "";
266 }
267 }
269 GSettings {
270 id: greeterSettings
271 schema.id: "com.canonical.Unity8.Greeter"
272 }
274 Timer {
275 id: forcedDelayTimer
277 // We use a short interval and check against the system wall clock
278 // because we have to consider the case that the system is suspended
279 // for a few minutes. When we wake up, we want to quickly be correct.
280 interval: 500
282 property var delayTarget
283 property int delayMinutes
285 function forceDelay() {
286 // Store the beginning time for a lockout in GSettings, so that
287 // we still lock the user out if they reboot. And we store
288 // starting time rather than end-time or how-long because:
289 // - If storing end-time and on boot we have a problem with NTP,
290 // we might get locked out for a lot longer than we thought.
291 // - If storing how-long, and user turns their phone off for an
292 // hour rather than wait, they wouldn't expect to still be locked
293 // out.
294 // - A malicious actor could manipulate either of the above
295 // settings to keep the user out longer. But by storing
296 // start-time, we never make the user wait longer than the full
297 // lock out time.
298 greeterSettings.lockedOutTime = new Date().getTime();
299 checkForForcedDelay();
300 }
302 onTriggered: {
303 var diff = delayTarget - new Date();
304 if (diff > 0) {
305 delayMinutes = Math.ceil(diff / 60000);
306 start(); // go again
307 } else {
308 delayMinutes = 0;
309 }
310 }
312 function checkForForcedDelay() {
313 if (greeterSettings.lockedOutTime === 0) {
314 return;
315 }
317 var now = new Date();
318 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
320 // If tooEarly is true, something went very wrong. Bug or NTP
321 // misconfiguration maybe?
322 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
323 var tooLate = now >= delayTarget;
325 // Compare stored time to system time. If a malicious actor is
326 // able to manipulate time to avoid our lockout, they already have
327 // enough access to cause damage. So we choose to trust this check.
328 if (tooEarly || tooLate) {
329 stop();
330 delayMinutes = 0;
331 } else {
332 triggered();
333 }
334 }
336 Component.onCompleted: checkForForcedDelay()
337 }
339 // event eater
340 // Nothing should leak to items behind the greeter
341 MouseArea { anchors.fill: parent; hoverEnabled: true }
343 Loader {
344 id: loader
345 objectName: "loader"
347 anchors.fill: parent
349 active: root.required
350 source: root.viewSource.toString() ? root.viewSource :
351 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
353 onLoaded: {
354 root.lockedApp = "";
355 item.forceActiveFocus();
356 d.selectUser(d.currentIndex);
357 LightDMService.infographic.readyForDataChange();
358 }
360 Connections {
361 target: loader.item
362 onSelected: {
363 d.selectUser(index);
364 }
365 onResponded: {
366 if (root.locked) {
367 LightDMService.greeter.respond(response);
368 } else {
369 d.login();
370 }
371 }
372 onTease: root.tease()
373 onEmergencyCall: root.emergencyCall()
374 onRequiredChanged: {
375 if (!loader.item.required) {
376 root.hide();
377 }
378 }
379 }
381 Binding {
382 target: loader.item
383 property: "backgroundTopMargin"
384 value: -root.y
385 }
387 Binding {
388 target: loader.item
389 property: "launcherOffset"
390 value: d.launcherOffsetProxy
391 }
393 Binding {
394 target: loader.item
395 property: "dragHandleLeftMargin"
396 value: root.dragHandleLeftMargin
397 }
399 Binding {
400 target: loader.item
401 property: "delayMinutes"
402 value: forcedDelayTimer.delayMinutes
403 }
405 Binding {
406 target: loader.item
407 property: "background"
408 value: root.background
409 }
411 Binding {
412 target: loader.item
413 property: "hasCustomBackground"
414 value: root.hasCustomBackground
415 }
417 Binding {
418 target: loader.item
419 property: "locked"
420 value: root.locked
421 }
423 Binding {
424 target: loader.item
425 property: "waiting"
426 value: d.waiting
427 }
429 Binding {
430 target: loader.item
431 property: "alphanumeric"
432 value: d.alphanumeric
433 }
435 Binding {
436 target: loader.item
437 property: "currentIndex"
438 value: d.currentIndex
439 }
441 Binding {
442 target: loader.item
443 property: "userModel"
444 value: LightDMService.users
445 }
447 Binding {
448 target: loader.item
449 property: "infographicModel"
450 value: LightDMService.infographic
451 }
452 }
454 Connections {
455 target: LightDMService.greeter
457 onShowGreeter: root.forceShow()
458 onHideGreeter: root.forcedUnlock = true
460 onLoginError: {
461 if (!loader.item) {
462 return;
463 }
465 loader.item.notifyAuthenticationFailed();
467 if (!automatic) {
468 AccountsService.failedLogins++;
470 // Check if we should initiate a forced login delay
471 if (failedLoginsDelayAttempts > 0
472 && AccountsService.failedLogins > 0
473 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
474 forcedDelayTimer.forceDelay();
475 }
477 d.selectUser(d.currentIndex);
478 }
479 }
481 onLoginSuccess: {
482 if (!automatic) {
483 d.login();
484 }
485 }
487 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
488 }
490 Connections {
491 target: DBusUnitySessionService
492 onLockRequested: root.forceShow()
493 onUnlocked: {
494 root.forcedUnlock = true;
495 root.hideNow();
496 }
497 }
499 Binding {
500 target: LightDMService.greeter
501 property: "active"
502 value: root.active
503 }
505 Binding {
506 target: LightDMService.infographic
507 property: "username"
508 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
509 }
511 Connections {
512 target: i18n
513 onLanguageChanged: LightDMService.infographic.readyForDataChange()
514 }
516 Observer {
517 id: biometryd
518 objectName: "biometryd"
520 property var operation: null
521 readonly property bool idEnabled: root.active &&
522 root.allowFingerprint &&
523 Powerd.status === Powerd.On &&
524 Biometryd.available &&
525 AccountsService.enableFingerprintIdentification
527 function cancelOperation() {
528 if (operation) {
529 operation.cancel();
530 operation = null;
531 }
532 }
534 function restartOperation() {
535 cancelOperation();
537 if (idEnabled) {
538 var identifier = Biometryd.defaultDevice.identifier;
539 operation = identifier.identifyUser();
540 operation.start(biometryd);
541 }
542 }
544 function failOperation(reason) {
545 console.log("Failed to identify user by fingerprint:", reason);
546 restartOperation();
547 if (!d.secureFingerprint) {
548 d.startUnlock(false /* toTheRight */); // use normal login instead
549 }
550 var msg = d.secureFingerprint ? i18n.tr("Try again") :
551 d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
552 i18n.tr("Enter passcode to unlock");
553 d.showFingerprintMessage(msg);
554 }
556 Component.onCompleted: restartOperation()
557 Component.onDestruction: cancelOperation()
558 onIdEnabledChanged: restartOperation()
560 onSucceeded: {
561 if (!d.secureFingerprint) {
562 failOperation("fingerprint reader is locked");
563 return;
564 }
565 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
566 AccountsService.failedFingerprintLogins++;
567 failOperation("not the selected user");
568 return;
569 }
570 console.log("Identified user by fingerprint:", result);
571 if (loader.item) {
572 loader.item.showFakePassword();
573 }
574 if (root.active)
575 root.forcedUnlock = true;
576 }
577 onFailed: {
578 if (!d.secureFingerprint) {
579 failOperation("fingerprint reader is locked");
580 } else {
581 AccountsService.failedFingerprintLogins++;
582 failOperation(reason);
583 }
584 }
585 }