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

161 lines
6.1 KiB

// The age files of the official capsules of testdata/fixtures, against
// test/vectors/age_fixtures.json, which Go wrote (tool/gen_age_vectors.go):
// the PAYLOAD_AGE of each capsule opens with its payload_identity to the
// plaintext of the fixture and its padding, and a stranger gets the error of
// spec §30.1; the INNER_ACCESS_AGE of each time_and_key capsule, which
// OUTER_TIME_AGE seals, opens with each credential of the fixture to its
// CONTROL_CBOR. The stanzas that the header of each file shows are those of
// the record of the fixture.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
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';
Map<String, Object?> readJson(String path) =>
jsonDecode(File(path).readAsStringSync()) as Map<String, Object?>;
List<Map<String, Object?>> listOf(Object? v) =>
(v! as List).cast<Map<String, Object?>>();
/// The stanzas as the records of the fixtures write them: type and args.
List<Map<String, Object?>> shown(List<AgeStanza> ss) => [
for (final s in ss) {'type': s.type, 'args': s.args},
];
void main() {
final vectors = readJson('test/vectors/age_fixtures.json');
final fixtures = listOf(vectors['fixtures']);
final quicknet = readJson('testdata/vectors/profile_quicknet.json');
test('every capsule of testdata/fixtures has its vectors', () {
final names = Directory('testdata/fixtures')
.listSync()
.map((f) => f.uri.pathSegments.last)
.where((n) => n.endsWith('.dkc'))
.map((n) => n.substring(0, n.length - 4))
.toSet();
expect(fixtures.map((f) => f['name']).toSet(), names);
expect(names, hasLength(26));
});
for (final f in fixtures) {
final name = f['name']! as String;
group(name, () {
final dkc = File('testdata/fixtures/$name.dkc').readAsBytesSync();
final record = readJson('testdata/fixtures/$name.json');
final offset = f['payload_offset']! as int;
final payload = Uint8List.sublistView(dkc, offset);
test('PAYLOAD_AGE opens with payload_identity', () {
// The offset is the one of the prelude: 16 + PUBLIC_HEADER_LEN +
// SEALED_CONTROL_LEN.
final b = ByteData.sublistView(dkc);
expect(16 + b.getUint32(8) + b.getUint32(12), offset);
expect(payload.length, f['payload_length']);
expect(toHex(sha256(payload)), f['payload_sha256']);
expect(f['payload_identity'], record['payload_identity']);
final stanzas = ageStanzas(payload);
checkPayloadStanzas(stanzas);
expect(shown(stanzas), record['payload_stanzas']);
final id = PayloadIdentity(
fromHex(record['payload_identity']! as String),
);
expect(resultOf(payload, [id]), f['payload_result']);
// The plaintext of the fixture, then zeros up to P in formats 2
// and 3.
final plain = ageDecrypt(payload, [id]);
final want = File('testdata/fixtures/${record['plaintext_file']}')
.readAsBytesSync();
final l = record['payload_length']! as int;
expect(want.length, l);
expect(Uint8List.sublistView(plain, 0, l), want);
expect(plain.length, record['padded_length'] ?? l);
expect(plain.skip(l).every((b) => b == 0), isTrue);
});
test('a stranger gets the error of spec §30.1', () {
final w = f['payload_wrong_identity']! as Map<String, Object?>;
final id = PayloadIdentity(fromHex(w['raw']! as String));
expect(resultOf(payload, [id]), w['result']);
});
test('OUTER_TIME_AGE shows the stanzas of the record', () {
final b = ByteData.sublistView(dkc);
final start = 16 + b.getUint32(8);
final sealed = Uint8List.sublistView(dkc, start, offset);
final stanzas = ageStanzas(sealed);
expect(shown(stanzas), record['outer_stanzas']);
final release = record['release']! as Map<String, Object?>;
checkTimeStanzas(
stanzas,
round: release['round']! as int,
chainHashHex: quicknet['chain_hash']! as String,
profileId: quicknet['profile_id']! as String,
);
});
final inner = f['inner_access_age'] as Map<String, Object?>?;
if (inner == null) return;
test('INNER_ACCESS_AGE opens with each credential', () {
final file = fromHex(inner['hex']! as String);
final slots = inner['slots']! as int;
expect(slots, record['format'] == 1 ? 0 : accessSlots);
final stanzas = ageStanzas(file);
checkAccessStanzas(stanzas, slots);
expect(shown(stanzas), record['inner_stanzas']);
final all = <AgeIdentity>[];
for (final c in listOf(inner['identities'])) {
final id = x25519IdentityFromRaw(fromHex(c['raw']! as String));
all.add(id);
expect(
resultOf(file, [
AccessIdentity(slots, [id]),
]),
c['result'],
reason: c['source'] as String?,
);
}
// The identities of the record, as strings.
for (final s
in (record['identities'] as List? ?? const []).cast<String>()) {
expect(
all.map((i) => i.toString()),
contains(X25519Identity.parse(s).toString()),
);
}
final control = ageDecrypt(file, [AccessIdentity(slots, all)]);
expect(toHex(control), record['control_cbor']);
expect(
resultOf(file, [AccessIdentity(slots, all)]),
inner['all_identities_result'],
);
expect(toHex(sha256(control)), inner['control_cbor_sha256']);
});
test('a stranger gets ERR_ACCESS_INVALID', () {
final file = fromHex(inner['hex']! as String);
final stranger = X25519Identity(sha256('stranger'.codeUnits));
final r = resultOf(file, [
AccessIdentity(inner['slots']! as int, [stranger]),
]);
expect(r, inner['stranger_result']);
});
});
}
}

Powered by TurnKey Linux.