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
5 changed files with 442 additions and 21 deletions
|
|
@ -77,6 +77,24 @@ final class const NewAccountMaterial({
|
||||||
String toString() => 'NewAccountMaterial(…)';
|
String toString() => 'NewAccountMaterial(…)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The keys of an account that signs in with SimpleLogin as the device
|
||||||
|
/// made them (ADR 0020, D19): the setup it sends, the recovery phrase it
|
||||||
|
/// shows once, and the keys it keeps unlocked.
|
||||||
|
final class const NewKeysMaterial({
|
||||||
|
required final protocol.KeysSetup setup,
|
||||||
|
required final String phrase,
|
||||||
|
required final UnlockedAccountKeys keys,
|
||||||
|
}) {
|
||||||
|
/// Never the phrase or the keys.
|
||||||
|
@override
|
||||||
|
String toString() => 'NewKeysMaterial(…)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The proof of the password of an account that signs in with SimpleLogin
|
||||||
|
/// ([AccountCrypto.proveKeysPassword]), and what the password unlocked,
|
||||||
|
/// which opens the keys the register then hands over.
|
||||||
|
typedef KeysPasswordProof = ({protocol.KeysProof proof, PasswordUnlock unlock});
|
||||||
|
|
||||||
/// The device's answer to SRP's first step: A and M1 for the register, and
|
/// The device's answer to SRP's first step: A and M1 for the register, and
|
||||||
/// whether the register's M2 is the one that proves it holds the verifier.
|
/// whether the register's M2 is the one that proves it holds the verifier.
|
||||||
/// A new recovery phrase made on the device ([AccountCrypto.newPhrase]): the
|
/// A new recovery phrase made on the device ([AccountCrypto.newPhrase]): the
|
||||||
|
|
@ -184,4 +202,33 @@ abstract interface class AccountCrypto() {
|
||||||
/// A new recovery phrase for [keys]: the keys wrapped under it, and the
|
/// A new recovery phrase for [keys]: the keys wrapped under it, and the
|
||||||
/// public half of its proof key.
|
/// public half of its proof key.
|
||||||
Future<NewPhraseMaterial> newPhrase(UnlockedAccountKeys keys);
|
Future<NewPhraseMaterial> newPhrase(UnlockedAccountKeys keys);
|
||||||
|
|
||||||
|
/// The keys of an account that signs in with SimpleLogin (ADR 0020, D19):
|
||||||
|
/// [keys] (an anonymous person's device keys, ADR 0017, 9) or new key
|
||||||
|
/// pairs, wrapped under [password] and a new recovery phrase, with the
|
||||||
|
/// public half of the password's proof key in place of SRP's verifier.
|
||||||
|
Future<NewKeysMaterial> newKeys(String password, {UnlockedAccountKeys? keys});
|
||||||
|
|
||||||
|
/// The proof of [password] for [challenge] on [instanceUrl], signed by
|
||||||
|
/// the password's proof key for unlocking the keys
|
||||||
|
/// (`KeyProofPurpose.unlockKeys`), with what it unlocked for the
|
||||||
|
/// challenge's salt and costs: one Argon2id for both. The caller forgets
|
||||||
|
/// the unlock once the keys are open.
|
||||||
|
Future<KeysPasswordProof> proveKeysPassword(
|
||||||
|
String password,
|
||||||
|
protocol.KeysChallenge challenge, {
|
||||||
|
required String instanceUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The signature proving the recovery [phrase] for [challenge] on
|
||||||
|
/// [instanceUrl], to link the SimpleLogin sign-in under [attempt] in
|
||||||
|
/// place of a lost one (ADR 0020, D7; `KeyProofPurpose.mendLink`): by the
|
||||||
|
/// phrase's own proof key, as [signKeyProof]'s. Refused
|
||||||
|
/// (`PasswordProblem.notAPhrase`) for what is no recovery phrase.
|
||||||
|
Future<ByteData> signMendProof(
|
||||||
|
String phrase,
|
||||||
|
protocol.KeyProofChallenge challenge, {
|
||||||
|
required String instanceUrl,
|
||||||
|
required protocol.UuidValue attempt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,117 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<NewKeysMaterial> newKeys(
|
||||||
|
String password, {
|
||||||
|
UnlockedAccountKeys? keys,
|
||||||
|
}) => Isolate.run(
|
||||||
|
() => _guarded(() async {
|
||||||
|
final sodium = await SodiumSumoInit.init();
|
||||||
|
final opened = keys == null
|
||||||
|
? AccountKeys.generate(sodium)
|
||||||
|
: _keysOf(sodium, keys);
|
||||||
|
try {
|
||||||
|
return _keysMaterial(
|
||||||
|
sodium,
|
||||||
|
password,
|
||||||
|
opened,
|
||||||
|
phrase: newRecoveryPhrase(sodium),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
opened.dispose();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<KeysPasswordProof> proveKeysPassword(
|
||||||
|
String password,
|
||||||
|
protocol.KeysChallenge challenge, {
|
||||||
|
required String instanceUrl,
|
||||||
|
}) => Isolate.run(
|
||||||
|
() => _guarded(() async {
|
||||||
|
final sodium = await SodiumSumoInit.init();
|
||||||
|
final salt = Uint8List.fromList(Uint8List.sublistView(challenge.salt));
|
||||||
|
final unlocked = derivePasswordKeys(
|
||||||
|
sodium,
|
||||||
|
password: password,
|
||||||
|
salt: salt,
|
||||||
|
kdf: _kdfOf(challenge.costs),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
final pair = unlocked.proofKeyPair();
|
||||||
|
final Uint8List signature;
|
||||||
|
try {
|
||||||
|
signature = _own(
|
||||||
|
sodium.crypto.sign.detached(
|
||||||
|
message: srp.keyProofMessage(
|
||||||
|
challenge.challenge.toBytes(),
|
||||||
|
Uint8List.sublistView(challenge.nonce),
|
||||||
|
instanceUrl: instanceUrl,
|
||||||
|
purpose: srp.KeyProofPurpose.unlockKeys,
|
||||||
|
),
|
||||||
|
secretKey: pair.secretKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
pair.secretKey.dispose();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
proof: protocol.KeysProof(
|
||||||
|
challenge: challenge.challenge,
|
||||||
|
signature: ByteData.sublistView(signature),
|
||||||
|
instanceUrl: instanceUrl,
|
||||||
|
),
|
||||||
|
unlock: PasswordUnlock(
|
||||||
|
salt: salt,
|
||||||
|
costs: challenge.costs,
|
||||||
|
srpSecret: unlocked.srpSecret.extractBytes(),
|
||||||
|
keyEncryptionKey: unlocked.keyEncryptionKey.extractBytes(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
unlocked.dispose();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ByteData> signMendProof(
|
||||||
|
String phrase,
|
||||||
|
protocol.KeyProofChallenge challenge, {
|
||||||
|
required String instanceUrl,
|
||||||
|
required protocol.UuidValue attempt,
|
||||||
|
}) => Isolate.run(
|
||||||
|
() => _guarded(() async {
|
||||||
|
final sodium = await SodiumSumoInit.init();
|
||||||
|
final KeyPair proof;
|
||||||
|
try {
|
||||||
|
proof = recoveryProofKeysOf(sodium, phrase);
|
||||||
|
} on KeyRefused {
|
||||||
|
throw const PasswordFailed(PasswordProblem.notAPhrase);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ByteData.sublistView(
|
||||||
|
_own(
|
||||||
|
sodium.crypto.sign.detached(
|
||||||
|
message: srp.keyProofMessage(
|
||||||
|
challenge.challenge.toBytes(),
|
||||||
|
Uint8List.sublistView(challenge.nonce),
|
||||||
|
instanceUrl: instanceUrl,
|
||||||
|
purpose: srp.KeyProofPurpose.mendLink,
|
||||||
|
digest: srp.mendLinkDigest(attempt.toBytes()),
|
||||||
|
),
|
||||||
|
secretKey: proof.secretKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
proof.secretKey.dispose();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<NewPhraseMaterial> newPhrase(UnlockedAccountKeys keys) => Isolate.run(
|
Future<NewPhraseMaterial> newPhrase(UnlockedAccountKeys keys) => Isolate.run(
|
||||||
() => _guarded(() async {
|
() => _guarded(() async {
|
||||||
|
|
@ -343,6 +454,77 @@ NewAccountMaterial _material(
|
||||||
String password,
|
String password,
|
||||||
AccountKeys keys, {
|
AccountKeys keys, {
|
||||||
required String phrase,
|
required String phrase,
|
||||||
|
}) => _wrapping(
|
||||||
|
sodium,
|
||||||
|
password,
|
||||||
|
keys,
|
||||||
|
phrase: phrase,
|
||||||
|
(salt, unlocked, recoveryKey) => NewAccountMaterial(
|
||||||
|
setup: protocol.PasswordSetup(
|
||||||
|
salt: ByteData.sublistView(salt),
|
||||||
|
costs: _costsOf(PasswordKdf.current),
|
||||||
|
verifier: _verifierOf(unlocked),
|
||||||
|
encryptionPublicKey: ByteData.sublistView(_own(keys.encryptionPublicKey)),
|
||||||
|
signingPublicKey: ByteData.sublistView(_own(keys.signingPublicKey)),
|
||||||
|
passwordKeys: _wrappedOf(
|
||||||
|
wrapAccountKeys(sodium, keys, unlocked.keyEncryptionKey),
|
||||||
|
),
|
||||||
|
recoveryKeys: _wrappedOf(wrapAccountKeys(sodium, keys, recoveryKey)),
|
||||||
|
// Copied by recoveryProofPublicKeyOf already.
|
||||||
|
recoveryProofKey: ByteData.sublistView(
|
||||||
|
recoveryProofPublicKeyOf(sodium, phrase),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
phrase: phrase,
|
||||||
|
keys: _unlockedOf(keys),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The keys of an account that signs in with SimpleLogin for [password]
|
||||||
|
/// around [keys], wrapped under the password and under [phrase], with the
|
||||||
|
/// password's proof key in place of SRP's verifier (ADR 0020, D19).
|
||||||
|
NewKeysMaterial _keysMaterial(
|
||||||
|
SodiumSumo sodium,
|
||||||
|
String password,
|
||||||
|
AccountKeys keys, {
|
||||||
|
required String phrase,
|
||||||
|
}) => _wrapping(
|
||||||
|
sodium,
|
||||||
|
password,
|
||||||
|
keys,
|
||||||
|
phrase: phrase,
|
||||||
|
(salt, unlocked, recoveryKey) => NewKeysMaterial(
|
||||||
|
setup: protocol.KeysSetup(
|
||||||
|
salt: ByteData.sublistView(salt),
|
||||||
|
costs: _costsOf(PasswordKdf.current),
|
||||||
|
// Copied by proofPublicKey already.
|
||||||
|
passwordProofKey: ByteData.sublistView(unlocked.proofPublicKey),
|
||||||
|
encryptionPublicKey: ByteData.sublistView(_own(keys.encryptionPublicKey)),
|
||||||
|
signingPublicKey: ByteData.sublistView(_own(keys.signingPublicKey)),
|
||||||
|
passwordKeys: _wrappedOf(
|
||||||
|
wrapAccountKeys(sodium, keys, unlocked.keyEncryptionKey),
|
||||||
|
),
|
||||||
|
recoveryKeys: _wrappedOf(wrapAccountKeys(sodium, keys, recoveryKey)),
|
||||||
|
// Copied by recoveryProofPublicKeyOf already.
|
||||||
|
recoveryProofKey: ByteData.sublistView(
|
||||||
|
recoveryProofPublicKeyOf(sodium, phrase),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
phrase: phrase,
|
||||||
|
keys: _unlockedOf(keys),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// What [make] builds of a new [password] for [keys] (checked, with a new
|
||||||
|
/// salt) and of [phrase]: the salt, the password's keys and the phrase's,
|
||||||
|
/// both wiped once it returns.
|
||||||
|
T _wrapping<T>(
|
||||||
|
SodiumSumo sodium,
|
||||||
|
String password,
|
||||||
|
AccountKeys keys,
|
||||||
|
T Function(Uint8List salt, PasswordKeys unlocked, SecureKey recoveryKey)
|
||||||
|
make, {
|
||||||
|
required String phrase,
|
||||||
}) {
|
}) {
|
||||||
checkNewPassword(password);
|
checkNewPassword(password);
|
||||||
final salt = _own(newPasswordSalt(sodium));
|
final salt = _own(newPasswordSalt(sodium));
|
||||||
|
|
@ -355,27 +537,7 @@ NewAccountMaterial _material(
|
||||||
throw const PasswordFailed(PasswordProblem.notAPhrase);
|
throw const PasswordFailed(PasswordProblem.notAPhrase);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return NewAccountMaterial(
|
return make(salt, unlocked, recoveryKey);
|
||||||
setup: protocol.PasswordSetup(
|
|
||||||
salt: ByteData.sublistView(salt),
|
|
||||||
costs: _costsOf(PasswordKdf.current),
|
|
||||||
verifier: _verifierOf(unlocked),
|
|
||||||
encryptionPublicKey: ByteData.sublistView(
|
|
||||||
_own(keys.encryptionPublicKey),
|
|
||||||
),
|
|
||||||
signingPublicKey: ByteData.sublistView(_own(keys.signingPublicKey)),
|
|
||||||
passwordKeys: _wrappedOf(
|
|
||||||
wrapAccountKeys(sodium, keys, unlocked.keyEncryptionKey),
|
|
||||||
),
|
|
||||||
recoveryKeys: _wrappedOf(wrapAccountKeys(sodium, keys, recoveryKey)),
|
|
||||||
// Copied by recoveryProofPublicKeyOf already.
|
|
||||||
recoveryProofKey: ByteData.sublistView(
|
|
||||||
recoveryProofPublicKeyOf(sodium, phrase),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
phrase: phrase,
|
|
||||||
keys: _unlockedOf(keys),
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
unlocked.dispose();
|
unlocked.dispose();
|
||||||
recoveryKey.dispose();
|
recoveryKey.dispose();
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,23 @@ class FakeAccountCrypto() implements AccountCrypto {
|
||||||
key == 'proof key of ${signed.substring('signed by '.length)}';
|
key == 'proof key of ${signed.substring('signed by '.length)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The public half of [password]'s proof key, as the fake makes it.
|
||||||
|
static ByteData passwordKeyOf(String password) =>
|
||||||
|
ByteData.sublistView(utf8.encode('password key of $password'));
|
||||||
|
|
||||||
|
/// Whether [proof] is signed by the password's proof key whose public
|
||||||
|
/// half is [passwordKey].
|
||||||
|
static bool provesPassword(protocol.KeysProof proof, ByteData? passwordKey) {
|
||||||
|
if (passwordKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final signed = utf8.decode(Uint8List.sublistView(proof.signature));
|
||||||
|
final key = utf8.decode(Uint8List.sublistView(passwordKey));
|
||||||
|
return signed.startsWith('signed by password ') &&
|
||||||
|
key ==
|
||||||
|
'password key of ${signed.substring('signed by password '.length)}';
|
||||||
|
}
|
||||||
|
|
||||||
static String _words(String phrase) =>
|
static String _words(String phrase) =>
|
||||||
phrase.trim().toLowerCase().split(RegExp(r'\s+')).join(' ');
|
phrase.trim().toLowerCase().split(RegExp(r'\s+')).join(' ');
|
||||||
|
|
||||||
|
|
@ -252,6 +269,60 @@ class FakeAccountCrypto() implements AccountCrypto {
|
||||||
return signatureOf(phrase);
|
return signatureOf(phrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<NewKeysMaterial> newKeys(
|
||||||
|
String password, {
|
||||||
|
UnlockedAccountKeys? keys,
|
||||||
|
}) async {
|
||||||
|
_checkNew(password);
|
||||||
|
final kept = keys ?? keysNumbered(10 * ++_accounts);
|
||||||
|
return NewKeysMaterial(
|
||||||
|
setup: protocol.KeysSetup(
|
||||||
|
salt: ByteData(16),
|
||||||
|
costs: _costs,
|
||||||
|
passwordProofKey: passwordKeyOf(password),
|
||||||
|
encryptionPublicKey: ByteData.sublistView(kept.encryptionPublicKey),
|
||||||
|
signingPublicKey: ByteData.sublistView(kept.signingPublicKey),
|
||||||
|
passwordKeys: wrap(password, kept),
|
||||||
|
recoveryKeys: wrap(fakePhrase, kept),
|
||||||
|
recoveryProofKey: proofKeyOf(fakePhrase),
|
||||||
|
),
|
||||||
|
phrase: fakePhrase,
|
||||||
|
keys: kept,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<KeysPasswordProof> proveKeysPassword(
|
||||||
|
String password,
|
||||||
|
protocol.KeysChallenge challenge, {
|
||||||
|
required String instanceUrl,
|
||||||
|
}) async {
|
||||||
|
final unlock = await this.unlock(
|
||||||
|
password,
|
||||||
|
salt: Uint8List.sublistView(challenge.salt),
|
||||||
|
costs: challenge.costs,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
proof: protocol.KeysProof(
|
||||||
|
challenge: challenge.challenge,
|
||||||
|
signature: ByteData.sublistView(
|
||||||
|
utf8.encode('signed by password $password'),
|
||||||
|
),
|
||||||
|
instanceUrl: instanceUrl,
|
||||||
|
),
|
||||||
|
unlock: unlock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ByteData> signMendProof(
|
||||||
|
String phrase,
|
||||||
|
protocol.KeyProofChallenge challenge, {
|
||||||
|
required String instanceUrl,
|
||||||
|
required protocol.UuidValue attempt,
|
||||||
|
}) => signKeyProof(phrase, challenge, instanceUrl: instanceUrl);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<NewPhraseMaterial> newPhrase(UnlockedAccountKeys keys) async => (
|
Future<NewPhraseMaterial> newPhrase(UnlockedAccountKeys keys) async => (
|
||||||
phrase: otherFakePhrase,
|
phrase: otherFakePhrase,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export 'src/password/password_config.dart';
|
||||||
export 'src/password/passwords.dart'
|
export 'src/password/passwords.dart'
|
||||||
show
|
show
|
||||||
gitHubSignInFreshFor,
|
gitHubSignInFreshFor,
|
||||||
|
isKeysSetup,
|
||||||
isNewPassword,
|
isNewPassword,
|
||||||
isPasswordSetup,
|
isPasswordSetup,
|
||||||
newPasswordCosts,
|
newPasswordCosts,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue