// The age files of the official capsules of testdata/fixtures, against // test/vectors/age_fixtures.json, which Go wrote (tool/gen_age_vectors.go): // the PAYLOAD_AGE of each capsule opens with its payload_identity to the // plaintext of the fixture and its padding, and a stranger gets the error of // spec §30.1; the INNER_ACCESS_AGE of each time_and_key capsule, which // OUTER_TIME_AGE seals, opens with each credential of the fixture to its // CONTROL_CBOR. The stanzas that the header of each file shows are those of // the record of the fixture. @TestOn('vm') library; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:datekeys/datekeys.dart'; import 'package:datekeys/src/age.dart'; import 'package:datekeys/src/agewrap.dart'; import 'package:datekeys/src/sha256.dart'; import 'package:test/test.dart'; import 'age_support.dart'; Map readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Map; List> listOf(Object? v) => (v! as List).cast>(); /// The stanzas as the records of the fixtures write them: type and args. List> shown(List ss) => [ for (final s in ss) {'type': s.type, 'args': s.args}, ]; void main() { final vectors = readJson('test/vectors/age_fixtures.json'); final fixtures = listOf(vectors['fixtures']); final quicknet = readJson('testdata/vectors/profile_quicknet.json'); test('every capsule of testdata/fixtures has its vectors', () { final names = Directory('testdata/fixtures') .listSync() .map((f) => f.uri.pathSegments.last) .where((n) => n.endsWith('.dkc')) .map((n) => n.substring(0, n.length - 4)) .toSet(); expect(fixtures.map((f) => f['name']).toSet(), names); expect(names, hasLength(26)); }); for (final f in fixtures) { final name = f['name']! as String; group(name, () { final dkc = File('testdata/fixtures/$name.dkc').readAsBytesSync(); final record = readJson('testdata/fixtures/$name.json'); final offset = f['payload_offset']! as int; final payload = Uint8List.sublistView(dkc, offset); test('PAYLOAD_AGE opens with payload_identity', () { // The offset is the one of the prelude: 16 + PUBLIC_HEADER_LEN + // SEALED_CONTROL_LEN. final b = ByteData.sublistView(dkc); expect(16 + b.getUint32(8) + b.getUint32(12), offset); expect(payload.length, f['payload_length']); expect(toHex(sha256(payload)), f['payload_sha256']); expect(f['payload_identity'], record['payload_identity']); final stanzas = ageStanzas(payload); checkPayloadStanzas(stanzas); expect(shown(stanzas), record['payload_stanzas']); final id = PayloadIdentity( fromHex(record['payload_identity']! as String), ); expect(resultOf(payload, [id]), f['payload_result']); // The plaintext of the fixture, then zeros up to P in formats 2 // and 3. final plain = ageDecrypt(payload, [id]); final want = File('testdata/fixtures/${record['plaintext_file']}') .readAsBytesSync(); final l = record['payload_length']! as int; expect(want.length, l); expect(Uint8List.sublistView(plain, 0, l), want); expect(plain.length, record['padded_length'] ?? l); expect(plain.skip(l).every((b) => b == 0), isTrue); }); test('a stranger gets the error of spec §30.1', () { final w = f['payload_wrong_identity']! as Map; final id = PayloadIdentity(fromHex(w['raw']! as String)); expect(resultOf(payload, [id]), w['result']); }); test('OUTER_TIME_AGE shows the stanzas of the record', () { final b = ByteData.sublistView(dkc); final start = 16 + b.getUint32(8); final sealed = Uint8List.sublistView(dkc, start, offset); final stanzas = ageStanzas(sealed); expect(shown(stanzas), record['outer_stanzas']); final release = record['release']! as Map; checkTimeStanzas( stanzas, round: release['round']! as int, chainHashHex: quicknet['chain_hash']! as String, profileId: quicknet['profile_id']! as String, ); }); final inner = f['inner_access_age'] as Map?; if (inner == null) return; test('INNER_ACCESS_AGE opens with each credential', () { final file = fromHex(inner['hex']! as String); final slots = inner['slots']! as int; expect(slots, record['format'] == 1 ? 0 : accessSlots); final stanzas = ageStanzas(file); checkAccessStanzas(stanzas, slots); expect(shown(stanzas), record['inner_stanzas']); final all = []; for (final c in listOf(inner['identities'])) { final id = x25519IdentityFromRaw(fromHex(c['raw']! as String)); all.add(id); expect( resultOf(file, [ AccessIdentity(slots, [id]), ]), c['result'], reason: c['source'] as String?, ); } // The identities of the record, as strings. for (final s in (record['identities'] as List? ?? const []).cast()) { expect( all.map((i) => i.toString()), contains(X25519Identity.parse(s).toString()), ); } final control = ageDecrypt(file, [AccessIdentity(slots, all)]); expect(toHex(control), record['control_cbor']); expect( resultOf(file, [AccessIdentity(slots, all)]), inner['all_identities_result'], ); expect(toHex(sha256(control)), inner['control_cbor_sha256']); }); test('a stranger gets ERR_ACCESS_INVALID', () { final file = fromHex(inner['hex']! as String); final stranger = X25519Identity(sha256('stranger'.codeUnits)); final r = resultOf(file, [ AccessIdentity(inner['slots']! as int, [stranger]), ]); expect(r, inner['stranger_result']); }); }); } }