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.
179 lines
5.9 KiB
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);
|
|
});
|
|
}
|