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