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.
dateKeys-dart/test/open_inspect_test.dart

322 lines
11 KiB

// Steps 1 to 8 against Go: fixtures/*.inspect.json, byte for byte the
// output of `datekeys inspect -json`; every mutation of
// testdata/vectors/inspect_differential.json with its code and step, and the
// text that capsule.Inspect of Go gives (test/vectors/open_inspect.json);
// the views of headers with a public note; and the prefix of
// inspectedLength, which gives the result of the whole file, whatever is
// after it, from a source read in pieces.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:datekeys/src/age.dart' show maxAgeHeaderLength;
import 'package:test/test.dart';
import 'open_vectors_support.dart';
import 'source_support.dart';
import 'tlock_support.dart' show applyEdits;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
final _fixtures = <String, Uint8List>{};
Uint8List fixture(String file) => _fixtures.putIfAbsent(
file,
() => File('testdata/fixtures/$file').readAsBytesSync(),
);
ExtensionRegistry? registryOf(String name) => switch (name) {
'standard' => const StandardExtensions(),
'reject_all' => const RejectAll('not today'),
_ => null,
};
/// The last check of an inspection: [step, ok, code, detail].
List<Object?> last(Inspection r) {
final c = r.checks.last;
return [c.step, c.ok, c.error, c.detail];
}
Uint8List concat(List<List<int>> parts) => concatBytes(parts);
/// The prelude of a .dkc of [format] with the two lengths.
Uint8List preludeWith(int ph, int sc, {int format = 1, int flags = 0}) {
final b = Uint8List(16);
b.setRange(0, 4, 'DKC1'.codeUnits);
b[4] = format;
b[5] = flags;
ByteData.sublistView(b)
..setUint32(8, ph)
..setUint32(12, sc);
return b;
}
void main() {
final names =
Directory('testdata/fixtures')
.listSync()
.map((f) => f.uri.pathSegments.last)
.where((n) => n.endsWith('.dkc'))
.toList()
..sort();
group('fixtures/*.inspect.json', () {
for (final name in names) {
test(name, () async {
final want = File(
'testdata/fixtures/${name.replaceAll('.dkc', '.inspect.json')}',
).readAsStringSync();
final dkc = fixture(name);
final r = inspectCapsule(dkc);
expect(inspectJson(inspectView(r, file: name)), want);
final s = await inspectCapsuleSource(ChunkySource(dkc, 1000));
expect(inspectJson(inspectView(s, file: name)), want);
// The other fields, from the record of the fixture.
final rec = readJson(
'testdata/fixtures/${name.replaceAll('.dkc', '.json')}',
);
final p = parsePrelude(dkc);
List<Object?> infos(List<StanzaInfo>? s) => [
for (final x in s!) {'type': x.type, 'args': x.args},
];
expect(
[
r.valid,
r.format?.version,
r.payloadOffset,
toHex(r.publicHeader!),
r.profile!.id,
formatRfc3339(r.unlockAt!),
canonical(infos(r.outerStanzas)),
canonical(infos(r.payloadStanzas)),
r.unusableExtensions,
r.publicNote,
r.unusableNote,
],
[
true,
rec['format'],
16 + p.publicHeaderLen + p.sealedControlLen,
rec['public_header'],
'datekeys:quicknet:v1',
rec['unlock_at'],
canonical(rec['outer_stanzas']),
canonical(rec['payload_stanzas']),
isEmpty,
null,
false,
],
);
});
}
});
final inspectVectors = readJson('test/vectors/open_inspect.json');
test(
'inspect_differential.json: every mutation, with the text of Go',
() async {
final f = readJson('testdata/vectors/inspect_differential.json');
expect(f['spec'], specVersion);
final bases = [
for (final b in (f['bases']! as List).cast<Json>()) str(b, 'file'),
];
final mutations = (f['mutations']! as List).cast<Json>();
expect(mutations, hasLength(5110));
final texts = (inspectVectors['texts']! as List).cast<String>();
final results = (inspectVectors['results']! as List).cast<int>();
expect(results, hasLength(mutations.length));
for (var i = 0; i < mutations.length; i++) {
final m = mutations[i];
final dkc = applyEdits(
fixture(bases[m['base']! as int]),
m['edits']! as List<Object?>,
);
final r = inspectCapsule(dkc);
final want = m['result'] == 'ok'
? [8, true, null]
: [m['step'], false, m['result']];
expect(last(r).sublist(0, 3), want, reason: 'mutation $i');
expect(r.valid, m['result'] == 'ok', reason: 'mutation $i');
if (results[i] >= 0) {
expect(r.error!.message, texts[results[i]], reason: 'mutation $i');
}
// The prefix that inspectedLength names gives the same inspection.
if (i % 5 == 0) {
final s = await inspectCapsuleSource(ChunkySource(dkc, 777));
expect(
inspectJson(inspectView(s, file: 'x')),
inspectJson(inspectView(r, file: 'x')),
reason: 'mutation $i',
);
}
}
},
);
test('open_inspect.json: the views of the notes and the extensions', () {
final views = (inspectVectors['views']! as List).cast<Json>();
expect(views, hasLength(10));
for (final v in views) {
final dkc = capsuleOf(v, fixture);
final r = inspectCapsule(dkc, extensions: registryOf(str(v, 'registry')));
expect(
inspectJson(inspectView(r, file: 'capsule.dkc')),
v['view'],
reason: str(v, 'name'),
);
}
});
group('inspectedLength', () {
final timeOnly = fixture('time_only.dkc');
final r = inspectCapsule(timeOnly);
final payloadStart = r.payloadOffset!;
test('stops one byte after the largest age header of PAYLOAD_AGE', () {
expect(
inspectedLength(1000000000, timeOnly.sublist(0, 16)),
payloadStart + maxAgeHeaderLength + 1,
);
expect(
inspectedLength(timeOnly.length, timeOnly.sublist(0, 16)),
timeOnly.length,
);
});
test('reads short files whole', () {
expect(inspectedLength(7, timeOnly.sublist(0, 7)), 7);
expect(inspectedLength(0, Uint8List(0)), 0);
});
final rejected = <String, Uint8List>{
'not DKC1': Uint8List.fromList('DKC2'.codeUnits + Uint8List(12)),
'version 4': preludeWith(10, 10, format: 4),
'FLAGS 1': preludeWith(10, 10, flags: 1),
'PUBLIC_HEADER_LEN 0': preludeWith(0, 10),
'SEALED_CONTROL_LEN 0': preludeWith(10, 0),
'PUBLIC_HEADER_LEN above 1 MiB': preludeWith((1 << 20) + 1, 10),
'SEALED_CONTROL_LEN above 64 MiB': preludeWith(10, (64 << 20) + 1),
'both lengths 0xffffffff': preludeWith(0xffffffff, 0xffffffff),
};
test('reads 16 bytes of a prelude that steps 1 and 2 reject', () async {
final tail = Uint8List(3 << 20)..fillRange(0, 3 << 20, 0xff);
for (final MapEntry(key: what, value: head) in rejected.entries) {
expect(inspectedLength(1 << 40, head), 16, reason: what);
expect(inspectedLength(16, head), 16, reason: what);
final dkc = concat([head, tail]);
final s = ChunkySource(dkc);
final r = await inspectCapsuleSource(s);
expect(s.farthest, 16, reason: what);
expect(last(r), last(inspectCapsule(dkc)), reason: what);
}
});
test('reaches at most the largest sections and one age header', () {
expect(
inspectedLength(1 << 40, preludeWith(1 << 20, 64 << 20)),
16 + (1 << 20) + (64 << 20) + maxAgeHeaderLength + 1,
);
});
test('reads every fixture whole, and a .dkk in 16 bytes', () async {
for (final name in names) {
final dkc = fixture(name);
final s = ChunkySource(dkc);
await inspectCapsuleSource(s);
expect(s.farthest, dkc.length, reason: name);
}
final dkk = fixture('time_and_key_portable.dkk');
final s = ChunkySource(dkk);
final r = await inspectCapsuleSource(s);
expect(
[s.farthest, ...last(r).sublist(0, 3)],
[16, 1, false, 'ERR_INVALID_MAGIC'],
);
});
test(
'skips the payload after its age header, with the same result',
() async {
final big = concat([
timeOnly,
Uint8List(3 << 20)..fillRange(0, 3 << 20, 0x61),
]);
final s = ChunkySource(big, 1 << 20);
final r = await inspectCapsuleSource(s);
expect(s.farthest, payloadStart + maxAgeHeaderLength + 1);
expect(last(r), last(inspectCapsule(big)));
},
);
test('keeps the result of an age header at and around 2 MiB', () async {
final parts = splitCapsule(timeOnly);
Uint8List frame(List<int> payload) =>
concat([timeOnly.sublist(0, payloadStart), payload]);
final intro = 'age-encryption.org/v1\n-> X25519 '.codeUnits;
for (final end in [
maxAgeHeaderLength - 2,
maxAgeHeaderLength - 1,
maxAgeHeaderLength,
maxAgeHeaderLength + 1,
]) {
final line = Uint8List(end - intro.length)
..fillRange(0, end - intro.length, 0x41);
final dkc = frame(
concat([intro, line, '\n\n--- AAAA\n'.codeUnits, Uint8List(1 << 20)]),
);
final s = ChunkySource(dkc, 1 << 20);
final r = await inspectCapsuleSource(s);
expect(s.farthest, payloadStart + maxAgeHeaderLength + 1);
expect(last(r), last(inspectCapsule(dkc)), reason: '$end');
}
final endless = frame(
concat([intro, Uint8List(3 << 20)..fillRange(0, 3 << 20, 0x41)]),
);
final r = await inspectCapsuleSource(ChunkySource(endless, 1 << 20));
expect(last(r), last(inspectCapsule(endless)));
expect(last(r), [
6,
false,
'ERR_INTEGRITY',
'capsule: PAYLOAD_AGE: agewrap: not an age v1 header: malformed, '
'truncated or beyond the parser limits: ERR_INTEGRITY',
]);
expect(parts.prelude.format, CapsuleFormat.format1);
});
});
group('maxAccessKeyRead', () {
String decodeError(Uint8List dkk) {
try {
decodeAccessKey(dkk).wipe();
return 'ok';
} on DateKeysException catch (e) {
return e.message;
}
}
final dkk = fixture('time_and_key_portable.dkk');
test('is one byte past the largest valid .dkk, with the same error', () {
expect(maxAccessKeyRead, 12 + (16 << 20) + 1);
final long = concat([dkk, Uint8List(16 << 20)]);
final prefix = long.sublist(0, maxAccessKeyRead);
expect(decodeError(prefix), decodeError(long));
expect(decodeError(long), contains('data after BODY_CBOR'));
final over = concat([
dkk.sublist(0, 8),
[1, 0, 0, 1],
Uint8List((16 << 20) + 8),
]);
expect(decodeError(over.sublist(0, maxAccessKeyRead)), decodeError(over));
expect(decodeError(over), contains('outside 1..'));
});
});
}

Powered by TurnKey Linux.