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.

This records when the user was locked out on disk, so that after a reboot, we can notice the user is still locked out. Fixes: #1383086 Approved by: Albert Astals Cid, Michael Zanetti

commit37f734d74211fbcc57cc4a356d043e93aff15329 authorMichael Terry <mike@mterry.name> dateMon 7 Sep 2015, 14:04 (11 years ago) parentebc72d8d97d06e
5 files changed+126−12
data/com.canonical.Unity8.gschema.xml+8 qml/Greeter/Greeter.qml+48−9 tests/mocks/GSettings.1.0/fake_gsettings.cpp+31 tests/mocks/GSettings.1.0/fake_gsettings.h+9 tests/qmltests/Greeter/tst_Greeter.qml+30−3
data/com.canonical.Unity8.gschema.xml+8
@@ -12,4 +12,12 @@
1212 <description>The usage mode chosen will affect the Window Management behaviour.</description>
1313 </key>
1414 </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>
1523</schemalist>
qml/Greeter/Greeter.qml+48−9
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.3
1818import AccountsService 0.1
19+import GSettings 1.0
1920import Ubuntu.Components 1.1
2021import Ubuntu.SystemImage 0.1
2122import Unity.Launcher 0.1
@@ -181,6 +182,11 @@ Showable {
181182 }
182183 }
183184
185+ GSettings {
186+ id: greeterSettings
187+ schema.id: "com.canonical.Unity8.Greeter"
188+ }
189+
184190 Timer {
185191 id: forcedDelayTimer
186192
@@ -189,14 +195,24 @@ Showable {
189195 // for a few minutes. When we wake up, we want to quickly be correct.
190196 interval: 500
191197
192− property var delayTarget;
193− property int delayMinutes;
194−
195− function forceDelay(delay /* in minutes */) {
196− delayTarget = new Date();
197− delayTarget.setTime(delayTarget.getTime() + delay * 60000);
198− delayMinutes = Math.ceil(delay);
199− start();
198+ property var delayTarget
199+ property int delayMinutes
200+
201+ function forceDelay() {
202+ // Store the beginning time for a lockout in GSettings, so that
203+ // we still lock the user out if they reboot. And we store
204+ // starting time rather than end-time or how-long because:
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();
200216 }
201217
202218 onTriggered: {
@@ -208,6 +224,29 @@ Showable {
208224 delayMinutes = 0;
209225 }
210226 }
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()
211250 }
212251
213252 // event eater
@@ -380,7 +419,7 @@ Showable {
380419 if (failedLoginsDelayAttempts > 0
381420 && AccountsService.failedLogins > 0
382421 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
383− forcedDelayTimer.forceDelay(failedLoginsDelayMinutes);
422+ forcedDelayTimer.forceDelay();
384423 }
385424
386425 loader.item.notifyAuthenticationFailed();
tests/mocks/GSettings.1.0/fake_gsettings.cpp+31
@@ -62,6 +62,19 @@ void GSettingsControllerQml::setUsageMode(const QString &usageMode)
6262 }
6363}
6464
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+
6578GSettingsSchemaQml::GSettingsSchemaQml(QObject *parent): QObject(parent) {
6679}
6780
@@ -99,6 +112,8 @@ GSettingsQml::GSettingsQml(QObject *parent)
99112 this, &GSettingsQml::pictureUriChanged);
100113 connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::usageModeChanged,
101114 this, &GSettingsQml::usageModeChanged);
115+ connect(GSettingsControllerQml::instance(), &GSettingsControllerQml::lockedOutTimeChanged,
116+ this, &GSettingsQml::lockedOutTimeChanged);
102117}
103118
104119GSettingsSchemaQml * GSettingsQml::schema() const {
@@ -136,3 +151,19 @@ void GSettingsQml::setUsageMode(const QString &usageMode)
136151 GSettingsControllerQml::instance()->setUsageMode(usageMode);
137152 }
138153}
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
4747 Q_PROPERTY(GSettingsSchemaQml* schema READ schema NOTIFY schemaChanged)
4848 Q_PROPERTY(QString pictureUri READ pictureUri WRITE setPictureUri NOTIFY pictureUriChanged)
4949 Q_PROPERTY(QString usageMode READ usageMode WRITE setUsageMode NOTIFY usageModeChanged)
50+ Q_PROPERTY(qint64 lockedOutTime READ lockedOutTime WRITE setLockedOutTime NOTIFY lockedOutTimeChanged)
5051
5152public:
5253 GSettingsQml(QObject *parent = nullptr);
@@ -54,14 +55,17 @@ public:
5455 GSettingsSchemaQml * schema() const;
5556 QString pictureUri() const;
5657 QString usageMode() const;
58+ qint64 lockedOutTime() const;
5759
5860 void setPictureUri(const QString &str);
5961 void setUsageMode(const QString &usageMode);
62+ void setLockedOutTime(qint64 timestamp);
6063
6164Q_SIGNALS:
6265 void schemaChanged();
6366 void pictureUriChanged(const QString&);
6467 void usageModeChanged(const QString&);
68+ void lockedOutTimeChanged(qint64);
6569
6670private:
6771 GSettingsSchemaQml* m_schema;
@@ -83,15 +87,20 @@ public:
8387 QString usageMode() const;
8488 Q_INVOKABLE void setUsageMode(const QString &usageMode);
8589
90+ qint64 lockedOutTime() const;
91+ Q_INVOKABLE void setLockedOutTime(qint64 timestamp);
92+
8693Q_SIGNALS:
8794 void pictureUriChanged(const QString&);
8895 void usageModeChanged(const QString&);
96+ void lockedOutTimeChanged(qint64 timestamp);
8997
9098private:
9199 GSettingsControllerQml();
92100
93101 QString m_pictureUri;
94102 QString m_usageMode;
103+ qint64 m_lockedOutTime;
95104
96105 static GSettingsControllerQml* s_controllerInstance;
97106 QList<GSettingsQml *> m_registeredGSettings;
tests/qmltests/Greeter/tst_Greeter.qml+30−3
@@ -20,6 +20,7 @@ import ".."
2020import "../../../qml/Greeter"
2121import Ubuntu.Components 0.1
2222import AccountsService 0.1
23+import GSettings 1.0
2324import IntegratedLightDM 0.1 as LightDM
2425import Unity.Test 0.1 as UT
2526
@@ -75,6 +76,11 @@ Item {
7576 signalName: "emergencyCall"
7677 }
7778
79+ GSettings {
80+ id: greeterSettings
81+ schema.id: "com.canonical.Unity8.Greeter"
82+ }
83+
7884 UT.UnityTestCase {
7985 id: testCase
8086 name: "Greeter"
@@ -132,6 +138,8 @@ Item {
132138 }
133139
134140 function init() {
141+ greeterSettings.lockedOutTime = 0;
142+ resetLoader();
135143 teaseSpy.clear();
136144 sessionStartedSpy.clear();
137145 emergencyCallSpy.clear();
@@ -150,7 +158,7 @@ Item {
150158 greeter.failedLoginsDelayMinutes = 5;
151159 }
152160
153− function cleanup() {
161+ function resetLoader() {
154162 loader.itemDestroyed = false;
155163 loader.active = false;
156164 tryCompare(loader, "status", Loader.Null);
@@ -431,7 +439,26 @@ Item {
431439 compare(LightDM.Infographic.username, "has-password");
432440 }
433441
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() {
435462 greeter.failedLoginsDelayAttempts = 1;
436463 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
437464
@@ -440,7 +467,7 @@ Item {
440467
441468 compare(view.delayMinutes, 0);
442469 view.responded("wr0ng p4ssw0rd");
443− tryCompare(view, "delayMinutes", 1);
470+ compare(view.delayMinutes, 1);
444471 tryCompare(view, "delayMinutes", 0);
445472 }
446473