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.
608 lines
21 KiB
608 lines
21 KiB
// The age writer against test/vectors/age_writer.json, which Go wrote with
|
|
// filippo.io/age v1.3.2 and agewrap while crypto/rand read the keystream of
|
|
// SeededRandomSource (tool/age_writer_go_vectors.go): with the same seed,
|
|
// AgeEncryptor draws the same values in the same order and writes the same
|
|
// bytes, whole or in pieces, and this library opens what it writes. Also
|
|
// the errors of the writer with Go's texts, the recipients, the STREAM and
|
|
// the lengths. The vectors come from a Dart constant, so that these tests
|
|
// also run compiled to JavaScript; there the expensive cases are left out.
|
|
|
|
import 'dart:convert';
|
|
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/base64.dart';
|
|
import 'package:datekeys/src/chacha20poly1305.dart';
|
|
import 'package:datekeys/src/random.dart';
|
|
import 'package:datekeys/src/recipient.dart';
|
|
import 'package:datekeys/src/sha256.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'age_writer_support.dart';
|
|
import 'vectors/age_writer.g.dart';
|
|
|
|
final Json vectors = jsonDecode(ageWriterJson) as Json;
|
|
|
|
bool affordable(Json c) => !isWeb || c['node'] != false;
|
|
|
|
String? ageError(void Function() f) {
|
|
try {
|
|
f();
|
|
return null;
|
|
} on AgeException catch (e) {
|
|
return e.message;
|
|
} on DateKeysException catch (e) {
|
|
return e.message;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
group('the files of Go', () {
|
|
for (final c in listOf(vectors['encrypt']).where(affordable)) {
|
|
final name = c['name']! as String;
|
|
test(name, () {
|
|
final specs = listOf(c['recipients']);
|
|
final n = c['length']! as int;
|
|
final plain = pattern(n);
|
|
final source = RecordingSource(seeded(c['seed']! as String));
|
|
final file = ageEncrypt(plain, [
|
|
for (final s in specs) recipientOf(s),
|
|
], random: source);
|
|
// The same draws, in the order and of the sizes of Go.
|
|
expect(source.json, c['draws']);
|
|
final header = fromHex(c['header']! as String);
|
|
expect(
|
|
toHex(Uint8List.sublistView(file, 0, header.length)),
|
|
c['header'],
|
|
);
|
|
expect(file.length, c['file_length']);
|
|
expect(toHex(sha256(file)), c['file_sha256']);
|
|
if (c['file'] != null) expect(toHex(file), c['file']);
|
|
|
|
// The lengths, from the shapes of the stanzas alone.
|
|
final lengths = stanzaLengths(header);
|
|
expect(ageHeaderLength(lengths), header.length);
|
|
expect(ageFileLength(lengths, n), file.length);
|
|
for (var i = 0; i < specs.length; i++) {
|
|
final r = recipientOf(specs[i]);
|
|
final want = switch (r) {
|
|
X25519Recipient(:final stanzaLength) => stanzaLength,
|
|
ScryptRecipient(:final stanzaLength) => stanzaLength,
|
|
TimeRecipient(:final stanzaLength) => stanzaLength,
|
|
_ => -1,
|
|
};
|
|
expect(lengths[i], want, reason: 'stanza $i');
|
|
}
|
|
|
|
// The same file when the plaintext arrives in pieces.
|
|
if (n > 0 && n <= 300000) {
|
|
final again = encryptInPieces(
|
|
plain,
|
|
[for (final s in specs) recipientOf(s)],
|
|
seeded(c['seed']! as String),
|
|
[1, 7, 65535, 65537, 1000],
|
|
);
|
|
expect(again, file);
|
|
}
|
|
|
|
// This library opens it with each identity that the case knows.
|
|
for (final s in specs) {
|
|
final id = identityOf(s);
|
|
if (id == null) continue;
|
|
expect(toHex(sha256(ageDecrypt(file, [id]))), toHex(sha256(plain)));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
group('the errors of age.Encrypt, with the draws before them', () {
|
|
for (final c in listOf(vectors['errors']).where(affordable)) {
|
|
test(c['name'], () {
|
|
final source = RecordingSource(seeded(c['seed']! as String));
|
|
final rs = [for (final s in listOf(c['recipients'])) recipientOf(s)];
|
|
expect(
|
|
ageError(() => ageEncrypt(pattern(10), rs, random: source)),
|
|
c['error'],
|
|
);
|
|
expect(source.json, c['draws']);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('the errors of the constructors', () {
|
|
for (final c in listOf(vectors['constructors'])) {
|
|
if (c['work_factor'] case final int wf) {
|
|
expect(
|
|
() => ScryptRecipient('p', workFactor: wf),
|
|
throwsA(
|
|
isA<ArgumentError>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
c['panic'],
|
|
),
|
|
),
|
|
reason: c['name'] as String?,
|
|
);
|
|
} else if (c['round'] case final int round) {
|
|
expect(ageError(() => TimeRecipient(quicknet(), round)), c['error']);
|
|
} else if (c['max_round'] case final int max) {
|
|
expect(quicknet().maxRound, max);
|
|
expect(TimeRecipient(quicknet(), max).round, max);
|
|
expect(TimeRecipient(quicknet(), 1).round, 1);
|
|
} else {
|
|
expect(ageError(() => ScryptRecipient('')), c['error']);
|
|
expect(ageError(() => ScryptRecipient.bytes(const [])), c['error']);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('the STREAM once closed, with the texts of Go', () {
|
|
final texts = vectors['stream']! as Json;
|
|
final e = AgeEncryptor([
|
|
X25519Recipient.of(labelIdentity('stream')),
|
|
], random: seeded('age writer stream'));
|
|
e.close();
|
|
Matcher closed(Object? text) =>
|
|
throwsA(isA<StateError>().having((e) => e.message, 'message', text));
|
|
expect(() => e.add([1]), closed(texts['write_after_close']));
|
|
expect(() => e.add(const []), closed(texts['empty_write_after_close']));
|
|
expect(e.close, closed(texts['close_after_close']));
|
|
// An abandoned one refuses everything too.
|
|
final w = AgeEncryptor([
|
|
X25519Recipient.of(labelIdentity('stream')),
|
|
], random: seeded('abandoned'));
|
|
w.add(pattern(70000));
|
|
w.wipe();
|
|
expect(
|
|
w.close,
|
|
throwsA(
|
|
isA<StateError>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
'the encryption of the payload was abandoned',
|
|
),
|
|
),
|
|
);
|
|
e.wipe(); // nothing once closed
|
|
});
|
|
|
|
test('age1… recipients, parsed with the texts of Go', () {
|
|
for (final c in listOf(vectors['parse'])) {
|
|
final input = c['input']! as String;
|
|
if (c['error'] case final String text) {
|
|
expect(
|
|
ageError(() => X25519Recipient.parse(input)),
|
|
text,
|
|
reason: input,
|
|
);
|
|
} else {
|
|
final r = X25519Recipient.parse(input);
|
|
expect(toHex(r.publicKey), c['raw']);
|
|
expect(toHex(rawX25519Recipient(r)), c['raw']);
|
|
expect(r.toString(), c['string']);
|
|
}
|
|
}
|
|
expect(
|
|
ageError(() => X25519Recipient(Uint8List(31))),
|
|
'invalid X25519 public key',
|
|
);
|
|
});
|
|
|
|
test(
|
|
'the rules of spec §37, with the texts of agewrap.CheckX25519Recipient',
|
|
() {
|
|
for (final c in listOf(vectors['check'])) {
|
|
final r = X25519Recipient(fromHex(c['raw']! as String));
|
|
expect(r.toString(), c['recipient']);
|
|
final text = c['error'] as String?;
|
|
if (text == null) {
|
|
checkX25519Recipient(r);
|
|
} else {
|
|
expect(
|
|
() => checkX25519Recipient(r),
|
|
throwsA(
|
|
isA<ArgumentError>().having((e) => e.message, 'message', text),
|
|
),
|
|
reason: c['raw'] as String?,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
test('X25519 identities generated as age.GenerateX25519Identity', () {
|
|
for (final c in listOf(vectors['generate'])) {
|
|
final source = RecordingSource(seeded(c['seed']! as String));
|
|
final id = generateX25519Identity(source);
|
|
expect(source.json, c['draws']);
|
|
expect(toHex(rawX25519Identity(id)), c['raw']);
|
|
expect(id.toString(), c['identity']);
|
|
expect(X25519Recipient.of(id).toString(), c['recipient']);
|
|
expect(id.recipientString, c['recipient']);
|
|
}
|
|
});
|
|
|
|
test('the lengths of Go: testkit.StreamLen and capsule.PayloadAgeLength', () {
|
|
final l = vectors['lengths']! as Json;
|
|
for (final c in listOf(l['stream'])) {
|
|
expect(ageStreamLength(c['n']! as int), c['length'], reason: '${c['n']}');
|
|
}
|
|
for (final c in listOf(l['payload_age'])) {
|
|
final p = c['p']! as int;
|
|
expect(ageFileLength([x25519StanzaLength], p), c['length']);
|
|
expect(payloadAgeLength(p), c['length']);
|
|
}
|
|
});
|
|
|
|
test('the lengths of spec §62.1, note', () {
|
|
int c(int n) => n == 0 ? 1 : (n + 65535) ~/ 65536;
|
|
for (final n in [0, 1, 103, 127, 65535, 65536, 65537, 1 << 20]) {
|
|
expect(ageFileLength([x25519StanzaLength], n), 184 + n + 16 * c(n));
|
|
final inner = ageFileLength(List.filled(16, x25519StanzaLength), n);
|
|
expect(inner, 86 + 98 * 16 + n + 16 * c(n));
|
|
for (final round in [1, 1000, 83903165811]) {
|
|
final d = '$round'.length;
|
|
expect(
|
|
ageFileLength([tlockStanzaLength(round)], n),
|
|
335 + d + n + 16 * c(n),
|
|
);
|
|
}
|
|
}
|
|
// SEALED_CONTROL_LEN with C = 103 at round 1000: 458 and 2128.
|
|
expect(ageFileLength([tlockStanzaLength(1000)], 103), 458);
|
|
final inner = ageFileLength(List.filled(16, x25519StanzaLength), 103);
|
|
expect(ageFileLength([tlockStanzaLength(1000)], inner), 2128);
|
|
// A scrypt stanza: 78 bytes and the digits of the work factor.
|
|
expect(scryptStanzaLength(1), 79);
|
|
expect(scryptStanzaLength(16), 80);
|
|
expect(ScryptRecipient('p', workFactor: 9).stanzaLength, 79);
|
|
expect(ageStanzaLength('X25519', ['a' * 43], 32), x25519StanzaLength);
|
|
// The body lines: 48 bytes fill a line, and an empty one ends it.
|
|
expect(ageStanzaLength('t', const [], 0), 6);
|
|
expect(ageStanzaLength('t', const [], 47), 6 + 63);
|
|
expect(ageStanzaLength('t', const [], 48), 6 + 65);
|
|
expect(ageStanzaLength('t', const ['ab', 'c'], 49), 10 + 68);
|
|
for (final n in [0, 1, 47, 48, 49, 95, 96, 97, 300]) {
|
|
final s = AgeStanza('t', const ['ab', 'c'], Uint8List(n));
|
|
expect(
|
|
marshalAgeHeaderWithoutMac([s]).length - 25,
|
|
ageStanzaLength('t', const ['ab', 'c'], n),
|
|
reason: '$n',
|
|
);
|
|
}
|
|
});
|
|
|
|
group('the STREAM', () {
|
|
final key = sha256('stream key'.codeUnits);
|
|
|
|
List<Uint8List> chunksOf(int n, List<int> steps) {
|
|
final e = AgePayloadEncryptor(Uint8List.fromList(key));
|
|
final out = <Uint8List>[];
|
|
var k = 0;
|
|
final p = pattern(n);
|
|
for (var i = 0; i < n;) {
|
|
final m = steps[k++ % steps.length];
|
|
final end = i + m < n ? i + m : n;
|
|
out.addAll(e.add(p, i, end));
|
|
i = end;
|
|
}
|
|
out.add(e.close());
|
|
return out;
|
|
}
|
|
|
|
test('chunks of 64 KiB, the last one flagged, full or short', () {
|
|
for (final n in [0, 1, 65535, 65536, 65537, 131072, 131073]) {
|
|
final chunks = chunksOf(n, [n == 0 ? 1 : n]);
|
|
final want = n == 0 ? 1 : (n + 65535) ~/ 65536;
|
|
expect(chunks, hasLength(want), reason: '$n');
|
|
for (var i = 0; i < chunks.length; i++) {
|
|
final last = i == chunks.length - 1;
|
|
final size = last ? n - 65536 * (chunks.length - 1) : 65536;
|
|
expect(chunks[i].length, size + 16, reason: '$n, chunk $i');
|
|
// Its nonce: the counter i, big-endian, and the flag of the last.
|
|
final nonce = Uint8List(12);
|
|
var c = i;
|
|
for (var k = 10; k >= 0; k--) {
|
|
nonce[k] = c & 0xff;
|
|
c ~/= 256;
|
|
}
|
|
nonce[11] = last ? 1 : 0;
|
|
expect(
|
|
chacha20Poly1305Open(key, nonce, chunks[i]),
|
|
Uint8List.sublistView(pattern(n), 65536 * i, 65536 * i + size),
|
|
reason: '$n, chunk $i',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('pieces of every size give the same chunks', () {
|
|
final whole = chunksOf(140000, [140000]);
|
|
for (final steps in [
|
|
[1000],
|
|
[65535],
|
|
[65536],
|
|
[65537],
|
|
[1, 65535, 3],
|
|
[70000, 0, 1],
|
|
]) {
|
|
expect(chunksOf(140000, steps), whole, reason: '$steps');
|
|
}
|
|
// A full chunk is held until a later byte arrives.
|
|
final e = AgePayloadEncryptor(Uint8List.fromList(key));
|
|
expect(e.add(pattern(65536)), isEmpty);
|
|
expect(e.add(const []), isEmpty);
|
|
expect(e.add([1]), hasLength(1));
|
|
expect(e.close(), hasLength(17));
|
|
});
|
|
|
|
test('this library reads what it writes', () {
|
|
for (final n in [0, 1, 65536, 65537, 200000]) {
|
|
final d = AgePayloadDecryptor(Uint8List.fromList(key));
|
|
final out = BytesBuilder();
|
|
for (final c in chunksOf(n, [n == 0 ? 1 : 9999])) {
|
|
d.add(c).forEach(out.add);
|
|
}
|
|
out.add(d.close());
|
|
expect(out.takeBytes(), pattern(n), reason: '$n');
|
|
}
|
|
});
|
|
|
|
test('its key is 32 bytes, and it wipes it', () {
|
|
expect(() => AgePayloadEncryptor(Uint8List(31)), throwsArgumentError);
|
|
final k = Uint8List.fromList(key);
|
|
AgePayloadEncryptor(k).close();
|
|
expect(k, Uint8List(32));
|
|
final w = Uint8List.fromList(key);
|
|
AgePayloadEncryptor(w)
|
|
..add(pattern(10))
|
|
..wipe();
|
|
expect(w, Uint8List(32));
|
|
});
|
|
});
|
|
|
|
group('recipients', () {
|
|
final fileKey = sha256('file key'.codeUnits).sublist(0, 16);
|
|
|
|
test('an X25519 stanza: 98 bytes, a fresh share each time', () {
|
|
final id = labelIdentity('fresh');
|
|
final r = X25519Recipient.of(id);
|
|
final source = seeded('fresh shares');
|
|
final shares = <String>{};
|
|
for (var i = 0; i < 20; i++) {
|
|
final w = r.wrap(fileKey, source);
|
|
expect(w.labels, isEmpty);
|
|
final s = w.stanzas.single;
|
|
expect(s.type, stanzaX25519);
|
|
expect(shares.add(s.args.single), isTrue);
|
|
expect(id.unwrap([s]), fileKey);
|
|
}
|
|
final file = ageEncrypt(pattern(5), [
|
|
for (var i = 0; i < 16; i++) X25519Recipient.of(labelIdentity('$i')),
|
|
], random: seeded('sixteen'));
|
|
final stanzas = ageStanzas(file);
|
|
checkAccessStanzas(stanzas, accessSlots);
|
|
for (var i = 0; i < 16; i++) {
|
|
expect(
|
|
ageDecrypt(file, [
|
|
AccessIdentity(accessSlots, [labelIdentity('$i')]),
|
|
]),
|
|
pattern(5),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('PAYLOAD_AGE opens with I_PAYLOAD, as agewrap', () {
|
|
final id = labelIdentity('payload');
|
|
final file = ageEncrypt(pattern(70000), [
|
|
X25519Recipient.of(id),
|
|
], random: seeded('payload'));
|
|
expect(ageDecrypt(file, [PayloadIdentity(id.secretKey)]), pattern(70000));
|
|
expect(
|
|
ageFileLength([x25519StanzaLength], 70000),
|
|
payloadAgeLength(70000),
|
|
);
|
|
expect(file.length, payloadAgeLength(70000));
|
|
});
|
|
|
|
test('the recipient of a raw key, and of a parsed one', () {
|
|
final id = labelIdentity('raw');
|
|
final r = X25519Recipient(id.recipient);
|
|
expect(X25519Recipient.parse(r.toString()).publicKey, id.recipient);
|
|
expect(r.toString(), id.recipientString);
|
|
final copy = r.publicKey..[0] ^= 1;
|
|
expect(r.publicKey, isNot(copy));
|
|
});
|
|
|
|
test('scrypt: alone in its file, and its passphrase opens it', () {
|
|
final r = ScryptRecipient('pass', workFactor: 2);
|
|
expect(r.workFactor, 2);
|
|
expect(ScryptRecipient('pass').workFactor, defaultScryptWorkFactor);
|
|
final w = r.wrap(fileKey, seeded('scrypt alone'));
|
|
expect(w.labels.single, matches(RegExp(r'^[0-9a-f]{32}$')));
|
|
final s = w.stanzas.single;
|
|
expect(s.args[1], '2');
|
|
expect(goBase64Decode(s.args[0].codeUnits, padded: false), hasLength(16));
|
|
expect(ScryptIdentity('pass').unwrap([s]), fileKey);
|
|
expect(
|
|
ageError(() => ScryptIdentity('wrong').unwrap([s])),
|
|
'incorrect identity for recipient block: incorrect passphrase',
|
|
);
|
|
final bytes = ScryptRecipient.bytes(utf8.encode('pass'), workFactor: 2);
|
|
expect(
|
|
ScryptIdentity('pass').unwrap(bytes.wrap(fileKey, seeded('b')).stanzas),
|
|
fileKey,
|
|
);
|
|
r.wipe();
|
|
});
|
|
|
|
test('tlock: the stanza of the round, alone, with a random label', () {
|
|
final r = TimeRecipient(quicknet(), 1000);
|
|
final w = r.wrap(fileKey, seeded('tlock label'));
|
|
expect(
|
|
w.labels.single,
|
|
matches(RegExp(r'^datekeys-tlock-[0-9a-f]{32}$')),
|
|
);
|
|
final s = w.stanzas.single;
|
|
checkTimeStanzas(
|
|
w.stanzas,
|
|
round: 1000,
|
|
chainHashHex: quicknet().chainHashHex,
|
|
profileId: quicknet().id,
|
|
);
|
|
final id = TimeIdentity(
|
|
quicknet(),
|
|
1000,
|
|
Release(1000, fromHex(releases[1000]!)),
|
|
);
|
|
expect(id.unwrap([s]), fileKey);
|
|
});
|
|
|
|
test('a recipient that throws, and stanzas that cannot be marshalled', () {
|
|
expect(
|
|
ageError(
|
|
() => ageEncrypt(const [], [
|
|
_Fails(const AgeException('nope')),
|
|
], random: seeded('x')),
|
|
),
|
|
'failed to wrap key for recipient #0: nope',
|
|
);
|
|
final e = DateKeysException(ErrorCode.integrity, 'bad');
|
|
expect(
|
|
() => ageEncrypt(const [], [
|
|
X25519Recipient.of(labelIdentity('a')),
|
|
_Fails(e),
|
|
], random: seeded('x')),
|
|
throwsA(
|
|
isA<DateKeysException>()
|
|
.having((e) => e.code, 'code', ErrorCode.integrity)
|
|
.having(
|
|
(e) => e.message,
|
|
'message',
|
|
'failed to wrap key for recipient #1: bad: ERR_INTEGRITY',
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
ageError(() => ageEncrypt(const [], [_Gives([])], random: seeded('x'))),
|
|
'failed to compute header MAC: no recipient stanzas',
|
|
);
|
|
expect(
|
|
ageError(
|
|
() => ageEncrypt(const [], [
|
|
_Gives([AgeStanza('a b', const [], Uint8List(0))]),
|
|
], random: seeded('x')),
|
|
),
|
|
'failed to compute header MAC: invalid stanza type: "a b"',
|
|
);
|
|
expect(
|
|
ageError(
|
|
() => ageEncrypt(const [], [
|
|
_Gives([
|
|
AgeStanza('t', const ['é'], Uint8List(0)),
|
|
]),
|
|
], random: seeded('x')),
|
|
),
|
|
'failed to compute header MAC: invalid stanza argument: "é"',
|
|
);
|
|
// A recipient that gives no stanza beside one that does.
|
|
final id = labelIdentity('alone');
|
|
final file = ageEncrypt(pattern(3), [
|
|
_Gives([]),
|
|
X25519Recipient.of(id),
|
|
], random: seeded('x'));
|
|
expect(ageDecrypt(file, [id]), pattern(3));
|
|
// The labels are compared sorted, and a post-quantum one alone is
|
|
// named.
|
|
expect(
|
|
ageError(
|
|
() => ageEncrypt(const [], [
|
|
_Gives([], ['b', 'a']),
|
|
_Gives([], ['a', 'c']),
|
|
], random: seeded('x')),
|
|
),
|
|
'incompatible recipients: ["a" "b"] and ["a" "c"] can\'t be mixed',
|
|
);
|
|
expect(
|
|
ageError(
|
|
() => ageEncrypt(const [], [
|
|
_Gives([], ['postquantum']),
|
|
_Gives([], []),
|
|
], random: seeded('x')),
|
|
),
|
|
"incompatible recipients: can't mix post-quantum and classic "
|
|
'recipients, or the file would be vulnerable to quantum computers',
|
|
);
|
|
final both = ageEncrypt(pattern(3), [
|
|
_Gives([], ['b', 'a']),
|
|
_Labeled(X25519Recipient.of(id), ['a', 'b']),
|
|
], random: seeded('x'));
|
|
expect(ageDecrypt(both, [id]), pattern(3));
|
|
});
|
|
|
|
test('the file key it wraps is wiped once the header is written', () {
|
|
final capture = _Capture();
|
|
final e = AgeEncryptor([capture], random: seeded('wipe'));
|
|
expect(capture.seen, hasLength(16));
|
|
expect(capture.seen, Uint8List(16));
|
|
expect(e.payloadOffset, e.header.length + 16);
|
|
expect(e.stanzas.single.type, 'X25519');
|
|
});
|
|
});
|
|
|
|
test('the default source is the CSPRNG of the platform', testOn: 'vm', () {
|
|
final id = generateX25519Identity();
|
|
final a = ageEncrypt(pattern(10), [X25519Recipient.of(id)]);
|
|
final b = ageEncrypt(pattern(10), [X25519Recipient.of(id)]);
|
|
expect(a, isNot(b));
|
|
expect(ageDecrypt(a, [id]), pattern(10));
|
|
expect(ageDecrypt(b, [id]), pattern(10));
|
|
expect(generateX25519Identity().secretKey, isNot(id.secretKey));
|
|
});
|
|
}
|
|
|
|
// A recipient that throws.
|
|
final class _Fails implements AgeRecipient {
|
|
_Fails(this.error);
|
|
final Exception error;
|
|
|
|
@override
|
|
AgeWrap wrap(Uint8List fileKey, RandomSource random) => throw error;
|
|
}
|
|
|
|
// A recipient that gives the stanzas and labels it was given.
|
|
final class _Gives implements AgeRecipient {
|
|
_Gives(this.stanzas, [this.labels = const []]);
|
|
final List<AgeStanza> stanzas;
|
|
final List<String> labels;
|
|
|
|
@override
|
|
AgeWrap wrap(Uint8List fileKey, RandomSource random) =>
|
|
(stanzas: stanzas, labels: labels);
|
|
}
|
|
|
|
// Another recipient with labels.
|
|
final class _Labeled implements AgeRecipient {
|
|
_Labeled(this.inner, this.labels);
|
|
final AgeRecipient inner;
|
|
final List<String> labels;
|
|
|
|
@override
|
|
AgeWrap wrap(Uint8List fileKey, RandomSource random) =>
|
|
(stanzas: inner.wrap(fileKey, random).stanzas, labels: labels);
|
|
}
|
|
|
|
// A recipient that keeps the file key it is given, to see it wiped.
|
|
final class _Capture implements AgeRecipient {
|
|
Uint8List seen = Uint8List(0);
|
|
|
|
@override
|
|
AgeWrap wrap(Uint8List fileKey, RandomSource random) {
|
|
seen = fileKey;
|
|
return X25519Recipient.of(labelIdentity('capture')).wrap(fileKey, random);
|
|
}
|
|
}
|