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/locator_vm_test.dart

273 lines
9.3 KiB

// The locator against files, on the VM: every case of
// testdata/vectors/locator.json but the data of the extension, with the
// result and the text of Go; every case of test/vectors/locator_vectors.json, of which
// locator_test.dart runs a part also compiled to JavaScript; the sealed
// locators whose plaintext passes 1 MiB, which Go reads through
// io.LimitReader. The constants of locator_uris.g.dart and
// locator_vectors.g.dart are checked against their files.
@TestOn('vm')
library;
import 'dart:io';
import 'dart:typed_data';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/chacha20poly1305.dart';
import 'package:datekeys/src/locator.dart';
import 'package:datekeys/src/profile.dart';
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'locator_support.dart';
import 'locator_test.dart' show locatorCases;
import 'vectors/locator_uris.g.dart';
import 'vectors/locator_vectors.g.dart';
Json readJson(String path) => decodeJson(File(path).readAsStringSync());
void main() {
final v = readJson('test/vectors/locator_vectors.json');
final uris = readJson('test/vectors/locator_uris.json');
final td = readJson('testdata/vectors/locator.json');
group('the constants', () {
test('locator_uris.g.dart holds locator_uris.json', () {
expect(
locatorUrisJson,
File('test/vectors/locator_uris.json').readAsStringSync(),
);
});
test('locator_vectors.g.dart holds a part of locator_vectors.json', () {
final part = decodeJson(locatorVectorsJson);
List<Object?> every(Object? xs, int n) => [
for (final (i, x) in (xs! as List<Object?>).indexed)
if (i % n == 0) x,
];
for (final MapEntry(:key, :value) in part.entries) {
if (key == 'description') continue;
final want = switch (key) {
'plaintexts' => every(v[key], 3),
'open' => (v[key]! as List<Object?>).sublist(0, 24),
'parse' => every(v[key], 2),
_ => v[key],
};
expect(value, want, reason: key);
}
expect(
part.keys.toSet(),
v.keys.toSet().difference({'limit', 'capsule'}),
);
});
});
group('locator_vectors.json', () => locatorCases(v));
group('testdata/vectors/locator.json', () {
final p = quicknet();
final round = td['round']! as int;
final release = releaseOf(v, round);
final tdTexts = v['testdata']! as Json;
test('its locator opens with the release of its round, and the rest '
'in the host opens the envelope', () {
final loc = openLocator(
p,
round,
release,
fromHex(td['locator_sealed']! as String),
);
expect(summaryOf(loc), expandSummary(tdTexts['main']));
expect(toHex(loc.marshal()), td['locator_plaintext']);
expect(loc.marshal(), hasLength(locatorBlock));
expect(toHex(loc.envelopeKey), td['envelope_key']);
expect(toHex(loc.restDigest), td['rest_digest']);
expect(loc.restSize, td['rest_size']);
expect(toHex(loc.capsuleDigest), td['capsule_digest']);
expect(toHex(loc.envelopeHeader), td['envelope_header']);
final addresses = (td['addresses']! as List<Object?>).cast<Json>();
expect(loc.addresses, hasLength(addresses.length));
for (final (i, a) in addresses.indexed) {
expect(
[
loc.addresses[i].uri,
loc.addresses[i].offset,
loc.addresses[i].host,
],
[a['uri'], a['offset'], a['host']],
);
}
final rest = loc.restIn(
fromHex(td['host']! as String),
td['host_offset']! as int,
);
expect(toHex(rest), td['rest']);
expect(toHex(loc.openEnvelope(rest)), td['dkc']);
});
test('the padding of each base', () {
for (final c in (td['padding_cases']! as List<Object?>).cast<Json>()) {
expect(locatorPlaintextLength(c['base']! as int), c['total']);
expect((c['total']! as int) % locatorBlock, 0);
}
});
test('every address, with the text of Go', () {
final cases = (td['uri_cases']! as List<Object?>).cast<Json>();
final texts = rows(uris, 'testdata');
expect(texts, hasLength(cases.length));
for (final (i, c) in cases.indexed) {
final uri = c['uri']! as String;
expect(texts[i][0], uri);
final got = errorText(() => checkAddressUri(uri));
expect(got.isEmpty, c['ok'], reason: uri);
expect(got, textOf(uris, texts[i][1]), reason: uri);
expect(LocatorAddress(uri).host, texts[i][2], reason: uri);
}
});
test(
'the mixed locator reads, and a reader uses the address it accepts',
() {
final m = td['mixed']! as Json;
final mixed = openLocator(
p,
round,
release,
fromHex(m['locator_sealed']! as String),
);
final back = unmarshalLocator(
fromHex(m['locator_plaintext']! as String),
);
expect(summaryOf(mixed), expandSummary(tdTexts['mixed']));
expect(summaryOf(back), summaryOf(mixed));
final usable = <LocatorAddress>[];
for (final (i, a)
in (m['addresses']! as List<Object?>).cast<Json>().indexed) {
expect(
mixed.addresses[i],
LocatorAddress(a['uri']! as String, a['offset']! as int),
);
if (a['usable'] == true) usable.add(mixed.addresses[i]);
}
expect(usable, isNotEmpty);
expect(mixed.usable, usable);
// It cannot be written: a writer never writes an address that a reader
// rejects.
expect(() => mixed.marshal(), throwsA(isA<LocatorException>()));
final rest = mixed.restIn(
fromHex(td['host']! as String),
usable.first.offset,
);
expect(toHex(mixed.openEnvelope(rest)), td['dkc']);
},
);
test('the rests: rest_size bytes from the offset, used only when their '
'SHA-256 is resto_digest, with the texts of Go', () {
final loc = openLocator(
p,
round,
release,
fromHex(td['locator_sealed']! as String),
);
final texts = rows(tdTexts, 'rest_cases');
for (final (i, c)
in (td['rest_cases']! as List<Object?>).cast<Json>().indexed) {
Uint8List? r;
expect(
errorText(
() => r = loc.restIn(
fromHex(c['resource']! as String),
c['offset']! as int,
),
),
textOf(v, texts[i][0]),
reason: '${c['name']}',
);
var opens = false;
if (r != null) {
Uint8List? dkc;
expect(
errorText(() => dkc = loc.openEnvelope(r!)),
textOf(v, texts[i][1]),
reason: '${c['name']}',
);
opens = dkc != null && toHex(dkc!) == td['dkc'];
}
expect(opens, c['opens'], reason: '${c['name']}');
}
});
test('the plaintexts of the locator, with the texts of Go', () {
final texts = tdTexts['plaintext_cases']! as List<Object?>;
for (final (i, c)
in (td['plaintext_cases']! as List<Object?>).cast<Json>().indexed) {
final want = textOf(v, texts[i]);
expect(
errorText(
() => unmarshalLocator(fromHex(c['locator_plaintext']! as String)),
),
want,
reason: '${c['name']}',
);
expect(want.isEmpty, c['ok'], reason: '${c['name']}');
}
});
});
test('openLocator reads at most 1 MiB of plaintext, as Go through '
'io.LimitReader: what follows is neither decrypted nor checked', () {
final lim = v['limit']! as Json;
final header = fromHex(lim['header']! as String);
final nonce = fromHex(lim['nonce']! as String);
final streamKey = hkdfSha256(
fromHex(lim['file_key']! as String),
nonce,
'payload'.codeUnits,
32,
);
expect(toHex(streamKey), lim['stream_key']);
final plain = fromHex(
(v['plaintext_bases']! as List<Object?>)[0]! as String,
);
final release = releaseOf(v, lim['round']! as int);
for (final c in (lim['cases']! as List<Object?>).cast<Json>()) {
final chunks = rows(c, 'chunks');
final total = chunks.fold(0, (n, ch) => n + (ch[0]! as int));
final content = Uint8List(total)
..setRange(0, plain.length < total ? plain.length : total, plain);
final out = BytesBuilder(copy: false)
..add(header)
..add(nonce);
var at = 0;
for (final (i, ch) in chunks.indexed) {
final n = ch[0]! as int;
// An 11-byte big-endian counter and the flag of the last chunk.
final chunkNonce = Uint8List(12)
..[7] = i >> 24
..[8] = (i >> 16) & 0xff
..[9] = (i >> 8) & 0xff
..[10] = i & 0xff
..[11] = ch[1] == true ? 1 : 0;
final ct = chacha20Poly1305Seal(
streamKey,
chunkNonce,
Uint8List.sublistView(content, at, at + n),
);
if (ch[2] == true) ct[0] ^= 1;
out.add(ct);
at += n;
}
out.add(Uint8List(c['trailing']! as int));
final file = out.takeBytes();
expect([file.length, sha256Hex(file)], [c['length'], c['sha256']]);
expect(
errorText(() => openLocator(quicknet(), 1000, release, file)),
textOf(v, c['text']),
reason: '${c['name']}',
);
}
});
}

Powered by TurnKey Linux.