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.
273 lines
9.3 KiB
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']}',
|
|
);
|
|
}
|
|
});
|
|
}
|