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