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_stream_test.dart

209 lines
7.2 KiB

// Streaming: capsules whose content is several MiB, made from the fixtures
// (large_capsule.dart), opened from a source read in pieces, which holds
// only the bytes that steps 1 to 8 need and streams PAYLOAD_AGE, and in
// memory, with the same result. The output receives the content as age
// authenticates it and is closed only at step 18; a failure in a later
// chunk aborts it, never closed (spec §56). The capsule_digest of a .dkk is
// computed over the source.
@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:datekeys/src/sha256.dart' show sha256;
import 'package:test/test.dart';
import 'large_capsule.dart';
import 'open_support.dart';
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
Uint8List fixture(String file) =>
File('testdata/fixtures/$file').readAsBytesSync();
Release releaseOf(Json r) {
final rel = r['release']! as Json;
return Release(rel['round']! as int, fromHex(str(rel, 'signature')));
}
void main() {
final r1 = readJson('testdata/fixtures/time_only.json');
final r3 = readJson('testdata/fixtures/format3_single.json');
final content = pattern((3 << 20) + 12345);
final big1 = withContent1(
fixture('time_only.dkc'),
fromHex(str(r1, 'payload_identity')),
content,
);
OpenOptions options1({ByteSink? output, AccessKey? key}) => OpenOptions(
source: suppliedRelease(releaseOf(r1)),
now: () => parseRfc3339(str(r1, 'unlock_at')),
output: output,
accessKey: key,
);
test('format 1: 3 MiB from a source, in pieces, as in memory', () async {
final source = ChunkySource(big1, 1 << 20);
final out = RecordingOutput();
final r = await openCapsuleSource(source, options1(output: out));
expect(r.ok, isTrue);
expect(out.received.toBytes(), content);
expect([r.payloadLength, out.closed], [content.length, true]);
// Each chunk of 64 KiB as age authenticates it, and the close last.
expect(out.log.where((l) => l == 'add 65536'), hasLength(48));
expect(out.log.last, 'close');
// The source was read once in prefix, and then in pieces of at most
// 1 MiB, never whole.
final payloadAt = inspectCapsule(big1).payloadOffset!;
expect(source.largest, lessThanOrEqualTo(1 << 20));
expect(source.farthest, big1.length);
expect(
source.bytesRead,
lessThan(big1.length + payloadAt + maxAgeHeaderLength + 100),
);
final memory = RecordingOutput();
final m = await openCapsule(big1, options1(output: memory));
expect([m.ok, memory.received.toBytes()], [true, content]);
expect(
canonical([for (final c in m.checks) c.toJson()]),
canonical([for (final c in r.checks) c.toJson()]),
);
});
test('format 1: a chunk that fails after others were written aborts the '
'output, never closed', () async {
for (final at in [
big1.length - 10,
big1.length - (1 << 20),
big1.length ~/ 2,
]) {
final bad = Uint8List.fromList(big1)..[at] ^= 1;
for (final fromSource in [false, true]) {
final out = RecordingOutput();
final r = fromSource
? await openCapsuleSource(
ChunkySource(bad, 1 << 20),
options1(output: out),
)
: await openCapsule(bad, options1(output: out));
expect(
r.error!.message,
'capsule: PAYLOAD_AGE: the age payload is '
'truncated, has trailing data or fails STREAM authentication: '
'ERR_INTEGRITY',
);
expect(r.checks.last.step, 17);
expect(out.log.where((l) => l.startsWith('add ')), isNotEmpty);
expect([out.closed, out.aborted], [false, same(r.error)]);
expect(out.log.last, 'abort');
}
}
});
test('format 1: the capsule_digest of a .dkk over a large source', () async {
// The .dkk of time_and_key_portable, for a capsule that is not its own:
// its capsule_digest fails at step 9, read from the source; one whose
// digest is that of the capsule passes it.
final rk = readJson('testdata/fixtures/time_and_key_portable.json');
final big = withContent1(
fixture('time_and_key_portable.dkc'),
fromHex(str(rk, 'payload_identity')),
content,
);
final key = decodeAccessKey(fixture('time_and_key_portable.dkk'));
OpenOptions options(AccessKey k, ByteSink out) => OpenOptions(
source: suppliedRelease(releaseOf(rk)),
now: () => parseRfc3339(str(rk, 'unlock_at')),
accessKey: k,
output: out,
);
final source = ChunkySource(big, 1 << 20);
final r = await openCapsuleSource(source, options(key, RecordingOutput()));
expect(
[r.error?.message, r.checks.last.step],
[
'capsule: the .dkk capsule_digest does not match this .dkc: '
'ERR_ACCESS_INVALID',
9,
],
);
expect(source.farthest, big.length);
final own = AccessKey(
credentialId: key.credentialId,
capsuleId: key.capsuleId,
type: key.type,
material: key.material,
verification: Verification(sha256(big)),
);
final out = RecordingOutput();
final s = await openCapsuleSource(
ChunkySource(big, 1 << 20),
options(own, out),
);
expect([s.ok, out.received.toBytes()], [true, content]);
});
final files = [
('a.txt', pattern(1000)),
('fotos/grande.bin', pattern((3 << 20) + 7)),
('vacio', Uint8List(0)),
];
final big3 = withFiles3(
fixture('format3_single.dkc'),
fromHex(str(r3, 'payload_identity')),
releaseOf(r3),
files,
);
OpenOptions options3(FileSink sink) => OpenOptions(
source: suppliedRelease(releaseOf(r3)),
now: () => parseRfc3339(str(r3, 'unlock_at')),
sink: sink,
);
test('format 3: files of 3 MiB from a source, in pieces, as in '
'memory', () async {
for (final fromSource in [false, true]) {
final sink = RecordingSink();
final r = fromSource
? await openCapsuleSource(ChunkySource(big3, 1 << 20), options3(sink))
: await openCapsule(big3, options3(sink));
expect(r.ok, isTrue, reason: r.error?.message);
expect(
[for (final f in r.head!.files) f.path],
[for (final (p, _) in files) p],
);
for (var i = 0; i < files.length; i++) {
expect(sink.files[i].toBytes(), files[i].$2);
}
expect(sink.log.last, 'commit');
expect(sink.log.where((l) => l == 'abort'), isEmpty);
}
});
test('format 3: a chunk that fails after a file was written aborts the '
'sink', () async {
final bad = Uint8List.fromList(big3)..[big3.length - 100] ^= 1;
for (final fromSource in [false, true]) {
final sink = RecordingSink();
final r = fromSource
? await openCapsuleSource(ChunkySource(bad, 1 << 20), options3(sink))
: await openCapsule(bad, options3(sink));
expect(
r.error!.message,
'capsule: PAYLOAD_AGE: the age payload is '
'truncated, has trailing data or fails STREAM authentication: '
'ERR_INTEGRITY',
);
expect(sink.log, contains('close 0'));
expect(
[sink.committed, sink.aborted, sink.log.last],
[false, same(r.error), 'abort'],
);
}
});
}

Powered by TurnKey Linux.