on xenial help sign in
Fix the fact that a user that is locked out from their account for five minutes after entering too many wrong passwords can simply reboot to try again.
37f734dMichael Terry
data/com.canonical.Unity8.gschema.xml+8
@@ -12,4 +12,12 @@
12 <description>The usage mode chosen will affect the Window Management behaviour.</description>
12 <description>The usage mode chosen will affect the Window Management behaviour.</description>
13 </key>
13 </key>
14 </schema>
14 </schema>
15+
16+ <schema path="/com/canonical/unity8/greeter/" id="com.canonical.Unity8.Greeter" gettext-domain="unity8">
17+ <key type="x" name="locked-out-time">
18+ <default>0</default>
19+ <summary>When the greeter was last locked.</summary>
20+ <description>If too many unsuccessful login attempts have been made in a row, the greeter locks you out for a while. This unix timestamp indicates when you were locked out, so unity8 knows when to let you back in.</description>
21+ </key>
22+ </schema>
15</schemalist>
23</schemalist>
qml/Greeter/Greeter.qml+48−9
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.3
17import QtQuick 2.3
18import AccountsService 0.1
18import AccountsService 0.1
19+import GSettings 1.0
19import Ubuntu.Components 1.1
20import Ubuntu.Components 1.1
20import Ubuntu.SystemImage 0.1
21import Ubuntu.SystemImage 0.1
21import Unity.Launcher 0.1
22import Unity.Launcher 0.1
@@ -181,6 +182,11 @@ Showable {
181 }
182 }
182 }
183 }
183
184
185+ GSettings {
186+ id: greeterSettings
187+ schema.id: "com.canonical.Unity8.Greeter"
188+ }
189+
184 Timer {
190 Timer {
185 id: forcedDelayTimer
191 id: forcedDelayTimer
186
192
@@ -189,14 +195,24 @@ Showable {
189 // for a few minutes. When we wake up, we want to quickly be correct.
195 // for a few minutes. When we wake up, we want to quickly be correct.
190 interval: 500
196 interval: 500
191
197
192− property var delayTarget;
198+ property var delayTarget
193− property int delayMinutes;
199+ property int delayMinutes
194−
200+
195− function forceDelay(delay /* in minutes */) {
201+ function forceDelay() {
196− delayTarget = new Date();
202+ // Store the beginning time for a lockout in GSettings, so that
197− delayTarget.setTime(delayTarget.getTime() + delay * 60000);
203+ // we still lock the user out if they reboot. And we store
198− delayMinutes = Math.ceil(delay);
204+ // starting time rather than end-time or how-long because:
199− start();
205+ // - If storing end-time and on boot we have a problem with NTP,
206+ // we might get locked out for a lot longer than we thought.
207+ // - If storing how-long, and user turns their phone off for an
208+ // hour rather than wait, they wouldn't expect to still be locked
209+ // out.
210+ // - A malicious actor could manipulate either of the above
211+ // settings to keep the user out longer. But by storing
212+ // start-time, we never make the user wait longer than the full
213+ // lock out time.
214+ greeterSettings.lockedOutTime = new Date().getTime();
215+ checkForForcedDelay();
200 }
216 }
201
217
202 onTriggered: {
218 onTriggered: {
@@ -208,6 +224,29 @@ Showable {
208 delayMinutes = 0;
224 delayMinutes = 0;
209 }
225 }
210 }
226 }
227+
228+ function checkForForcedDelay() {
229+ var now = new Date();
230+ delayTarget = now;
231+ delayTarget.setTime(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
232+
233+ // If tooEarly is true, something went very wrong. Bug or NTP
234+ // misconfiguration maybe?
235+ var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
236+ var tooLate = now > delayTarget;
237+
238+ // Compare stored time to system time. If a malicious actor is
239+ // able to manipulate time to avoid our lockout, they already have
240+ // enough access to cause damage. So we choose to trust this check.
241+ if (tooEarly || tooLate) {
242+ stop();
243+ delayMinutes = 0;
244+ } else {
245+ triggered();
246+ }
247+ }
248+
249+ Component.onCompleted: checkForForcedDelay()
211 }
250 }
212
251
213 // event eater
252 // event eater
@@ -380,7 +419,7 @@ Showable {
380 if (failedLoginsDelayAttempts > 0
419 if (failedLoginsDelayAttempts > 0
381 && AccountsService.failedLogins > 0
420 && AccountsService.failedLogins > 0
382 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
421 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
383− forcedDelayTimer.forceDelay(failedLoginsDelayMinutes);
422+ forcedDelayTimer.forceDelay();
384 }
423 }
385
424
386 loader.item.notifyAuthenticationFailed();
425 loader.item.notifyAuthenticationFailed();
tests/mocks/GSettings.1.0/fake_gsettings.cpp+31
@@ -62,6 +62,19 @@ void GSettingsControllerQml::setUsageMode(const QString &usageMode)
62 }
62 }
63}
63}
64
64
65+qint64 GSettingsControllerQml::lockedOutTime() const
66+{
67+ return m_lockedOutTime;
68+}
69+
70+void GSettingsControllerQml::setLockedOutTime(qint64 timestamp)
71+{
72+ if (m_lockedOutTime != timestamp) {
73+ m_lockedOutTime = timestamp;
74+ Q_EMIT lockedOutTimeChanged(m_lockedOutTime);
75+ }
76+}
77+
65GSettingsSchemaQml::GSettingsSchemaQml(QObject *parent): QObject(parent) {
78GSettingsSchemaQml::GSettingsSchemaQml(QObject *parent): QObject(parent) {
66}
79}
67
80
@@ -99,6 +112,8 @@ GSettingsQml::GSettingsQml(QObject *parent)
99 this, &GSettingsQml::pictureUriChanged);
112 this, &GSettingsQml::pictureUriChanged);
100 connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::usageModeChanged,
113 connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::usageModeChanged,
101 this, &GSettingsQml::usageModeChanged);
114 this, &GSettingsQml::usageModeChanged);
115+ connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::lockedOutTimeChanged,
116+ this, &GSettingsQml::lockedOutTimeChanged);
102}
117}
103
118
104GSettingsSchemaQml * GSettingsQml::schema() const {
119GSettingsSchemaQml * GSettingsQml::schema() const {
@@ -136,3 +151,19 @@ void GSettingsQml::setUsageMode(const QString &usageMode)
136 GSettingsControllerQml::instance()->setUsageMode(usageMode);
151 GSettingsControllerQml::instance()->setUsageMode(usageMode);
137 }
152 }
138}
153}
154+
155+qint64 GSettingsQml::lockedOutTime() const
156+{
157+ if (m_schema->id() == "com.canonical.Unity8.Greeter") {
158+ return GSettingsControllerQml::instance()->lockedOutTime();
159+ } else {
160+ return 0;
161+ }
162+}
163+
164+void GSettingsQml::setLockedOutTime(qint64 timestamp)
165+{
166+ if (m_schema->id() == "com.canonical.Unity8.Greeter") {
167+ GSettingsControllerQml::instance()->setLockedOutTime(timestamp);
168+ }
169+}
tests/mocks/GSettings.1.0/fake_gsettings.h+9
@@ -47,6 +47,7 @@ class GSettingsQml: public QObject
47 Q_PROPERTY(GSettingsSchemaQml* schema READ schema NOTIFY schemaChanged)
47 Q_PROPERTY(GSettingsSchemaQml* schema READ schema NOTIFY schemaChanged)
48 Q_PROPERTY(QString pictureUri READ pictureUri WRITE setPictureUri NOTIFY pictureUriChanged)
48 Q_PROPERTY(QString pictureUri READ pictureUri WRITE setPictureUri NOTIFY pictureUriChanged)
49 Q_PROPERTY(QString usageMode READ usageMode WRITE setUsageMode NOTIFY usageModeChanged)
49 Q_PROPERTY(QString usageMode READ usageMode WRITE setUsageMode NOTIFY usageModeChanged)
50+ Q_PROPERTY(qint64 lockedOutTime READ lockedOutTime WRITE setLockedOutTime NOTIFY lockedOutTimeChanged)
50
51
51public:
52public:
52 GSettingsQml(QObject *parent = nullptr);
53 GSettingsQml(QObject *parent = nullptr);
@@ -54,14 +55,17 @@ public:
54 GSettingsSchemaQml * schema() const;
55 GSettingsSchemaQml * schema() const;
55 QString pictureUri() const;
56 QString pictureUri() const;
56 QString usageMode() const;
57 QString usageMode() const;
58+ qint64 lockedOutTime() const;
57
59
58 void setPictureUri(const QString &str);
60 void setPictureUri(const QString &str);
59 void setUsageMode(const QString &usageMode);
61 void setUsageMode(const QString &usageMode);
62+ void setLockedOutTime(qint64 timestamp);
60
63
61Q_SIGNALS:
64Q_SIGNALS:
62 void schemaChanged();
65 void schemaChanged();
63 void pictureUriChanged(const QString&);
66 void pictureUriChanged(const QString&);
64 void usageModeChanged(const QString&);
67 void usageModeChanged(const QString&);
68+ void lockedOutTimeChanged(qint64);
65
69
66private:
70private:
67 GSettingsSchemaQml* m_schema;
71 GSettingsSchemaQml* m_schema;
@@ -83,15 +87,20 @@ public:
83 QString usageMode() const;
87 QString usageMode() const;
84 Q_INVOKABLE void setUsageMode(const QString &usageMode);
88 Q_INVOKABLE void setUsageMode(const QString &usageMode);
85
89
90+ qint64 lockedOutTime() const;
91+ Q_INVOKABLE void setLockedOutTime(qint64 timestamp);
92+
86Q_SIGNALS:
93Q_SIGNALS:
87 void pictureUriChanged(const QString&);
94 void pictureUriChanged(const QString&);
88 void usageModeChanged(const QString&);
95 void usageModeChanged(const QString&);
96+ void lockedOutTimeChanged(qint64 timestamp);
89
97
90private:
98private:
91 GSettingsControllerQml();
99 GSettingsControllerQml();
92
100
93 QString m_pictureUri;
101 QString m_pictureUri;
94 QString m_usageMode;
102 QString m_usageMode;
103+ qint64 m_lockedOutTime;
95
104
96 static GSettingsControllerQml* s_controllerInstance;
105 static GSettingsControllerQml* s_controllerInstance;
97 QList<GSettingsQml *> m_registeredGSettings;
106 QList<GSettingsQml *> m_registeredGSettings;
tests/qmltests/Greeter/tst_Greeter.qml+30−3
@@ -20,6 +20,7 @@ import ".."
20import "../../../qml/Greeter"
20import "../../../qml/Greeter"
21import Ubuntu.Components 0.1
21import Ubuntu.Components 0.1
22import AccountsService 0.1
22import AccountsService 0.1
23+import GSettings 1.0
23import IntegratedLightDM 0.1 as LightDM
24import IntegratedLightDM 0.1 as LightDM
24import Unity.Test 0.1 as UT
25import Unity.Test 0.1 as UT
25
26
@@ -75,6 +76,11 @@ Item {
75 signalName: "emergencyCall"
76 signalName: "emergencyCall"
76 }
77 }
77
78
79+ GSettings {
80+ id: greeterSettings
81+ schema.id: "com.canonical.Unity8.Greeter"
82+ }
83+
78 UT.UnityTestCase {
84 UT.UnityTestCase {
79 id: testCase
85 id: testCase
80 name: "Greeter"
86 name: "Greeter"
@@ -132,6 +138,8 @@ Item {
132 }
138 }
133
139
134 function init() {
140 function init() {
141+ greeterSettings.lockedOutTime = 0;
142+ resetLoader();
135 teaseSpy.clear();
143 teaseSpy.clear();
136 sessionStartedSpy.clear();
144 sessionStartedSpy.clear();
137 emergencyCallSpy.clear();
145 emergencyCallSpy.clear();
@@ -150,7 +158,7 @@ Item {
150 greeter.failedLoginsDelayMinutes = 5;
158 greeter.failedLoginsDelayMinutes = 5;
151 }
159 }
152
160
153− function cleanup() {
161+ function resetLoader() {
154 loader.itemDestroyed = false;
162 loader.itemDestroyed = false;
155 loader.active = false;
163 loader.active = false;
156 tryCompare(loader, "status", Loader.Null);
164 tryCompare(loader, "status", Loader.Null);
@@ -431,7 +439,26 @@ Item {
431 compare(LightDM.Infographic.username, "has-password");
439 compare(LightDM.Infographic.username, "has-password");
432 }
440 }
433
441
434− function test_forcedDelay() {
442+ function test_forcedDelayIntoGSettings() {
443+ greeter.failedLoginsDelayAttempts = 1;
444+ greeter.failedLoginsDelayMinutes = 1;
445+ selectUser("has-password");
446+ tryCompare(viewShowPromptSpy, "count", 1);
447+ compare(greeterSettings.lockedOutTime, 0);
448+ view.responded("wr0ng p4ssw0rd");
449+
450+ var timestamp = new Date().getTime();
451+ verify(Math.abs(greeterSettings.lockedOutTime - timestamp) < 2000);
452+ }
453+
454+ function test_forcedDelayOnConstruction() {
455+ greeterSettings.lockedOutTime = new Date().getTime();
456+ resetLoader();
457+ view = findChild(greeter, "testView");
458+ compare(view.delayMinutes, greeter.failedLoginsDelayMinutes);
459+ }
460+
461+ function test_forcedDelayRoundTrip() {
435 greeter.failedLoginsDelayAttempts = 1;
462 greeter.failedLoginsDelayAttempts = 1;
436 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
463 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
437
464
@@ -440,7 +467,7 @@ Item {
440
467
441 compare(view.delayMinutes, 0);
468 compare(view.delayMinutes, 0);
442 view.responded("wr0ng p4ssw0rd");
469 view.responded("wr0ng p4ssw0rd");
443− tryCompare(view, "delayMinutes", 1);
470+ compare(view.delayMinutes, 1);
444 tryCompare(view, "delayMinutes", 0);
471 tryCompare(view, "delayMinutes", 0);
445 }
472 }
446
473