// The samples of the interoperability of the age writer: age files that // this library writes with SeededRandomSource, from recipes, for Go to open // (tool/age_interop_dart_samples.dart writes them, and // tool/age_interop_go_verdicts.go opens them with filippo.io/age and the // identities of agewrap and writes test/vectors/age_interop.json). Each // recipe names its seed, its recipients, its plaintext (the pattern of its // length, or another age file, as INNER_ACCESS_AGE inside OUTER_TIME_AGE), // the pieces in which the plaintext arrives, and the identities that open // it. They read no file. import 'dart:typed_data'; import 'package:datekeys/datekeys.dart'; import 'package:datekeys/src/age.dart'; import 'package:datekeys/src/age_writer.dart'; import 'package:datekeys/src/agewrap.dart'; import 'package:datekeys/src/sha256.dart'; import 'age_writer_support.dart'; String _x(String label) => X25519Recipient.of(labelIdentity(label)).toString(); String _secret(String label) => labelIdentity(label).toString(); String _raw(String label) => toHex(labelIdentity(label).secretKey); /// The recipes of the samples, in the form of age_interop.json. List interopSamples() { final sixteen = [for (var i = 0; i < 16; i++) 'interop slot $i']; Json access(List labels, int slots) => { 'access': [for (final l in labels) _raw(l)], 'slots': slots, }; return [ for (final n in [0, 1, 65535, 65536, 65537, 131072, 3 << 20 | 1]) { 'name': 'x25519, $n bytes', 'seed': 'interop x25519 $n', 'recipients': [ {'x25519': _x('interop one')}, ], 'length': n, 'node': n <= 131072, 'openers': [ {'x25519': _secret('interop one')}, {'payload': _raw('interop one')}, {'x25519': _secret('interop stranger')}, {'payload': _raw('interop stranger')}, ], }, { 'name': 'x25519, 200000 bytes in pieces', 'seed': 'interop pieces', 'recipients': [ {'x25519': _x('interop one')}, ], 'length': 200000, 'pieces': [1, 7, 65535, 65537, 1000, 3], 'node': true, 'openers': [ {'x25519': _secret('interop one')}, ], }, { 'name': 'x25519, three recipients', 'seed': 'interop three', 'recipients': [ for (final l in ['interop a', 'interop b', 'interop c']) {'x25519': _x(l)}, ], 'length': 1000, 'node': true, 'openers': [ for (final l in ['interop a', 'interop b', 'interop c']) {'x25519': _secret(l)}, access(['interop b'], 0), access(['interop a', 'interop stranger'], 0), {'payload': _raw('interop a')}, access(['interop a'], 16), ], }, { 'name': 'x25519, sixteen recipients', 'seed': 'interop sixteen', 'recipients': [ for (final l in sixteen) {'x25519': _x(l)}, ], 'length': 103, 'node': true, 'openers': [ access([sixteen[0]], 16), access([sixteen[7]], 16), access([sixteen[15]], 16), access([sixteen[3], sixteen[9]], 16), access(['interop stranger'], 16), access([sixteen[5]], 0), ], }, { 'name': 'tlock, round 1000', 'seed': 'interop tlock', 'recipients': [ {'tlock': 1000}, ], 'length': 103, 'node': true, 'openers': [ {'tlock': 1000, 'release': 1000}, {'tlock': 1001, 'release': 1001}, ], }, { 'name': 'tlock over sixteen x25519, as SEALED_CONTROL', 'seed': 'interop sealed control', 'recipients': [ {'tlock': 1000}, ], 'inner': { 'seed': 'interop inner access', 'recipients': [ for (final l in sixteen) {'x25519': _x(l)}, ], 'length': 103, }, 'node': false, 'openers': [ { 'tlock': 1000, 'release': 1000, 'then': access([sixteen[11]], 16), }, { 'tlock': 1000, 'release': 1000, 'then': access(['interop stranger'], 16), }, ], }, for (final wf in [10, 16]) { 'name': 'scrypt, work factor $wf', 'seed': 'interop scrypt $wf', 'recipients': [ {'scrypt': 'una contraseña larga de prueba', 'work_factor': wf}, ], 'length': 120, 'node': wf <= 10, 'openers': [ {'scrypt': 'una contraseña larga de prueba', 'max_work_factor': 16}, {'scrypt': 'otra contraseña', 'max_work_factor': 16}, { 'scrypt': 'una contraseña larga de prueba', 'max_work_factor': wf - 1, }, ], }, ]; } /// The file of [sample], as this library writes it from its recipe. Uint8List writeSample(Json sample) { final rs = [for (final r in listOf(sample['recipients'])) recipientOf(r)]; final inner = sample['inner'] as Json?; final plain = inner == null ? pattern(sample['length']! as int) : writeSample(inner); final source = seeded(sample['seed']! as String); final pieces = sample['pieces'] as List?; if (pieces == null) return ageEncrypt(plain, rs, random: source); return encryptInPieces(plain, rs, source, pieces.cast()); } /// The plaintext that [sample] holds in the end: the pattern of its length, /// in its inner file when it has one. Uint8List finalPlaintext(Json sample) { final inner = sample['inner'] as Json?; return pattern((inner ?? sample)['length']! as int); } /// What this library makes of [file] with [opener], in the form of the /// verdicts of Go: the SHA-256 and the length of the plaintext, or the text /// of the error. Json openWith(Uint8List file, Json opener) { try { final plain = _open(file, opener); return { 'plaintext_length': plain.length, 'plaintext_sha256': toHex(sha256(plain)), }; } on AgeException catch (e) { return {'error': e.message}; } on DateKeysException catch (e) { return {'error': e.message}; } } Uint8List _open(Uint8List file, Json o) { final AgeIdentity id; if (o['x25519'] case final String s) { id = X25519Identity.parse(s); } else if (o['payload'] case final String raw) { id = PayloadIdentity(fromHex(raw)); } else if (o['access'] case final List ids) { id = AccessIdentity(o['slots']! as int, [ for (final r in ids.cast()) X25519Identity(fromHex(r)), ]); } else if (o['scrypt'] case final String pass) { id = ScryptIdentity(pass, maxWorkFactor: o['max_work_factor']! as int); } else { final round = o['tlock']! as int; final release = o['release']! as int; id = TimeIdentity( quicknet(), round, Release(release, fromHex(releases[release]!)), ); } final plain = ageDecrypt(file, [id]); final then = o['then'] as Json?; return then == null ? plain : _open(plain, then); }