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