feat(base,app,keys): keys of shared spaces, sealed to each member
Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.
- halkyon_keys: the safety code of two accounts' public keys
(`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
(`public-keys-of`, local and remote); the table `shared_space_key`
and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
mine). Only the Owner and Editors seal, the first key sealed to a
member stays, and a member's keys go with their membership. Every
host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
opens one sealed by another; `SharedKeys` shares a space's key with
its members, seals keys to newcomers and opens the person's own,
holding each member to the public keys `KeyPins` pinned on first
sight (`key_pins.json` in the person's folder), a change refused
until trusted anew.
Learn takes them into its `Privacy` in step 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 11:28:13 +01:00
|
|
|
import 'dart:io';
|
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
refactor(content,core,app,editor,learn): Learn's content in pure Dart, for its agent
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>
2026-11-11 21:24:30 +01:00
|
|
|
import 'package:halkyon_content/halkyon_content.dart';
|
|
|
|
|
import 'package:test/test.dart';
|
feat(base,app,keys): keys of shared spaces, sealed to each member
Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.
- halkyon_keys: the safety code of two accounts' public keys
(`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
(`public-keys-of`, local and remote); the table `shared_space_key`
and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
mine). Only the Owner and Editors seal, the first key sealed to a
member stays, and a member's keys go with their membership. Every
host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
opens one sealed by another; `SharedKeys` shares a space's key with
its members, seals keys to newcomers and opens the person's own,
holding each member to the public keys `KeyPins` pinned on first
sight (`key_pins.json` in the person's folder), a change refused
until trusted anew.
Learn takes them into its `Privacy` in step 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 11:28:13 +01:00
|
|
|
|
|
|
|
|
/// 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');
|
feat(app): a shared space's content taken from its members' signatures
ADR 0044, D20: a row in a shared space counts as genuine when any
member this device pinned signed it, current or former, never by the
author column the row names.
- `ContentCipher.trustSigners`: content sealed with a key is checked
against the signing keys given for it, in place of the account's
alone; `FakeContentCipher` tells authors apart likewise.
- `KeyPins` keeps who it saw in each space (`sawIn`) and the signing
keys pinned for someone before a reset (`earlierSigning`), so what
they wrote stays readable; `signersIn` gives a space's signers.
`key_pins.json` holds both (`pins`, `spaces`); a file written by
step 3, on no released app, counts as none.
- `SharedKeys` pins the person's own keys too, records the members it
checks, and trusts the space's signers for each key it shares or
opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 13:22:30 +01:00
|
|
|
var book = await FileKeyPinStore(file).read();
|
|
|
|
|
expect(book.pins, isEmpty);
|
|
|
|
|
expect(book.spaces, isEmpty);
|
feat(base,app,keys): keys of shared spaces, sealed to each member
Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.
- halkyon_keys: the safety code of two accounts' public keys
(`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
(`public-keys-of`, local and remote); the table `shared_space_key`
and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
mine). Only the Owner and Editors seal, the first key sealed to a
member stays, and a member's keys go with their membership. Every
host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
opens one sealed by another; `SharedKeys` shares a space's key with
its members, seals keys to newcomers and opens the person's own,
holding each member to the public keys `KeyPins` pinned on first
sight (`key_pins.json` in the person's folder), a change refused
until trusted anew.
Learn takes them into its `Privacy` in step 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 11:28:13 +01:00
|
|
|
await file.writeAsString(
|
feat(app): a shared space's content taken from its members' signatures
ADR 0044, D20: a row in a shared space counts as genuine when any
member this device pinned signed it, current or former, never by the
author column the row names.
- `ContentCipher.trustSigners`: content sealed with a key is checked
against the signing keys given for it, in place of the account's
alone; `FakeContentCipher` tells authors apart likewise.
- `KeyPins` keeps who it saw in each space (`sawIn`) and the signing
keys pinned for someone before a reset (`earlierSigning`), so what
they wrote stays readable; `signersIn` gives a space's signers.
`key_pins.json` holds both (`pins`, `spaces`); a file written by
step 3, on no released app, counts as none.
- `SharedKeys` pins the person's own keys too, records the members it
checks, and trusts the space's signers for each key it shares or
opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 13:22:30 +01:00
|
|
|
'{"pins": {"bob": {"encryption": "%%", "signing": "", '
|
|
|
|
|
'"verified": false, "earlierSigning": []}, "carol": 3}, '
|
|
|
|
|
'"spaces": {"s": 4}}',
|
feat(base,app,keys): keys of shared spaces, sealed to each member
Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.
- halkyon_keys: the safety code of two accounts' public keys
(`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
(`public-keys-of`, local and remote); the table `shared_space_key`
and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
mine). Only the Owner and Editors seal, the first key sealed to a
member stays, and a member's keys go with their membership. Every
host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
opens one sealed by another; `SharedKeys` shares a space's key with
its members, seals keys to newcomers and opens the person's own,
holding each member to the public keys `KeyPins` pinned on first
sight (`key_pins.json` in the person's folder), a change refused
until trusted anew.
Learn takes them into its `Privacy` in step 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 11:28:13 +01:00
|
|
|
);
|
feat(app): a shared space's content taken from its members' signatures
ADR 0044, D20: a row in a shared space counts as genuine when any
member this device pinned signed it, current or former, never by the
author column the row names.
- `ContentCipher.trustSigners`: content sealed with a key is checked
against the signing keys given for it, in place of the account's
alone; `FakeContentCipher` tells authors apart likewise.
- `KeyPins` keeps who it saw in each space (`sawIn`) and the signing
keys pinned for someone before a reset (`earlierSigning`), so what
they wrote stays readable; `signersIn` gives a space's signers.
`key_pins.json` holds both (`pins`, `spaces`); a file written by
step 3, on no released app, counts as none.
- `SharedKeys` pins the person's own keys too, records the members it
checks, and trusts the space's signers for each key it shares or
opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 13:22:30 +01:00
|
|
|
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);
|
feat(base,app,keys): keys of shared spaces, sealed to each member
Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.
- halkyon_keys: the safety code of two accounts' public keys
(`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
(`public-keys-of`, local and remote); the table `shared_space_key`
and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
mine). Only the Owner and Editors seal, the first key sealed to a
member stays, and a member's keys go with their membership. Every
host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
opens one sealed by another; `SharedKeys` shares a space's key with
its members, seals keys to newcomers and opens the person's own,
holding each member to the public keys `KeyPins` pinned on first
sight (`key_pins.json` in the person's folder), a change refused
until trusted anew.
Learn takes them into its `Privacy` in step 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 11:28:13 +01:00
|
|
|
});
|
|
|
|
|
}
|