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