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