// The locator against files, on the VM: every case of // testdata/vectors/locator.json with the result, the code and the text of // Go; every case of test/vectors/locator_vectors.json, of which // locator_test.dart runs a part also compiled to JavaScript; the sealed // locators whose plaintext passes 1 MiB, which Go reads through // io.LimitReader; and the opening of a fixture whose .dkk carries // datekeys.capsule, with the registry of locator.Standard. The constants of // locator_uris.g.dart and locator_vectors.g.dart are checked against their // files. @TestOn('vm') library; import 'dart:io'; import 'dart:typed_data'; import 'package:datekeys/src/accesskey.dart'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/chacha20poly1305.dart'; import 'package:datekeys/src/datekey.dart'; import 'package:datekeys/src/errors.dart'; import 'package:datekeys/src/extension.dart'; import 'package:datekeys/src/locator.dart'; import 'package:datekeys/src/open.dart' show OpenOptions, openCapsule; import 'package:datekeys/src/profile.dart'; import 'package:datekeys/src/release.dart' show suppliedRelease; import 'package:datekeys/src/sha256.dart'; import 'package:datekeys/src/sink.dart'; import 'package:test/test.dart'; import 'locator_support.dart'; import 'locator_test.dart' show locatorCases; import 'vectors/locator_uris.g.dart'; import 'vectors/locator_vectors.g.dart'; Json readJson(String path) => decodeJson(File(path).readAsStringSync()); void main() { final v = readJson('test/vectors/locator_vectors.json'); final uris = readJson('test/vectors/locator_uris.json'); final td = readJson('testdata/vectors/locator.json'); group('the constants', () { test('locator_uris.g.dart holds locator_uris.json', () { expect( locatorUrisJson, File('test/vectors/locator_uris.json').readAsStringSync(), ); }); test('locator_vectors.g.dart holds a part of locator_vectors.json', () { final part = decodeJson(locatorVectorsJson); List every(Object? xs, int n) => [ for (final (i, x) in (xs! as List).indexed) if (i % n == 0) x, ]; for (final MapEntry(:key, :value) in part.entries) { if (key == 'description') continue; final want = switch (key) { 'plaintexts' => every(v[key], 3), 'open' => (v[key]! as List).sublist(0, 24), 'parse' => every(v[key], 2), _ => v[key], }; expect(value, want, reason: key); } expect( part.keys.toSet(), v.keys.toSet().difference({'limit', 'capsule'}), ); }); }); group('locator_vectors.json', () => locatorCases(v)); group('testdata/vectors/locator.json', () { final p = quicknet(); final round = td['round']! as int; final release = releaseOf(v, round); final tdTexts = v['testdata']! as Json; test('the extension reads, its locator opens with the release of its ' 'round, and the rest in the host opens the envelope', () { final info = parseCapsuleInfo( Extension( capsuleExtensionId, 1, fromHex(td['extension_data']! as String), ), ); expect(info.note, td['note']); expect(compactDateKey(info.dateKey), td['datekey']); expect(info.dateKey.round, round); expect(toHex(info.sealed!), td['locator_sealed']); final loc = info.openLocator(release); expect(summaryOf(loc), expandSummary(tdTexts['main'])); expect(toHex(loc.marshal()), td['locator_plaintext']); expect(loc.marshal(), hasLength(locatorBlock)); expect(toHex(loc.envelopeKey), td['envelope_key']); expect(toHex(loc.restDigest), td['rest_digest']); expect(loc.restSize, td['rest_size']); expect(toHex(loc.capsuleDigest), td['capsule_digest']); expect(toHex(loc.envelopeHeader), td['envelope_header']); final addresses = (td['addresses']! as List).cast(); expect(loc.addresses, hasLength(addresses.length)); for (final (i, a) in addresses.indexed) { expect( [ loc.addresses[i].uri, loc.addresses[i].offset, loc.addresses[i].host, ], [a['uri'], a['offset'], a['host']], ); } final rest = loc.restIn( fromHex(td['host']! as String), td['host_offset']! as int, ); expect(toHex(rest), td['rest']); expect(toHex(loc.openEnvelope(rest)), td['dkc']); // The same with the top-level opening of Go's Open. final again = openLocator(p, round, release, info.sealed!); expect(summaryOf(again), summaryOf(loc)); }); test('the padding of each base', () { for (final c in (td['padding_cases']! as List).cast()) { expect(locatorPlaintextLength(c['base']! as int), c['total']); expect((c['total']! as int) % locatorBlock, 0); } }); test('every address, with the text of Go', () { final cases = (td['uri_cases']! as List).cast(); final texts = rows(uris, 'testdata'); expect(texts, hasLength(cases.length)); for (final (i, c) in cases.indexed) { final uri = c['uri']! as String; expect(texts[i][0], uri); final got = errorText(() => checkAddressUri(uri)); expect(got.isEmpty, c['ok'], reason: uri); expect(got, textOf(uris, texts[i][1]), reason: uri); expect(LocatorAddress(uri).host, texts[i][2], reason: uri); } }); test( 'the mixed locator reads, and a reader uses the address it accepts', () { final m = td['mixed']! as Json; final mixed = openLocator( p, round, release, fromHex(m['locator_sealed']! as String), ); final back = unmarshalLocator( fromHex(m['locator_plaintext']! as String), ); expect(summaryOf(mixed), expandSummary(tdTexts['mixed'])); expect(summaryOf(back), summaryOf(mixed)); final usable = []; for (final (i, a) in (m['addresses']! as List).cast().indexed) { expect( mixed.addresses[i], LocatorAddress(a['uri']! as String, a['offset']! as int), ); if (a['usable'] == true) usable.add(mixed.addresses[i]); } expect(usable, isNotEmpty); expect(mixed.usable, usable); // It cannot be written: a writer never writes an address that a reader // rejects. expect(() => mixed.marshal(), throwsA(isA())); final rest = mixed.restIn( fromHex(td['host']! as String), usable.first.offset, ); expect(toHex(mixed.openEnvelope(rest)), td['dkc']); }, ); test('the rests: rest_size bytes from the offset, used only when their ' 'SHA-256 is resto_digest, with the texts of Go', () { final loc = openLocator( p, round, release, fromHex(td['locator_sealed']! as String), ); final texts = rows(tdTexts, 'rest_cases'); for (final (i, c) in (td['rest_cases']! as List).cast().indexed) { Uint8List? r; expect( errorText( () => r = loc.restIn( fromHex(c['resource']! as String), c['offset']! as int, ), ), textOf(v, texts[i][0]), reason: '${c['name']}', ); var opens = false; if (r != null) { Uint8List? dkc; expect( errorText(() => dkc = loc.openEnvelope(r!)), textOf(v, texts[i][1]), reason: '${c['name']}', ); opens = dkc != null && toHex(dkc!) == td['dkc']; } expect(opens, c['opens'], reason: '${c['name']}'); } }); test('the data of the extension: a reader cannot use some, and that has ' 'ERR_EXTENSION_DATA_INVALID only', () { final texts = tdTexts['extension_cases']! as List; for (final (i, c) in (td['extension_cases']! as List).cast().indexed) { final x = Extension( capsuleExtensionId, 1, fromHex(c['extension_data']! as String), ); final want = textOf(v, texts[i]); expect( errorText(() => parseCapsuleInfo(x)), want, reason: '${c['name']}', ); expect(want.isEmpty, c['ok'], reason: '${c['name']}'); if (want.isNotEmpty) { expect( () => parseCapsuleInfo(x), throwsA( isA().having( (e) => e.code, 'code', ErrorCode.extensionDataInvalid, ), ), ); } } }); test('the plaintexts of the locator, with the texts of Go', () { final texts = tdTexts['plaintext_cases']! as List; for (final (i, c) in (td['plaintext_cases']! as List).cast().indexed) { final want = textOf(v, texts[i]); expect( errorText( () => unmarshalLocator(fromHex(c['locator_plaintext']! as String)), ), want, reason: '${c['name']}', ); expect(want.isEmpty, c['ok'], reason: '${c['name']}'); } }); }); test('openLocator reads at most 1 MiB of plaintext, as Go through ' 'io.LimitReader: what follows is neither decrypted nor checked', () { final lim = v['limit']! as Json; final header = fromHex(lim['header']! as String); final nonce = fromHex(lim['nonce']! as String); final streamKey = hkdfSha256( fromHex(lim['file_key']! as String), nonce, 'payload'.codeUnits, 32, ); expect(toHex(streamKey), lim['stream_key']); final plain = fromHex( (v['plaintext_bases']! as List)[0]! as String, ); final release = releaseOf(v, lim['round']! as int); for (final c in (lim['cases']! as List).cast()) { final chunks = rows(c, 'chunks'); final total = chunks.fold(0, (n, ch) => n + (ch[0]! as int)); final content = Uint8List(total) ..setRange(0, plain.length < total ? plain.length : total, plain); final out = BytesBuilder(copy: false) ..add(header) ..add(nonce); var at = 0; for (final (i, ch) in chunks.indexed) { final n = ch[0]! as int; // An 11-byte big-endian counter and the flag of the last chunk. final chunkNonce = Uint8List(12) ..[7] = i >> 24 ..[8] = (i >> 16) & 0xff ..[9] = (i >> 8) & 0xff ..[10] = i & 0xff ..[11] = ch[1] == true ? 1 : 0; final ct = chacha20Poly1305Seal( streamKey, chunkNonce, Uint8List.sublistView(content, at, at + n), ); if (ch[2] == true) ct[0] ^= 1; out.add(ct); at += n; } out.add(Uint8List(c['trailing']! as int)); final file = out.takeBytes(); expect([file.length, sha256Hex(file)], [c['length'], c['sha256']]); expect( errorText(() => openLocator(quicknet(), 1000, release, file)), textOf(v, c['text']), reason: '${c['name']}', ); } }); test('openCapsule with a .dkk that carries datekeys.capsule reports its ' 'data as Go does with locator.Standard', () async { final cap = v['capsule']! as Json; final name = cap['fixture']! as String; final dkc = File('testdata/fixtures/$name.dkc').readAsBytesSync(); final dkk = decodeAccessKey( File('testdata/fixtures/$name.dkk').readAsBytesSync(), ); final now = parseRfc3339(cap['now']! as String); for (final c in (cap['cases']! as List).cast()) { final k = AccessKey( credentialId: dkk.credentialId, capsuleId: dkk.capsuleId, type: dkk.type, material: Uint8List.fromList(dkk.material), verification: dkk.verification, critical: dkk.critical, noncritical: [ for (final x in rows(c, 'noncritical')) Extension(x[0]! as String, x[1]! as int, fromHex(x[3]! as String)), ], ); final file = encodeAccessKey(k); expect(sha256Hex(file), c['dkk_sha256']); final output = MemoryByteSink(); final opened = await openCapsule( dkc, OpenOptions( source: suppliedRelease(releaseOf(v, 1000)), now: () => now, accessKeyFile: file, output: output, extensions: c['standard'] == true ? const StandardExtensions() : null, ), ); expect(opened.error?.message ?? '', textOf(v, c['text'])); expect( [ for (final u in opened.unusableAccessKeyExtensions) [u.id, u.version, u.error.message], ], [ for (final u in rows(c, 'unusable')) [u[0], u[1], textOf(v, u[2])], ], ); expect([ for (final ch in opened.checks) [ch.step, ch.name, ch.ok, ch.detail, ch.error ?? ''], ], c['checks']); expect(sha256Hex(output.bytes ?? Uint8List(0)), c['content']); } }); }