// Helpers of the tests of stage 3 (BLS12-381, the IBE, tlock and releases): // a pinned profile, and the little of age and of DKC1 that the tests read // to reach a tlock stanza and to check a file key. They read no file, so // that the tests that run on Node.js can use them; age itself is stage 2. library; import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:datekeys/datekeys.dart' show concatBytes, fromHex; import 'package:datekeys/src/release.dart'; /// A pinned profile for the tests. final class TestProfile implements PinnedProfile { TestProfile({ required this.id, required this.scheme, required this.publicKey, required this.chainHash, required this.maxRound, }); @override final String id; @override final String scheme; @override final Uint8List publicKey; @override final Uint8List chainHash; @override final int maxRound; /// This profile with another [scheme] or [publicKey]. TestProfile copyWith({String? scheme, List? publicKey}) => TestProfile( id: id, scheme: scheme ?? this.scheme, publicKey: publicKey == null ? this.publicKey : Uint8List.fromList(publicKey), chainHash: chainHash, maxRound: maxRound, ); } /// The Quicknet Provider Profile V1 of testdata/vectors/ /// profile_quicknet.json, which tlock_support_test.dart checks against it. const quicknetId = 'datekeys:quicknet:v1'; const quicknetChainHash = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971'; const quicknetPublicKey = '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b' '6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809' 'bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a'; const quicknetGenesisTime = 1692803367; const quicknetPeriod = 3; /// 9999-12-31T23:59:59Z, the last representable instant (spec §15). const maxUnixTime = 253402300799; /// The pinned Quicknet profile. TestProfile quicknet() => TestProfile( id: quicknetId, scheme: quicknetScheme, publicKey: fromHex(quicknetPublicKey), chainHash: fromHex(quicknetChainHash), maxRound: (maxUnixTime - quicknetGenesisTime) ~/ quicknetPeriod + 1, ); /// The published release signatures of rounds 1000 and 1001 of Quicknet, as /// in the fixtures (time_only.json and empty_payload.json). const signature1000 = 'b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a' '8dd2bacbe47e4b6b63ed5e39'; const signature1001 = 'b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b2' '5883abf2853d10337fb8fa41'; /// The edits of the mutation corpus applied to [base] in one pass, as /// applyEdits of datekeys-ts: each [at, delete, insert hex] refers to /// offsets of the unmodified base, sorted and not overlapping. Uint8List applyEdits(List base, List edits) { final parts = >[]; var pos = 0; for (final e in edits.cast>()) { final at = e[0]! as int; final delete = e[1]! as int; if (at < pos || at + delete > base.length) { throw StateError('edit at $at out of order or beyond the base'); } parts ..add(base.sublist(pos, at)) ..add(fromHex(e[2]! as String)); pos = at + delete; } parts.add(base.sublist(pos)); return concatBytes(parts); } /// The round of the DateKey that the dk1_ string in [dkc] names: the tests /// need it where the opening would read it from the public header, which /// stage 4 decodes. int dateKeyRound(List dkc) { final text = latin1.decode(dkc, allowInvalid: true); final m = RegExp(r'dk1_([A-Za-z0-9_-]+)').firstMatch(text)!; final json = utf8.decode(base64Url.decode(base64Url.normalize(m[1]!))); return (jsonDecode(json) as Map)['round']! as int; } /// SEALED_CONTROL of a DKC1 file: its PRELUDE gives the lengths of the /// public header and of SEALED_CONTROL (spec §22). Uint8List sealedControl(List dkc) { final view = ByteData.sublistView(Uint8List.fromList(dkc)); final header = view.getUint32(8); final sealed = view.getUint32(12); return Uint8List.fromList(dkc.sublist(16 + header, 16 + header + sealed)); } /// A recipient stanza as the tests read it. typedef Stanza = ({String type, List args, Uint8List body}); /// The recipient stanzas and the MAC of the age header at the start of /// [file], read leniently: enough for the files of the tests, which age /// wrote. The grammar and its checks are those of stage 2. ({List stanzas, Uint8List macInput, Uint8List mac}) readAgeHeader( List file, ) { var start = 0; final stanzas = []; for (var i = 0; i < file.length; i++) { if (file[i] != 0x0a) continue; final line = latin1.decode(file.sublist(start, i)); if (line.startsWith('---')) { // The MAC covers the header up to and including "---". return ( stanzas: stanzas, macInput: Uint8List.fromList(file.sublist(0, start + 3)), mac: base64.decode(base64.normalize(line.substring(4))), ); } start = i + 1; if (line.startsWith('-> ')) { final words = line.substring(3).split(' '); final body = StringBuffer(); // The body: lines of 64 columns, the last one shorter. var j = i + 1; for (;;) { final end = file.indexOf(0x0a, j); final bodyLine = latin1.decode(file.sublist(j, end)); body.write(bodyLine); j = end + 1; if (bodyLine.length < 64) break; } stanzas.add(( type: words.first, args: words.sublist(1), body: base64.decode(base64.normalize(body.toString())), )); i = j - 1; start = j; } } throw StateError('no MAC line'); } /// Whether [fileKey] authenticates the age header whose MAC input and MAC /// [readAgeHeader] returns: HMAC-SHA-256 under HKDF-SHA-256(fileKey, "", /// "header"), as age computes it. bool ageHeaderMacValid(List fileKey, Uint8List macInput, Uint8List mac) { final prk = Hmac(sha256, Uint8List(32)).convert(fileKey).bytes; final key = Hmac(sha256, prk).convert([...ascii.encode('header'), 1]).bytes; final got = Hmac(sha256, key).convert(macInput).bytes; var diff = 0; for (var i = 0; i < 32; i++) { diff |= got[i] ^ mac[i]; } return mac.length == 32 && diff == 0; }