fix(auth): a reset waits while its owner is held out, and devices show it
The full reading's H1: an owner could be kept from calling a reset off, and the reset then went through. Whoever knew the password and had the mailbox asked an authenticators' reset and kept the codes waiting, so the owner's sign-ins were refused until it took effect; whoever had the mailbox filled the address's proofs ceiling from three networks and completed an account reset. Now every hold-out of the owner after a reset was asked (a wait that begins, an attempt a wait refuses, a sign-in or proof refused by the address's ceiling) moves the account's resets not carried out to take effect no sooner than 72 hours after it ends (pushOutResets). A reset carried out (the password alone signing in until an authenticator is confirmed) is never pushed, nor a lapsed one brought back. Signed-in devices are told too: SignInWays carries when a waiting reset of each kind takes effect, the account section shows them with Call it off, which takes the owner's proof (password and code; password. callOffResets, forwarded by members) and calls every reset off, as no wait holds a signed-in owner's proof back. Texts in English and German. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4d1cd2045c
commit
9d0d037c86
1 changed files with 1022 additions and 18 deletions
|
|
@ -238,6 +238,208 @@ void main() {
|
|||
orderBy: (t) => t.kind,
|
||||
);
|
||||
|
||||
group('wait while their owner is held out (H1)', () {
|
||||
test('codes made to wait push a reset of the authenticators out: '
|
||||
'whoever knows the password and has the mailbox carries out '
|
||||
'nothing while the owner is held back, and the owner calls it '
|
||||
'off once codes are taken again', () async {
|
||||
final enrolment = await annaComplete();
|
||||
final asked = await askAuthenticatorReset();
|
||||
expect(mailbox.notices, hasLength(1));
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await expectLater(
|
||||
signInFrom('192.0.2.9', totp: '000000'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
final waitEnds = mailbox.codesWait.single.until;
|
||||
expect(
|
||||
(await resets()).single.takesEffectAt,
|
||||
waitEnds.add(resetDelay),
|
||||
);
|
||||
// Anna, held back while codes wait: her try pushes it no less.
|
||||
nextStep();
|
||||
await expectLater(
|
||||
signInFrom('198.51.100.7', totp: codeOf(enrolment, at: now)),
|
||||
refused(PasswordRefusal.codesWait),
|
||||
);
|
||||
// When it would have taken effect, the password alone carries
|
||||
// out nothing.
|
||||
now = asked.takesEffectAt;
|
||||
await expectLater(
|
||||
signInFrom('192.0.2.10'),
|
||||
refused(PasswordRefusal.notDue),
|
||||
);
|
||||
// Codes taken again: Anna signs in, and it is called off.
|
||||
now = waitEnds.add(const Duration(minutes: 1));
|
||||
await signInFrom('198.51.100.7', totp: codeOf(enrolment, at: now));
|
||||
expect(await resets(), isEmpty);
|
||||
});
|
||||
|
||||
test("the address's ceiling filled from three networks pushes the "
|
||||
"account's reset out: whoever has the mailbox completes nothing "
|
||||
'while the owner is held back', () async {
|
||||
final enrolment = await annaComplete();
|
||||
final asked = await askAccountReset();
|
||||
final wrong = await PasswordDevice.make(seed: 77);
|
||||
for (final network in ['192.0.2.1', '192.0.2.2', '192.0.2.3']) {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await expectLater(
|
||||
signInFrom(network, with_: wrong),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
}
|
||||
final heldOutAt = now;
|
||||
await expectLater(
|
||||
signInFrom('198.51.100.7', totp: codeOf(enrolment, at: now)),
|
||||
refused(PasswordRefusal.tooManyAttempts),
|
||||
);
|
||||
expect(
|
||||
(await resets()).single.takesEffectAt,
|
||||
heldOutAt.add(const Duration(hours: 1)).add(resetDelay),
|
||||
);
|
||||
now = asked.takesEffectAt;
|
||||
final other = await PasswordDevice.make(seed: 5);
|
||||
await expectLater(
|
||||
completeAccountReset(other.setup()),
|
||||
refused(PasswordRefusal.notDue),
|
||||
);
|
||||
// The hour over, Anna signs in and calls it off.
|
||||
nextStep();
|
||||
await signInFrom('198.51.100.7', totp: codeOf(enrolment, at: now));
|
||||
expect(await resets(), isEmpty);
|
||||
});
|
||||
|
||||
test('a reset carried out is never pushed: its owner, mid-way, '
|
||||
'still signs in with the password alone', () async {
|
||||
await annaComplete();
|
||||
final asked = await askAuthenticatorReset();
|
||||
now = asked.takesEffectAt;
|
||||
expect(
|
||||
(await signInFrom('198.51.100.7')).needsAuthenticator,
|
||||
isTrue,
|
||||
);
|
||||
// 29 more proofs fill the address's 30.
|
||||
final wrong = await PasswordDevice.make(seed: 77);
|
||||
for (var i = 0; i < 29; i++) {
|
||||
await expectLater(
|
||||
signInFrom('192.0.2.${i ~/ 10 + 1}', with_: wrong),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
signInFrom('198.51.100.8'),
|
||||
refused(PasswordRefusal.tooManyAttempts),
|
||||
);
|
||||
now = now.add(const Duration(hours: 1, minutes: 1));
|
||||
expect(
|
||||
(await signInFrom('198.51.100.8')).needsAuthenticator,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('a reset that lapsed is never brought back by a push', () async {
|
||||
await annaComplete();
|
||||
final asked = await askAccountReset();
|
||||
now = asked.takesEffectAt.add(
|
||||
resetLapsesAfter + const Duration(minutes: 1),
|
||||
);
|
||||
final wrong = await PasswordDevice.make(seed: 77);
|
||||
for (final network in ['192.0.2.1', '192.0.2.2', '192.0.2.3']) {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await expectLater(
|
||||
signInFrom(network, with_: wrong),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
}
|
||||
await expectLater(
|
||||
signInFrom('198.51.100.7'),
|
||||
refused(PasswordRefusal.tooManyAttempts),
|
||||
);
|
||||
expect((await resets()).single.takesEffectAt, asked.takesEffectAt);
|
||||
});
|
||||
|
||||
test("are shown to the owner's signed-in devices, each kind with "
|
||||
'when it takes effect, none lapsed or carried out', () async {
|
||||
await annaComplete();
|
||||
var ways = await endpoints.account.signInWays(asAnna());
|
||||
expect(ways.authenticatorResetAt, isNull);
|
||||
expect(ways.accountResetAt, isNull);
|
||||
final authenticator = await askAuthenticatorReset();
|
||||
final account = await askAccountReset();
|
||||
ways = await endpoints.account.signInWays(asAnna());
|
||||
expect(ways.authenticatorResetAt, authenticator.takesEffectAt);
|
||||
expect(ways.accountResetAt, account.takesEffectAt);
|
||||
// Lapsed: shown no more.
|
||||
now = account.takesEffectAt.add(
|
||||
resetLapsesAfter + const Duration(minutes: 1),
|
||||
);
|
||||
ways = await atNow(() => endpoints.account.signInWays(asAnna()));
|
||||
expect(ways.authenticatorResetAt, isNull);
|
||||
expect(ways.accountResetAt, isNull);
|
||||
});
|
||||
|
||||
test("are called off by the signed-in owner's proof, even while "
|
||||
'codes wait', () async {
|
||||
final enrolment = await annaComplete();
|
||||
await askAuthenticatorReset();
|
||||
await askAccountReset();
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await expectLater(
|
||||
signInFrom('192.0.2.9', totp: '000000'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
nextStep();
|
||||
await atNow(
|
||||
() async => await endpoints.password.callOffResets(
|
||||
asAnna(),
|
||||
proof: device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(await resets(), isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'are called off only with the password and a code: a session '
|
||||
'alone, a wrong password or a wrong code calls off nothing',
|
||||
() async {
|
||||
final enrolment = await annaComplete();
|
||||
await askAuthenticatorReset();
|
||||
final wrong = await PasswordDevice.make(seed: 77);
|
||||
for (final proof in <Future<OwnerProof> Function()>[
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
),
|
||||
() async => wrong.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
totpCode: '000000',
|
||||
),
|
||||
]) {
|
||||
await expectLater(
|
||||
atNow(
|
||||
() async => await endpoints.password.callOffResets(
|
||||
asAnna(),
|
||||
proof: await proof(),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
expect(await resets(), hasLength(1));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group("the phrase's codes count apart from the password's (M3)", () {
|
||||
test('codes waiting after the password never hold back the phrase: '
|
||||
'its owner resets the password with it and a code', () async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue