tool/capsule_writer_go_vectors_test.go runs capsule.EncryptFiles of Go on 87 recipes while crypto/rand reads the keystream of SeededRandomSource, as a test in an export of datekeys-go so that the CMS signatures and tokens of its hooks come out the same on every run. It records the size of each draw, what each hook was given and returned, the capsule, the .dkk and the openings of Go with each credential, and the text, the code and the bytes written of each error. The tests write each recipe again: the same draws, the same requests to the hooks, and the same bytes or the same error, in 21 capsules and 66 errors; and this library opens each capsule as Go did. The cases marked node also run compiled to JavaScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.11
parent
4c9bc723a4
commit
07a525f9c8
@ -0,0 +1,71 @@
|
|||||||
|
// The cases of test/vectors/capsule_writer.json as tests: each recipe is
|
||||||
|
// written again with the seeded source of its seed and the hooks that give
|
||||||
|
// what Go's gave, and must draw what Go drew, ask the hooks what Go's were
|
||||||
|
// asked, and write the bytes that capsule.EncryptFiles wrote, or fail with
|
||||||
|
// its text and code after writing as many bytes. The capsule is then opened
|
||||||
|
// by this library with each credential, as Go opened it, with the same
|
||||||
|
// files, verdicts and area. capsule_writer_vm_test.dart runs them all from
|
||||||
|
// the file, and capsule_writer_test.dart those marked node from the Dart
|
||||||
|
// constant, also compiled to JavaScript.
|
||||||
|
|
||||||
|
import 'package:datekeys/datekeys.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'capsule_writer_support.dart';
|
||||||
|
|
||||||
|
/// The tests of the cases of [doc]; with [allOpens] false, only the first
|
||||||
|
/// opening of each capsule, which costs a verification of the round.
|
||||||
|
void writerCases(Json doc, {bool allOpens = true}) {
|
||||||
|
final releases = releasesOf(doc);
|
||||||
|
for (final c in listOf(doc['cases'])) {
|
||||||
|
final r = c['recipe']! as Json;
|
||||||
|
test(r['name'], () async {
|
||||||
|
final w = await writeRecipe(r, c['hooks'] as Json? ?? const {});
|
||||||
|
expect(w.draws, c['draws'], reason: 'the draws');
|
||||||
|
expect(w.asked, c['hooks'] ?? const {}, reason: 'the hooks');
|
||||||
|
final e = c['error'] as Json?;
|
||||||
|
if (e != null) {
|
||||||
|
final err = w.error;
|
||||||
|
expect(err, isNotNull, reason: 'Go fails with ${e['text']}');
|
||||||
|
expect(errorText(err!), e['text']);
|
||||||
|
expect(writerCode(err), e['code']);
|
||||||
|
expect(w.dkc.length, e['written'], reason: 'the bytes written');
|
||||||
|
expect(w.sink.aborted, same(err));
|
||||||
|
expect(w.sink.closed, isFalse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(w.error, isNull);
|
||||||
|
final g = c['written']! as Json;
|
||||||
|
final res = w.result!;
|
||||||
|
final header = hexOf(g['header']);
|
||||||
|
expect(toHex(w.dkc.sublist(16, 16 + header.length)), g['header']);
|
||||||
|
expect(w.dkc.length, g['length_dkc']);
|
||||||
|
expect(toHex(sha256Of(w.dkc)), g['sha256']);
|
||||||
|
if (g['dkc'] != null) expect(toHex(w.dkc), g['dkc']);
|
||||||
|
expect(w.sink.closed, isTrue);
|
||||||
|
expect(w.sink.aborted, isNull);
|
||||||
|
expect(res.dateKey.round, g['round']);
|
||||||
|
expect(res.unlockAt.toString(), g['unlock_at']);
|
||||||
|
expect(toHex(res.capsuleId), g['capsule_id']);
|
||||||
|
expect(res.length, g['length']);
|
||||||
|
expect(res.padding.code, g['padding']);
|
||||||
|
expect(res.paddedLength, g['padded_length']);
|
||||||
|
expect(toHex(encodeHead(res.head)), g['head']);
|
||||||
|
final k = res.portableKey;
|
||||||
|
expect(k == null ? null : toHex(encodeAccessKey(k)), g['dkk']);
|
||||||
|
expect(g['reencoded'], isTrue);
|
||||||
|
|
||||||
|
final want = listOf(c['opens']);
|
||||||
|
final got = await opensOf(
|
||||||
|
r,
|
||||||
|
w.dkc,
|
||||||
|
releases,
|
||||||
|
res.dateKey.round,
|
||||||
|
res.capsuleId,
|
||||||
|
k == null ? null : encodeAccessKey(k),
|
||||||
|
limit: allOpens ? null : 1,
|
||||||
|
);
|
||||||
|
expect(got, want.sublist(0, got.length));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,479 @@
|
|||||||
|
// Helpers of the tests of the writer of capsules: the recipes of
|
||||||
|
// test/vectors/capsule_writer.json made into options and sources of
|
||||||
|
// encryptFiles, as tool/capsule_writer_go_vectors_test.go makes them for Go,
|
||||||
|
// the hooks that give the writer what the hooks of Go gave, the sink that
|
||||||
|
// receives a capsule, and the opening of a capsule with each credential, in
|
||||||
|
// the form of the vectors. They read no file, so that the tests that run on
|
||||||
|
// Node.js can use them.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:datekeys/datekeys.dart';
|
||||||
|
import 'package:datekeys/src/age.dart' show X25519Identity;
|
||||||
|
import 'package:datekeys/src/bytes.dart' show utf8Bytes;
|
||||||
|
import 'package:datekeys/src/encrypt3.dart';
|
||||||
|
import 'package:datekeys/src/random.dart';
|
||||||
|
import 'package:datekeys/src/recipient.dart';
|
||||||
|
import 'package:datekeys/src/sha256.dart';
|
||||||
|
|
||||||
|
import 'age_support.dart' show pattern;
|
||||||
|
import 'age_writer_support.dart' show labelIdentity;
|
||||||
|
import 'random_support.dart';
|
||||||
|
|
||||||
|
export 'age_support.dart' show pattern;
|
||||||
|
export 'age_writer_support.dart' show labelIdentity;
|
||||||
|
export 'random_support.dart';
|
||||||
|
|
||||||
|
Uint8List hexOf(Object? v) => fromHex(v! as String);
|
||||||
|
|
||||||
|
/// The published signatures of the rounds of the vectors.
|
||||||
|
Map<int, Uint8List> releasesOf(Json doc) => {
|
||||||
|
for (final e in (doc['releases']! as Json).entries)
|
||||||
|
int.parse(e.key): fromHex(e.value! as String),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A source of the releases of [releases], which counts its requests.
|
||||||
|
final class KnownReleases implements ReleaseSource {
|
||||||
|
KnownReleases(this.releases);
|
||||||
|
|
||||||
|
final Map<int, Uint8List> releases;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Release> fetch(PinnedProfile p, int round) async {
|
||||||
|
final s = releases[round];
|
||||||
|
if (s == null) {
|
||||||
|
throw DateKeysException(
|
||||||
|
ErrorCode.releaseUnavailable,
|
||||||
|
'test: no release of round $round',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Release(round, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The genesis of Quicknet.
|
||||||
|
final Instant genesis = Instant(quicknetGenesisTime);
|
||||||
|
|
||||||
|
/// [i] plus [ns] nanoseconds.
|
||||||
|
Instant addNanos(Instant i, int ns) {
|
||||||
|
final total = i.nanos + ns;
|
||||||
|
final s = total >= 0
|
||||||
|
? total ~/ 1000000000
|
||||||
|
: -((-total + 999999999) ~/ 1000000000);
|
||||||
|
return Instant(i.seconds + s, total - s * 1000000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Extension> extsOf(Object? v) => [
|
||||||
|
for (final e in (v as List? ?? const []).cast<Json>())
|
||||||
|
Extension(
|
||||||
|
e['id']! as String,
|
||||||
|
e['version']! as int,
|
||||||
|
e['data'] == null ? null : hexOf(e['data']),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The content of a file of a recipe: its text, or n bytes of the pattern.
|
||||||
|
Uint8List contentOf(Json f) {
|
||||||
|
final t = f['text'] as String?;
|
||||||
|
return t != null ? utf8Bytes(t) : pattern(f['pattern'] as int? ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error whose text is its message, as Go's errors.New.
|
||||||
|
final class TextError implements Exception {
|
||||||
|
const TextError(this.message);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The source of a file of a recipe, with its fault, as sourceOf of the
|
||||||
|
/// generator: each Open counts, and the fault of the reading it names
|
||||||
|
/// happens there.
|
||||||
|
FileSource sourceOf(Json f) {
|
||||||
|
final content = contentOf(f);
|
||||||
|
final size = f['size'] as int? ?? content.length;
|
||||||
|
final mtime = f['mtime'] as List?;
|
||||||
|
final fault = f['fault'] as String? ?? '';
|
||||||
|
var calls = 0;
|
||||||
|
return FileSource(
|
||||||
|
path: f['path']! as String,
|
||||||
|
size: size,
|
||||||
|
modTime: mtime == null ? null : Instant(mtime[0] as int, mtime[1] as int),
|
||||||
|
open: () {
|
||||||
|
if (size != content.length) {
|
||||||
|
throw StateError('a source opened whose size is not its content\'s');
|
||||||
|
}
|
||||||
|
calls++;
|
||||||
|
var c = content;
|
||||||
|
switch ('$fault/$calls') {
|
||||||
|
case 'open1/1' || 'open2/2':
|
||||||
|
throw const TextError('the file is gone');
|
||||||
|
case 'short1/1' || 'short2/2':
|
||||||
|
c = Uint8List.sublistView(c, 0, c.length - 1);
|
||||||
|
case 'long1/1' || 'long2/2':
|
||||||
|
c = Uint8List.fromList([...c, 0x78]);
|
||||||
|
case 'change2/2':
|
||||||
|
c = Uint8List.fromList(c)..[0] ^= 1;
|
||||||
|
case 'read2/2':
|
||||||
|
return _failing(Uint8List.sublistView(c, 0, c.length ~/ 2));
|
||||||
|
}
|
||||||
|
return _chunks(c);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bytes of c in pieces of 32 KiB, as Go's readSource reads them.
|
||||||
|
Stream<List<int>> _chunks(Uint8List c) async* {
|
||||||
|
for (var i = 0; i < c.length; i += 32 << 10) {
|
||||||
|
yield Uint8List.sublistView(
|
||||||
|
c,
|
||||||
|
i,
|
||||||
|
i + (32 << 10) < c.length ? i + (32 << 10) : c.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<int>> _failing(Uint8List first) async* {
|
||||||
|
yield* _chunks(first);
|
||||||
|
throw const TextError('the disk failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sources of a recipe, with its many empty files.
|
||||||
|
List<FileSource> sourcesOf(Json r) => [
|
||||||
|
for (final f in listOf(r['files'] ?? const [])) sourceOf(f),
|
||||||
|
for (var i = 0; i < (r['many_files'] as int? ?? 0); i++)
|
||||||
|
FileSource.bytes('f${'$i'.padLeft(5, '0')}', const []),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The profile of a recipe: Quicknet, or Quicknet with a genesis one second
|
||||||
|
/// later.
|
||||||
|
Profile profileOf(Json r) {
|
||||||
|
final p = quicknet();
|
||||||
|
return switch (r['profile'] as String? ?? '') {
|
||||||
|
'' => p,
|
||||||
|
'genesis_plus_one' => p.copyWith(genesisTime: p.genesisTime + 1),
|
||||||
|
final s => throw ArgumentError('profile $s'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hooks of a recipe, which give what the hooks of Go gave and record
|
||||||
|
/// what the writer asks of them.
|
||||||
|
final class Hooks {
|
||||||
|
Hooks(this.recipe, this.recorded);
|
||||||
|
|
||||||
|
final Json recipe;
|
||||||
|
final Json recorded;
|
||||||
|
final Json asked = {};
|
||||||
|
|
||||||
|
AuthorKey? get authorKey =>
|
||||||
|
recipe['author_key'] == null ? null : _ReplayAuthor(this);
|
||||||
|
|
||||||
|
CmsSigner? get cmsSigner => recipe['cms'] == null ? null : _ReplayCms(this);
|
||||||
|
|
||||||
|
Sealer? get sealer => recipe['sealer'] == null ? null : _ReplaySealer(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _ReplayAuthor implements AuthorKey {
|
||||||
|
_ReplayAuthor(this.h);
|
||||||
|
|
||||||
|
final Hooks h;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Uint8List get publicKey {
|
||||||
|
final pub = hexOf(h.recorded['author_public']);
|
||||||
|
h.asked['author_public'] = toHex(pub);
|
||||||
|
return pub;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Uint8List sign(Uint8List message) {
|
||||||
|
h.asked['author_message'] = toHex(message);
|
||||||
|
final sig = hexOf(h.recorded['author_signature']);
|
||||||
|
h.asked['author_signature'] = toHex(sig);
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _ReplayCms implements CmsSigner {
|
||||||
|
_ReplayCms(this.h);
|
||||||
|
|
||||||
|
final Hooks h;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Uint8List> get signers {
|
||||||
|
final list = (h.recorded['cms_signers'] as List? ?? const [])
|
||||||
|
.cast<String>();
|
||||||
|
h.asked['cms_signers'] = list;
|
||||||
|
return [for (final s in list) fromHex(s)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> sign(Uint8List message) async {
|
||||||
|
h.asked['cms_message'] = toHex(message);
|
||||||
|
final e = (h.recipe['cms']! as Json)['error'] as String?;
|
||||||
|
if (e != null) throw TextError(e);
|
||||||
|
final der = hexOf(h.recorded['cms_der']);
|
||||||
|
h.asked['cms_der'] = toHex(der);
|
||||||
|
return der;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _ReplaySealer implements Sealer {
|
||||||
|
_ReplaySealer(this.h);
|
||||||
|
|
||||||
|
final Hooks h;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> seal(Uint8List subject) async {
|
||||||
|
h.asked['seal_subject'] = toHex(subject);
|
||||||
|
final e = (h.recipe['sealer']! as Json)['error'] as String?;
|
||||||
|
if (e != null) throw TextError(e);
|
||||||
|
final token = hexOf(h.recorded['seal_token']);
|
||||||
|
h.asked['seal_token'] = toHex(token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The options of a recipe, with the source [random] and the [hooks].
|
||||||
|
EncryptOptions optionsOf(Json r, RandomSource random, Hooks hooks) {
|
||||||
|
final q = quicknet();
|
||||||
|
final unlock = addNanos(
|
||||||
|
roundTime(q, r['round']! as int),
|
||||||
|
r['unlock_ns'] as int? ?? 0,
|
||||||
|
);
|
||||||
|
final nowText = r['now'] as String?;
|
||||||
|
final now = nowText == null ? genesis : parseRfc3339(nowText);
|
||||||
|
final padding = r['padding'] as int? ?? 0;
|
||||||
|
return EncryptOptions(
|
||||||
|
profile: profileOf(r),
|
||||||
|
unlockAt: unlock,
|
||||||
|
now: () => now,
|
||||||
|
policy: r['policy'] == 'time_and_key'
|
||||||
|
? AccessPolicy.timeAndKey
|
||||||
|
: AccessPolicy.timeOnly,
|
||||||
|
recipients: [
|
||||||
|
for (final l in (r['identities'] as List? ?? const []).cast<String>())
|
||||||
|
X25519Recipient.of(labelIdentity(l)),
|
||||||
|
for (final raw
|
||||||
|
in (r['raw_recipients'] as List? ?? const []).cast<String>())
|
||||||
|
X25519Recipient(fromHex(raw)),
|
||||||
|
],
|
||||||
|
newPortableKey: r['portable'] as bool? ?? false,
|
||||||
|
words: (r['words'] as List? ?? const []).cast<String>(),
|
||||||
|
padding: padding == 0 ? null : PaddingRule.fromCode(padding),
|
||||||
|
critical: extsOf(r['critical']),
|
||||||
|
noncritical: extsOf(r['noncritical']),
|
||||||
|
controlCritical: extsOf(r['control_critical']),
|
||||||
|
controlNoncritical: extsOf(r['control_noncritical']),
|
||||||
|
headCritical: extsOf(r['head_critical']),
|
||||||
|
headNoncritical: extsOf(r['head_noncritical']),
|
||||||
|
comment: r['comment'] as String? ?? '',
|
||||||
|
author: r['author'] as String? ?? '',
|
||||||
|
publicNote: r['note'] as String? ?? '',
|
||||||
|
authorKey: hooks.authorKey,
|
||||||
|
cmsSigner: hooks.cmsSigner,
|
||||||
|
sealer: hooks.sealer,
|
||||||
|
largeArea: r['large_area'] as bool? ?? false,
|
||||||
|
testVectors: r['test_vectors'] as bool? ?? false,
|
||||||
|
testAreaLen: r['test_area_len'] as int? ?? 0,
|
||||||
|
random: random,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sink that keeps what it receives, and whether it was closed or
|
||||||
|
/// aborted.
|
||||||
|
final class CapsuleSink implements ByteSink {
|
||||||
|
final BytesBuilder _received = BytesBuilder();
|
||||||
|
bool closed = false;
|
||||||
|
Object? aborted;
|
||||||
|
int adds = 0;
|
||||||
|
|
||||||
|
int get length => _received.length;
|
||||||
|
|
||||||
|
Uint8List get bytes => _received.toBytes();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void add(Uint8List bytes) {
|
||||||
|
if (closed || aborted != null) throw StateError('add after the end');
|
||||||
|
adds++;
|
||||||
|
_received.add(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close() {
|
||||||
|
if (aborted != null) throw StateError('close after abort');
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void abort(Object reason) => aborted = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a recipe gave: the capsule and the result, or the error, with the
|
||||||
|
/// sizes of the draws and the hooks as asked.
|
||||||
|
typedef Written = ({
|
||||||
|
Uint8List dkc,
|
||||||
|
EncryptResult? result,
|
||||||
|
Object? error,
|
||||||
|
List<int> draws,
|
||||||
|
Json asked,
|
||||||
|
CapsuleSink sink,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Writes the capsule of the recipe [r] with the seeded source of its seed,
|
||||||
|
/// and the hooks that give what [recorded] says Go's gave.
|
||||||
|
Future<Written> writeRecipe(Json r, Json recorded) async {
|
||||||
|
final random = RecordingSource(seeded(r['seed']! as String));
|
||||||
|
final hooks = Hooks(r, recorded);
|
||||||
|
final sink = CapsuleSink();
|
||||||
|
EncryptResult? res;
|
||||||
|
Object? error;
|
||||||
|
try {
|
||||||
|
res = await encryptFiles(sink, sourcesOf(r), optionsOf(r, random, hooks));
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
dkc: sink.bytes,
|
||||||
|
result: res,
|
||||||
|
error: error,
|
||||||
|
draws: [for (final d in random.draws) d.length],
|
||||||
|
asked: hooks.asked,
|
||||||
|
sink: sink,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The text of an error of the writer, as Go's err.Error().
|
||||||
|
String errorText(Object e) => switch (e) {
|
||||||
|
DateKeysException(:final message) => message,
|
||||||
|
CapsuleWriteException(:final message) => message,
|
||||||
|
ArgumentError(:final message) => '$message',
|
||||||
|
_ => 'unexpected ${e.runtimeType}: $e',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The code of an error of the writer, or null.
|
||||||
|
String? writerCode(Object e) => e is DateKeysException ? e.code.code : null;
|
||||||
|
|
||||||
|
/// The extensions of a recipe, known to the opening, as extSet of the
|
||||||
|
/// generator.
|
||||||
|
ExtensionRegistry extSetOf(Json r) {
|
||||||
|
final known = <String, List<int>>{};
|
||||||
|
for (final k in const [
|
||||||
|
'critical',
|
||||||
|
'noncritical',
|
||||||
|
'control_critical',
|
||||||
|
'control_noncritical',
|
||||||
|
'head_critical',
|
||||||
|
'head_noncritical',
|
||||||
|
]) {
|
||||||
|
for (final e in extsOf(r[k])) {
|
||||||
|
(known[e.id] ??= []).add(e.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ExtensionSet(known);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw identity of the key of words of [r] for the capsule [capsuleId]
|
||||||
|
/// of [round].
|
||||||
|
Uint8List wordIdentityOf(Json r, int round, List<int> capsuleId) => wordKey(
|
||||||
|
(r['words']! as List).cast<String>(),
|
||||||
|
quicknet().chainHash,
|
||||||
|
round,
|
||||||
|
capsuleId,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Opens [dkc] as the generator does: with the identities, the .dkk, or
|
||||||
|
/// both, and returns the verdict in the form of the vectors.
|
||||||
|
Future<Json> openedJson(
|
||||||
|
Json r,
|
||||||
|
Uint8List dkc,
|
||||||
|
Map<int, Uint8List> releases, {
|
||||||
|
List<Uint8List> identities = const [],
|
||||||
|
Uint8List? dkk,
|
||||||
|
}) async {
|
||||||
|
final files = MemoryFileSink();
|
||||||
|
final o = await openCapsule(
|
||||||
|
dkc,
|
||||||
|
OpenOptions(
|
||||||
|
source: KnownReleases(releases),
|
||||||
|
now: () => Instant(1791244800),
|
||||||
|
extensions: extSetOf(r),
|
||||||
|
identities: identities,
|
||||||
|
accessKeyFile: dkk,
|
||||||
|
sink: files,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final err = o.error;
|
||||||
|
if (err != null) {
|
||||||
|
return {'result': err.code.code, 'step': o.inspection.checks.last.step};
|
||||||
|
}
|
||||||
|
final head = o.head!;
|
||||||
|
return {
|
||||||
|
'result': 'ok',
|
||||||
|
'files': [
|
||||||
|
for (var i = 0; i < head.files.length; i++)
|
||||||
|
{
|
||||||
|
'path': head.files[i].path,
|
||||||
|
'size': head.files[i].size,
|
||||||
|
'sha256': toHex(sha256(files.files![i])),
|
||||||
|
if (head.files[i].mtime != null) 'mtime': head.files[i].mtime,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'head': toHex(encodeHead(head)),
|
||||||
|
'verdicts': [o.verdicts!.signature!.code, o.verdicts!.seal!.code],
|
||||||
|
'area_len': o.areaLen,
|
||||||
|
'length': o.payloadLength,
|
||||||
|
'padded_length': o.paddedLength,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The openings of a capsule with each credential of its recipe, all
|
||||||
|
/// together and none, as opens of the generator: the first [limit] of them,
|
||||||
|
/// all without one.
|
||||||
|
Future<List<Json>> opensOf(
|
||||||
|
Json r,
|
||||||
|
Uint8List dkc,
|
||||||
|
Map<int, Uint8List> releases,
|
||||||
|
int round,
|
||||||
|
Uint8List capsuleId,
|
||||||
|
Uint8List? dkk, {
|
||||||
|
int? limit,
|
||||||
|
}) async {
|
||||||
|
final todo = <(String, List<Uint8List>, Uint8List?)>[];
|
||||||
|
if (r['policy'] != 'time_and_key') {
|
||||||
|
todo.add(('time', const [], null));
|
||||||
|
} else {
|
||||||
|
final all = <Uint8List>[];
|
||||||
|
for (final l in (r['identities'] as List? ?? const []).cast<String>()) {
|
||||||
|
final id = labelSecret(l);
|
||||||
|
todo.add(('identity $l', [id], null));
|
||||||
|
all.add(id);
|
||||||
|
}
|
||||||
|
if ((r['words'] as List? ?? const []).isNotEmpty) {
|
||||||
|
final id = wordIdentityOf(r, round, capsuleId);
|
||||||
|
todo.add(('words', [id], null));
|
||||||
|
all.add(id);
|
||||||
|
}
|
||||||
|
if (dkk != null) todo.add(('portable', const [], dkk));
|
||||||
|
todo.add(('all', all, dkk));
|
||||||
|
todo.add(('none', const [], null));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
for (final (name, ids, k) in todo.take(limit ?? todo.length))
|
||||||
|
{
|
||||||
|
...await openedJson(r, dkc, releases, identities: ids, dkk: k),
|
||||||
|
'with': name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SHA-256 of [b].
|
||||||
|
Uint8List sha256Of(List<int> b) => sha256(b);
|
||||||
|
|
||||||
|
/// The identity of a label, raw.
|
||||||
|
Uint8List labelSecret(String label) =>
|
||||||
|
X25519Identity(sha256(utf8Bytes('identity $label'))).secretKey;
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
// The writer of capsules against Go, the cases marked node of
|
||||||
|
// test/vectors/capsule_writer.json, from a Dart constant, so that they also
|
||||||
|
// run compiled to JavaScript (see capsule_writer_cases.dart). Each capsule is
|
||||||
|
// opened only with its first credential there: every opening verifies the
|
||||||
|
// release of the round.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'capsule_writer_cases.dart';
|
||||||
|
import 'capsule_writer_support.dart';
|
||||||
|
import 'vectors/capsule_writer.g.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
writerCases(jsonDecode(capsuleWriterJson) as Json, allOpens: !isWeb);
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
// The writer of capsules against Go: every case of
|
||||||
|
// test/vectors/capsule_writer.json (see capsule_writer_cases.dart).
|
||||||
|
@TestOn('vm')
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'capsule_writer_cases.dart';
|
||||||
|
import 'capsule_writer_support.dart';
|
||||||
|
import 'vectors/capsule_writer.g.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final text = File('test/vectors/capsule_writer.json').readAsStringSync();
|
||||||
|
final doc = jsonDecode(text) as Json;
|
||||||
|
|
||||||
|
test('the Dart constant holds the cases marked node', () {
|
||||||
|
final node = jsonDecode(capsuleWriterJson) as Json;
|
||||||
|
expect(node['releases'], doc['releases']);
|
||||||
|
expect(node['cases'], [
|
||||||
|
for (final c in listOf(doc['cases']))
|
||||||
|
if ((c['recipe']! as Json)['node'] == true) c,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
writerCases(doc);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue