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.
161 lines
6.1 KiB
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']);
|
|
});
|
|
});
|
|
}
|
|
}
|