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:
Tizian 「ティツィアーン」 2026-10-24 00:00:38 +02:00
commit c9fedce258
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -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,
);
},
);
});
}