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