// The frames of a .dkc and of a .dkk (lib/src/framing.dart), the frame of // BODY (lib/src/body.dart) and the incremental SHA-256 of the // capsule_digest (lib/src/digest.dart), as framing.test.ts, body.test.ts // and digest.test.ts of datekeys-ts, on capsules built here: the order of // the checks, the steps of the inspection, the views and the limits. The // texts of Go of every failure are in the differential // (formats_framing.json and formats_body.json). It reads no file: it runs on // the VM and compiled to JavaScript. library; import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; import 'package:datekeys/src/big_endian.dart'; import 'package:datekeys/src/body.dart'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/digest.dart'; import 'package:datekeys/src/errors.dart'; import 'package:datekeys/src/framing.dart'; import 'package:test/test.dart'; String codeOf(void Function() body) { try { body(); return 'ok'; } on DateKeysException catch (e) { return e.code.code; } } /// A .dkc of [format] whose sections are [header], [control] and /// [payload] bytes of 1, 2 and 3. Uint8List capsule(int format, int header, int control, int payload) { final out = Uint8List(16 + header + control + payload) ..setAll(0, 'DKC1'.codeUnits); out[4] = format; writeUint32BE(out, 8, header); writeUint32BE(out, 12, control); out ..fillRange(16, 16 + header, 1) ..fillRange(16 + header, 16 + header + control, 2) ..fillRange(16 + header + control, out.length, 3); return out; } void main() { group('PRELUDE', () { final ok = capsule(1, 121, 446, 0); Uint8List withByte(int i, int v) => Uint8List.fromList(ok)..[i] = v; test('reads the format and the lengths, and writes them back', () { for (final f in CapsuleFormat.values) { final p = parsePrelude(withByte(4, f.version)); expect(p, Prelude(f, 121, 446)); expect(preludeBytes(p), withByte(4, f.version).sublist(0, 16)); expect(payloadOffset(p), 16 + 121 + 446); expect(f.isPadded, f != CapsuleFormat.format1); expect(CapsuleFormat.fromVersion(f.version), f); } expect([0, 4, 255].map(CapsuleFormat.fromVersion), everyElement(isNull)); }); test('checks the magic, a whole prelude, the version, FLAGS and ' 'RESERVED, and then the lengths, in the order of spec §23', () { expect(codeOf(() => parsePrelude(Uint8List(0))), 'ERR_INVALID_MAGIC'); expect(codeOf(() => parsePrelude(ok.sublist(0, 3))), 'ERR_INVALID_MAGIC'); expect( codeOf(() => parsePrelude(withByte(3, 0x32))), 'ERR_INVALID_MAGIC', ); expect(codeOf(() => parsePrelude(ok.sublist(0, 4))), 'ERR_INTEGRITY'); expect(codeOf(() => parsePrelude(ok.sublist(0, 15))), 'ERR_INTEGRITY'); for (final v in [0, 4, 0xff]) { expect( codeOf(() => parsePrelude(withByte(4, v))), 'ERR_UNSUPPORTED_VERSION', ); } // Every bit of FLAGS and of RESERVED, alone. for (final i in [5, 6, 7]) { for (var bit = 1; bit < 256; bit *= 2) { expect( codeOf(() => parsePrelude(withByte(i, bit))), 'ERR_INVALID_FLAGS', reason: 'byte $i, bit $bit', ); } } Uint8List lengths(int h, int s) { final b = Uint8List.fromList(ok.sublist(0, 16)); writeUint32BE(b, 8, h); writeUint32BE(b, 12, s); return b; } expect( parsePrelude(lengths(maxPublicHeaderLen, maxSealedControlLen)), Prelude(CapsuleFormat.format1, maxPublicHeaderLen, maxSealedControlLen), ); expect(parsePrelude(lengths(1, 1)), Prelude(CapsuleFormat.format1, 1, 1)); for (final (h, s) in [ (0, 1), (1, 0), (maxPublicHeaderLen + 1, 1), (1, maxSealedControlLen + 1), (0xffffffff, 0xffffffff), ]) { expect(codeOf(() => parsePrelude(lengths(h, s))), 'ERR_INTEGRITY'); } // The version and FLAGS come before the lengths. final both = lengths(0, 0)..[7] = 1; expect(codeOf(() => parsePrelude(both)), 'ERR_INVALID_FLAGS'); both[4] = 9; expect(codeOf(() => parsePrelude(both)), 'ERR_UNSUPPORTED_VERSION'); }); test('header_binding is the SHA-256 of the exact PRELUDE and header', () { final s = splitCapsule(capsule(2, 20, 30, 40)); expect( headerBinding(s.preludeBytes, s.publicHeader), crypto.sha256.convert(capsule(2, 20, 30, 40).sublist(0, 36)).bytes, ); }); }); group('splitCapsule', () { final dkc = capsule(3, 20, 30, 40); FramingException failure(List b) { try { splitCapsule(b); } on FramingException catch (e) { return e; } fail('no failure'); } test('gives views of the sections', () { final s = splitCapsule(dkc); expect(s.prelude, Prelude(CapsuleFormat.format3, 20, 30)); expect(s.preludeBytes, dkc.sublist(0, 16)); expect(s.publicHeader, Uint8List(20)..fillRange(0, 20, 1)); expect(s.sealedControl, Uint8List(30)..fillRange(0, 30, 2)); expect(s.payload, Uint8List(40)..fillRange(0, 40, 3)); // Views, not copies. s.publicHeader[0] = 9; expect(dkc[16], 9); dkc[16] = 1; }); test('reports the step of each failure of the frame', () { for (final (cut, step, code) in [ (0, 1, 'ERR_INVALID_MAGIC'), (3, 1, 'ERR_INVALID_MAGIC'), (4, 1, 'ERR_INTEGRITY'), (15, 1, 'ERR_INTEGRITY'), (16, 3, 'ERR_INTEGRITY'), (35, 3, 'ERR_INTEGRITY'), ]) { final e = failure(dkc.sublist(0, cut)); expect([e.step, e.error.code.code], [step, code], reason: '$cut'); expect(e.prelude, step == 3 ? splitCapsule(dkc).prelude : isNull); expect('$e', e.error.message); } expect(failure(Uint8List.fromList(dkc)..[4] = 9).step, 2); expect(failure(Uint8List.fromList(dkc)..[5] = 1).step, 2); }); test('leaves a short SEALED_CONTROL to step 5', () { for (final cut in [36, 37, 65]) { final s = splitCapsule(dkc.sublist(0, cut)); expect(s.publicHeader, hasLength(20)); expect(s.sealedControl, isNull); expect(s.payload, isEmpty); } expect(truncatedSealedControl().code, ErrorCode.integrity); expect(splitCapsule(dkc.sublist(0, 66)).payload, isEmpty); expect(splitCapsule(dkc.sublist(0, 66)).sealedControl, hasLength(30)); }); }); group('DKK1', () { test('frames and unframes a body', () { final body = Uint8List.fromList([0xa0, 1, 2]); final framed = concatBytes([dkkPreludeBytes(body.length), body]); expect(toHex(framed.sublist(0, 12)), '444b4b310100000000000003'); expect(splitAccessKey(framed), body); }); test('checks the frame in the order of spec §40', () { final ok = concatBytes([dkkPreludeBytes(2), Uint8List(2)]); Uint8List withByte(int i, int v) => Uint8List.fromList(ok)..[i] = v; expect(codeOf(() => splitAccessKey(Uint8List(0))), 'ERR_INVALID_MAGIC'); expect(codeOf(() => splitAccessKey(withByte(0, 0))), 'ERR_INVALID_MAGIC'); expect(codeOf(() => splitAccessKey(ok.sublist(0, 11))), 'ERR_INTEGRITY'); expect( codeOf(() => splitAccessKey(withByte(4, 2))), 'ERR_UNSUPPORTED_VERSION', ); for (final i in [5, 6, 7]) { for (var bit = 1; bit < 256; bit *= 2) { expect( codeOf(() => splitAccessKey(withByte(i, bit))), 'ERR_INVALID_FLAGS', reason: 'byte $i, bit $bit', ); } } for (final n in [0, maxDkkBodyLen + 1]) { expect( codeOf(() => splitAccessKey(dkkPreludeBytes(n))), 'ERR_INTEGRITY', ); } // BODY_LEN 0 after FLAGS. expect( codeOf(() => splitAccessKey(dkkPreludeBytes(0)..[5] = 1)), 'ERR_INVALID_FLAGS', ); expect(codeOf(() => splitAccessKey(ok.sublist(0, 13))), 'ERR_INTEGRITY'); expect( codeOf(() => splitAccessKey(concatBytes([ok, Uint8List(1)]))), 'ERR_INTEGRITY', ); }); }); group('BODY', () { BodyFrame parse(int area, int security, int head, int l) => parseBodyFrame(bodyFrameBytes(BodyFrame(area, security, head)), l); test('reads the frame at its limits', () { for (final (area, security, head, l, c) in [ (512, 22, 53, 577, 0), (512, 512, 1, 525, 0), (65536, 1, 1 << 24, 12 + 65536 + (1 << 24), 0), (1024, 1024, 100, 1099511627776, 1099511626640), (32768, 22, 53, 8936830510563328, 8936830510563328 - 12 - 32768 - 53), ]) { final f = parse(area, security, head, l); expect(f, BodyFrame(area, security, head)); expect(contentLength(f, l), c); } expect( [areaUnit, maxAreaLen, areaLen, largeAreaLen, maxHeadLen], [512, 65536, 32768, 65536, 16777216], ); }); test('rejects every violation of the frame with ERR_INTEGRITY', () { for (final (area, security, head, l) in [ (512, 22, 53, 11), (0, 22, 53, 1000), (511, 22, 53, 1000), (513, 22, 53, 1000), (66048, 22, 53, 100000), (512, 0, 53, 1000), (512, 513, 53, 1000), (512, 22, 0, 1000), (512, 22, (1 << 24) + 1, 1 << 30), (512, 22, 53, 576), ]) { expect( codeOf(() => parse(area, security, head, l)), 'ERR_INTEGRITY', reason: '$area $security $head $l', ); } expect(() => parseBodyFrame(Uint8List(11), 100), throwsArgumentError); }); test('requires zeros after SECURITY_CBOR up to AREA_LEN', () { final area = Uint8List(512)..fillRange(0, 22, 9); checkArea(area, 22); area[511] = 1; expect(codeOf(() => checkArea(area, 22)), 'ERR_INTEGRITY'); checkArea(area, 512); }); }); group('the digest of a .dkc', () { final dkc = Uint8List.fromList([ for (var i = 0; i < 200003; i++) i * 7 % 251, ]); final want = crypto.sha256.convert(dkc).bytes; test('is the SHA-256 of the whole file, however it is cut', () { expect(capsuleDigest(dkc), want); for (final cut in [1, 7, 63, 64, 65, 4096, 65536, dkc.length]) { final h = sha256Hasher(); for (var at = 0; at < dkc.length; at += cut) { h.add(dkc.sublist(at, at + cut < dkc.length ? at + cut : dkc.length)); } expect(h.digest(), want, reason: '$cut'); expect(h.digest, throwsStateError); expect(() => h.add([1]), throwsStateError); } final empty = Sha256Hasher()..add(Uint8List(0)); expect(empty.digest(), crypto.sha256.convert(const []).bytes); }); test('of a stream', () async { final stream = Stream.fromIterable([ dkc.sublist(0, 1), dkc.sublist(1, 70000), dkc.sublist(70000), ]); expect(await sha256Stream(stream), want); expect( await sha256Stream(const Stream.empty()), crypto.sha256.convert(const []).bytes, ); }); test('a different one is ERR_ACCESS_INVALID', () { checkCapsuleDigest(capsuleDigest(dkc), want); final other = Uint8List.fromList(want)..[31] ^= 1; expect( codeOf(() => checkCapsuleDigest(capsuleDigest(dkc), other)), 'ERR_ACCESS_INVALID', ); expect( codeOf( () => checkCapsuleDigest(capsuleDigest(dkc), want.sublist(0, 31)), ), 'ERR_ACCESS_INVALID', ); }); }); }