on xenial help sign in
Rejigger stored timestamp to be start of lock-out
a7220cfMichael Terry
data/com.canonical.Unity8.gschema.xml+3−3
@@ -14,10 +14,10 @@
14 </schema>
14 </schema>
15
15
16 <schema path="/com/canonical/unity8/greeter/" id="com.canonical.Unity8.Greeter" gettext-domain="unity8">
16 <schema path="/com/canonical/unity8/greeter/" id="com.canonical.Unity8.Greeter" gettext-domain="unity8">
17− <key type="t" name="locked-out-until">
17+ <key type="t" name="locked-out-time">
18 <default>0</default>
18 <default>0</default>
19− <summary>When the greeter will no longer be locked.</summary>
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 will be able to log back in.</description>
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>
21 </key>
22 </schema>
22 </schema>
23</schemalist>
23</schemalist>
qml/Greeter/Greeter.qml+35−13
@@ -196,13 +196,25 @@ Showable {
196 // for a few minutes. When we wake up, we want to quickly be correct.
196 // for a few minutes. When we wake up, we want to quickly be correct.
197 interval: 500
197 interval: 500
198
198
199− property var delayTarget;
199+ property Date delayTarget;
200 property int delayMinutes;
200 property int delayMinutes;
201
201
202− function forceDelay(delay /* in minutes */) {
202+ function forceDelay() {
203− var when = new Date().getTime() + delay * 60000;
203+ // Store the beginning time for a lockout in GSettings, so that
204− greeterSettings.lockedOutUntil = when;
204+ // we still lock the user out if they reboot. And we store
205+ // starting time rather than end-time or how-long because:
206+ // - If storing end-time and on boot we have a problem with NTP,
207+ // we might get locked out for a lot longer than we thought.
208+ // - If storing how-long, and user turns their phone off for an
209+ // hour rather than wait, they wouldn't expect to still be locked
210+ // out.
211+ // - A malicious actor could manipulate either of the above
212+ // settings to keep the user out longer. But by storing
213+ // start-time, we never make the user wait longer than the full
214+ // lock out time.
205− // code below that watches GSettings will pick up the change
215+ // The code below that watches GSettings will pick up the change
216+ // for us.
217+ greeterSettings.lockedOutTime = new Date().getTime();
206 }
218 }
207
219
208 onTriggered: {
220 onTriggered: {
@@ -212,17 +224,27 @@ Showable {
212 start(); // go again
224 start(); // go again
213 } else {
225 } else {
214 delayMinutes = 0;
226 delayMinutes = 0;
215−
216− // clean up, to avoid accidentally locking user out if phone
217− // gets time wrong for whatever reason in the future.
218− greeterSettings.lockedOutUntil = 0;
219 }
227 }
220 }
228 }
221
229
222 function checkForForcedDelay() {
230 function checkForForcedDelay() {
223− delayTarget = new Date();
231+ var now = new Date();
224− delayTarget.setTime(greeterSettings.lockedOutUntil);
232+ delayTarget.setTime(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
233+
234+ // If tooEarly is true, something went very wrong. Bug or NTP
235+ // misconfiguration maybe?
236+ var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
237+ var tooLate = now > delayTarget;
238+
239+ // Compare stored time to system time. If a malicious actor is
240+ // able to manipulate time to avoid our lockout, they already have
241+ // enough access to cause damage. So we choose to trust this check.
242+ if (tooEarly || tooLate) {
243+ stop();
244+ delayMinutes = 0;
245+ } else {
225− triggered();
246+ triggered();
247+ }
226 }
248 }
227
249
228 Component.onCompleted: checkForForcedDelay()
250 Component.onCompleted: checkForForcedDelay()
@@ -398,7 +420,7 @@ Showable {
398 if (failedLoginsDelayAttempts > 0
420 if (failedLoginsDelayAttempts > 0
399 && AccountsService.failedLogins > 0
421 && AccountsService.failedLogins > 0
400 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
422 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
401− forcedDelayTimer.forceDelay(failedLoginsDelayMinutes);
423+ forcedDelayTimer.forceDelay();
402 }
424 }
403
425
404 loader.item.notifyAuthenticationFailed();
426 loader.item.notifyAuthenticationFailed();
tests/mocks/GSettings.1.0/fake_gsettings.cpp+12−12
@@ -62,16 +62,16 @@ void GSettingsControllerQml::setUsageMode(const QString &usageMode)
62 }
62 }
63}
63}
64
64
65−quint64 GSettingsControllerQml::lockedOutUntil() const
65+quint64 GSettingsControllerQml::lockedOutTime() const
66{
66{
67− return m_lockedOutUntil;
67+ return m_lockedOutTime;
68}
68}
69
69
70−void GSettingsControllerQml::setLockedOutUntil(quint64 timestamp)
70+void GSettingsControllerQml::setLockedOutTime(quint64 timestamp)
71{
71{
72− if (m_lockedOutUntil != timestamp) {
72+ if (m_lockedOutTime != timestamp) {
73− m_lockedOutUntil = timestamp;
73+ m_lockedOutTime = timestamp;
74− Q_EMIT lockedOutUntilChanged(m_lockedOutUntil);
74+ Q_EMIT lockedOutTimeChanged(m_lockedOutTime);
75 }
75 }
76}
76}
77
77
@@ -112,8 +112,8 @@ GSettingsQml::GSettingsQml(QObject *parent)
112 this, SIGNAL(pictureUriChanged(const QString &)));
112 this, SIGNAL(pictureUriChanged(const QString &)));
113 connect(GSettingsControllerQml::instance(), SIGNAL(usageModeChanged(const QString &)),
113 connect(GSettingsControllerQml::instance(), SIGNAL(usageModeChanged(const QString &)),
114 this, SIGNAL(usageModeChanged(const QString &)));
114 this, SIGNAL(usageModeChanged(const QString &)));
115− connect(GSettingsControllerQml::instance(), SIGNAL(lockedOutUntilChanged(quint64)),
115+ connect(GSettingsControllerQml::instance(), SIGNAL(lockedOutTimeChanged(quint64)),
116− this, SIGNAL(lockedOutUntilChanged(quint64)));
116+ this, SIGNAL(lockedOutTimeChanged(quint64)));
117}
117}
118
118
119GSettingsSchemaQml * GSettingsQml::schema() const {
119GSettingsSchemaQml * GSettingsQml::schema() const {
@@ -152,18 +152,18 @@ void GSettingsQml::setUsageMode(const QString &usageMode)
152 }
152 }
153}
153}
154
154
155−quint64 GSettingsQml::lockedOutUntil() const
155+quint64 GSettingsQml::lockedOutTime() const
156{
156{
157 if (m_schema->id() == "com.canonical.Unity8.Greeter") {
157 if (m_schema->id() == "com.canonical.Unity8.Greeter") {
158− return GSettingsControllerQml::instance()->lockedOutUntil();
158+ return GSettingsControllerQml::instance()->lockedOutTime();
159 } else {
159 } else {
160 return 0;
160 return 0;
161 }
161 }
162}
162}
163
163
164−void GSettingsQml::setLockedOutUntil(quint64 timestamp)
164+void GSettingsQml::setLockedOutTime(quint64 timestamp)
165{
165{
166 if (m_schema->id() == "com.canonical.Unity8.Greeter") {
166 if (m_schema->id() == "com.canonical.Unity8.Greeter") {
167− GSettingsControllerQml::instance()->setLockedOutUntil(timestamp);
167+ GSettingsControllerQml::instance()->setLockedOutTime(timestamp);
168 }
168 }
169}
169}
tests/mocks/GSettings.1.0/fake_gsettings.h+8−8
@@ -47,7 +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(quint64 lockedOutUntil READ lockedOutUntil WRITE setLockedOutUntil NOTIFY lockedOutUntilChanged)
50+ Q_PROPERTY(quint64 lockedOutTime READ lockedOutTime WRITE setLockedOutTime NOTIFY lockedOutTimeChanged)
51
51
52public:
52public:
53 GSettingsQml(QObject *parent = nullptr);
53 GSettingsQml(QObject *parent = nullptr);
@@ -55,17 +55,17 @@ public:
55 GSettingsSchemaQml * schema() const;
55 GSettingsSchemaQml * schema() const;
56 QString pictureUri() const;
56 QString pictureUri() const;
57 QString usageMode() const;
57 QString usageMode() const;
58− quint64 lockedOutUntil() const;
58+ quint64 lockedOutTime() const;
59
59
60 void setPictureUri(const QString &str);
60 void setPictureUri(const QString &str);
61 void setUsageMode(const QString &usageMode);
61 void setUsageMode(const QString &usageMode);
62− void setLockedOutUntil(quint64 timestamp);
62+ void setLockedOutTime(quint64 timestamp);
63
63
64Q_SIGNALS:
64Q_SIGNALS:
65 void schemaChanged();
65 void schemaChanged();
66 void pictureUriChanged(const QString&);
66 void pictureUriChanged(const QString&);
67 void usageModeChanged(const QString&);
67 void usageModeChanged(const QString&);
68− void lockedOutUntilChanged(quint64);
68+ void lockedOutTimeChanged(quint64);
69
69
70private:
70private:
71 GSettingsSchemaQml* m_schema;
71 GSettingsSchemaQml* m_schema;
@@ -87,20 +87,20 @@ public:
87 QString usageMode() const;
87 QString usageMode() const;
88 Q_INVOKABLE void setUsageMode(const QString &usageMode);
88 Q_INVOKABLE void setUsageMode(const QString &usageMode);
89
89
90− quint64 lockedOutUntil() const;
90+ quint64 lockedOutTime() const;
91− Q_INVOKABLE void setLockedOutUntil(quint64 timestamp);
91+ Q_INVOKABLE void setLockedOutTime(quint64 timestamp);
92
92
93Q_SIGNALS:
93Q_SIGNALS:
94 void pictureUriChanged(const QString&);
94 void pictureUriChanged(const QString&);
95 void usageModeChanged(const QString&);
95 void usageModeChanged(const QString&);
96− void lockedOutUntilChanged(quint64 timestamp);
96+ void lockedOutTimeChanged(quint64 timestamp);
97
97
98private:
98private:
99 GSettingsControllerQml();
99 GSettingsControllerQml();
100
100
101 QString m_pictureUri;
101 QString m_pictureUri;
102 QString m_usageMode;
102 QString m_usageMode;
103− quint64 m_lockedOutUntil;
103+ quint64 m_lockedOutTime;
104
104
105 static GSettingsControllerQml* s_controllerInstance;
105 static GSettingsControllerQml* s_controllerInstance;
106 QList<GSettingsQml *> m_registeredGSettings;
106 QList<GSettingsQml *> m_registeredGSettings;
tests/qmltests/Greeter/tst_Greeter.qml+8−8
@@ -154,7 +154,7 @@ Item {
154 verifySelected(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
154 verifySelected(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
155 greeter.failedLoginsDelayAttempts = 7;
155 greeter.failedLoginsDelayAttempts = 7;
156 greeter.failedLoginsDelayMinutes = 5;
156 greeter.failedLoginsDelayMinutes = 5;
157− greeterSettings.lockedOutUntil = 0;
157+ greeterSettings.lockedOutTime = 0;
158 }
158 }
159
159
160 function cleanup() {
160 function cleanup() {
@@ -443,22 +443,23 @@ Item {
443 greeter.failedLoginsDelayMinutes = 1;
443 greeter.failedLoginsDelayMinutes = 1;
444 selectUser("has-password");
444 selectUser("has-password");
445 tryCompare(viewShowPromptSpy, "count", 1);
445 tryCompare(viewShowPromptSpy, "count", 1);
446+ compare(greeterSettings.lockedOutTime, 0);
446 view.responded("wr0ng p4ssw0rd");
447 view.responded("wr0ng p4ssw0rd");
447
448
448− var timestamp = new Date().getTime() + 60000;
449+ var timestamp = new Date().getTime();
449− verify(Math.abs(greeterSettings.lockedOutUntil - timestamp) < 3);
450+ verify(Math.abs(greeterSettings.lockedOutTime - timestamp) < 3);
450 }
451 }
451
452
452 function test_forcedDelayFromGSettings() {
453 function test_forcedDelayFromGSettings() {
454+ greeter.failedLoginsDelayMinutes = 1;
453 compare(view.delayMinutes, 0);
455 compare(view.delayMinutes, 0);
454− var timestamp = new Date().getTime() + 30; // in 30 seconds
456+ greeterSettings.lockedOutTime = new Date().getTime();
455− greeterSettings.lockedOutUntil = timestamp;
456 compare(view.delayMinutes, 1);
457 compare(view.delayMinutes, 1);
457 }
458 }
458
459
459 function test_forcedDelayOnConstruction() {
460 function test_forcedDelayOnConstruction() {
460− var timestamp = new Date().getTime() + 30; // in 30 seconds
461+ greeter.failedLoginsDelayMinutes = 1;
461− greeterSettings.lockedOutUntil = timestamp;
462+ greeterSettings.lockedOutTime = new Date().getTime();
462 cleanup();
463 cleanup();
463 view = findChild(greeter, "testView");
464 view = findChild(greeter, "testView");
464 compare(view.delayMinutes, 1);
465 compare(view.delayMinutes, 1);
@@ -475,7 +476,6 @@ Item {
475 view.responded("wr0ng p4ssw0rd");
476 view.responded("wr0ng p4ssw0rd");
476 compare(view.delayMinutes, 1);
477 compare(view.delayMinutes, 1);
477 tryCompare(view, "delayMinutes", 0);
478 tryCompare(view, "delayMinutes", 0);
478− compare(greeterSettings.lockedOutUntil, 0);
479 }
479 }
480
480
481 function test_dbusRequestAuthenticationUser() {
481 function test_dbusRequestAuthenticationUser() {