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.
dateKeys-dart/test/age_support.dart

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),
]);

Powered by TurnKey Linux.