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.
166 lines
5.1 KiB
166 lines
5.1 KiB
// 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<String, Object?> 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<String, Object?> resultOf(Uint8List file, List<AgeIdentity> 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<String, Object?> resultInPieces(
|
|
Uint8List file,
|
|
List<AgeIdentity> 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<Object?>? parts) {
|
|
final out = BytesBuilder(copy: false);
|
|
for (final p in (parts ?? const []).cast<Map<String, Object?>>()) {
|
|
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<AgeStanza> 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),
|
|
]);
|