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>
This commit is contained in:
parent
9b9b8d08ca
commit
839d49c4cc
2 changed files with 249 additions and 36 deletions
|
|
@ -30,4 +30,5 @@ export 'src/recovery_phrase.dart'
|
|||
recoveryPhraseOf,
|
||||
recoveryProofKeysOf,
|
||||
recoveryProofPublicKeyOf;
|
||||
export 'src/row_ids.dart' show keyedRowId;
|
||||
export 'src/sealed_content.dart' show SealedContent, openContent, sealContent;
|
||||
|
|
|
|||
41
packages/halkyon_keys/lib/src/row_ids.dart
Normal file
41
packages/halkyon_keys/lib/src/row_ids.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_keys/src/account_keys.dart';
|
||||
import 'package:sodium/sodium_sumo.dart';
|
||||
|
||||
/// Row IDs keyed by the account (Halkyon Train's ADR 0030, D57): a row that
|
||||
/// every device of the account must name alike without asking each other
|
||||
/// (a day's, by its date) takes its ID from its name under a key only the
|
||||
/// account holds, so that two devices offline still write the same row, and
|
||||
/// the instance, which sees the ID, learns nothing of the name. A name
|
||||
/// hashed in the open (a UUID of version 5) would tell it: the instance can
|
||||
/// hash every date itself.
|
||||
|
||||
/// What the ID's hash binds itself to before the name: a label naming the
|
||||
/// format, so that no other use of the key meets it.
|
||||
final _rowIdLabel = utf8.encode('halkyon row id v1');
|
||||
|
||||
/// The ID of the row named [name] for the account of [keys]: BLAKE2b keyed
|
||||
/// with the account's X25519 secret key (16 bytes, over the label and the
|
||||
/// name), as a UUID of version 8 (RFC 9562, for IDs of one's own making).
|
||||
/// The same on every device of the account, another for any other name or
|
||||
/// account, and nothing of [name] read from it without the key. Throws an
|
||||
/// [ArgumentError] for an empty name.
|
||||
String keyedRowId(SodiumSumo sodium, AccountKeys keys, String name) {
|
||||
if (name.isEmpty) {
|
||||
throw ArgumentError.value(name, 'name', 'A row ID needs a name');
|
||||
}
|
||||
final hash = sodium.crypto.genericHash(
|
||||
message: Uint8List.fromList([..._rowIdLabel, 0, ...utf8.encode(name)]),
|
||||
key: keys.encryption.secretKey,
|
||||
outLen: 16,
|
||||
);
|
||||
hash[6] = hash[6] & 0x0f | 0x80;
|
||||
hash[8] = hash[8] & 0x3f | 0x80;
|
||||
final hex = [
|
||||
for (final byte in hash) byte.toRadixString(16).padLeft(2, '0'),
|
||||
].join();
|
||||
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-'
|
||||
'${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}';
|
||||
}
|
||||
Loading…
Reference in a new issue