feat(keys): the password's own proof key (ADR 0020, D19)
A third subkey of the password's Argon2id (context `hlkpass1`, subkey 3) seeds an Ed25519 pair: `PasswordKeys.proofKeyPair` and `proofPublicKey`. An account that keeps no SRP verifier proves its password by signing the register's nonce with it, as it proves the recovery phrase; neither SRP's verifier nor the unwrapped account keys a signed-in device keeps can make it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b4c93e03de
commit
e8953d1560
2 changed files with 114 additions and 10 deletions
|
|
@ -1,5 +1,6 @@
|
|||
/// An account's keys on the device (ADR 0017): what the password unlocks
|
||||
/// (Argon2id, split into SRP's x and the key-encryption key), the account's
|
||||
/// (Argon2id, split into SRP's x, the key-encryption key and the password's
|
||||
/// proof key), the account's
|
||||
/// key pairs and their wrapping, the recovery phrase and its key, data keys
|
||||
/// sealed to the account, and content sealed and signed in ADR 0002's
|
||||
/// envelope. libsodium (the `sodium` package, its sumo API for Argon2id)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import 'package:sodium/sodium_sumo.dart';
|
|||
/// The password's keys (ADR 0017): Argon2id over the password and the
|
||||
/// account's salt makes one master key, which libsodium's KDF splits into
|
||||
/// SRP's secret (from which x is made, the one value SRP takes in place of
|
||||
/// the password) and the key-encryption key that wraps the account's private
|
||||
/// keys. The password, the master key and both halves never leave the
|
||||
/// device.
|
||||
/// the password), the key-encryption key that wraps the account's private
|
||||
/// keys, and the seed of the password's proof key (Halkyon Learn's ADR
|
||||
/// 0020, D19), with which an account that keeps no SRP verifier proves the
|
||||
/// password. The password, the master key and the three subkeys never leave
|
||||
/// the device; of the proof key, only its public half does.
|
||||
|
||||
/// libsodium's KDF context for the password's subkeys (exactly 8 bytes).
|
||||
const _passwordContext = 'hlkpass1';
|
||||
|
|
@ -20,6 +22,9 @@ final _srpSubkey = BigInt.one;
|
|||
/// The subkey that wraps the account's private keys.
|
||||
final _keyEncryptionSubkey = BigInt.two;
|
||||
|
||||
/// The subkey that seeds the password's proof key.
|
||||
final _proofSubkey = BigInt.from(3);
|
||||
|
||||
/// How long each derived key is.
|
||||
const _keyBytes = 32;
|
||||
|
||||
|
|
@ -99,6 +104,11 @@ final class PasswordKeys._(
|
|||
|
||||
/// The key that wraps the account's private keys.
|
||||
final SecureKey keyEncryptionKey,
|
||||
|
||||
/// The seed of the password's proof key ([proofKeyPair]).
|
||||
final SecureKey _proofSeed,
|
||||
|
||||
final SodiumSumo _sodium,
|
||||
) {
|
||||
/// The number x SRP takes in place of the password: the SRP subkey's 256
|
||||
/// bits, big-endian. It is the password's Argon2id in all but name, so a
|
||||
|
|
@ -111,11 +121,30 @@ final class PasswordKeys._(
|
|||
return value;
|
||||
});
|
||||
|
||||
/// Wipes and frees both keys; neither may be read afterwards (libsodium
|
||||
/// The password's proof key: an Ed25519 pair seeded by a subkey of its
|
||||
/// own, so neither SRP's verifier nor the account's keys (which every
|
||||
/// signed-in device keeps unwrapped) can make it. The register keeps its
|
||||
/// public half ([proofPublicKey]) and takes a signature by it as proof of
|
||||
/// the password, as it takes the recovery phrase's (`recoveryProofKeysOf`).
|
||||
/// The caller disposes of its secret key.
|
||||
KeyPair proofKeyPair() => _sodium.crypto.sign.seedKeyPair(_proofSeed);
|
||||
|
||||
/// The public half of the password's proof key, which the register keeps.
|
||||
Uint8List get proofPublicKey {
|
||||
final pair = proofKeyPair();
|
||||
try {
|
||||
return Uint8List.fromList(pair.publicKey);
|
||||
} finally {
|
||||
pair.secretKey.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Wipes and frees the three keys; none may be read afterwards (libsodium
|
||||
/// has freed their memory).
|
||||
void dispose() {
|
||||
srpSecret.dispose();
|
||||
keyEncryptionKey.dispose();
|
||||
_proofSeed.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +183,13 @@ PasswordKeys derivePasswordKeys(
|
|||
subkeyId: _keyEncryptionSubkey,
|
||||
subkeyLen: _keyBytes,
|
||||
),
|
||||
sodium.crypto.kdf.deriveFromKey(
|
||||
masterKey: masterKey,
|
||||
context: _passwordContext,
|
||||
subkeyId: _proofSubkey,
|
||||
subkeyLen: _keyBytes,
|
||||
),
|
||||
sodium,
|
||||
);
|
||||
} finally {
|
||||
masterKey.dispose();
|
||||
|
|
|
|||
Loading…
Reference in a new issue