feat(app): the keys of accounts that sign in with SimpleLogin, on the device (ADR 0020, D19)
AccountCrypto makes the keys of an account that signs in with SimpleLogin (newKeys): new key pairs or an anonymous person's device keys, wrapped under the password and a new recovery phrase, with the public half of the password's proof key in place of SRP's verifier. It proves the password to open them (proveKeysPassword: one Argon2id for the proof and the unwrapping) and the recovery phrase to link a new SimpleLogin account in place of a lost one (signMendProof, bound to the attempt). The interop tests check all three against the register's own checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3cb2000927
commit
c9fedce258
1 changed files with 442 additions and 21 deletions
|
|
@ -184,4 +184,144 @@ void main() {
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('an account that signs in with SimpleLogin (ADR 0020, D19)', () {
|
||||
late NewKeysMaterial keys;
|
||||
late register.KeysSetup keysSetup;
|
||||
|
||||
setUpAll(() async {
|
||||
keys = await crypto.newKeys(password);
|
||||
keysSetup = register.KeysSetup.fromJson(keys.setup.toJson());
|
||||
});
|
||||
|
||||
test('its keys are a setup the register takes, keeping the keys an '
|
||||
"anonymous person's device made", () async {
|
||||
expect(register.isKeysSetup(keysSetup), isTrue);
|
||||
|
||||
final deviceKeys = await crypto.newDeviceKeys();
|
||||
final kept = await crypto.newKeys(password, keys: deviceKeys);
|
||||
|
||||
expect(
|
||||
register.isKeysSetup(register.KeysSetup.fromJson(kept.setup.toJson())),
|
||||
isTrue,
|
||||
);
|
||||
expect(kept.keys, deviceKeys);
|
||||
expect(
|
||||
Uint8List.sublistView(kept.setup.signingPublicKey),
|
||||
deviceKeys.signingPublicKey,
|
||||
);
|
||||
});
|
||||
|
||||
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 '
|
||||
'hands back open with what it unlocked',
|
||||
() async {
|
||||
final nonce = Uint8List.fromList(List.filled(32, 9));
|
||||
final proven = await crypto.proveKeysPassword(
|
||||
password,
|
||||
protocol.KeysChallenge(
|
||||
challenge: challengeId,
|
||||
nonce: ByteData.sublistView(nonce),
|
||||
salt: keys.setup.salt,
|
||||
costs: keys.setup.costs,
|
||||
),
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
Future<bool> checks(
|
||||
ByteData publicKey, {
|
||||
String instance = 'https://learn.halkyon.eu/',
|
||||
register.KeyProofPurpose purpose =
|
||||
register.KeyProofPurpose.unlockKeys,
|
||||
}) => register.isAccountSignature(
|
||||
register.keyProofMessage(
|
||||
challengeId,
|
||||
nonce,
|
||||
instanceUrl: instance,
|
||||
purpose: purpose,
|
||||
),
|
||||
signature: Uint8List.sublistView(proven.proof.signature),
|
||||
publicKey: Uint8List.sublistView(publicKey),
|
||||
);
|
||||
|
||||
expect(proven.proof.challenge, challengeId);
|
||||
expect(proven.proof.instanceUrl, instanceUrl);
|
||||
expect(await checks(keysSetup.passwordProofKey), isTrue);
|
||||
expect(await checks(keysSetup.signingPublicKey), isFalse);
|
||||
expect(await checks(keysSetup.recoveryProofKey), isFalse);
|
||||
expect(
|
||||
await checks(
|
||||
keysSetup.passwordProofKey,
|
||||
instance: 'https://learn.evil.example/',
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
await checks(
|
||||
keysSetup.passwordProofKey,
|
||||
purpose: register.KeyProofPurpose.deleteAccount,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
await crypto.openKeys(
|
||||
proven.unlock,
|
||||
keys.setup.passwordKeys,
|
||||
encryptionPublicKey: Uint8List.sublistView(
|
||||
keys.setup.encryptionPublicKey,
|
||||
),
|
||||
signingPublicKey: Uint8List.sublistView(
|
||||
keys.setup.signingPublicKey,
|
||||
),
|
||||
),
|
||||
keys.keys,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"linking again: the phrase's proof checks against the phrase's own "
|
||||
'key, bound to the sign-in it links',
|
||||
() async {
|
||||
final attempt = UuidValue.fromString(
|
||||
'01994a3c-7b2e-7c00-8a1b-0123456789cd',
|
||||
);
|
||||
final nonce = Uint8List.fromList(List.filled(32, 5));
|
||||
final signature = await crypto.signMendProof(
|
||||
keys.phrase,
|
||||
protocol.KeyProofChallenge(
|
||||
challenge: challengeId,
|
||||
nonce: ByteData.sublistView(nonce),
|
||||
encryptionPublicKey: keys.setup.encryptionPublicKey,
|
||||
signingPublicKey: keys.setup.signingPublicKey,
|
||||
recoveryKeys: keys.setup.recoveryKeys,
|
||||
),
|
||||
instanceUrl: instanceUrl,
|
||||
attempt: attempt,
|
||||
);
|
||||
Future<bool> checks(ByteData publicKey, UuidValue linked) =>
|
||||
register.isAccountSignature(
|
||||
register.keyProofMessage(
|
||||
challengeId,
|
||||
nonce,
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: register.KeyProofPurpose.mendLink,
|
||||
digest: register.mendLinkDigest(linked),
|
||||
),
|
||||
signature: Uint8List.sublistView(signature),
|
||||
publicKey: Uint8List.sublistView(publicKey),
|
||||
);
|
||||
|
||||
expect(await checks(keysSetup.recoveryProofKey, attempt), isTrue);
|
||||
expect(await checks(keysSetup.passwordProofKey, attempt), isFalse);
|
||||
expect(
|
||||
await checks(
|
||||
keysSetup.recoveryProofKey,
|
||||
UuidValue.fromString('01994a3c-7b2e-7c00-8a1b-0123456789ef'),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue