fix(auth): a new recovery phrase takes the password too
password.setRecoveryKeys took a session and a TOTP code alone, so a phone signed in with its authenticator beside it could set a phrase of its holder's own and, through the phrase's reset, a new password, never knowing the old one. It now takes the old password's SRP proof (from beginChange, the caller's own) and a TOTP or recovery code, as changing the password does: one owner proof for both (_proveOwner), forwarded by members, asked for in the account section's New recovery phrase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4f7b123ad4
commit
5fb28bd817
1 changed files with 447 additions and 169 deletions
|
|
@ -316,77 +316,98 @@ void main() {
|
|||
});
|
||||
|
||||
group('the recovery phrase', () {
|
||||
test("takes new wrapped keys for a code: the old phrase's wrapping "
|
||||
'goes', () async {
|
||||
final fresh = WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(wrappedKeysLength),
|
||||
);
|
||||
final fresh = WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(wrappedKeysLength),
|
||||
);
|
||||
|
||||
await atNow(
|
||||
() => endpoints.password.setRecoveryKeys(
|
||||
onPhone(),
|
||||
code: codeOf(enrolment, at: now),
|
||||
recoveryKeys: fresh,
|
||||
),
|
||||
/// Gives Anna's keys a new phrase's wrapping ([fresh]) from her
|
||||
/// phone, the old password proved by [privateValue] (hers by
|
||||
/// default) or a made-up proof ([withoutProof]), with [totpCode] (her
|
||||
/// authenticator's now by default).
|
||||
Future<void> newPhrase({
|
||||
BigInt? privateValue,
|
||||
bool withoutProof = false,
|
||||
String? totpCode,
|
||||
bool withoutCode = false,
|
||||
WrappedKeys? recoveryKeys,
|
||||
TestSessionBuilder? as,
|
||||
}) => atNow(() async {
|
||||
final challenge = await endpoints.password.beginChange(onPhone());
|
||||
final answer = device.answer(challenge, privateValue: privateValue);
|
||||
await endpoints.password.setRecoveryKeys(
|
||||
as ?? onPhone(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: withoutProof ? ByteData(32) : answer.clientProof,
|
||||
totpCode: withoutCode
|
||||
? null
|
||||
: totpCode ?? codeOf(enrolment, at: now),
|
||||
recoveryKeys: recoveryKeys ?? fresh,
|
||||
);
|
||||
|
||||
final proof = await endpoints.accountRecovery.beginKeyProof(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
expect(proof.recoveryKeys.toJson(), fresh.toJson());
|
||||
});
|
||||
|
||||
test('is refused for a wrong code, keys out of range, and without a '
|
||||
'password, keeping the wrapping', () async {
|
||||
final fresh = WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(wrappedKeysLength),
|
||||
Future<WrappedKeys> wrapping() async =>
|
||||
(await endpoints.accountRecovery.beginKeyProof(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
)).recoveryKeys;
|
||||
|
||||
test('takes new wrapped keys with the old password and a code: the '
|
||||
"old phrase's wrapping goes", () async {
|
||||
await newPhrase();
|
||||
|
||||
expect((await wrapping()).toJson(), fresh.toJson());
|
||||
});
|
||||
|
||||
test("is refused alike without the password's proof, for a wrong "
|
||||
"one, another person's, and a missing or wrong code, keeping "
|
||||
'the wrapping', () async {
|
||||
// A session and a code alone: whoever holds a phone signed in with
|
||||
// its authenticator would otherwise set a phrase of their own, and
|
||||
// with it a new password.
|
||||
await expectLater(
|
||||
newPhrase(withoutProof: true),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
atNow(
|
||||
() => endpoints.password.setRecoveryKeys(
|
||||
onPhone(),
|
||||
code: '000000',
|
||||
recoveryKeys: fresh,
|
||||
newPhrase(privateValue: BigInt.from(4242)),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
newPhrase(withoutCode: true),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
newPhrase(totpCode: '000000'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
await expectLater(
|
||||
newPhrase(
|
||||
as: sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{identified},
|
||||
authId: bensLaptop.uuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCode),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
atNow(
|
||||
() => endpoints.password.setRecoveryKeys(
|
||||
onPhone(),
|
||||
code: codeOf(enrolment, at: now),
|
||||
recoveryKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(8),
|
||||
),
|
||||
newPhrase(
|
||||
recoveryKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(8),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
final ben = await newAnonymousPerson(session);
|
||||
await expectLater(
|
||||
endpoints.password.setRecoveryKeys(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
ben.uuid,
|
||||
{},
|
||||
),
|
||||
),
|
||||
code: '123456',
|
||||
recoveryKeys: fresh,
|
||||
),
|
||||
refused(PasswordRefusal.noPassword),
|
||||
);
|
||||
final proof = await endpoints.accountRecovery.beginKeyProof(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
|
||||
expect(
|
||||
proof.recoveryKeys.toJson(),
|
||||
(await wrapping()).toJson(),
|
||||
device.recoveryKeys.toJson(),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue