// Every official fixture of testdata/ through the formats: the PRELUDE, the // sections and header_binding of each .dkc, its PUBLIC_HEADER and its // CONTROL_CBOR, decoded and written back to the same bytes, the profile and // the round time of its DateKey, P = rule(L), and in format 3 the frame of // BODY, the security area and the place of the head and of the files; and // each .dkk, whose capsule_digest is the SHA-256 of its .dkc. The values are // those of the records .json and .dkk.json, which Go wrote. @TestOn('vm') library; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:datekeys/src/accesskey.dart'; import 'package:datekeys/src/body.dart'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/control.dart'; import 'package:datekeys/src/datekey.dart'; import 'package:datekeys/src/digest.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'; typedef Json = Map; Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json; Uint8List readBytes(String path) => File(path).readAsBytesSync(); /// The extensions of a record, as `{critical, id, version, data}`. List recordExtensions(Object? v) => [ for (final e in ((v as List?) ?? const []).cast()) [e['critical'], e['id'], e['version'], e['data']], ]; List extensionsOf(List critical, List non) => [ for (final (c, list) in [(true, critical), (false, non)]) for (final e in list) [c, e.id, e.version, e.data == null ? null : toHex(e.data!)], ]; String codeOf(void Function() body) { try { body(); return 'ok'; } on DateKeysException catch (e) { return e.code.code; } } void main() { 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)) .toList() ..sort(); final keys = Directory('testdata/fixtures') .listSync() .map((f) => f.uri.pathSegments.last) .where((n) => n.endsWith('.dkk')) .toList() ..sort(); test('there are the 26 capsules and the 6 access keys of spec §67, §68', () { expect(names, hasLength(26)); expect(keys, hasLength(6)); }); for (final name in names) { test(name, () { final r = readJson('testdata/fixtures/$name.json'); final dkc = readBytes('testdata/fixtures/$name.dkc'); expect(toHex(capsuleDigest(dkc)), r['sha256']); // The frame. final format = CapsuleFormat.fromVersion(r['format']! as int)!; final prelude = parsePrelude(dkc.sublist(0, dkcPreludeSize)); expect(prelude.format, format); expect(toHex(preludeBytes(prelude)), r['prelude']); final s = splitCapsule(dkc); expect(s.prelude, prelude); expect(toHex(s.preludeBytes), r['prelude']); expect(toHex(s.publicHeader), r['public_header']); expect(s.sealedControl, hasLength(prelude.sealedControlLen)); expect(s.payload.length, dkc.length - payloadOffset(prelude)); expect( toHex(headerBinding(s.preludeBytes, s.publicHeader)), r['header_binding'], ); // PUBLIC_HEADER, its profile and its round. final h = decodeHeader(s.publicHeader); expect( [h.capsuleIdHex, compactDateKey(h.dateKey), h.policy.label], [r['capsule_id'], r['datekey'], r['access_policy']], ); expect( extensionsOf(h.critical, h.noncritical), recordExtensions(r['header_extensions']), ); expect(toHex(encodeHeader(h)), r['public_header']); final p = defaultRegistry().lookup(h.dateKey.profileId)!; validateDateKey(h.dateKey, p); expect(formatRfc3339(unlockAt(h.dateKey, p)!), r['unlock_at']); // CONTROL_CBOR, in its format and in no other. final control = decodeControl( fromHex(r['control_cbor']! as String), format, ); expect( [toHex(control.headerBinding), toHex(control.payloadIdentity)], [r['header_binding'], r['payload_identity']], ); expect( extensionsOf(control.critical, control.noncritical), recordExtensions(r['control_extensions']), ); expect(toHex(encodeControl(control, format)), r['control_cbor']); for (final other in CapsuleFormat.values.where((f) => f != format)) { expect( codeOf( () => decodeControl(fromHex(r['control_cbor']! as String), other), ), 'ERR_UNSUPPORTED_VERSION', ); } final plaintext = readBytes('testdata/fixtures/${r['plaintext_file']}'); if (!format.isPadded) { expect([control.payloadLength, control.padding], [null, null]); expect(r.containsKey('padding'), isFalse); expect(plaintext.length, r['payload_length']); return; } final l = control.payloadLength!; final rule = control.padding!; expect([l, rule.code], [r['payload_length'], r['padding']]); final padded = paddedLength(l, rule); expect(padded, r['padded_length']); expect(plaintext.length, l); // The plaintext of PAYLOAD_AGE is the content and zeros up to P. final check = PaddingCheck(l, padded); final content = BytesBuilder() ..add(check.add(plaintext)) ..add(check.add(Uint8List(padded - l))); check.close(); expect(content.takeBytes(), plaintext); if (format != CapsuleFormat.format3) return; // BODY: its frame, the security area, the head and the files. final frame = parseBodyFrame(plaintext.sublist(0, bodyFrameSize), l); expect(frame.areaLen, r['area_len']); final area = plaintext.sublist( bodyFrameSize, bodyFrameSize + frame.areaLen, ); checkArea(area, frame.securityLen); expect(toHex(area.sublist(0, frame.securityLen)), r['security_cbor']); final headStart = bodyFrameSize + frame.areaLen; expect( toHex(plaintext.sublist(headStart, headStart + frame.headLen)), r['head_cbor'], ); expect(headStart + frame.headLen, r['content_offset']); final files = (r['files'] as List?) ?? const []; expect( contentLength(frame, l), [ 0, for (final f in files) (f! as Json)['size']! as int, ].reduce((a, b) => a + b), ); }); } for (final key in keys) { test(key, () { final r = readJson('testdata/fixtures/$key.json'); final raw = readBytes('testdata/fixtures/$key'); expect(toHex(capsuleDigest(raw)), r['sha256']); final k = decodeAccessKey(raw); expect( [ toHex(k.credentialId), toHex(k.capsuleId), k.type, toHex(k.material), k.verification == null ? null : toHex(k.verification!.capsuleDigest), ], [ r['credential_id'], r['capsule_id'], r['access_type'], r['access_material'], r['capsule_digest'], ], ); expect( extensionsOf(k.critical, k.noncritical), recordExtensions(r['extensions']), ); checkAccessKeyMaterial(k); // capsule_digest is the SHA-256 of the exact bytes of its .dkc, and its // capsule_id that of the header of that capsule. final dkc = readBytes('testdata/fixtures/${r['capsule']}'); checkCapsuleDigest(capsuleDigest(dkc), k.verification!.capsuleDigest); expect( decodeHeader(splitCapsule(dkc).publicHeader).capsuleId, k.capsuleId, ); expect( codeOf( () => checkCapsuleDigest( capsuleDigest(raw), k.verification!.capsuleDigest, ), ), 'ERR_ACCESS_INVALID', ); expect(toHex(encodeAccessKey(k)), toHex(raw)); k.wipe(); expect(k.material.every((b) => b == 0), isTrue); }); } }