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