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
2 changed files with 2711 additions and 248 deletions
|
|
@ -14,6 +14,7 @@ void main() {
|
|||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
),
|
||||
proofKey: Uint8List.fromList(List.filled(32, 5)),
|
||||
srpSecret: Uint8List.fromList(List.filled(32, 7)),
|
||||
keyEncryptionKey: Uint8List.fromList(List.filled(32, 9)),
|
||||
);
|
||||
|
|
@ -24,6 +25,11 @@ void main() {
|
|||
|
||||
expect(x, everyElement(0));
|
||||
expect(key, everyElement(0));
|
||||
expect(
|
||||
unlock.proofKey,
|
||||
everyElement(5),
|
||||
reason: 'the public half of the proof key is no secret',
|
||||
);
|
||||
});
|
||||
|
||||
test('a forgotten unlock is spent: its secrets are read no more, so no '
|
||||
|
|
@ -35,6 +41,7 @@ void main() {
|
|||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
),
|
||||
proofKey: Uint8List.fromList(List.filled(32, 5)),
|
||||
srpSecret: Uint8List.fromList(List.filled(32, 7)),
|
||||
keyEncryptionKey: Uint8List.fromList(List.filled(32, 9)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -185,6 +185,101 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'a password that signs in gives its keys the proof key it makes (ADR '
|
||||
"0020, D34): the owner's SRP proof binds that key for the register, "
|
||||
"and the password's proof for unlocking then checks against it",
|
||||
() async {
|
||||
final verifier = register.numberOf(Uint8List.sublistView(setup.verifier));
|
||||
final challenge = register.srpChallenge(register.passwordGroup, verifier);
|
||||
final unlock = await crypto.unlock(
|
||||
password,
|
||||
salt: Uint8List.sublistView(made.setup.salt),
|
||||
costs: made.setup.costs,
|
||||
);
|
||||
final proofKey = ByteData.sublistView(
|
||||
Uint8List.fromList(unlock.proofKey),
|
||||
);
|
||||
final answer = await crypto.answer(
|
||||
unlock,
|
||||
protocol.PasswordChallenge(
|
||||
challenge: challengeId,
|
||||
salt: made.setup.salt,
|
||||
costs: made.setup.costs,
|
||||
serverValue: ByteData.sublistView(
|
||||
register.padded(
|
||||
challenge.publicValue,
|
||||
register.passwordGroup.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: device.PasswordProofPurpose.proofKey,
|
||||
digest: device.proofKeyDigest(unlock.proofKey),
|
||||
);
|
||||
unlock.forget();
|
||||
|
||||
register.SrpVerification verify(ByteData key) => register.srpVerify(
|
||||
register.passwordGroup,
|
||||
identity: register.passwordIdentity(
|
||||
challengeId,
|
||||
'https://learn.halkyon.eu/',
|
||||
purpose: register.PasswordProofPurpose.proofKey,
|
||||
digest: register.proofKeyDigest(key),
|
||||
),
|
||||
salt: Uint8List.sublistView(setup.salt),
|
||||
verifier: verifier,
|
||||
challenge: challenge,
|
||||
clientValue: register.numberOf(
|
||||
Uint8List.sublistView(answer.clientValue),
|
||||
),
|
||||
clientProof: Uint8List.sublistView(answer.clientProof),
|
||||
);
|
||||
|
||||
expect(proofKey.lengthInBytes, 32);
|
||||
expect(verify(proofKey), isA<register.SrpAccepted>());
|
||||
// Another key swapped in on the way: refused.
|
||||
expect(verify(ByteData(32)), isA<register.SrpRefused>());
|
||||
|
||||
final nonce = Uint8List.fromList(List.filled(32, 5));
|
||||
final proven = await crypto.proveKeysPassword(
|
||||
password,
|
||||
protocol.KeysChallenge(
|
||||
challenge: challengeId,
|
||||
nonce: ByteData.sublistView(nonce),
|
||||
salt: made.setup.salt,
|
||||
costs: made.setup.costs,
|
||||
),
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
expect(
|
||||
await register.isAccountSignature(
|
||||
register.keyProofMessage(
|
||||
challengeId,
|
||||
nonce,
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: register.KeyProofPurpose.unlockKeys,
|
||||
),
|
||||
signature: Uint8List.sublistView(proven.proof.signature),
|
||||
publicKey: Uint8List.sublistView(proofKey),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
await crypto.openKeys(
|
||||
proven.unlock,
|
||||
made.setup.passwordKeys,
|
||||
encryptionPublicKey: Uint8List.sublistView(
|
||||
made.setup.encryptionPublicKey,
|
||||
),
|
||||
signingPublicKey: Uint8List.sublistView(made.setup.signingPublicKey),
|
||||
),
|
||||
made.keys,
|
||||
);
|
||||
expect(proven.unlock.proofKey, unlock.proofKey);
|
||||
},
|
||||
);
|
||||
|
||||
group('an account that signs in with SimpleLogin (ADR 0020, D19)', () {
|
||||
late NewKeysMaterial keys;
|
||||
late register.KeysSetup keysSetup;
|
||||
|
|
@ -212,6 +307,40 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test(
|
||||
"calling off a reset of the keys: the password's proof made for it "
|
||||
'checks against its proof key for that alone (D32)',
|
||||
() async {
|
||||
final nonce = Uint8List.fromList(List.filled(32, 7));
|
||||
final proven = await crypto.proveKeysPassword(
|
||||
password,
|
||||
protocol.KeysChallenge(
|
||||
challenge: challengeId,
|
||||
nonce: ByteData.sublistView(nonce),
|
||||
salt: keys.setup.salt,
|
||||
costs: keys.setup.costs,
|
||||
),
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: device.KeyProofPurpose.callOffResets,
|
||||
);
|
||||
proven.unlock.forget();
|
||||
Future<bool> checks(register.KeyProofPurpose purpose) =>
|
||||
register.isAccountSignature(
|
||||
register.keyProofMessage(
|
||||
challengeId,
|
||||
nonce,
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: purpose,
|
||||
),
|
||||
signature: Uint8List.sublistView(proven.proof.signature),
|
||||
publicKey: Uint8List.sublistView(keysSetup.passwordProofKey),
|
||||
);
|
||||
|
||||
expect(await checks(register.KeyProofPurpose.callOffResets), isTrue);
|
||||
expect(await checks(register.KeyProofPurpose.unlockKeys), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"unlocking: the password's proof checks against its own proof key, "
|
||||
'bound to the instance and to what it is for; the keys the register '
|
||||
|
|
|
|||
Loading…
Reference in a new issue