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_vectors_test.dart

283 lines
8.9 KiB

// age against test/vectors/age.json, which filippo.io/age v1.3.2 and the
// agewrap package of datekeys-go wrote and read (tool/gen_age_vectors.go):
// X25519 and scrypt files, their truncations and manipulations, a corpus of
// headers against the grammar and the 2 MiB limit, the stanza rules and
// the identities of agewrap, with the text of Go's error for every case.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:datekeys/src/age.dart';
import 'package:datekeys/src/agewrap.dart';
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'age_support.dart';
final Map<String, Object?> vectors = jsonDecode(
File('test/vectors/age.json').readAsStringSync(),
) as Map<String, Object?>;
List<Map<String, Object?>> section(String name) =>
(vectors[name]! as List).cast<Map<String, Object?>>();
Map<String, Object?> asMap(Object? v) => v! as Map<String, Object?>;
List<AgeStanza> stanzasOf(Object? list) => [
for (final s in (list! as List).cast<Map<String, Object?>>())
AgeStanza(
s['type']! as String,
(s['args']! as List).cast<String>(),
Uint8List(0),
),
];
Map<String, Object?> describe(AgeStanza s) => {
'type': s.type,
'args': s.args,
'body_length': s.body.length,
'body_sha256': toHex(sha256(s.body)),
};
String? errorOf(void Function() f) {
try {
f();
return null;
} on DateKeysException catch (e) {
return e.message;
}
}
void main() {
final ids = asMap(vectors['identities']);
final files = <String, Uint8List>{
for (final f in section('files'))
f['name']! as String: fromHex(f['hex']! as String),
};
group('the large files, rebuilt', () {
for (final f in section('large')) {
test(f['name'], () {
final header = fromHex(f['header']! as String);
final nonce = fromHex(f['nonce']! as String);
final file = concatBytes([
header,
nonce,
streamSeal(
fromHex(f['file_key']! as String),
nonce,
pattern(f['length']! as int),
),
]);
expect(file.length, f['file_length']);
expect(toHex(sha256(file)), f['file_sha256']);
files[f['name']! as String] = file;
});
}
});
test('each file decrypts to its plaintext', () {
for (final f in section('files')) {
final h = parseAgeHeader(files[f['name']]!);
expect(h.stanzas, isNotEmpty);
}
final main = X25519Identity.parse(ids['main']! as String);
expect(main.recipientString, ids['main_recipient']);
expect(toHex(main.recipient), ids['main_recipient_raw']);
expect(main.toString(), ids['main']);
expect(ageDecrypt(files['x25519_100']!, [main]), pattern(100));
});
test('the cases: decryption, truncation and manipulation, as Go', () {
var n = 0;
for (final c in section('cases')) {
var file = Uint8List.fromList(files[c['file']]!);
final e = c['edit'] as Map<String, Object?>?;
if (e != null) {
if (e['truncate'] != null) {
file = Uint8List.sublistView(file, 0, e['truncate']! as int);
}
if (e['at'] != null) {
final x = fromHex(e['xor']! as String);
for (var i = 0; i < x.length; i++) {
file[(e['at']! as int) + i] ^= x[i];
}
}
if (e['append'] != null) {
file = concatBytes([file, fromHex(e['append']! as String)]);
}
}
final identities = [
for (final s in (c['identities']! as List).cast<Map<String, Object?>>())
identityOf(s),
];
expect(
resultOf(file, identities),
c['result'],
reason: c['name'] as String?,
);
n++;
}
expect(n, greaterThan(150));
});
test('the STREAM fed in pieces gives the same results', () {
final r = Random(5);
final main = [X25519Identity.parse(ids['main']! as String)];
for (final c in section('cases')) {
if (!(c['file']! as String).startsWith('x25519_large') ||
c['edit'] == null) {
continue;
}
final e = c['edit']! as Map<String, Object?>;
var file = Uint8List.fromList(files[c['file']]!);
if (e['truncate'] != null) {
file = Uint8List.sublistView(file, 0, e['truncate']! as int);
}
if (e['at'] != null) {
file[e['at']! as int] ^= fromHex(e['xor']! as String)[0];
}
if (e['append'] != null) {
file = concatBytes([file, fromHex(e['append']! as String)]);
}
for (final step in [() => 1 + r.nextInt(100000), () => 65552, () => 7]) {
if (step() == 7 && file.length > 70000) continue;
expect(
resultInPieces(file, main, step),
c['result'],
reason: c['name'] as String?,
);
}
}
});
test('the headers: the grammar of spec §28.1 and the limits of age', () {
final main = X25519Identity.parse(ids['main']! as String);
for (final c in section('headers')) {
final file = build(c['parts'] as List?);
expect(file.length, c['length'], reason: c['name'] as String?);
expect(toHex(sha256(file)), c['sha256']);
expect(resultOf(file, [main]), c['result'], reason: c['name'] as String?);
final probe = asMap(c['probe']);
try {
final ss = ageStanzas(file);
expect(probe['error'], isNull, reason: c['name'] as String?);
expect(ss.length, probe['count']);
if (probe['stanzas'] != null) {
expect([for (final s in ss) describe(s)], probe['stanzas']);
} else {
expect(describe(ss.first), probe['first']);
expect(describe(ss.last), probe['last']);
}
} on DateKeysException catch (e) {
expect(e.message, probe['error'], reason: c['name'] as String?);
expect(e.code, ErrorCode.integrity);
}
}
});
test('a prefix longer than 2 MiB parses as the whole file', () {
// parseAgeHeader of the first 2 MiB + 1 bytes of a file gives what the
// file gives, whatever follows.
for (final c in section('headers')) {
final file = build(c['parts'] as List?);
if (file.length <= maxAgeHeaderLength + 1) continue;
final prefix = Uint8List.sublistView(file, 0, maxAgeHeaderLength + 1);
String outcome(Uint8List b) {
try {
return '${parseAgeHeader(b).length}';
} on AgeException catch (e) {
return e.message;
}
}
expect(outcome(prefix), outcome(file), reason: c['name'] as String?);
}
});
test('the stanza rules of agewrap', () {
final time = asMap(vectors['time_stanzas']);
for (final c in (time['cases']! as List).cast<Map<String, Object?>>()) {
expect(
errorOf(
() => checkTimeStanzas(
stanzasOf(c['stanzas']),
round: c['round']! as int,
chainHashHex: time['chain_hash']! as String,
profileId: time['profile_id']! as String,
),
),
c['error'],
reason: c['name'] as String?,
);
}
for (final c in section('payload_stanzas')) {
expect(
errorOf(() => checkPayloadStanzas(stanzasOf(c['stanzas']))),
c['error'],
reason: c['name'] as String?,
);
}
for (final c in section('access_stanzas')) {
expect(
errorOf(
() => checkAccessStanzas(stanzasOf(c['stanzas']), c['slots']! as int),
),
c['error'],
reason: '${c['name']}, ${c['slots']} slots',
);
}
});
test('the identities of agewrap', () {
for (final c in section('agewrap')) {
final raws = [
for (final r in (c['raw_identities']! as List).cast<String>())
fromHex(r),
];
final file = fromHex(c['file']! as String);
Map<String, Object?> result;
try {
final AgeIdentity id = c['kind'] == 'payload'
? PayloadIdentity(raws.single)
: AccessIdentity(c['slots']! as int, [
for (final r in raws) x25519IdentityFromRaw(r),
]);
result = resultOf(file, [id]);
} on DateKeysException catch (e) {
result = {'error': e.message, 'phase': 'identity'};
}
expect(result, c['result'], reason: c['name'] as String?);
}
expect(
() => AccessIdentity(-1, [X25519Identity(Uint8List(32))]),
throwsArgumentError,
);
// Null entries are dropped, as in Go.
expect(
errorOf(() => AccessIdentity(16, [null])),
'agewrap: time_and_key needs an access identity: ERR_ACCESS_REQUIRED',
);
});
test('age.ParseX25519Identity', () {
for (final c in section('parse_identity')) {
final s = c['input']! as String;
try {
final id = X25519Identity.parse(s);
expect(c['error'], isNull, reason: s);
expect(toHex(id.secretKey), c['raw']);
expect(id.recipientString, c['recipient']);
expect(id.toString(), c['string']);
} on AgeException catch (e) {
expect(e.message, c['error'], reason: s);
}
}
});
}

Powered by TurnKey Linux.