halkyon/packages/halkyon_keys/test/row_ids_test.dart
Tizian 「ティツィアーン」 839d49c4cc
fix(train): no row's ID tells the instance a date: the days' IDs are keyed by the account (ADR 0030, D57)
A day's, night's and heart chunk's ID was a UUID of version 5 of the
person and the date, which the instance can hash for every date itself:
each ID told it which days the band was worn. halkyon_keys' keyedRowId
hashes the name under the account's X25519 secret key (BLAKE2b, a UUID of
version 8), the same on every device of the account and unreadable
without its keys; ContentCipher.rowId and TrainKeys.rowId hand it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 16:12:01 +01:00

55 lines
1.9 KiB
Dart

import 'package:halkyon_keys/halkyon_keys.dart';
import 'package:sodium/sodium_sumo.dart';
import 'package:test/test.dart';
Future<void> main() async {
final sodium = await SodiumSumoInit.init();
group('row IDs keyed by the account', () {
final account = AccountKeys.generate(sodium);
final secrets = (
encryption: account.encryption.secretKey.extractBytes(),
signing: account.signing.secretKey.extractBytes(),
);
test('every device of the account names a row alike: its keys, unlocked '
'anew, give the same ID', () {
final elsewhere = AccountKeys.fromSecretKeys(
sodium,
encryptionSecretKey: secrets.encryption,
signingSecretKey: secrets.signing,
);
expect(
keyedRowId(sodium, elsewhere, 'train/day/2026-10-30'),
keyedRowId(sodium, account, 'train/day/2026-10-30'),
);
});
test('another name, or the same name of another account, gives another '
'ID', () {
final other = AccountKeys.generate(sodium);
final id = keyedRowId(sodium, account, 'train/day/2026-10-30');
expect(keyedRowId(sodium, account, 'train/day/2026-10-31'), isNot(id));
expect(keyedRowId(sodium, account, 'train/night/2026-10-30'), isNot(id));
expect(keyedRowId(sodium, other, 'train/day/2026-10-30'), isNot(id));
});
test("the ID is a UUID of version 8, RFC 9562's for IDs of one's own "
'making, and not a name hashed in the open (version 5)', () {
final id = keyedRowId(sodium, account, 'train/day/2026-10-30');
expect(
id,
matches(
RegExp(
'^[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-'
r'[0-9a-f]{12}$',
),
),
);
});
test('a row without a name has no ID', () {
expect(() => keyedRowId(sodium, account, ''), throwsArgumentError);
});
});
}