tool/capsule_interop_dart_samples.dart writes the capsules of twelve recipes, six with SeededRandomSource and six with the CSPRNG of the platform, among them three MiB in three files, two hundred files, and a capsule of fourteen recipients, a key of words and a portable key. tool/capsule_interop_go_verdicts.go inspects and opens each with capsule.Open of Go, with each credential alone, all together and none, encodes PUBLIC_HEADER, CONTROL_CBOR and the .dkk again, and writes each seeded one with capsule.EncryptFiles. Go opens every capsule to the files of its recipe, refuses each without a credential, finds the layers and the .dkk encoded as it encodes them, and writes the seeded ones byte for byte. The tests check those verdicts and write the seeded samples again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.11
parent
07a525f9c8
commit
9fd06f2990
@ -0,0 +1,91 @@
|
||||
// The writer of capsules against Go, in the other direction: the capsules
|
||||
// that this library writes, opened by Go. test/vectors/capsule_interop.json
|
||||
// holds the recipes of capsule_interop_support.dart with what Go made of
|
||||
// each capsule (tool/capsule_interop_go_verdicts.go). Go must open it with
|
||||
// each credential, alone and together, to the files of the recipe, in the
|
||||
// byte order of their paths, with their SHA-256 and their mtime as the
|
||||
// writer stores it, refuse it without a credential, find 16 stanzas in
|
||||
// INNER_ACCESS_AGE, encode each layer and the .dkk again to the bytes
|
||||
// written, and, for a seeded sample, write the same bytes itself. A seeded
|
||||
// sample is written again here and must be the one that Go read.
|
||||
|
||||
import 'package:datekeys/datekeys.dart';
|
||||
import 'package:datekeys/src/body.dart' show areaLen;
|
||||
import 'package:datekeys/src/bytes.dart' show utf8Bytes;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'capsule_interop_support.dart';
|
||||
import 'capsule_writer_support.dart';
|
||||
|
||||
/// The files that the recipe [r] puts in its capsule, as Go must open
|
||||
/// them.
|
||||
List<Json> expectedFiles(Json r) {
|
||||
final files = [...listOf(r['files'] ?? const [])]
|
||||
..sort(
|
||||
(a, b) => compareBytes(
|
||||
utf8Bytes(a['path']! as String),
|
||||
utf8Bytes(b['path']! as String),
|
||||
),
|
||||
);
|
||||
return [
|
||||
for (final f in files)
|
||||
{
|
||||
'path': f['path'],
|
||||
'size': contentOf(f).length,
|
||||
'sha256': toHex(sha256Of(contentOf(f))),
|
||||
if (f['mtime'] case [final int s, _] when s >= 0 && s <= maxMTime)
|
||||
'mtime': s,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/// The tests of the samples of [doc], with the recipes that wrote them.
|
||||
void interopCases(Json doc, {required bool rewrite}) {
|
||||
final recipes = {for (final s in interopSamples()) s['name']: s};
|
||||
for (final g in listOf(doc['samples'])) {
|
||||
final r = recipes[g['name']]!;
|
||||
test(g['name'], () async {
|
||||
expect(g['reencoded'], isTrue);
|
||||
final inspect = g['inspect']! as Json;
|
||||
expect(inspect['format'], 3);
|
||||
expect(inspect['policy'], r['policy'] ?? 'time_only');
|
||||
expect(inspect['note'], r['note'] ?? '');
|
||||
final tak = r['policy'] == 'time_and_key';
|
||||
expect(g['inner_stanzas'], tak ? 16 : null);
|
||||
expect(g['dkk_capsule_digest'], r['portable'] == true ? true : null);
|
||||
expect(g['go_same'], r['random'] == 'seeded' ? true : null);
|
||||
final comment = (r['comment'] as String? ?? '').replaceAll('\r\n', '\n');
|
||||
final opens = listOf(g['opens']);
|
||||
final names = [for (final o in opens) o['with']];
|
||||
expect(names, [
|
||||
if (!tak) 'time',
|
||||
if (tak) ...[
|
||||
for (final l in (r['identities'] as List? ?? const [])) 'identity $l',
|
||||
if (r['words'] != null) 'words',
|
||||
if (r['portable'] == true) 'portable',
|
||||
'all',
|
||||
'none',
|
||||
],
|
||||
]);
|
||||
for (final o in opens) {
|
||||
if (o['with'] == 'none') {
|
||||
expect(o['result'], 'ERR_ACCESS_REQUIRED');
|
||||
continue;
|
||||
}
|
||||
expect(o['result'], 'ok', reason: '${o['with']}');
|
||||
expect(o['files'], expectedFiles(r));
|
||||
expect(o['comment'], comment);
|
||||
expect(o['author'], r['author'] ?? '');
|
||||
expect(o['verdicts'], ['F0', 'S0']);
|
||||
expect(o['area_len'], r['test_area_len'] ?? areaLen);
|
||||
}
|
||||
if (rewrite && r['random'] == 'seeded') {
|
||||
final w = await writeSample(r);
|
||||
expect(w.dkc.length, g['length_dkc']);
|
||||
expect(toHex(sha256Of(w.dkc)), g['sha256']);
|
||||
final dkk = w.dkk;
|
||||
expect(dkk == null ? null : toHex(sha256Of(dkk)), g['dkk_sha256']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
// The recipes of the interoperability of the writer of capsules, stage 6b:
|
||||
// capsules that this library writes and Go opens
|
||||
// (tool/capsule_interop_dart_samples.dart, tool/capsule_interop_go_verdicts.go,
|
||||
// test/vectors/capsule_interop.json). They are in the form of the recipes of
|
||||
// capsule_writer.json (capsule_writer_support.dart), with `random` seeded,
|
||||
// for a capsule that the tests write again and compare, or secure, for one
|
||||
// written with the CSPRNG of the platform, as an application writes it,
|
||||
// which only Go's verdicts describe. They read no file.
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:datekeys/datekeys.dart';
|
||||
import 'package:datekeys/src/encrypt3.dart';
|
||||
import 'package:datekeys/src/random.dart';
|
||||
|
||||
import 'capsule_writer_support.dart';
|
||||
|
||||
const _words = ['faro', 'nube', 'trigo', 'menta', 'barco', 'lince'];
|
||||
|
||||
List<String> _ids(int n) => [for (var i = 0; i < n; i++) 'holder $i'];
|
||||
|
||||
/// The recipes of the samples.
|
||||
List<Json> interopSamples() {
|
||||
final out = <Json>[];
|
||||
for (final random in ['seeded', 'secure']) {
|
||||
Json s(String name, Json r) => {
|
||||
'name': '$name ($random)',
|
||||
'seed': 'capsule interop $name',
|
||||
'random': random,
|
||||
'round': 1000,
|
||||
...r,
|
||||
};
|
||||
out.addAll([
|
||||
s('time_only, one file', {
|
||||
'files': [
|
||||
{'path': 'nota.txt', 'text': 'Hola.\n'},
|
||||
],
|
||||
'node': true,
|
||||
}),
|
||||
s('time_only, a comment alone, bloque256, area of 512', {
|
||||
'comment': 'Una línea.\r\nOtra.',
|
||||
'author': 'Ana López',
|
||||
'padding': 1,
|
||||
'test_vectors': true,
|
||||
'test_area_len': 512,
|
||||
'node': true,
|
||||
}),
|
||||
s('time_and_key, a portable key and a note', {
|
||||
'policy': 'time_and_key',
|
||||
'portable': true,
|
||||
'note': 'Para Ana',
|
||||
'files': [
|
||||
{'path': 'a.txt', 'text': 'a'},
|
||||
{'path': 'b/c.txt', 'text': ''},
|
||||
],
|
||||
}),
|
||||
s('time_and_key, fourteen recipients, words and a portable key', {
|
||||
'policy': 'time_and_key',
|
||||
'identities': _ids(14),
|
||||
'words': _words,
|
||||
'portable': true,
|
||||
'round': 1001,
|
||||
'files': [
|
||||
{'path': 'x.bin', 'pattern': 70000},
|
||||
],
|
||||
'node': false,
|
||||
}),
|
||||
s('three MiB in three files, with mtimes', {
|
||||
'files': [
|
||||
{
|
||||
'path': 'grande.bin',
|
||||
'pattern': 3 << 20,
|
||||
'mtime': [1727712000, 5],
|
||||
},
|
||||
{
|
||||
'path': 'fotos/b.jpg',
|
||||
'pattern': 65536,
|
||||
'mtime': [-1, 0],
|
||||
},
|
||||
{'path': 'fotos/a.jpg', 'pattern': 65535},
|
||||
],
|
||||
'node': false,
|
||||
}),
|
||||
s('two hundred small files and extensions', {
|
||||
'files': [
|
||||
for (var i = 0; i < 200; i++)
|
||||
{'path': 'd${i % 7}/f$i.txt', 'text': 'file $i\n'},
|
||||
],
|
||||
'critical': [
|
||||
{'id': 'org.example.crit', 'version': 1, 'data': '01'},
|
||||
],
|
||||
'control_noncritical': [
|
||||
{'id': 'org.example.c', 'version': 2},
|
||||
],
|
||||
'head_noncritical': [
|
||||
{'id': 'org.example.h', 'version': 1, 'data': 'ff'},
|
||||
],
|
||||
'node': false,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// The capsule of the sample [s] and its .dkk, if any: written with the
|
||||
/// seeded source of its seed, or with [secureRandom].
|
||||
Future<({Uint8List dkc, Uint8List? dkk, EncryptResult result})> writeSample(
|
||||
Json s,
|
||||
) async {
|
||||
final random = s['random'] == 'seeded'
|
||||
? seeded(s['seed']! as String)
|
||||
: secureRandom;
|
||||
final sink = CapsuleSink();
|
||||
final res = await encryptFiles(
|
||||
sink,
|
||||
sourcesOf(s),
|
||||
optionsOf(s, random, Hooks(s, const {})),
|
||||
);
|
||||
final k = res.portableKey;
|
||||
return (
|
||||
dkc: sink.bytes,
|
||||
dkk: k == null ? null : encodeAccessKey(k),
|
||||
result: res,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
// The capsules that this library writes, opened by Go: the samples of
|
||||
// test/vectors/capsule_interop.json that are not marked node false, from a
|
||||
// Dart constant, so that they also run compiled to JavaScript, the seeded
|
||||
// ones written again (see capsule_interop_cases.dart).
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'capsule_interop_cases.dart';
|
||||
import 'capsule_writer_support.dart';
|
||||
import 'vectors/capsule_interop.g.dart';
|
||||
|
||||
void main() {
|
||||
interopCases(jsonDecode(capsuleInteropJson) as Json, rewrite: true);
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
// The capsules that this library writes, opened by Go: every sample of
|
||||
// test/vectors/capsule_interop.json, the seeded ones written again (see
|
||||
// capsule_interop_cases.dart).
|
||||
@TestOn('vm')
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'capsule_interop_cases.dart';
|
||||
import 'capsule_writer_support.dart';
|
||||
import 'vectors/capsule_interop.g.dart';
|
||||
|
||||
void main() {
|
||||
final doc = jsonDecode(
|
||||
File('test/vectors/capsule_interop.json').readAsStringSync(),
|
||||
) as Json;
|
||||
|
||||
test('the Dart constant holds the samples not marked node false', () {
|
||||
expect((jsonDecode(capsuleInteropJson) as Json)['samples'], [
|
||||
for (final s in listOf(doc['samples']))
|
||||
if (!(s['name']! as String).contains('fourteen') &&
|
||||
!(s['name']! as String).contains('MiB') &&
|
||||
!(s['name']! as String).contains('two hundred'))
|
||||
s,
|
||||
]);
|
||||
});
|
||||
|
||||
interopCases(doc, rewrite: true);
|
||||
}
|
||||
@ -0,0 +1,293 @@
|
||||
// Generated by tool/capsule_interop_go_verdicts.go: the samples of
|
||||
// test/vectors/capsule_interop.json that are not marked node false, for the
|
||||
// tests that also run compiled to JavaScript. Do not edit.
|
||||
|
||||
/// Part of test/vectors/capsule_interop.json.
|
||||
const capsuleInteropJson = r'''
|
||||
{
|
||||
"description": "Capsules that datekeys-dart writes from the recipes of test/capsule_interop_support.dart, with SeededRandomSource or the CSPRNG of the platform, inspected and opened by capsule.Open of datekeys-go with each credential, all together and none, their layers encoded again, and, for a seeded one, whether capsule.EncryptFiles writes the same bytes (tool/capsule_interop_go_verdicts.go).",
|
||||
"go": "go1.26.8",
|
||||
"samples": [
|
||||
{
|
||||
"go_same": true,
|
||||
"inspect": {
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"format": 3,
|
||||
"note": "",
|
||||
"policy": "time_only"
|
||||
},
|
||||
"length_dkc": 35611,
|
||||
"name": "time_only, one file (seeded)",
|
||||
"opens": [
|
||||
{
|
||||
"area_len": 32768,
|
||||
"author": "",
|
||||
"comment": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "nota.txt",
|
||||
"sha256": "0397fe19bf28bbbef703f292f55d0cc96e9f535b6db7944504bde08c3709d685",
|
||||
"size": 6
|
||||
}
|
||||
],
|
||||
"head": "a4006d646174656b6579732d6865616401010258209fb051d18640caa5ceb7d6a6a57c8bf382074f98ae7abfd6bb9b0fcf45b221db0581a500686e6f74612e7478740106020003060458200397fe19bf28bbbef703f292f55d0cc96e9f535b6db7944504bde08c3709d685",
|
||||
"length": 32893,
|
||||
"padded_length": 34816,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "time"
|
||||
}
|
||||
],
|
||||
"reencoded": true,
|
||||
"sha256": "e2e1c4630d5bcebef2841be25db6374c611e1fcbf1d10a58a497e38099c776a8"
|
||||
},
|
||||
{
|
||||
"go_same": true,
|
||||
"inspect": {
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"format": 3,
|
||||
"note": "",
|
||||
"policy": "time_only"
|
||||
},
|
||||
"length_dkc": 1563,
|
||||
"name": "time_only, a comment alone, bloque256, area of 512 (seeded)",
|
||||
"opens": [
|
||||
{
|
||||
"area_len": 512,
|
||||
"author": "Ana López",
|
||||
"comment": "Una línea.\nOtra.",
|
||||
"files": [],
|
||||
"head": "a5006d646174656b6579732d68656164010102582062d6033aa55ff10494692a79747fbec2f33e64b01aa801ec7ac2cfcc4055a9fc0371556e61206cc3ad6e65612e0a4f7472612e046a416e61204cc3b370657a",
|
||||
"length": 608,
|
||||
"padded_length": 768,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "time"
|
||||
}
|
||||
],
|
||||
"reencoded": true,
|
||||
"sha256": "3286ee451273bb6e54eb790f865a3c3ba17c53ee175fb3be73c364579a1a99a8"
|
||||
},
|
||||
{
|
||||
"dkk_capsule_digest": true,
|
||||
"dkk_sha256": "a017be51ababc550ed158e52cf86a5b8a27b2240ba79c700a8262df21d78c47b",
|
||||
"go_same": true,
|
||||
"inner_stanzas": 16,
|
||||
"inspect": {
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"format": 3,
|
||||
"note": "Para Ana",
|
||||
"policy": "time_and_key"
|
||||
},
|
||||
"length_dkc": 37311,
|
||||
"name": "time_and_key, a portable key and a note (seeded)",
|
||||
"opens": [
|
||||
{
|
||||
"area_len": 32768,
|
||||
"author": "",
|
||||
"comment": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "a.txt",
|
||||
"sha256": "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "b/c.txt",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
}
|
||||
],
|
||||
"head": "a4006d646174656b6579732d6865616401010258208bcd6ff1800431142e3e1f98e3b14290ace6de0f16e727bf879418d5942f6eef0582a50065612e747874010102000301045820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bba50067622f632e747874010002010301045820e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"length": 32936,
|
||||
"padded_length": 34816,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "portable"
|
||||
},
|
||||
{
|
||||
"area_len": 32768,
|
||||
"author": "",
|
||||
"comment": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "a.txt",
|
||||
"sha256": "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "b/c.txt",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
}
|
||||
],
|
||||
"head": "a4006d646174656b6579732d6865616401010258208bcd6ff1800431142e3e1f98e3b14290ace6de0f16e727bf879418d5942f6eef0582a50065612e747874010102000301045820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bba50067622f632e747874010002010301045820e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"length": 32936,
|
||||
"padded_length": 34816,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "all"
|
||||
},
|
||||
{
|
||||
"result": "ERR_ACCESS_REQUIRED",
|
||||
"step": 9,
|
||||
"with": "none"
|
||||
}
|
||||
],
|
||||
"reencoded": true,
|
||||
"sha256": "6042d6946aa143ce85e36c23b9a48acb2b00a784096e95747bb23522887658d9"
|
||||
},
|
||||
{
|
||||
"inspect": {
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"format": 3,
|
||||
"note": "",
|
||||
"policy": "time_only"
|
||||
},
|
||||
"length_dkc": 35611,
|
||||
"name": "time_only, one file (secure)",
|
||||
"opens": [
|
||||
{
|
||||
"area_len": 32768,
|
||||
"author": "",
|
||||
"comment": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "nota.txt",
|
||||
"sha256": "0397fe19bf28bbbef703f292f55d0cc96e9f535b6db7944504bde08c3709d685",
|
||||
"size": 6
|
||||
}
|
||||
],
|
||||
"head": "a4006d646174656b6579732d686561640101025820dcb97599496389439e89748e891f1cd69b4fb062ff8a38040c8661cd0ab12e420581a500686e6f74612e7478740106020003060458200397fe19bf28bbbef703f292f55d0cc96e9f535b6db7944504bde08c3709d685",
|
||||
"length": 32893,
|
||||
"padded_length": 34816,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "time"
|
||||
}
|
||||
],
|
||||
"reencoded": true,
|
||||
"sha256": "114c64f7a00b4a71d4b502e7fad42a01c5a217e87d6d6c3404a871bb9410832f"
|
||||
},
|
||||
{
|
||||
"inspect": {
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"format": 3,
|
||||
"note": "",
|
||||
"policy": "time_only"
|
||||
},
|
||||
"length_dkc": 1563,
|
||||
"name": "time_only, a comment alone, bloque256, area of 512 (secure)",
|
||||
"opens": [
|
||||
{
|
||||
"area_len": 512,
|
||||
"author": "Ana López",
|
||||
"comment": "Una línea.\nOtra.",
|
||||
"files": [],
|
||||
"head": "a5006d646174656b6579732d686561640101025820fe30146357dbeb109540a68e37157cdbf021fbc2dc028057ac8196b1a847fc3d0371556e61206cc3ad6e65612e0a4f7472612e046a416e61204cc3b370657a",
|
||||
"length": 608,
|
||||
"padded_length": 768,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "time"
|
||||
}
|
||||
],
|
||||
"reencoded": true,
|
||||
"sha256": "1f424c1171a89f89270d40a1fc88b922875c8437ced6806c746e166cc31574ab"
|
||||
},
|
||||
{
|
||||
"dkk_capsule_digest": true,
|
||||
"dkk_sha256": "6421639469f03c3c51d7d06c109e0a2b8243be6ca84eb263636c1d1eadf47a0e",
|
||||
"inner_stanzas": 16,
|
||||
"inspect": {
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"format": 3,
|
||||
"note": "Para Ana",
|
||||
"policy": "time_and_key"
|
||||
},
|
||||
"length_dkc": 37311,
|
||||
"name": "time_and_key, a portable key and a note (secure)",
|
||||
"opens": [
|
||||
{
|
||||
"area_len": 32768,
|
||||
"author": "",
|
||||
"comment": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "a.txt",
|
||||
"sha256": "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "b/c.txt",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
}
|
||||
],
|
||||
"head": "a4006d646174656b6579732d6865616401010258201a27afd7c4ccd1decf7cb2c73082e64ecd1d734e1257817ade1443d0d19dc02b0582a50065612e747874010102000301045820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bba50067622f632e747874010002010301045820e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"length": 32936,
|
||||
"padded_length": 34816,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "portable"
|
||||
},
|
||||
{
|
||||
"area_len": 32768,
|
||||
"author": "",
|
||||
"comment": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "a.txt",
|
||||
"sha256": "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "b/c.txt",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
}
|
||||
],
|
||||
"head": "a4006d646174656b6579732d6865616401010258201a27afd7c4ccd1decf7cb2c73082e64ecd1d734e1257817ade1443d0d19dc02b0582a50065612e747874010102000301045820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bba50067622f632e747874010002010301045820e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"length": 32936,
|
||||
"padded_length": 34816,
|
||||
"result": "ok",
|
||||
"verdicts": [
|
||||
"F0",
|
||||
"S0"
|
||||
],
|
||||
"with": "all"
|
||||
},
|
||||
{
|
||||
"result": "ERR_ACCESS_REQUIRED",
|
||||
"step": 9,
|
||||
"with": "none"
|
||||
}
|
||||
],
|
||||
"reencoded": true,
|
||||
"sha256": "4dd1abec3da92586b62eaf54403a1f46f43d187736bcdfc575860ab1b0e38220"
|
||||
}
|
||||
],
|
||||
"source": "c531e936fa8188a4aab01575607cc85d4f5d4fa4"
|
||||
}
|
||||
''';
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,43 @@
|
||||
// Writes the samples of the interoperability of the writer of capsules of
|
||||
// datekeys-dart, stage 6b: the capsules of the recipes of
|
||||
// test/capsule_interop_support.dart, which this library writes with
|
||||
// SeededRandomSource or with the CSPRNG of the platform, their .dkk, and
|
||||
// samples.json, the recipes with the name of each file, into a directory.
|
||||
// tool/capsule_interop_go_verdicts.go then opens them with Go and writes
|
||||
// test/vectors/capsule_interop.json; the tests write the seeded ones again
|
||||
// and compare them with what Go read.
|
||||
//
|
||||
// dart run tool/capsule_interop_dart_samples.dart <directory>
|
||||
//
|
||||
// ignore_for_file: avoid_print
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import '../test/capsule_interop_support.dart';
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
if (args.length != 1) {
|
||||
stderr.writeln(
|
||||
'usage: dart run tool/capsule_interop_dart_samples.dart <dir>',
|
||||
);
|
||||
exit(2);
|
||||
}
|
||||
final dir = Directory(args.single)..createSync(recursive: true);
|
||||
final samples = interopSamples();
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
final s = samples[i];
|
||||
final w = await writeSample(s);
|
||||
File('${dir.path}/$i.dkc').writeAsBytesSync(w.dkc);
|
||||
s['file'] = '$i.dkc';
|
||||
final dkk = w.dkk;
|
||||
if (dkk != null) {
|
||||
File('${dir.path}/$i.dkk').writeAsBytesSync(dkk);
|
||||
s['dkk'] = '$i.dkk';
|
||||
}
|
||||
print('${s['name']}: ${w.dkc.length} bytes');
|
||||
}
|
||||
File('${dir.path}/samples.json').writeAsStringSync(
|
||||
const JsonEncoder.withIndent(' ').convert({'samples': samples}),
|
||||
);
|
||||
print('wrote ${samples.length} samples to ${dir.path}');
|
||||
}
|
||||
@ -0,0 +1,376 @@
|
||||
//go:build ignore
|
||||
|
||||
// Opens with Go the capsules that the writer of datekeys-dart wrote, stage
|
||||
// 6b of docs/PLAN_dart.md, and writes test/vectors/capsule_interop.json and
|
||||
// capsule_interop.g.dart: each recipe of
|
||||
// tool/capsule_interop_dart_samples.dart with the length and the SHA-256 of
|
||||
// its capsule and its .dkk, and
|
||||
//
|
||||
// - inspect: capsule.Inspect, its format, policy, DateKey, public note and
|
||||
// the number of stanzas of INNER_ACCESS_AGE;
|
||||
// - opens: capsule.Open with the published release of the round and each
|
||||
// credential alone (the identity of each label, the key of words of the
|
||||
// capsule_id of its header, the .dkk), all together and none: the result
|
||||
// and the step, and of a capsule opened, its files with their SHA-256,
|
||||
// the head encoded again, the verdicts and the area;
|
||||
// - reencoded: whether PUBLIC_HEADER, CONTROL_CBOR, opened layer by layer
|
||||
// with agewrap, and the .dkk encode again to the bytes written;
|
||||
// - go_same: for a sample written with the seeded source, whether
|
||||
// capsule.EncryptFiles writes the same bytes while crypto/rand reads the
|
||||
// same keystream.
|
||||
//
|
||||
// It imports internal/testkit, so it runs in an export of datekeys-go made
|
||||
// with git archive, without changing the repository, on the branch v0.12 at
|
||||
// c531e93, after the Dart tool wrote the samples:
|
||||
//
|
||||
// commit=$(git -C ../datekeys-go rev-parse v0.12)
|
||||
// root=$PWD
|
||||
// tmp=$(mktemp -d)
|
||||
// dart run tool/capsule_interop_dart_samples.dart "$tmp/samples"
|
||||
// git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp"
|
||||
// cp tool/capsule_interop_go_verdicts.go "$tmp"
|
||||
// (cd "$tmp" && go run ./capsule_interop_go_verdicts.go -source "$commit" \
|
||||
// -samples "$tmp/samples" -out "$root/test/vectors")
|
||||
// rm -rf "$tmp"
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/chacha20"
|
||||
|
||||
datekeys "g.activething.com/go/DateKeys"
|
||||
"g.activething.com/go/DateKeys/accesskey"
|
||||
"g.activething.com/go/DateKeys/agewrap"
|
||||
"g.activething.com/go/DateKeys/capsule"
|
||||
"g.activething.com/go/DateKeys/datekey"
|
||||
"g.activething.com/go/DateKeys/extension"
|
||||
"g.activething.com/go/DateKeys/internal/testkit"
|
||||
"g.activething.com/go/DateKeys/profile"
|
||||
"g.activething.com/go/DateKeys/wordkey"
|
||||
)
|
||||
|
||||
type obj = map[string]any
|
||||
|
||||
func h(b []byte) string { return hex.EncodeToString(b) }
|
||||
|
||||
func sum(b []byte) string {
|
||||
s := sha256.Sum256(b)
|
||||
return h(s[:])
|
||||
}
|
||||
|
||||
func must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func unhex(s string) []byte { return must(hex.DecodeString(s)) }
|
||||
|
||||
type extIn struct {
|
||||
ID string `json:"id"`
|
||||
Version uint64 `json:"version"`
|
||||
Data *string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type fileIn struct {
|
||||
Path string `json:"path"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Pattern int `json:"pattern,omitempty"`
|
||||
MTime []int64 `json:"mtime,omitempty"`
|
||||
}
|
||||
|
||||
// sample is a recipe of test/capsule_interop_support.dart, in the form of
|
||||
// the recipes of tool/capsule_writer_go_vectors_test.go, with its files.
|
||||
type sample struct {
|
||||
Name string `json:"name"`
|
||||
Seed string `json:"seed"`
|
||||
Random string `json:"random"`
|
||||
Round uint64 `json:"round"`
|
||||
Policy string `json:"policy,omitempty"`
|
||||
Identities []string `json:"identities,omitempty"`
|
||||
Portable bool `json:"portable,omitempty"`
|
||||
Words []string `json:"words,omitempty"`
|
||||
Padding int `json:"padding,omitempty"`
|
||||
Files []fileIn `json:"files,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Critical []extIn `json:"critical,omitempty"`
|
||||
Noncritical []extIn `json:"noncritical,omitempty"`
|
||||
ControlCritical []extIn `json:"control_critical,omitempty"`
|
||||
ControlNoncritical []extIn `json:"control_noncritical,omitempty"`
|
||||
HeadCritical []extIn `json:"head_critical,omitempty"`
|
||||
HeadNoncritical []extIn `json:"head_noncritical,omitempty"`
|
||||
TestVectors bool `json:"test_vectors,omitempty"`
|
||||
TestAreaLen uint32 `json:"test_area_len,omitempty"`
|
||||
Node *bool `json:"node,omitempty"`
|
||||
File string `json:"file"`
|
||||
DKK string `json:"dkk,omitempty"`
|
||||
}
|
||||
|
||||
func identity(label string) *age.X25519Identity {
|
||||
s := sha256.Sum256([]byte("identity " + label))
|
||||
return must(agewrap.X25519IdentityFromRaw(s[:]))
|
||||
}
|
||||
|
||||
func exts(list []extIn) []extension.Extension {
|
||||
var out []extension.Extension
|
||||
for _, e := range list {
|
||||
x := extension.Extension{ID: e.ID, Version: e.Version}
|
||||
if e.Data != nil {
|
||||
x.Data = unhex(*e.Data)
|
||||
}
|
||||
out = append(out, x)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pattern(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = byte(31*i + 7)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func extSet(s sample) extension.Set {
|
||||
set := extension.Set{}
|
||||
for _, l := range [][]extIn{s.Critical, s.Noncritical, s.ControlCritical, s.ControlNoncritical, s.HeadCritical, s.HeadNoncritical} {
|
||||
for _, e := range l {
|
||||
set[e.ID] = append(set[e.ID], e.Version)
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// seeded is crypto/rand.Reader reading the keystream of SeededRandomSource.
|
||||
type seeded struct{ c *chacha20.Cipher }
|
||||
|
||||
func (s *seeded) Read(p []byte) (int, error) {
|
||||
clear(p)
|
||||
s.c.XORKeyStream(p, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// goWrites writes the sample with capsule.EncryptFiles while crypto/rand
|
||||
// reads the keystream of its seed.
|
||||
func goWrites(s sample) []byte {
|
||||
q := profile.Quicknet()
|
||||
opts := capsule.EncryptOptions{
|
||||
Profile: q, UnlockAt: must(datekey.RoundTime(q, s.Round)),
|
||||
Now: func() time.Time { return time.Unix(q.GenesisTime, 0) },
|
||||
NewPortableKey: s.Portable, Words: s.Words, Padding: capsule.Padding(s.Padding),
|
||||
Critical: exts(s.Critical), Noncritical: exts(s.Noncritical),
|
||||
ControlCritical: exts(s.ControlCritical), ControlNoncritical: exts(s.ControlNoncritical),
|
||||
HeadCritical: exts(s.HeadCritical), HeadNoncritical: exts(s.HeadNoncritical),
|
||||
Comment: s.Comment, Author: s.Author, PublicNote: s.Note,
|
||||
TestVectors: s.TestVectors, TestAreaLen: s.TestAreaLen,
|
||||
}
|
||||
if s.Policy == "time_and_key" {
|
||||
opts.Policy = capsule.TimeAndKey
|
||||
}
|
||||
for _, l := range s.Identities {
|
||||
opts.Recipients = append(opts.Recipients, identity(l).Recipient())
|
||||
}
|
||||
var sources []capsule.Source
|
||||
for _, f := range s.Files {
|
||||
content := pattern(f.Pattern)
|
||||
if f.Text != nil {
|
||||
content = []byte(*f.Text)
|
||||
}
|
||||
var mtime time.Time
|
||||
if f.MTime != nil {
|
||||
mtime = time.Unix(f.MTime[0], f.MTime[1])
|
||||
}
|
||||
sources = append(sources, capsule.Source{Path: f.Path, Size: int64(len(content)), ModTime: mtime,
|
||||
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(content)), nil }})
|
||||
}
|
||||
key := sha256.Sum256([]byte(s.Seed))
|
||||
old := rand.Reader
|
||||
rand.Reader = &seeded{must(chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize)))}
|
||||
defer func() { rand.Reader = old }()
|
||||
var b bytes.Buffer
|
||||
must(capsule.EncryptFiles(&b, sources, opts))
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func opened(s sample, dkc []byte, ids []age.Identity, dkk []byte) obj {
|
||||
files := &testkit.MemorySink{}
|
||||
o := capsule.OpenOptions{
|
||||
Registry: testkit.Registry(), Extensions: extSet(s),
|
||||
Source: testkit.NewSource(testkit.Release(1000), testkit.Release(1001)),
|
||||
Identities: ids, Sink: files,
|
||||
Now: func() time.Time { return time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC) },
|
||||
}
|
||||
if dkk != nil {
|
||||
o.AccessKeyFile = bytes.NewReader(dkk)
|
||||
}
|
||||
res, err := capsule.Open(context.Background(), nil, bytes.NewReader(dkc), o)
|
||||
v := obj{}
|
||||
if err != nil {
|
||||
v["result"] = datekeys.Code(err)
|
||||
if v["result"] == "" {
|
||||
v["result"] = "error: " + err.Error()
|
||||
}
|
||||
if res != nil && res.Inspection != nil && len(res.Inspection.Checks) > 0 {
|
||||
v["step"] = res.Inspection.Checks[len(res.Inspection.Checks)-1].Step
|
||||
}
|
||||
return v
|
||||
}
|
||||
v["result"] = "ok"
|
||||
fs := []obj{}
|
||||
for i, f := range res.Head.Files {
|
||||
fj := obj{"path": f.Path, "size": f.Size, "sha256": sum(files.Files[i])}
|
||||
if f.HasMTime {
|
||||
fj["mtime"] = f.MTime
|
||||
}
|
||||
fs = append(fs, fj)
|
||||
}
|
||||
v["files"] = fs
|
||||
v["comment"] = res.Head.Comment
|
||||
v["author"] = res.Head.Author
|
||||
v["head"] = h(must(capsule.EncodeHead(res.Head)))
|
||||
v["verdicts"] = []string{string(res.Verdicts.Signature), string(res.Verdicts.Seal)}
|
||||
v["area_len"] = res.AreaLen
|
||||
v["length"] = res.PayloadLength
|
||||
v["padded_length"] = res.PaddedLength
|
||||
return v
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "", "where the vectors go")
|
||||
src := flag.String("source", "", "the commit of datekeys-go")
|
||||
dir := flag.String("samples", "", "the directory of the samples")
|
||||
flag.Parse()
|
||||
if *out == "" || *src == "" || *dir == "" {
|
||||
log.Fatal("usage: -source <commit> -samples <dir> -out <dir>")
|
||||
}
|
||||
var in struct {
|
||||
Samples []sample `json:"samples"`
|
||||
}
|
||||
must(0, json.Unmarshal(must(os.ReadFile(filepath.Join(*dir, "samples.json"))), &in))
|
||||
q := profile.Quicknet()
|
||||
var results, node []obj
|
||||
for _, s := range in.Samples {
|
||||
dkc := must(os.ReadFile(filepath.Join(*dir, s.File)))
|
||||
var dkk []byte
|
||||
if s.DKK != "" {
|
||||
dkk = must(os.ReadFile(filepath.Join(*dir, s.DKK)))
|
||||
}
|
||||
insp, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry(), Extensions: extSet(s)})
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", s.Name, err)
|
||||
}
|
||||
note, _ := insp.Header.PublicNote()
|
||||
capsuleID := insp.Header.CapsuleID
|
||||
r := obj{
|
||||
"name": s.Name, "length_dkc": len(dkc), "sha256": sum(dkc),
|
||||
"inspect": obj{
|
||||
"format": int(insp.Prelude.Format), "policy": insp.Header.Policy.String(),
|
||||
"datekey": insp.Header.DateKey.String(), "note": note,
|
||||
},
|
||||
}
|
||||
if dkk != nil {
|
||||
r["dkk_sha256"] = sum(dkk)
|
||||
}
|
||||
var opens []obj
|
||||
with := func(name string, ids []age.Identity, k []byte) {
|
||||
v := opened(s, dkc, ids, k)
|
||||
v["with"] = name
|
||||
opens = append(opens, v)
|
||||
}
|
||||
round := insp.Header.DateKey.Round
|
||||
tid := must(agewrap.NewTimeIdentity(q, round, testkit.Release(round)))
|
||||
pre := insp.Prelude
|
||||
header := dkc[capsule.PreludeSize : capsule.PreludeSize+int(pre.PublicHeaderLen)]
|
||||
sealed := dkc[capsule.PreludeSize+int(pre.PublicHeaderLen) : capsule.PreludeSize+int(pre.PublicHeaderLen)+int(pre.SealedControlLen)]
|
||||
same := bytes.Equal(must(capsule.EncodeHeader(insp.Header)), header)
|
||||
control := must(io.ReadAll(must(age.Decrypt(bytes.NewReader(sealed), tid))))
|
||||
if s.Policy != "time_and_key" {
|
||||
with("time", nil, nil)
|
||||
} else {
|
||||
var all []age.Identity
|
||||
for _, l := range s.Identities {
|
||||
with("identity "+l, []age.Identity{identity(l)}, nil)
|
||||
all = append(all, identity(l))
|
||||
}
|
||||
if len(s.Words) != 0 {
|
||||
id := must(wordkey.Identity(s.Words, q.ChainHash[:], round, capsuleID[:]))
|
||||
with("words", []age.Identity{id}, nil)
|
||||
all = append(all, id)
|
||||
}
|
||||
if dkk != nil {
|
||||
with("portable", nil, dkk)
|
||||
}
|
||||
with("all", all, dkk)
|
||||
with("none", nil, nil)
|
||||
stanzas := must(agewrap.Stanzas(bytes.NewReader(control)))
|
||||
r["inner_stanzas"] = len(stanzas)
|
||||
var id age.Identity
|
||||
if len(all) > 0 {
|
||||
id = all[0]
|
||||
} else {
|
||||
k := must(accesskey.Decode(bytes.NewReader(dkk)))
|
||||
id = must(agewrap.X25519IdentityFromRaw(k.Material))
|
||||
}
|
||||
control = must(io.ReadAll(must(age.Decrypt(bytes.NewReader(control), must(agewrap.NewAccessIdentity(agewrap.AccessSlots, id))))))
|
||||
}
|
||||
c := must(capsule.DecodeControl(control, pre.Format))
|
||||
same = same && bytes.Equal(must(capsule.EncodeControl(c, pre.Format)), control)
|
||||
if dkk != nil {
|
||||
k := must(accesskey.Decode(bytes.NewReader(dkk)))
|
||||
var b bytes.Buffer
|
||||
must(0, accesskey.Encode(&b, k))
|
||||
same = same && bytes.Equal(b.Bytes(), dkk)
|
||||
r["dkk_capsule_digest"] = bytes.Equal(k.Verification.CapsuleDigest, must(hex.DecodeString(sum(dkc))))
|
||||
}
|
||||
r["reencoded"] = same
|
||||
r["opens"] = opens
|
||||
if s.Random == "seeded" {
|
||||
r["go_same"] = bytes.Equal(goWrites(s), dkc)
|
||||
}
|
||||
results = append(results, r)
|
||||
if s.Node == nil || *s.Node {
|
||||
node = append(node, r)
|
||||
}
|
||||
fmt.Printf("%s: %d bytes\n", s.Name, len(dkc))
|
||||
}
|
||||
doc := func(results []obj) []byte {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
must(0, enc.Encode(obj{
|
||||
"description": "Capsules that datekeys-dart writes from the recipes of test/capsule_interop_support.dart, with SeededRandomSource or the CSPRNG of the platform, inspected and opened by capsule.Open of datekeys-go with each credential, all together and none, their layers encoded again, and, for a seeded one, whether capsule.EncryptFiles writes the same bytes (tool/capsule_interop_go_verdicts.go).",
|
||||
"source": *src,
|
||||
"go": runtime.Version(),
|
||||
"samples": results,
|
||||
}))
|
||||
return buf.Bytes()
|
||||
}
|
||||
full := doc(results)
|
||||
must(0, os.WriteFile(filepath.Join(*out, "capsule_interop.json"), full, 0o644))
|
||||
dart := "// Generated by tool/capsule_interop_go_verdicts.go: the samples of\n" +
|
||||
"// test/vectors/capsule_interop.json that are not marked node false, for the\n" +
|
||||
"// tests that also run compiled to JavaScript. Do not edit.\n\n" +
|
||||
"/// Part of test/vectors/capsule_interop.json.\n" +
|
||||
"const capsuleInteropJson = r'''\n" + string(doc(node)) + "''';\n"
|
||||
must(0, os.WriteFile(filepath.Join(*out, "capsule_interop.g.dart"), []byte(dart), 0o644))
|
||||
fmt.Printf("wrote %d samples\n", len(results))
|
||||
}
|
||||
Loading…
Reference in new issue