You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
332 lines
11 KiB
332 lines
11 KiB
// 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<int> 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',
|
|
);
|
|
});
|
|
});
|
|
}
|