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