feat(base): the changes of SimpleLogin accounts' keys, each with the password and a fresh sign-in (ADR 0020, D20)
Every change takes the password's proof by its own proof key (the recovery phrase's for a lost password) and a SimpleLogin sign-in of the last ten minutes on the device (`KeysOwnerProof`, its attempt), as ADR 0017's take the password and a code. The sign-in is checked before the proof and taken with the change, so a wrong proof leaves it standing and each sign-in serves one change. - `keys.set(attempt, setup, handle?)`: the first keys of an account that SimpleLogin was added to (anonymous before, or signing in with GitHub or a code), with the handle where it has none; the sign-in alone, as there is no password to prove yet. - `keys.changePassword`, `keys.beginPhrase` and `keys.resetPassword` (a lost password, with the phrase): every other session ends, in every service, as ADR 0017's change has it. - `keys.setRecoveryKeys`: a new phrase. - `account.delete(keysProof)`: an account without SRP's verifier deletes itself with the proof and the sign-in; `requireOwnerToDelete` no longer asks such an account for SRP. - `newKeysPasswordDigest` on both sides, held to each other by a test; a challenge purpose of its own for the phrase's proof while signed in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the five new operations and the deletion's proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
090d901e92
commit
7c58d23424
1 changed files with 3163 additions and 78 deletions
|
|
@ -321,6 +321,76 @@ final class _Register implements AccountRegister {
|
|||
return account;
|
||||
});
|
||||
|
||||
/// The first keys set, by person, with the sign-in and handle they
|
||||
/// came with; the changes of keys asked, by name, with the proof (as the
|
||||
/// member bound it) and the session kept.
|
||||
final firstKeys = <UuidValue, ({UuidValue attempt, String? handle})>{};
|
||||
final keysChanges =
|
||||
<({String change, KeysOwnerProof proof, UuidValue? keepSession})>[];
|
||||
|
||||
@override
|
||||
Future<void> setFirstKeys(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
String? handle,
|
||||
}) => _answer('setFirstKeys', () {
|
||||
firstKeys[person] = (attempt: attempt, handle: handle);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<KeyProofChallenge> beginPhraseKeyProof(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
) => _answer(
|
||||
'beginPhraseKeyProof',
|
||||
() => KeyProofChallenge(
|
||||
challenge: const Uuid().v7obj(),
|
||||
nonce: ByteData(32),
|
||||
encryptionPublicKey: ByteData(32),
|
||||
signingPublicKey: ByteData(32),
|
||||
recoveryKeys: WrappedKeys(nonce: ByteData(24), cipherText: ByteData(112)),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> changeKeysPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required KeysPassword password,
|
||||
UuidValue? keepSession,
|
||||
}) => _answer('changeKeysPassword', () {
|
||||
keysChanges.add((
|
||||
change: 'password',
|
||||
proof: proof,
|
||||
keepSession: keepSession,
|
||||
));
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> resetKeysPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required KeysPassword password,
|
||||
UuidValue? keepSession,
|
||||
}) => _answer('resetKeysPassword', () {
|
||||
keysChanges.add((change: 'reset', proof: proof, keepSession: keepSession));
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> setKeysRecovery(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required WrappedKeys recoveryKeys,
|
||||
required ByteData recoveryProofKey,
|
||||
}) => _answer('setKeysRecovery', () {
|
||||
keysChanges.add((change: 'phrase', proof: proof, keepSession: null));
|
||||
});
|
||||
|
||||
@override
|
||||
Future<KeysChallenge> beginPasswordKeyProof(
|
||||
Session session,
|
||||
|
|
@ -602,14 +672,17 @@ final class _Register implements AccountRegister {
|
|||
Session session,
|
||||
UuidValue person, {
|
||||
DeletionProof? proof,
|
||||
KeysOwnerProof? keysProof,
|
||||
}) => _answer('deleteAccount', () {
|
||||
deleted.add(person);
|
||||
deletedWith.add(proof);
|
||||
deletedWithKeys.add(keysProof);
|
||||
accounts.remove(person);
|
||||
});
|
||||
|
||||
/// The proofs deletions came with, as the member bound them.
|
||||
final deletedWith = <DeletionProof?>[];
|
||||
final deletedWithKeys = <KeysOwnerProof?>[];
|
||||
final changedWith = <OwnerProof>[];
|
||||
|
||||
@override
|
||||
|
|
@ -1348,6 +1421,99 @@ void main() {
|
|||
expect(register.asked, isEmpty);
|
||||
});
|
||||
|
||||
test('are set first at the register with the sign-in and handle, '
|
||||
'for the caller', () async {
|
||||
final attempt = const Uuid().v7obj();
|
||||
|
||||
await endpoints.keys.set(
|
||||
asBen(),
|
||||
attempt: attempt,
|
||||
setup: (await KeysDevice.make()).setup(),
|
||||
handle: 'ben',
|
||||
);
|
||||
|
||||
expect(register.asked, ['setFirstKeys']);
|
||||
expect(register.firstKeys, {ben: (attempt: attempt, handle: 'ben')});
|
||||
});
|
||||
|
||||
test('are changed at the register, each proof bound to the base URL '
|
||||
'the device addressed here, this session kept where every other '
|
||||
'ends', () async {
|
||||
final proof = KeysOwnerProof(
|
||||
challenge: const Uuid().v7obj(),
|
||||
signature: ByteData(64),
|
||||
instanceUrl: testInstanceUrl,
|
||||
attempt: const Uuid().v7obj(),
|
||||
);
|
||||
final password = KeysPassword(
|
||||
salt: ByteData(16),
|
||||
costs: newPasswordCosts,
|
||||
passwordProofKey: ByteData(32),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
);
|
||||
|
||||
await endpoints.keys.changePassword(
|
||||
asBen(),
|
||||
proof: proof,
|
||||
password: password,
|
||||
);
|
||||
await endpoints.keys.beginPhrase(asBen());
|
||||
await endpoints.keys.resetPassword(
|
||||
asBen(),
|
||||
proof: proof,
|
||||
password: password,
|
||||
);
|
||||
await endpoints.keys.setRecoveryKeys(
|
||||
asBen(),
|
||||
proof: proof,
|
||||
recoveryKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
recoveryProofKey: ByteData(32),
|
||||
);
|
||||
|
||||
expect(register.asked, [
|
||||
'changeKeysPassword',
|
||||
'beginPhraseKeyProof',
|
||||
'resetKeysPassword',
|
||||
'setKeysRecovery',
|
||||
]);
|
||||
expect(
|
||||
[
|
||||
for (final change in register.keysChanges)
|
||||
(change.change, change.keepSession),
|
||||
],
|
||||
[
|
||||
('password', bensLaptop),
|
||||
('reset', bensLaptop),
|
||||
('phrase', null),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
register.keysChanges.map((change) => change.proof.instanceUrl),
|
||||
everyElement(testInstanceUrl),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.keys.changePassword(
|
||||
asBen(),
|
||||
proof: proof.copyWith(instanceUrl: 'https://evil.example/'),
|
||||
password: password,
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refusal) => refusal.reason,
|
||||
'reason',
|
||||
PasswordRefusal.otherInstance,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(register.asked, hasLength(4));
|
||||
});
|
||||
|
||||
test("count the session's own wrong proofs, which the register "
|
||||
'refuses: the tenth in a row ends the session here', () async {
|
||||
register.failing = PasswordRefusedException(
|
||||
|
|
@ -1445,6 +1611,29 @@ void main() {
|
|||
expect(register.deletedWith.single?.totpCode, '123456');
|
||||
});
|
||||
|
||||
test('passes the proof of keys without SRP on to the register, with '
|
||||
'its SimpleLogin sign-in, bound alike', () async {
|
||||
final attempt = const Uuid().v7obj();
|
||||
|
||||
await endpoints.account.delete(
|
||||
asBen(),
|
||||
keysProof: KeysOwnerProof(
|
||||
challenge: const Uuid().v7obj(),
|
||||
signature: ByteData(64),
|
||||
instanceUrl: 'HTTP://LOCALHOST:0',
|
||||
attempt: attempt,
|
||||
),
|
||||
);
|
||||
|
||||
expect(register.deleted, [ben]);
|
||||
expect(register.deletedWith.single, isNull);
|
||||
expect(
|
||||
register.deletedWithKeys.single?.instanceUrl,
|
||||
testInstanceUrl,
|
||||
);
|
||||
expect(register.deletedWithKeys.single?.attempt, attempt);
|
||||
});
|
||||
|
||||
test('of an anonymous person deletes it here alone, once the '
|
||||
'register says it has no such account', () async {
|
||||
await endpoints.account.delete(asAnna());
|
||||
|
|
|
|||
Loading…
Reference in a new issue