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 
18 import QtQuick 2.12
19 import AccountsService 0.1
20 import Biometryd 0.0
21 import GSettings 1.0
22 import Powerd 0.1
23 import Lomiri.Components 1.3
24 import Lomiri.Launcher 0.1
25 import Lomiri.Session 0.1
26 
27 import "." 0.1
28 import ".." 0.1
29 import "../Components"
30 
31 Showable {
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 }