on bedf8bc help sign in
unity8/qml/Greeter/Greeter.qml
595 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
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 maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
59 property int failedLoginsDelayAttempts: 7 // number of failed logins
60 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
61 property int failedFingerprintLoginsDisableAttempts: 3 // number of failed fingerprint logins
63 readonly property bool animating: loader.item ? loader.item.animating : false
65 signal tease()
66 signal sessionStarted()
67 signal emergencyCall()
69 function forceShow() {
70 if (!active) {
71 d.isLockscreen = true;
72 }
73 forcedUnlock = false;
74 if (required) {
75 // Normally loader.onLoaded will select a user, but if we're
76 // already shown, do it manually.
77 d.selectUser(d.currentIndex, true);
78 }
80 // Even though we may already be shown, we want to call show() for its
81 // possible side effects, like hiding indicators and such.
82 //
83 // We re-check forcedUnlock here, because selectUser above might
84 // process events during authentication, and a request to unlock could
85 // have come in in the meantime.
86 if (!forcedUnlock) {
87 showNow();
88 }
89 }
91 function notifyAppFocusRequested(appId) {
92 if (!active) {
93 return;
94 }
96 if (hasLockedApp) {
97 if (appId === lockedApp) {
98 hide(); // show locked app
99 } else {
100 show();
101 d.startUnlock(false /* toTheRight */);
102 }
103 } else if (appId !== "unity8-dash") { // dash isn't started by user
104 d.startUnlock(false /* toTheRight */);
105 }
106 }
108 // Notify that the user has explicitly requested the given app through unity8 GUI.
109 function notifyUserRequestedApp(appId) {
110 if (!active) {
111 return;
112 }
114 // A hint that we're about to focus an app. This way we can look
115 // a little more responsive, rather than waiting for the above
116 // notifyAppFocusRequested call. We also need this in case we have a locked
117 // app, in order to show lockscreen instead of new app.
118 d.startUnlock(false /* toTheRight */);
119 }
121 // This is a just a glorified notifyUserRequestedApp(), but it does one
122 // other thing: it hides any cover pages to the RIGHT, because the user
123 // just came from a launcher drag starting on the left.
124 // It also returns a boolean value, indicating whether there was a visual
125 // change or not (the shell only wants to hide the launcher if there was
126 // a change).
127 function notifyShowingDashFromDrag() {
128 if (!active) {
129 return false;
130 }
132 return d.startUnlock(true /* toTheRight */);
133 }
135 QtObject {
136 id: d
138 readonly property bool multiUser: LightDMService.users.count > 1
139 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
140 property int currentIndex: Math.max(selectUserIndex, 0)
141 property bool waiting
142 property bool isLockscreen // true when we are locking an active session, rather than first user login
143 readonly property bool secureFingerprint: isLockscreen &&
144 AccountsService.failedFingerprintLogins <
145 root.failedFingerprintLoginsDisableAttempts
146 readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
148 // We want 'launcherOffset' to animate down to zero. But not to animate
149 // while being dragged. So ideally we change this only when the user
150 // lets go and launcherOffset drops to zero. But we need to wait for
151 // the behavior to be enabled first. So we cache the last known good
152 // launcherOffset value to cover us during that brief gap between
153 // release and the behavior turning on.
154 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
155 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
156 Behavior on launcherOffsetProxy {
157 id: launcherOffsetProxyBehavior
158 enabled: launcherOffset === 0
159 UbuntuNumberAnimation {}
160 }
162 function getUserIndex(username) {
163 if (username === "")
164 return -1;
166 // Find index for requested user, if it exists
167 for (var i = 0; i < LightDMService.users.count; i++) {
168 if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
169 return i;
170 }
171 }
173 return -1;
174 }
176 function selectUser(index, reset) {
177 if (index < 0 || index >= LightDMService.users.count)
178 return;
179 d.waiting = true;
180 if (reset) {
181 loader.item.reset();
182 }
183 currentIndex = index;
184 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
185 AccountsService.user = user;
186 LauncherModel.setUser(user);
187 LightDMService.greeter.authenticate(user); // always resets auth state
188 }
190 function hideView() {
191 if (loader.item) {
192 loader.item.enabled = false; // drop OSK and prevent interaction
193 loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
194 loader.item.hide();
195 }
196 }
198 function login() {
199 d.waiting = true;
200 if (LightDMService.greeter.startSessionSync()) {
201 sessionStarted();
202 hideView();
203 } else if (loader.item) {
204 loader.item.notifyAuthenticationFailed();
205 }
206 d.waiting = false;
207 }
209 function startUnlock(toTheRight) {
210 if (loader.item) {
211 return loader.item.tryToUnlock(toTheRight);
212 } else {
213 return false;
214 }
215 }
217 function checkForcedUnlock(hideNow) {
218 if (forcedUnlock && shown) {
219 hideView();
220 if (hideNow) {
221 root.hideNow(); // skip hide animation
222 }
223 }
224 }
225 }
227 onLauncherOffsetChanged: {
228 if (launcherOffset > 0) {
229 d.lastKnownPositiveOffset = launcherOffset;
230 }
231 }
233 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
234 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
236 onLockedChanged: {
237 if (!locked) {
238 AccountsService.failedLogins = 0;
239 AccountsService.failedFingerprintLogins = 0;
241 // Stop delay timer if they logged in with fingerprint
242 forcedDelayTimer.stop();
243 forcedDelayTimer.delayMinutes = 0;
244 }
245 }
247 onRequiredChanged: {
248 if (required) {
249 d.waiting = true;
250 lockedApp = "";
251 }
252 }
254 GSettings {
255 id: greeterSettings
256 schema.id: "com.canonical.Unity8.Greeter"
257 }
259 Timer {
260 id: forcedDelayTimer
262 // We use a short interval and check against the system wall clock
263 // because we have to consider the case that the system is suspended
264 // for a few minutes. When we wake up, we want to quickly be correct.
265 interval: 500
267 property var delayTarget
268 property int delayMinutes
270 function forceDelay() {
271 // Store the beginning time for a lockout in GSettings, so that
272 // we still lock the user out if they reboot. And we store
273 // starting time rather than end-time or how-long because:
274 // - If storing end-time and on boot we have a problem with NTP,
275 // we might get locked out for a lot longer than we thought.
276 // - If storing how-long, and user turns their phone off for an
277 // hour rather than wait, they wouldn't expect to still be locked
278 // out.
279 // - A malicious actor could manipulate either of the above
280 // settings to keep the user out longer. But by storing
281 // start-time, we never make the user wait longer than the full
282 // lock out time.
283 greeterSettings.lockedOutTime = new Date().getTime();
284 checkForForcedDelay();
285 }
287 onTriggered: {
288 var diff = delayTarget - new Date();
289 if (diff > 0) {
290 delayMinutes = Math.ceil(diff / 60000);
291 start(); // go again
292 } else {
293 delayMinutes = 0;
294 }
295 }
297 function checkForForcedDelay() {
298 if (greeterSettings.lockedOutTime === 0) {
299 return;
300 }
302 var now = new Date();
303 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
305 // If tooEarly is true, something went very wrong. Bug or NTP
306 // misconfiguration maybe?
307 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
308 var tooLate = now >= delayTarget;
310 // Compare stored time to system time. If a malicious actor is
311 // able to manipulate time to avoid our lockout, they already have
312 // enough access to cause damage. So we choose to trust this check.
313 if (tooEarly || tooLate) {
314 stop();
315 delayMinutes = 0;
316 } else {
317 triggered();
318 }
319 }
321 Component.onCompleted: checkForForcedDelay()
322 }
324 // event eater
325 // Nothing should leak to items behind the greeter
326 MouseArea { anchors.fill: parent; hoverEnabled: true }
328 Loader {
329 id: loader
330 objectName: "loader"
332 anchors.fill: parent
334 active: root.required
335 source: root.viewSource.toString() ? root.viewSource :
336 (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
338 onLoaded: {
339 root.lockedApp = "";
340 item.forceActiveFocus();
341 d.selectUser(d.currentIndex, true);
342 LightDMService.infographic.readyForDataChange();
343 }
345 Connections {
346 target: loader.item
347 onSelected: {
348 d.selectUser(index, true);
349 }
350 onResponded: {
351 if (root.locked) {
352 LightDMService.greeter.respond(response);
353 } else {
354 d.login();
355 }
356 }
357 onTease: root.tease()
358 onEmergencyCall: root.emergencyCall()
359 onRequiredChanged: {
360 if (!loader.item.required) {
361 root.hide();
362 }
363 }
364 }
366 Binding {
367 target: loader.item
368 property: "backgroundTopMargin"
369 value: -root.y
370 }
372 Binding {
373 target: loader.item
374 property: "launcherOffset"
375 value: d.launcherOffsetProxy
376 }
378 Binding {
379 target: loader.item
380 property: "dragHandleLeftMargin"
381 value: root.dragHandleLeftMargin
382 }
384 Binding {
385 target: loader.item
386 property: "delayMinutes"
387 value: forcedDelayTimer.delayMinutes
388 }
390 Binding {
391 target: loader.item
392 property: "background"
393 value: root.background
394 }
396 Binding {
397 target: loader.item
398 property: "locked"
399 value: root.locked
400 }
402 Binding {
403 target: loader.item
404 property: "waiting"
405 value: d.waiting
406 }
408 Binding {
409 target: loader.item
410 property: "alphanumeric"
411 value: d.alphanumeric
412 }
414 Binding {
415 target: loader.item
416 property: "currentIndex"
417 value: d.currentIndex
418 }
420 Binding {
421 target: loader.item
422 property: "userModel"
423 value: LightDMService.users
424 }
426 Binding {
427 target: loader.item
428 property: "infographicModel"
429 value: LightDMService.infographic
430 }
431 }
433 Connections {
434 target: LightDMService.greeter
436 onShowGreeter: root.forceShow()
437 onHideGreeter: root.forcedUnlock = true
439 onShowMessage: {
440 // inefficient, but we only rarely deal with messages
441 var html = text.replace(/&/g, "&amp;")
442 .replace(/</g, "&lt;")
443 .replace(/>/g, "&gt;")
444 .replace(/\n/g, "<br>");
445 if (isError) {
446 html = "<font color=\"#df382c\">" + html + "</font>";
447 }
449 if (loader.item) {
450 loader.item.showMessage(html);
451 }
452 }
454 onShowPrompt: {
455 if (loader.item) {
456 loader.item.showPrompt(text, isSecret, isDefaultPrompt);
457 }
459 d.waiting = false;
460 }
462 onAuthenticationComplete: {
463 d.waiting = false;
465 if (LightDMService.greeter.authenticated) {
466 if (!LightDMService.greeter.promptless) {
467 d.login();
468 }
469 } else {
470 if (!LightDMService.greeter.promptless) {
471 AccountsService.failedLogins++;
472 }
474 // Check if we should initiate a factory reset
475 if (maxFailedLogins >= 2) { // require at least a warning
476 if (AccountsService.failedLogins === maxFailedLogins - 1) {
477 loader.item.showLastChance();
478 } else if (AccountsService.failedLogins >= maxFailedLogins) {
479 SystemImage.factoryReset(); // Ouch!
480 }
481 }
483 // Check if we should initiate a forced login delay
484 if (failedLoginsDelayAttempts > 0
485 && AccountsService.failedLogins > 0
486 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
487 forcedDelayTimer.forceDelay();
488 }
490 loader.item.notifyAuthenticationFailed();
491 if (!LightDMService.greeter.promptless) {
492 d.selectUser(d.currentIndex, false);
493 }
494 }
495 }
497 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
498 }
500 Connections {
501 target: DBusUnitySessionService
502 onLockRequested: root.forceShow()
503 onUnlocked: {
504 root.forcedUnlock = true;
505 root.hideNow();
506 }
507 }
509 Binding {
510 target: LightDMService.greeter
511 property: "active"
512 value: root.active
513 }
515 Binding {
516 target: LightDMService.infographic
517 property: "username"
518 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
519 }
521 Connections {
522 target: i18n
523 onLanguageChanged: LightDMService.infographic.readyForDataChange()
524 }
526 Observer {
527 id: biometryd
528 objectName: "biometryd"
530 property var operation: null
531 readonly property bool idEnabled: root.active &&
532 root.allowFingerprint &&
533 Powerd.status === Powerd.On &&
534 Biometryd.available &&
535 AccountsService.enableFingerprintIdentification
537 function cancelOperation() {
538 if (operation) {
539 operation.cancel();
540 operation = null;
541 }
542 }
544 function restartOperation() {
545 cancelOperation();
547 if (idEnabled) {
548 var identifier = Biometryd.defaultDevice.identifier;
549 operation = identifier.identifyUser();
550 operation.start(biometryd);
551 }
552 }
554 function failOperation(reason) {
555 console.log("Failed to identify user by fingerprint:", reason);
556 restartOperation();
557 if (!d.secureFingerprint) {
558 d.startUnlock(false /* toTheRight */); // use normal login instead
559 }
560 if (loader.item) {
561 var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
562 loader.item.showErrorMessage(msg);
563 }
564 }
566 Component.onCompleted: restartOperation()
567 Component.onDestruction: cancelOperation()
568 onIdEnabledChanged: restartOperation()
570 onSucceeded: {
571 if (!d.secureFingerprint) {
572 failOperation("fingerprint reader is locked");
573 return;
574 }
575 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
576 AccountsService.failedFingerprintLogins++;
577 failOperation("not the selected user");
578 return;
579 }
580 console.log("Identified user by fingerprint:", result);
581 if (loader.item)
582 loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
583 if (root.active)
584 root.forcedUnlock = true;
585 }
586 onFailed: {
587 if (!d.secureFingerprint) {
588 failOperation("fingerprint reader is locked");
589 } else {
590 AccountsService.failedFingerprintLogins++;
591 failOperation(reason);
592 }
593 }
594 }