halkyon/packages/halkyon_keys/test/argon2id_test.dart
Tizian 「ティツィアーン」 3bc0bae6e3
feat(keys): halkyon_keys, an account's keys on the device
ADR 0017's step 1, on libsodium (sodium 4.1.0+1, whose build hook
compiles the libsodium 1.0.22 it ships: a C compiler and make wherever
it builds):
- Argon2id (64 MiB, 3 passes, kept with the account and never below
  that) makes a master key, which libsodium's KDF splits into SRP's
  secret (x) and the key-encryption key.
- The account's X25519 and Ed25519 key pairs, their private halves
  wrapped with XChaCha20-Poly1305 under the KEK and under the recovery
  key, bound to their public keys.
- A 24-word BIP-39 recovery phrase (the English list, MIT) and the
  recovery key derived from it.
- Data keys sealed to the account with crypto_box_seal, bound to their
  IDs; content in ADR 0002's envelope, encrypted with a data key and
  signed with Ed25519 (detached), bound to a context the caller names.
- Secrets in libsodium's guarded memory where its API takes them.
- Tests: libsodium's Argon2id known answers, BIP-39's English vectors,
  round trips and every refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-22 18:11:09 +02:00

90 lines
3.4 KiB
Dart

import 'dart:typed_data';
import 'package:sodium/sodium_sumo.dart';
import 'package:test/test.dart';
/// [hex] as bytes.
Uint8List _bytes(String hex) => Uint8List.fromList([
for (var i = 0; i < hex.length; i += 2)
int.parse(hex.substring(i, i + 2), radix: 16),
]);
/// [bytes] in lower-case hex.
String _hex(List<int> bytes) =>
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
Future<void> main() async {
final sodium = await SodiumSumoInit.init();
// libsodium's own known answers for crypto_pwhash with Argon2id
// (libsodium 1.0.18, test/default/pwhash_argon2id.c's tv(), vectors 0
// and 6, and their lines 1 and 7 of pwhash_argon2id.exp): the password
// and salt as the test cuts them (the password to its length, the salt
// to 16 bytes), the output length, the passes and the memory.
const vectors =
<
({
String password,
int length,
String salt,
int outLen,
int opsLimit,
int memLimit,
String expected,
})
>[
(
password:
'a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffa'
'e065577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02'
'c08da232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de'
'97d6f58d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6',
length: 127,
salt: '5541fbc995d5c197ba290346d2c559de',
outLen: 155,
opsLimit: 5,
memLimit: 7256678,
expected:
'18acec5d6507739f203d1f5d9f1d862f7c2cdac4f19d2bdff64487e60d969e3c'
'ed615337b9eec6ac4461c6ca07f0939741e57c24d0005c7ea171a0ee1e734824'
'9d135b38f222e4dad7b9a033ed83f5ca27277393e316582033c74affe2566a2b'
'ea47f91f0fd9fe49ece7e1f79f3ad6e9b23e0277c8ecc4b313225748dd2a80f5'
'679534a0700e246a79a49b3f74eb89ec6205fe1eeb941c73b1fcf1',
),
(
password:
'b540beb016a5366524d4605156493f9874514a5aa58818cd0c6dfffaa9e90205'
'f17b',
length: 34,
salt: '44071f6d181561670bda728d43fb79b4',
outLen: 231,
opsLimit: 1,
memLimit: 1631659,
expected:
'7fb72409b0987f8190c3729710e98c3f80c5a8727d425fdcde7f3644d467fe97'
'3f5b5fee683bd3fce812cb9ae5e9921a2d06c2f1905e4e839692f2b934b682f1'
'1a2fe2b90482ea5dd234863516dba6f52dc0702d324ec77d860c2e181f84472b'
'd7104fedce071ffa93c5309494ad51623d214447a7b2b1462dc7d5d55a1f6fd5'
'b54ce024118d86f0c6489d16545aaa87b6689dad9f2fb47fda9894f8e12b87d9'
'78b483ccd4cc5fd9595cdc7a818452f915ce2f7df95ec12b1c72e3788d473441'
'd884f9748eb14703c21b45d82fd667b85f5b2d98c13303b3fe76285531a826b6'
'fc0fe8e3dddecf',
),
];
for (final (index, vector) in vectors.indexed) {
test("Argon2id gives libsodium's known answer $index", () {
final key = sodium.crypto.pwhash.callRaw(
outLen: vector.outLen,
password: Int8List.fromList(_bytes(vector.password)),
salt: _bytes(vector.salt),
opsLimit: vector.opsLimit,
memLimit: vector.memLimit,
alg: CryptoPwhashAlgorithm.argon2id13,
);
expect(_bytes(vector.password), hasLength(vector.length));
expect(_hex(key.extractBytes()), vector.expected);
key.dispose();
});
}
}