ADR 0044's D49 (amended in building it): what reads and seals Learn's content moves out of the Flutter packages, so Learn's agent, which runs without Flutter, shares it rather than repeating the privacy rules. packages/halkyon_content (new, pure Dart) takes from halkyon_app the envelope and its cipher, SealedFile, UnlockedAccountKeys, shared spaces' gateway with SharedKeys and KeyPins, FileCache and FileServerGateway, with their fakes and tests; halkyon_app exports it, so no app changes. Its rich_text.dart takes RichContent from halkyon_editor, which exports it. newAccountKeys makes an agent's key pair. halkyon_core takes DelayedDeletions and gains SyncRequests, what a synced gateway asks of the sync, which SyncService implements. apps/learn/halkyon_learn_core (new, pure Dart) takes Learn's privacy, sync, payloads and the models, codecs, gateways and repositories of studysets, cards, notes, tags and feedback, with what they reach of files, learning paths, citations and study options; the app imports them from there. A subject's icon, colour and name stay in the app (SubjectLook); what only the app shows (progress, exam coverage, the card browser, deletions' waits) stays too. Nothing changes for people using the apps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
105 lines
3.6 KiB
Dart
105 lines
3.6 KiB
Dart
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:halkyon_content/halkyon_content.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// The public keys a device pins for each person (Halkyon's ADR 0044, D18).
|
|
void main() {
|
|
late Directory folder;
|
|
late File file;
|
|
|
|
PublicKeys keys(int mark) => (
|
|
encryption: Uint8List.fromList(List.filled(32, mark)),
|
|
signing: Uint8List.fromList(List.filled(32, mark + 1)),
|
|
);
|
|
|
|
setUp(() async {
|
|
folder = await Directory.systemTemp.createTemp('key_pins');
|
|
file = File('${folder.path}/person/key_pins.json');
|
|
});
|
|
|
|
tearDown(() => folder.delete(recursive: true));
|
|
|
|
test('the first keys seen stay pinned, on the next start too; others '
|
|
'are a change until trusted anew', () async {
|
|
final pins = KeyPins(FileKeyPinStore(file));
|
|
expect(await pins.check('bob', keys(1)), PinCheck.firstSeen);
|
|
expect(await pins.check('bob', keys(1)), PinCheck.same);
|
|
expect(await pins.check('bob', keys(3)), PinCheck.changed);
|
|
|
|
final later = KeyPins(FileKeyPinStore(file));
|
|
expect(await later.check('bob', keys(3)), PinCheck.changed);
|
|
expect(await later.check('bob', keys(1)), PinCheck.same);
|
|
|
|
await later.trustAnew('bob', keys(3));
|
|
expect(
|
|
await KeyPins(FileKeyPinStore(file)).check('bob', keys(3)),
|
|
PinCheck.same,
|
|
);
|
|
});
|
|
|
|
test('compared keys are marked so, until they are trusted anew', () async {
|
|
final pins = KeyPins(FileKeyPinStore(file));
|
|
await pins.check('bob', keys(1));
|
|
expect((await pins.of('bob'))!.verified, isFalse);
|
|
|
|
await pins.markVerified('bob');
|
|
expect((await KeyPins(FileKeyPinStore(file)).of('bob'))!.verified, isTrue);
|
|
|
|
await pins.trustAnew('bob', keys(3));
|
|
expect((await pins.of('bob'))!.verified, isFalse);
|
|
});
|
|
|
|
test('a file not holding pins counts as none', () async {
|
|
await file.parent.create(recursive: true);
|
|
await file.writeAsString('{not json');
|
|
var book = await FileKeyPinStore(file).read();
|
|
expect(book.pins, isEmpty);
|
|
expect(book.spaces, isEmpty);
|
|
await file.writeAsString(
|
|
'{"pins": {"bob": {"encryption": "%%", "signing": "", '
|
|
'"verified": false, "earlierSigning": []}, "carol": 3}, '
|
|
'"spaces": {"s": 4}}',
|
|
);
|
|
book = await FileKeyPinStore(file).read();
|
|
expect(book.pins, isEmpty);
|
|
expect(book.spaces, isEmpty);
|
|
});
|
|
|
|
test('signatures count in a space from everyone seen there, by every '
|
|
'signing key pinned for them, earlier ones too (D20)', () async {
|
|
final pins = KeyPins(FileKeyPinStore(file));
|
|
await pins.check('bob', keys(1));
|
|
await pins.check('carol', keys(5));
|
|
await pins.check('mallory', keys(9));
|
|
await pins.sawIn('space', ['bob', 'carol']);
|
|
await pins.trustAnew('bob', keys(3));
|
|
|
|
final later = KeyPins(FileKeyPinStore(file));
|
|
final signers = await later.signersIn('space');
|
|
expect(
|
|
signers,
|
|
unorderedEquals([keys(3).signing, keys(1).signing, keys(5).signing]),
|
|
);
|
|
expect(signers, isNot(contains(keys(9).signing)));
|
|
expect(await later.signersIn('elsewhere'), isEmpty);
|
|
});
|
|
|
|
test(
|
|
'someone gone from a space still counts for what they signed there',
|
|
() async {
|
|
final pins = KeyPins(FileKeyPinStore(file));
|
|
await pins.check('bob', keys(1));
|
|
await pins.sawIn('space', ['bob']);
|
|
await pins.sawIn('space', <String>[]);
|
|
expect(await pins.signersIn('space'), [keys(1).signing]);
|
|
},
|
|
);
|
|
|
|
test('a seen person without pinned keys counts for nothing', () async {
|
|
final pins = KeyPins(FileKeyPinStore(file));
|
|
await pins.sawIn('space', ['bob']);
|
|
expect(await pins.signersIn('space'), isEmpty);
|
|
});
|
|
}
|