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:
parent
c4f863ff8b
commit
d53844bc93
1 changed files with 873 additions and 32 deletions
|
|
@ -504,6 +504,97 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('deleting the account', () {
|
||||
/// Deletes Anna's account from her phone, with the password's proof
|
||||
/// (hers by default, by [privateValue] otherwise, none [withoutProof])
|
||||
/// and [totpCode] (her authenticator's now by default), bound to
|
||||
/// [host].
|
||||
Future<void> delete({
|
||||
BigInt? privateValue,
|
||||
String? totpCode,
|
||||
bool withoutCode = false,
|
||||
bool withoutProof = false,
|
||||
String host = testHost,
|
||||
}) => atNow(() async {
|
||||
if (withoutProof) {
|
||||
return await endpoints.account.delete(onPhone());
|
||||
}
|
||||
final challenge = await endpoints.password.beginChange(onPhone());
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
privateValue: privateValue,
|
||||
host: host,
|
||||
);
|
||||
await endpoints.account.delete(
|
||||
onPhone(),
|
||||
proof: DeletionProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: withoutCode
|
||||
? null
|
||||
: totpCode ?? codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'takes the password and a code, as a change does: a session '
|
||||
"alone, or with a phone's authenticator, deletes nothing",
|
||||
() async {
|
||||
await expectLater(
|
||||
delete(withoutProof: true),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
delete(withoutCode: true),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
delete(privateValue: BigInt.from(4242)),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
delete(totpCode: '000000'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
delete(host: 'learn.evil.example'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
expect(await AuthUser.db.findById(session, anna), isNotNull);
|
||||
expect(await AccountPassword.db.count(session), 1);
|
||||
|
||||
nextStep();
|
||||
await delete();
|
||||
|
||||
expect(await AuthUser.db.findById(session, anna), isNull);
|
||||
expect(await AccountPassword.db.count(session), 0);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'of an anonymous person takes its session alone, as before',
|
||||
() async {
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
|
||||
await endpoints.account.delete(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{},
|
||||
authId: bensLaptop.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await AuthUser.db.findById(session, ben), isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('a reset', () {
|
||||
test('of the password with the phrase ends the sessions at the '
|
||||
'members too, the new session made after', () async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue