// PUBLIC_HEADER, CONTROL_CBOR, the .dkk and the Provider Profile // (lib/src/header.dart, control.dart, accesskey.dart and profile.dart), as // header.test.ts, control.test.ts, accesskey.test.ts and profile.test.ts of // datekeys-ts: values of a fixed seed that encode and decode back to // themselves, the copies of the secrets and their wiping, the texts that // hide them, and the pinned registry. The values and texts of Go are in the // differential (formats_header.json, formats_control.json, // formats_framing.json, formats_profile.json and formats_encode.json). It // reads no file: it runs on the VM and compiled to JavaScript. library; import 'dart:math'; import 'dart:typed_data'; import 'package:datekeys/src/accesskey.dart'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/control.dart'; import 'package:datekeys/src/datekey.dart'; import 'package:datekeys/src/errors.dart'; import 'package:datekeys/src/extension.dart'; import 'package:datekeys/src/framing.dart'; import 'package:datekeys/src/header.dart'; import 'package:datekeys/src/padding.dart'; import 'package:datekeys/src/profile.dart'; import 'package:test/test.dart'; String codeOf(void Function() body) { try { body(); return 'ok'; } on DateKeysException catch (e) { return e.code.code; } } Uint8List bytesOf(Random r, int n) => Uint8List.fromList([for (var i = 0; i < n; i++) r.nextInt(256)]); // 0 to 3 extensions with distinct identifiers, unsorted. List extensionsOf(Random r, String prefix) => [ for (var i = 0; i < r.nextInt(4); i++) Extension( '$prefix${r.nextInt(1000)}x$i', r.nextInt(3), r.nextBool() ? bytesOf(r, 1 + r.nextInt(5)) : null, ), ]; void main() { test('headers encode and decode back to themselves', () { final r = Random(17); for (var i = 0; i < 200; i++) { final h = Header( capsuleId: bytesOf(r, 16), dateKey: DateKey('datekeys:quicknet:v1', 1 + r.nextInt(1 << 30)), policy: AccessPolicy.values[r.nextInt(2)], critical: extensionsOf(r, 'c'), noncritical: extensionsOf(r, 'n'), ); final b = encodeHeader(h); final back = decodeHeader(b); expect( [back.capsuleId, back.dateKey, back.policy], [h.capsuleId, h.dateKey, h.policy], ); expect(back.critical, canonicalExtensions(h.critical)); expect(back.noncritical, canonicalExtensions(h.noncritical)); expect(encodeHeader(back), b); expect(back.capsuleIdHex, toHex(h.capsuleId)); } }); test('access policies by code and by name', () { expect( [ for (final p in AccessPolicy.values) [p.code, p.label, '$p'], ], [ [0, 'time_only', 'time_only'], [1, 'time_and_key', 'time_and_key'], ], ); expect([0, 1, 2].map(AccessPolicy.fromCode).toList(), [ AccessPolicy.timeOnly, AccessPolicy.timeAndKey, null, ]); expect(parsePolicy('time_and_key'), AccessPolicy.timeAndKey); expect(() => parsePolicy('x'), throwsArgumentError); expect( codeOf( () => encodeHeader( Header( capsuleId: Uint8List(15), dateKey: const DateKey('a', 1), policy: AccessPolicy.timeOnly, ), ), ), 'ERR_NON_CANONICAL_CBOR', ); }); test('controls encode and decode back, of a length that does not depend ' 'on L', () { final r = Random(19); for (var i = 0; i < 200; i++) { final format = CapsuleFormat.values[i % 3]; final c = Control( headerBinding: bytesOf(r, 32), payloadIdentity: bytesOf(r, 32), critical: extensionsOf(r, 'c'), noncritical: extensionsOf(r, 'n'), payloadLength: format.isPadded ? [0, 1, 4294967297, maxPayloadLength, r.nextInt(1 << 30)][i % 5] : null, padding: format.isPadded ? PaddingRule.values[r.nextInt(2)] : null, ); final b = encodeControl(c, format); final back = decodeControl(b, format); expect( [back.headerBinding, back.payloadIdentity, back.payloadLength], [c.headerBinding, c.payloadIdentity, c.payloadLength], ); expect(back.padding, c.padding); expect(encodeControl(back, format), b); if (c.critical.isEmpty && c.noncritical.isEmpty) { expect(b.length, format.isPadded ? 103 : 91); expect(b[20], format.version); } for (final other in CapsuleFormat.values.where((f) => f != format)) { expect( codeOf(() => decodeControl(b, other)), 'ERR_UNSUPPORTED_VERSION', ); } } }); test('a control keeps its own copy of I_PAYLOAD, wipes it, and never ' 'prints it', () { final c = Control( headerBinding: Uint8List(32), payloadIdentity: Uint8List(32)..fillRange(0, 32, 0xab), payloadLength: 1, padding: PaddingRule.reforzado, ); final b = encodeControl(c, CapsuleFormat.format2); final back = decodeControl(b, CapsuleFormat.format2); b.fillRange(0, b.length, 0); expect(back.payloadIdentity, Uint8List(32)..fillRange(0, 32, 0xab)); expect('$back', isNot(contains('abab'))); expect('$back', contains('REDACTED')); back.wipe(); expect(back.payloadIdentity, Uint8List(32)); expect(() => encodeControl(c, CapsuleFormat.format1), throwsArgumentError); expect( () => encodeControl( Control( headerBinding: Uint8List(32), payloadIdentity: Uint8List(32), padding: PaddingRule.bloque256, ), CapsuleFormat.format3, ), throwsArgumentError, ); }); test('access keys encode and decode back, and never print their ' 'material', () { final r = Random(23); for (var i = 0; i < 200; i++) { final k = AccessKey( credentialId: bytesOf(r, 16), capsuleId: bytesOf(r, 16), type: accessTypeX25519, material: bytesOf(r, 32), verification: r.nextBool() ? Verification(bytesOf(r, 32)) : null, critical: extensionsOf(r, 'c'), noncritical: extensionsOf(r, 'n'), ); final dkk = encodeAccessKey(k); final back = decodeAccessKey(dkk); expect( [back.credentialId, back.capsuleId, back.type, back.material], [k.credentialId, k.capsuleId, k.type, k.material], ); expect(back.verification, k.verification); expect(back.critical, canonicalExtensions(k.critical)); expect(encodeAccessKey(back), dkk); expect('$back', isNot(contains(toHex(k.material)))); // The key keeps its own copy of the material. dkk.fillRange(0, dkk.length, 0); expect(back.material, k.material); back.wipe(); expect(back.material, Uint8List(32)); expect(codeOf(() => checkAccessKeyMaterial(back)), 'ok'); } expect( codeOf( () => checkAccessKeyMaterial( AccessKey( credentialId: Uint8List(16), capsuleId: Uint8List(16), type: 'X25519', material: Uint8List(32), ), ), ), 'ERR_ACCESS_INVALID', ); expect( codeOf( () => marshalAccessKeyBody( AccessKey( credentialId: Uint8List(15), capsuleId: Uint8List(16), type: accessTypeX25519, material: Uint8List(32), ), ), ), 'ERR_NON_CANONICAL_CBOR', ); }); group('Provider Profiles', () { final q = quicknet(); test('Quicknet is its pinned CBOR, and its range ends in 9999', () { expect(toHex(canonicalCbor(q)), quicknetCanonicalCbor); expect(toHex(profileHash(q)), quicknetProfileHash); expect(decodeProfile(fromHex(quicknetCanonicalCbor)), q); validateProfile(q); expect(q.chainHashHex, quicknetChainHash); expect( formatRfc3339(roundTime(q, q.maxRound)), startsWith('9999-12-31T23:59:5'), ); expect(q.copyWith(genesisTime: maxUnixTime).maxRound, 1); expect(q.copyWith(genesisTime: maxUnixTime + 1).maxRound, 0); expect(q.copyWith(period: 0).maxRound, 0); }); test('the default registry is built once and hands out copies', () { final reg = defaultRegistry(); expect(defaultRegistry(), same(reg)); final p = reg.lookup(quicknetId)!; expect(p, q); p.chainHash.fillRange(0, 32, 0); expect(reg.lookup(quicknetId), q); expect(reg.lookup('datekeys:evmnet:v1'), isNull); final copy = q.copy(); copy.publicKey[0] ^= 1; expect(quicknet(), isNot(copy)); }); test('a network "default", or none, is left out of the chain hash', () { final d = q.copyWith(network: 'default'); expect(chainInfoHash(d), chainInfoHash(q.copyWith(network: ''))); expect( chainInfoHash(d), isNot(chainInfoHash(q.copyWith(network: 'defaults'))), ); validateProfile(d.copyWith(chainHash: chainInfoHash(d))); expect( codeOf(() => chainInfoHash(q.copyWith(period: maxChainHashPeriod + 1))), 'ERR_UNKNOWN_PROFILE', ); expect( chainInfoHash(q.copyWith(period: maxChainHashPeriod)), hasLength(32), ); }); test('a profile_id is [a-z0-9:._-], from 1 to 128, the first of ' '[a-z0-9]', () { for (final s in ['datekeys:quicknet:v1', '0${'a' * 127}', 'a', '0:._-']) { expect(validId(s), isTrue, reason: s); } for (final s in [ '', 'A', ':a', '.a', 'a b', 'a/b', 'é', '0${'a' * 128}', 'a\n', ]) { expect(validId(s), isFalse, reason: s); } }); }); }