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/open_words_test.dart

179 lines
5.9 KiB

// The key of words of spec §38.1 as a credential of the opening: a capsule
// whose INNER_ACCESS_AGE holds, in place of a dummy, a stanza for the key of
// some words, as its creator could write it, opens with the identity that
// wordKey derives from those words, the chain, the round and the capsule_id
// that the inspection gives; with other words it does not, with the text of
// the stanza rules of agewrap.
@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/base64.dart' show goBase64Encode;
import 'package:datekeys/src/curve25519.dart' show x25519PublicKey;
import 'package:datekeys/src/sha256.dart' show sha256;
import 'package:datekeys/src/tlock.dart';
import 'package:test/test.dart';
import 'age_support.dart' show x25519Stanza;
import 'large_capsule.dart';
typedef Json = Map<String, Object?>;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
Uint8List fixture(String file) =>
File('testdata/fixtures/$file').readAsBytesSync();
void main() {
final r = readJson('testdata/fixtures/format3_time_and_key_portable.json');
final rel = r['release']! as Json;
final release = Release(
rel['round']! as int,
fromHex(rel['signature']! as String),
);
final dkc = fixture('format3_time_and_key_portable.dkc');
final dkk = fixture('format3_time_and_key_portable.dkk');
final words = normalizeWords('Casa perro LUNA verde árbol cielo');
// The capsule with a stanza for the key of the words in place of a dummy:
// INNER_ACCESS_AGE gets a new header, whose MAC FK_ACCESS gives, and
// OUTER_TIME_AGE is sealed again over it with FK_TIME and its nonce. The
// stanza has the size of the one it replaces, so no length changes.
final inspection = inspectCapsule(dkc);
final h = inspection.header!;
final p = inspection.profile!;
final key = wordKey(words, p.chainHash, h.dateKey.round, h.capsuleId);
final s = splitCapsule(dkc);
final sealed = s.sealedControl!;
final tlock = parseAgeHeader(sealed).stanzas.single;
final fkTime = unwrapTlockStanza(
p,
h.dateKey.round,
release,
tlock.args,
tlock.body,
);
final inner = openStream(sealed, fkTime);
final innerHeader = parseAgeHeader(inner);
final access = decodeAccessKey(dkk);
final fkAccess = X25519Identity(access.material)
.unwrap([innerHeader.stanzas[r['access_key_stanza']! as int]]);
final dummy = ((r['access_key_stanza']! as int) + 1) % 16;
final stanzas = [...innerHeader.stanzas];
stanzas[dummy] = x25519Stanza(
fkAccess,
x25519PublicKey(key),
Uint8List.fromList(List.generate(32, (i) => i + 1)),
);
final newInner = concatBytes([
marshalAgeHeaderWithoutMac(stanzas),
' '.codeUnits,
goBase64Encode(ageHeaderMac(fkAccess, stanzas), padded: false).codeUnits,
'\n'.codeUnits,
Uint8List.sublistView(inner, innerHeader.length),
]);
if (newInner.length != inner.length) {
throw StateError('the new INNER_ACCESS_AGE changes its length');
}
final withWords = concatBytes([
s.preludeBytes,
s.publicHeader,
resealStream(sealed, fkTime, newInner),
s.payload,
]);
OpenOptions options(List<Uint8List> identities, FileSink sink) => OpenOptions(
source: suppliedRelease(release),
now: () => parseRfc3339(r['unlock_at']! as String),
identities: identities,
sink: sink,
);
test('opens with the key of the words, as one more credential', () async {
final sink = MemoryFileSink();
final o = await openCapsule(withWords, options([key], sink));
expect(o.ok, isTrue, reason: o.error?.message);
expect(
o.checks.firstWhere((c) => c.step == 9).detail,
'1 identities to try',
);
final files = (r['files']! as List).cast<Json>();
expect(
[for (final f in o.head!.files) f.path],
[for (final f in files) f['path']],
);
expect(
[for (final f in sink.files!) toHex(sha256(f))],
[for (final f in files) f['sha256']],
);
// With the .dkk too, two credentials, each with its stanza: its
// capsule_digest is that of the fixture, so it goes without it.
final noDigest = AccessKey(
credentialId: access.credentialId,
capsuleId: access.capsuleId,
type: access.type,
material: access.material,
);
final both = await openCapsule(
withWords,
OpenOptions(
source: suppliedRelease(release),
now: () => parseRfc3339(r['unlock_at']! as String),
identities: [key],
accessKey: noDigest,
sink: MemoryFileSink(),
),
);
expect(
[both.ok, both.checks.firstWhere((c) => c.step == 9).detail],
[true, '2 identities to try'],
);
// With its capsule_digest, the .dkk is not of this capsule.
final digest = await openCapsule(
withWords,
OpenOptions(
source: suppliedRelease(release),
now: () => parseRfc3339(r['unlock_at']! as String),
identities: [key],
accessKey: access,
sink: MemoryFileSink(),
),
);
expect(
[digest.error?.message, digest.checks.last.step],
[
'capsule: the .dkk capsule_digest does not match this .dkc: '
'ERR_ACCESS_INVALID',
9,
],
);
});
test('does not open with other words', () async {
final other = wordKey(
normalizeWords('casa perro luna verde arbol mar'),
p.chainHash,
h.dateKey.round,
h.capsuleId,
);
final o = await openCapsule(withWords, options([other], MemoryFileSink()));
expect(
[o.error?.message, o.checks.last.step],
[
'capsule: age: agewrap: no supplied identity is a recipient of '
'INNER_ACCESS_AGE: ERR_ACCESS_INVALID',
13,
],
);
// The fixture itself has no stanza for the words.
final f = await openCapsule(dkc, options([key], MemoryFileSink()));
expect(f.error?.code, ErrorCode.accessInvalid);
});
}

Powered by TurnKey Linux.