fix(auth): an account with a password deletes itself only with it and a code

A session alone deleted any account, with everything encrypted under its
keys (the second reading's lows; decided by the developer: the password
and a code, as a change of password takes). account.delete now takes a
DeletionProof (the password's SRP proof for a beginChange challenge, a
TOTP or recovery code, the host the device addressed); the register
checks it (requireOwnerToDelete), a member passing it on bound to its own
host. Accounts without a password, anonymous ones among them, delete with
their session as before. In the app, deleting an account with a password
asks for it and a code in a sheet of its own; a wrong one deletes
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-20 21:59:42 +02:00
commit d53844bc93
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -173,6 +173,54 @@ void main() {
expect(find.text('Signed out'), findsOneWidget);
});
testWidgets('deleting an account with a password asks for it and a '
'code, which a wrong one fails, deleting nothing', (tester) async {
// The first person the fake password gateway makes.
const person = '00000000-0000-4000-8000-000000000001';
gateway = FakeAuthGateway(
status: const SignedIn(authUserId: person, isIdentified: true),
);
await start(tester);
dependencies.passwordGateway.addAccount(
'anna@example.org',
password: 'correct horse',
crypto: dependencies.crypto,
);
Finder field(String label) => find.widgetWithText(TextField, label);
await tester.tap(find.text('Delete account'));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete account').last);
await tester.pumpAndSettle();
expect(gateway.accountDeletions, 0);
expect(field('Current password'), findsOneWidget);
await tester.enterText(field('Current password'), 'wrong horse');
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tester.tap(find.text('Delete account').last);
await tester.pumpAndSettle();
expect(
find.text("The address, the password or the code isn't right."),
findsOneWidget,
);
expect(gateway.accountDeletions, 0);
expect(dependencies.passwordGateway.accounts, contains('anna@example.org'));
await tester.enterText(field('Current password'), 'correct horse');
await tester.tap(find.text('Delete account').last);
await tester.pumpAndSettle();
expect(gateway.accountDeletions, 1);
expect(
dependencies.passwordGateway.accounts,
isNot(contains('anna@example.org')),
);
expect(find.text('Signed out'), findsOneWidget);
});
testWidgets('speaks German', (tester) async {
await start(tester);
final german = lookupHalkyonAppLocalizations(const Locale('de'));