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