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

376 lines
13 KiB

// The locator against files, on the VM: every case of
// testdata/vectors/locator.json with the result, the code 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; and the opening of a fixture whose .dkk carries
// datekeys.capsule, with the registry of locator.Standard. 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/accesskey.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/chacha20poly1305.dart';
import 'package:datekeys/src/datekey.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/extension.dart';
import 'package:datekeys/src/locator.dart';
import 'package:datekeys/src/open.dart' show OpenOptions, openCapsule;
import 'package:datekeys/src/profile.dart';
import 'package:datekeys/src/release.dart' show suppliedRelease;
import 'package:datekeys/src/sha256.dart';
import 'package:datekeys/src/sink.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('the extension reads, its locator opens with the release of its '
'round, and the rest in the host opens the envelope', () {
final info = parseCapsuleInfo(
Extension(
capsuleExtensionId,
1,
fromHex(td['extension_data']! as String),
),
);
expect(info.note, td['note']);
expect(compactDateKey(info.dateKey), td['datekey']);
expect(info.dateKey.round, round);
expect(toHex(info.sealed!), td['locator_sealed']);
final loc = info.openLocator(release);
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']);
// The same with the top-level opening of Go's Open.
final again = openLocator(p, round, release, info.sealed!);
expect(summaryOf(again), summaryOf(loc));
});
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 data of the extension: a reader cannot use some, and that has '
'ERR_EXTENSION_DATA_INVALID only', () {
final texts = tdTexts['extension_cases']! as List<Object?>;
for (final (i, c)
in (td['extension_cases']! as List<Object?>).cast<Json>().indexed) {
final x = Extension(
capsuleExtensionId,
1,
fromHex(c['extension_data']! as String),
);
final want = textOf(v, texts[i]);
expect(
errorText(() => parseCapsuleInfo(x)),
want,
reason: '${c['name']}',
);
expect(want.isEmpty, c['ok'], reason: '${c['name']}');
if (want.isNotEmpty) {
expect(
() => parseCapsuleInfo(x),
throwsA(
isA<DateKeysException>().having(
(e) => e.code,
'code',
ErrorCode.extensionDataInvalid,
),
),
);
}
}
});
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']}',
);
}
});
test('openCapsule with a .dkk that carries datekeys.capsule reports its '
'data as Go does with locator.Standard', () async {
final cap = v['capsule']! as Json;
final name = cap['fixture']! as String;
final dkc = File('testdata/fixtures/$name.dkc').readAsBytesSync();
final dkk = decodeAccessKey(
File('testdata/fixtures/$name.dkk').readAsBytesSync(),
);
final now = parseRfc3339(cap['now']! as String);
for (final c in (cap['cases']! as List<Object?>).cast<Json>()) {
final k = AccessKey(
credentialId: dkk.credentialId,
capsuleId: dkk.capsuleId,
type: dkk.type,
material: Uint8List.fromList(dkk.material),
verification: dkk.verification,
critical: dkk.critical,
noncritical: [
for (final x in rows(c, 'noncritical'))
Extension(x[0]! as String, x[1]! as int, fromHex(x[3]! as String)),
],
);
final file = encodeAccessKey(k);
expect(sha256Hex(file), c['dkk_sha256']);
final output = MemoryByteSink();
final opened = await openCapsule(
dkc,
OpenOptions(
source: suppliedRelease(releaseOf(v, 1000)),
now: () => now,
accessKeyFile: file,
output: output,
extensions: c['standard'] == true ? const StandardExtensions() : null,
),
);
expect(opened.error?.message ?? '', textOf(v, c['text']));
expect(
[
for (final u in opened.unusableAccessKeyExtensions)
[u.id, u.version, u.error.message],
],
[
for (final u in rows(c, 'unusable')) [u[0], u[1], textOf(v, u[2])],
],
);
expect([
for (final ch in opened.checks)
[ch.step, ch.name, ch.ok, ch.detail, ch.error ?? ''],
], c['checks']);
expect(sha256Hex(output.bytes ?? Uint8List(0)), c['content']);
}
});
}

Powered by TurnKey Linux.