You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
6.2 KiB
184 lines
6.2 KiB
// 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<int>? 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<int> base, List<Object?> edits) {
|
|
final parts = <List<int>>[];
|
|
var pos = 0;
|
|
for (final e in edits.cast<List<Object?>>()) {
|
|
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<int> 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<String, Object?>)['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<int> 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<String> 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<Stanza> stanzas, Uint8List macInput, Uint8List mac}) readAgeHeader(
|
|
List<int> file,
|
|
) {
|
|
var start = 0;
|
|
final stanzas = <Stanza>[];
|
|
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<int> 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;
|
|
}
|