feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35)
SimpleLogin's sheet, its password and its recovery phrase both lost: under the lost-password step's phrase field, Lost your recovery phrase too? says what stays and what is lost and asks for new keys (`keys.startReset`) with the sign-in of a moment ago, which the device then drops. The next sign-in says when while it waits, and once due offers new keys, the sign-up's own password and phrase steps (`keys.completeReset`), or the password, which calls it off (D31). Every app's home shows `KeysResetBanner` where "too old" shows, and the account section's SimpleLogin row the same words (`KeysResetNotice`), each with Call it off, the password's proof alone (`keys.callOffResets`, D32), and, once due on a device that holds the keys, Finish here, which wraps the same keys anew (D33, D35). Both show only a reset of an account that signs in with SimpleLogin alone; ADR 0017's resets show in its own section, which now shares `AlertBox` with them. An account whose password signs in has no Forgot your password? there, and is asked its password and a code once while its keys lack the proof key, at a sign-in or on adding SimpleLogin (`password.setProofKey`, D34). `PasswordUnlock` carries the proof key, which its forgetting leaves, and the keys' password proves for a purpose given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
fe2ef02c92
commit
1b79214764
1 changed files with 2711 additions and 248 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
|
|
@ -326,6 +327,192 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('the password and the phrase lost: a reset of the keys (ADR 0020, '
|
||||
'D31, D33)', () {
|
||||
final asked = DateTime.utc(2026, 10, 24, 12);
|
||||
final due = asked.add(const Duration(hours: 72));
|
||||
late NewKeysMaterial material;
|
||||
|
||||
setUp(() async {
|
||||
material = await crypto.newKeys(password);
|
||||
gateway.addAccount(
|
||||
'person-9',
|
||||
subject: 'simplelogin-anna',
|
||||
handle: 'anna',
|
||||
keys: material.setup,
|
||||
);
|
||||
});
|
||||
|
||||
test('is asked for from the lost password, saying when it takes '
|
||||
'effect; nobody is signed in meanwhile', () async {
|
||||
await withClock(Clock.fixed(asked), () async {
|
||||
final cubit = cubitFor();
|
||||
await cubit.started();
|
||||
cubit
|
||||
..forgotChosen()
|
||||
..phraseLost();
|
||||
expect(cubit.state, at(SimpleLoginStep.resetAsk));
|
||||
cubit.forgotChosen();
|
||||
expect(cubit.state, at(SimpleLoginStep.forgot));
|
||||
cubit.phraseLost();
|
||||
|
||||
await cubit.resetAsked();
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.resetAsked));
|
||||
expect(cubit.state.resetAt, due);
|
||||
expect(gateway.accounts['person-9']!.resetAt, due);
|
||||
expect(auth.status, const SignedOut());
|
||||
expect(keys.kept, isEmpty);
|
||||
cubit.finished();
|
||||
expect(cubit.state, at(SimpleLoginStep.closed));
|
||||
});
|
||||
});
|
||||
|
||||
test('waiting, the next sign-in says when, and the password opening the '
|
||||
'keys calls it off', () async {
|
||||
gateway.accounts['person-9']!.resetAt = due;
|
||||
await withClock(Clock.fixed(asked), () async {
|
||||
final cubit = cubitFor();
|
||||
await cubit.started();
|
||||
expect(cubit.state, at(SimpleLoginStep.unlock));
|
||||
expect(cubit.state.resetAt, due);
|
||||
|
||||
await cubit.unlockEntered(password);
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.done));
|
||||
expect(gateway.accounts['person-9']!.resetAt, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('due, gives new keys under a new password and phrase, or opens the '
|
||||
'old ones with the password', () async {
|
||||
gateway.accounts['person-9']!.resetAt = due;
|
||||
await withClock(Clock.fixed(due), () async {
|
||||
final cubit = cubitFor();
|
||||
await cubit.started();
|
||||
expect(cubit.state, at(SimpleLoginStep.resetDue));
|
||||
cubit.passwordChosen();
|
||||
expect(cubit.state, at(SimpleLoginStep.unlock));
|
||||
cubit.resetDueShownAgain();
|
||||
expect(cubit.state, at(SimpleLoginStep.resetDue));
|
||||
|
||||
cubit.newKeysChosen();
|
||||
expect(cubit.state, at(SimpleLoginStep.password));
|
||||
expect(cubit.state.resetting, isTrue);
|
||||
await makeKeys(cubit);
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.done));
|
||||
expect(
|
||||
auth.status,
|
||||
const SignedIn(authUserId: 'person-9', isIdentified: true),
|
||||
);
|
||||
final account = gateway.accounts['person-9']!;
|
||||
expect(account.keys, isNot(material.setup));
|
||||
expect(account.resetAt, isNull);
|
||||
expect(keys.kept['person-9'], isNot(material.keys));
|
||||
});
|
||||
});
|
||||
|
||||
test('due on a device that holds the keys, signed in, keeps them under '
|
||||
'the new password and phrase (D33)', () async {
|
||||
gateway.accounts['person-9']!.resetAt = due;
|
||||
await auth.useSession(const SessionGrant('person-9', 'session-9'));
|
||||
keys.kept['person-9'] = material.keys;
|
||||
await withClock(Clock.fixed(due), () async {
|
||||
final cubit = cubitFor(link: true);
|
||||
await cubit.started();
|
||||
expect(cubit.state, at(SimpleLoginStep.password));
|
||||
expect(cubit.state.resetting, isTrue);
|
||||
|
||||
await makeKeys(cubit);
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.done));
|
||||
expect(keys.kept, {'person-9': material.keys});
|
||||
expect(gateway.accounts['person-9']!.resetAt, isNull);
|
||||
expect(auth.usedSessions, hasLength(1), reason: 'no other session');
|
||||
});
|
||||
});
|
||||
|
||||
test('waiting on a device that holds the keys, adding SimpleLogin is done '
|
||||
'at once', () async {
|
||||
gateway.accounts['person-9']!.resetAt = due;
|
||||
await auth.useSession(const SessionGrant('person-9', 'session-9'));
|
||||
await withClock(Clock.fixed(asked), () async {
|
||||
final cubit = cubitFor(link: true);
|
||||
await cubit.started();
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.done));
|
||||
expect(gateway.accounts['person-9']!.resetAt, due);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('a password that signs in too (ADR 0020, D34)', () {
|
||||
late NewKeysMaterial material;
|
||||
|
||||
setUp(() async {
|
||||
material = await crypto.newKeys(password);
|
||||
gateway.addAccount(
|
||||
'person-9',
|
||||
subject: 'simplelogin-anna',
|
||||
handle: 'anna',
|
||||
keys: material.setup,
|
||||
passwordSigningIn: password,
|
||||
proofKeyMissing: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('opens the keys once it and a code set their proof key, a wrong '
|
||||
'code refused', () async {
|
||||
final cubit = cubitFor();
|
||||
await cubit.started();
|
||||
expect(cubit.state, at(SimpleLoginStep.proofKey));
|
||||
expect(cubit.state.signedInAs, 'anna');
|
||||
|
||||
await cubit.proofKeyEntered(password, '000000');
|
||||
expect(
|
||||
cubit.state,
|
||||
at(
|
||||
SimpleLoginStep.proofKey,
|
||||
problem: const PasswordFailed(PasswordProblem.wrongCode),
|
||||
),
|
||||
);
|
||||
expect(auth.status, const SignedOut());
|
||||
|
||||
await cubit.proofKeyEntered(password, fakeTotpCode);
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.done));
|
||||
expect(keys.kept, {'person-9': material.keys});
|
||||
expect(gateway.accounts['person-9']!.proofKeyMissing, isFalse);
|
||||
});
|
||||
|
||||
test('with the proof key, is asked alone, without the lost phrase, as its '
|
||||
"resets are ADR 0017's", () async {
|
||||
gateway.accounts['person-9']!.proofKeyMissing = false;
|
||||
final cubit = cubitFor();
|
||||
await cubit.started();
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.unlock));
|
||||
expect(cubit.state.passwordSignsIn, isTrue);
|
||||
});
|
||||
|
||||
test('adding SimpleLogin on a device signed in asks the same, and keeps '
|
||||
'its session', () async {
|
||||
await auth.useSession(const SessionGrant('person-9', 'session-9'));
|
||||
keys.kept['person-9'] = material.keys;
|
||||
final cubit = cubitFor(link: true);
|
||||
await cubit.started();
|
||||
expect(cubit.state, at(SimpleLoginStep.proofKey));
|
||||
|
||||
await cubit.proofKeyEntered(password, fakeTotpCode);
|
||||
|
||||
expect(cubit.state, at(SimpleLoginStep.done));
|
||||
expect(gateway.accounts['person-9']!.proofKeyMissing, isFalse);
|
||||
expect(auth.usedSessions, hasLength(1), reason: 'no other session');
|
||||
expect(keys.kept, {'person-9': material.keys});
|
||||
});
|
||||
});
|
||||
|
||||
group("SimpleLogin added to the account of the device's person", () {
|
||||
test('a person without keys keeps those their device holds, and their '
|
||||
'session, once the keys are set', () async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue