// Helpers of the age tests: the identities and results of test/vectors/ // age.json and age_fixtures.json, the parts of a file, and the STREAM of age // written again, to rebuild the large files of the vectors. import 'dart:typed_data'; import 'package:datekeys/datekeys.dart'; import 'package:datekeys/src/age.dart'; import 'package:datekeys/src/base64.dart'; import 'package:datekeys/src/bytes.dart' show utf8Bytes; import 'package:datekeys/src/chacha20poly1305.dart'; import 'package:datekeys/src/curve25519.dart'; import 'package:datekeys/src/sha256.dart'; /// The identity of a case: `{x25519: AGE-SECRET-KEY-1…}` or `{scrypt: /// passphrase, max_work_factor: n}`. AgeIdentity identityOf(Map spec) { final x = spec['x25519'] as String?; if (x != null) return X25519Identity.parse(x); return ScryptIdentity( spec['scrypt']! as String, maxWorkFactor: spec['max_work_factor']! as int, ); } /// The outcome of decrypting [file] with [ids], in the form of the vectors: /// the SHA-256 and length of the plaintext, or the error and its phase. A /// DateKeysException of an identity is a failure of age.Decrypt, the header /// phase, as in Go. Map resultOf(Uint8List file, List ids) { try { final plain = ageDecrypt(file, ids); return { 'plaintext_sha256': toHex(sha256(plain)), 'plaintext_length': plain.length, }; } on AgeException catch (e) { return {'error': e.message, 'phase': e.phase.name}; } on DateKeysException catch (e) { return {'error': e.message, 'phase': 'header'}; } } /// The same outcome, with the payload fed to an [AgePayloadDecryptor] in /// pieces of the sizes that [step] gives. Map resultInPieces( Uint8List file, List ids, int Function() step, ) { final out = BytesBuilder(); try { final opened = ageOpen(file, ids); final d = opened.payload; for (var i = opened.payloadOffset; i < file.length;) { final n = step(); final end = i + n < file.length ? i + n : file.length; for (final p in d.add(file, i, end)) { out.add(p); } i = end; } out.add(d.close()); final plain = out.takeBytes(); return { 'plaintext_sha256': toHex(sha256(plain)), 'plaintext_length': plain.length, }; } on AgeException catch (e) { return {'error': e.message, 'phase': e.phase.name}; } on DateKeysException catch (e) { return {'error': e.message, 'phase': 'header'}; } } /// The bytes of the parts of a file: text or hex, repeated. Uint8List build(List? parts) { final out = BytesBuilder(copy: false); for (final p in (parts ?? const []).cast>()) { final text = p['text'] as String?; final bytes = text != null ? utf8Bytes(text) : fromHex(p['hex']! as String); final n = p['repeat'] as int? ?? 1; for (var i = 0; i < n; i++) { out.add(bytes); } } return out.takeBytes(); } /// The plaintext of n bytes of the large files: byte i is (31·i + 7) mod 256. Uint8List pattern(int n) { final b = Uint8List(n); for (var i = 0; i < n; i++) { b[i] = (31 * i + 7) & 0xff; } return b; } /// The STREAM of age over [plaintext] with the key of [fileKey] and /// [nonce], as age's EncryptWriter writes it: chunks of 64 KiB, the last one /// flagged and full when the plaintext is a multiple of 64 KiB, one empty /// chunk for an empty plaintext. Uint8List streamSeal(Uint8List fileKey, Uint8List nonce, Uint8List plaintext) { final key = hkdfSha256(fileKey, nonce, 'payload'.codeUnits, 32); final out = BytesBuilder(copy: false); final n = Uint8List(12); var i = 0; var index = 0; for (;;) { final end = i + ageChunkSize < plaintext.length ? i + ageChunkSize : plaintext.length; final last = end == plaintext.length; var c = index; for (var k = 10; k >= 0; k--) { n[k] = c & 0xff; c ~/= 256; } n[11] = last ? 1 : 0; out.add( chacha20Poly1305Seal(key, n, Uint8List.sublistView(plaintext, i, end)), ); if (last) break; i = end; index++; } return out.takeBytes(); } /// An X25519 stanza of age that wraps [fileKey] to the public key /// [recipient] with the ephemeral scalar [ephemeral], as age's /// X25519Recipient.Wrap. AgeStanza x25519Stanza( Uint8List fileKey, Uint8List recipient, Uint8List ephemeral, ) { final share = x25519PublicKey(ephemeral); final secret = x25519Agree(ephemeral, recipient); final key = hkdfSha256( secret, concatBytes([share, recipient]), 'age-encryption.org/v1/X25519'.codeUnits, 32, ); return AgeStanza('X25519', [ goBase64Encode(share, padded: false), ], chacha20Poly1305Seal(key, Uint8List(12), fileKey)); } /// A whole age file of [stanzas] that wrap [fileKey], with the payload /// [plaintext] under [nonce], as age.Encrypt writes it. Uint8List ageFile( List stanzas, Uint8List fileKey, Uint8List nonce, Uint8List plaintext, ) => concatBytes([ marshalAgeHeaderWithoutMac(stanzas), ' '.codeUnits, goBase64Encode(ageHeaderMac(fileKey, stanzas), padded: false).codeUnits, '\n'.codeUnits, nonce, streamSeal(fileKey, nonce, plaintext), ]);