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.4
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 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 }