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  return LightDMService.greeter.defaultSession;
149  }
150 
151  QtObject {
152  id: d
153 
154  readonly property bool multiUser: LightDMService.users.count > 1
155  readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
156  property int currentIndex: Math.max(selectUserIndex, 0)
157  property bool waiting
158  property bool isLockscreen // true when we are locking an active session, rather than first user login
159  readonly property bool secureFingerprint: isLockscreen &&
160  AccountsService.failedFingerprintLogins <
161  root.failedFingerprintLoginsDisableAttempts
162  readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
163 
164  // We want 'launcherOffset' to animate down to zero. But not to animate
165  // while being dragged. So ideally we change this only when the user
166  // lets go and launcherOffset drops to zero. But we need to wait for
167  // the behavior to be enabled first. So we cache the last known good
168  // launcherOffset value to cover us during that brief gap between
169  // release and the behavior turning on.
170  property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
171  property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
172  Behavior on launcherOffsetProxy {
173  id: launcherOffsetProxyBehavior
174  enabled: launcherOffset === 0
175  UbuntuNumberAnimation {}
176  }
177 
178  function getUserIndex(username) {
179  if (username === "")
180  return -1;
181 
182  // Find index for requested user, if it exists
183  for (var i = 0; i < LightDMService.users.count; i++) {
184  if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
185  return i;
186  }
187  }
188 
189  return -1;
190  }
191 
192  function selectUser(index, reset) {
193  if (index < 0 || index >= LightDMService.users.count)
194  return;
195  d.waiting = true;
196  if (reset) {
197  loader.item.reset(false /* forceShow */);
198  }
199  currentIndex = index;
200  var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
201  AccountsService.user = user;
202  LauncherModel.setUser(user);
203  LightDMService.greeter.authenticate(user); // always resets auth state
204  }
205 
206  function hideView() {
207  if (loader.item) {
208  loader.item.enabled = false; // drop OSK and prevent interaction
209  loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
210  loader.item.hide();
211  }
212  }
213 
214  function login() {
215  d.waiting = true;
216  if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
217  sessionStarted();
218  hideView();
219  } else if (loader.item) {
220  loader.item.notifyAuthenticationFailed();
221  }
222  d.waiting = false;
223  }
224 
225  function startUnlock(toTheRight) {
226  if (loader.item) {
227  return loader.item.tryToUnlock(toTheRight);
228  } else {
229  return false;
230  }
231  }
232 
233  function checkForcedUnlock(hideNow) {
234  if (forcedUnlock && shown) {
235  hideView();
236  if (hideNow) {
237  root.hideNow(); // skip hide animation
238  }
239  }
240  }
241 
242  function showPromptMessage(text, isError) {
243  // inefficient, but we only rarely deal with messages
244  var html = text.replace(/&/g, "&amp;")
245  .replace(/</g, "&lt;")
246  .replace(/>/g, "&gt;")
247  .replace(/\n/g, "<br>");
248  if (isError) {
249  html = "<font color=\"#df382c\">" + html + "</font>";
250  }
251 
252  if (loader.item) {
253  loader.item.showMessage(html);
254  }
255  }
256 
257  function showFingerprintMessage(msg) {
258  if (loader.item) {
259  loader.item.reset(false /* forceShow */);
260  loader.item.showErrorMessage(msg);
261  }
262  showPromptMessage(msg, true);
263  }
264  }
265 
266  onLauncherOffsetChanged: {
267  if (launcherOffset > 0) {
268  d.lastKnownPositiveOffset = launcherOffset;
269  }
270  }
271 
272  onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
273  Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
274 
275  onLockedChanged: {
276  if (!locked) {
277  AccountsService.failedLogins = 0;
278  AccountsService.failedFingerprintLogins = 0;
279 
280  // Stop delay timer if they logged in with fingerprint
281  forcedDelayTimer.stop();
282  forcedDelayTimer.delayMinutes = 0;
283  }
284  }
285 
286  onRequiredChanged: {
287  if (required) {
288  d.waiting = true;
289  lockedApp = "";
290  }
291  }
292 
293  GSettings {
294  id: greeterSettings
295  schema.id: "com.canonical.Unity8.Greeter"
296  }
297 
298  Timer {
299  id: forcedDelayTimer
300 
301  // We use a short interval and check against the system wall clock
302  // because we have to consider the case that the system is suspended
303  // for a few minutes. When we wake up, we want to quickly be correct.
304  interval: 500
305 
306  property var delayTarget
307  property int delayMinutes
308 
309  function forceDelay() {
310  // Store the beginning time for a lockout in GSettings, so that
311  // we still lock the user out if they reboot. And we store
312  // starting time rather than end-time or how-long because:
313  // - If storing end-time and on boot we have a problem with NTP,
314  // we might get locked out for a lot longer than we thought.
315  // - If storing how-long, and user turns their phone off for an
316  // hour rather than wait, they wouldn't expect to still be locked
317  // out.
318  // - A malicious actor could manipulate either of the above
319  // settings to keep the user out longer. But by storing
320  // start-time, we never make the user wait longer than the full
321  // lock out time.
322  greeterSettings.lockedOutTime = new Date().getTime();
323  checkForForcedDelay();
324  }
325 
326  onTriggered: {
327  var diff = delayTarget - new Date();
328  if (diff > 0) {
329  delayMinutes = Math.ceil(diff / 60000);
330  start(); // go again
331  } else {
332  delayMinutes = 0;
333  }
334  }
335 
336  function checkForForcedDelay() {
337  if (greeterSettings.lockedOutTime === 0) {
338  return;
339  }
340 
341  var now = new Date();
342  delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
343 
344  // If tooEarly is true, something went very wrong. Bug or NTP
345  // misconfiguration maybe?
346  var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
347  var tooLate = now >= delayTarget;
348 
349  // Compare stored time to system time. If a malicious actor is
350  // able to manipulate time to avoid our lockout, they already have
351  // enough access to cause damage. So we choose to trust this check.
352  if (tooEarly || tooLate) {
353  stop();
354  delayMinutes = 0;
355  } else {
356  triggered();
357  }
358  }
359 
360  Component.onCompleted: checkForForcedDelay()
361  }
362 
363  // event eater
364  // Nothing should leak to items behind the greeter
365  MouseArea { anchors.fill: parent; hoverEnabled: true }
366 
367  Loader {
368  id: loader
369  objectName: "loader"
370 
371  anchors.fill: parent
372 
373  active: root.required
374  source: root.viewSource.toString() ? root.viewSource :
375  (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
376 
377  onLoaded: {
378  root.lockedApp = "";
379  item.forceActiveFocus();
380  d.selectUser(d.currentIndex, true);
381  LightDMService.infographic.readyForDataChange();
382  }
383 
384  Connections {
385  target: loader.item
386  onSelected: {
387  d.selectUser(index, true);
388  }
389  onResponded: {
390  if (root.locked) {
391  LightDMService.greeter.respond(response);
392  } else {
393  d.login();
394  }
395  }
396  onTease: root.tease()
397  onEmergencyCall: root.emergencyCall()
398  onRequiredChanged: {
399  if (!loader.item.required) {
400  root.hide();
401  }
402  }
403  }
404 
405  Binding {
406  target: loader.item
407  property: "backgroundTopMargin"
408  value: -root.y
409  }
410 
411  Binding {
412  target: loader.item
413  property: "launcherOffset"
414  value: d.launcherOffsetProxy
415  }
416 
417  Binding {
418  target: loader.item
419  property: "dragHandleLeftMargin"
420  value: root.dragHandleLeftMargin
421  }
422 
423  Binding {
424  target: loader.item
425  property: "delayMinutes"
426  value: forcedDelayTimer.delayMinutes
427  }
428 
429  Binding {
430  target: loader.item
431  property: "background"
432  value: root.background
433  }
434 
435  Binding {
436  target: loader.item
437  property: "hasCustomBackground"
438  value: root.hasCustomBackground
439  }
440 
441  Binding {
442  target: loader.item
443  property: "locked"
444  value: root.locked
445  }
446 
447  Binding {
448  target: loader.item
449  property: "waiting"
450  value: d.waiting
451  }
452 
453  Binding {
454  target: loader.item
455  property: "alphanumeric"
456  value: d.alphanumeric
457  }
458 
459  Binding {
460  target: loader.item
461  property: "currentIndex"
462  value: d.currentIndex
463  }
464 
465  Binding {
466  target: loader.item
467  property: "userModel"
468  value: LightDMService.users
469  }
470 
471  Binding {
472  target: loader.item
473  property: "infographicModel"
474  value: LightDMService.infographic
475  }
476  }
477 
478  Connections {
479  target: LightDMService.greeter
480 
481  onShowGreeter: root.forceShow()
482  onHideGreeter: root.forcedUnlock = true
483 
484  onShowMessage: d.showPromptMessage(text, isError)
485 
486  onShowPrompt: {
487  if (loader.item) {
488  loader.item.showPrompt(text, isSecret, isDefaultPrompt);
489  }
490 
491  d.waiting = false;
492  }
493 
494  onAuthenticationComplete: {
495  d.waiting = false;
496 
497  if (LightDMService.greeter.authenticated) {
498  if (!LightDMService.greeter.promptless) {
499  d.login();
500  }
501  } else {
502  if (!LightDMService.greeter.promptless) {
503  AccountsService.failedLogins++;
504  }
505 
506  // Check if we should initiate a factory reset
507  if (maxFailedLogins >= 2) { // require at least a warning
508  if (AccountsService.failedLogins === maxFailedLogins - 1) {
509  loader.item.showLastChance();
510  } else if (AccountsService.failedLogins >= maxFailedLogins) {
511  SystemImage.factoryReset(); // Ouch!
512  }
513  }
514 
515  // Check if we should initiate a forced login delay
516  if (failedLoginsDelayAttempts > 0
517  && AccountsService.failedLogins > 0
518  && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
519  forcedDelayTimer.forceDelay();
520  }
521 
522  loader.item.notifyAuthenticationFailed();
523  if (!LightDMService.greeter.promptless) {
524  d.selectUser(d.currentIndex, false);
525  }
526  }
527  }
528 
529  onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
530  }
531 
532  Connections {
533  target: DBusUnitySessionService
534  onLockRequested: root.forceShow()
535  onUnlocked: {
536  root.forcedUnlock = true;
537  root.hideNow();
538  }
539  }
540 
541  Binding {
542  target: LightDMService.greeter
543  property: "active"
544  value: root.active
545  }
546 
547  Binding {
548  target: LightDMService.infographic
549  property: "username"
550  value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
551  }
552 
553  Connections {
554  target: i18n
555  onLanguageChanged: LightDMService.infographic.readyForDataChange()
556  }
557 
558  Observer {
559  id: biometryd
560  objectName: "biometryd"
561 
562  property var operation: null
563  readonly property bool idEnabled: root.active &&
564  root.allowFingerprint &&
565  Powerd.status === Powerd.On &&
566  Biometryd.available &&
567  AccountsService.enableFingerprintIdentification
568 
569  function cancelOperation() {
570  if (operation) {
571  operation.cancel();
572  operation = null;
573  }
574  }
575 
576  function restartOperation() {
577  cancelOperation();
578 
579  if (idEnabled) {
580  var identifier = Biometryd.defaultDevice.identifier;
581  operation = identifier.identifyUser();
582  operation.start(biometryd);
583  }
584  }
585 
586  function failOperation(reason) {
587  console.log("Failed to identify user by fingerprint:", reason);
588  restartOperation();
589  if (!d.secureFingerprint) {
590  d.startUnlock(false /* toTheRight */); // use normal login instead
591  }
592  var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
593  d.showFingerprintMessage(msg);
594  }
595 
596  Component.onCompleted: restartOperation()
597  Component.onDestruction: cancelOperation()
598  onIdEnabledChanged: restartOperation()
599 
600  onSucceeded: {
601  if (!d.secureFingerprint) {
602  failOperation("fingerprint reader is locked");
603  return;
604  }
605  if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
606  AccountsService.failedFingerprintLogins++;
607  failOperation("not the selected user");
608  return;
609  }
610  console.log("Identified user by fingerprint:", result);
611  if (loader.item) {
612  loader.item.enabled = false;
613  loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
614  }
615  if (root.active)
616  root.forcedUnlock = true;
617  }
618  onFailed: {
619  if (!d.secureFingerprint) {
620  failOperation("fingerprint reader is locked");
621  } else {
622  AccountsService.failedFingerprintLogins++;
623  failOperation(reason);
624  }
625  }
626  }
627 }