docs(privacy): the final reading of private studysets (ADR 0019)
ADR 0019 gets "The final reading (22 Oct 2026)": what decides a key is bound into what is sealed or signed, the walk up the tree, a device without the keys writing nothing, settings' newest versions and Count me folding off-wins, publishing and Finish as shown, the rewrites; the hostile-instance test, its invariant and what it found, on the final code and on the code before; the residual risks as they stand. Learn's AGENTS.md gets the rule that every column deciding a key is bound into what is sealed or signed, and the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
15326f675e
commit
3bb90a725a
1 changed files with 111 additions and 1 deletions
|
|
@ -287,3 +287,113 @@ the real gateways; each probe is a test now.
|
|||
Each test of a fixed finding was run on the code before (`a1adc18`) and
|
||||
failed there; one of an older readable record against a newer private one
|
||||
passed there too, as it guards what was right already.
|
||||
|
||||
## The final reading (22 Oct 2026)
|
||||
|
||||
A third reading found that an item learned its first key from any sealed
|
||||
row pointed at it (the instance pointed a revision of private B at a card
|
||||
of private A; the card's next edit was sealed under B's key, and making B
|
||||
readable opened it), and that a device without the keys trusted a "no" from
|
||||
the register forever. The readings kept finding variants, each closing one
|
||||
path while most columns stayed unsigned, so the developer chose to bind
|
||||
what decides keys and to test against a hostile instance:
|
||||
|
||||
- **What decides a key is bound into what is sealed or signed.** A sealed
|
||||
payload's additional data, and a readable row's signature, name the row it
|
||||
belongs to wherever that decides its key: a revision's card, note or
|
||||
step, an answer's step, a path's or a PDF's studyset (sealed format v2;
|
||||
nothing sealed before was released). A row the instance points at another
|
||||
item doesn't open there and reads as locked, and an item learns its key
|
||||
only from its own rows, before any write decides on one. A studyset's
|
||||
`parentId` isn't bound: a move on one device and an edit on another merge
|
||||
column by column, and the row would lock for nobody's fault. Instead each
|
||||
studyset on the way up decides on its own before any above it: its own
|
||||
record (private, or made readable) or the key its own row was seen sealed
|
||||
with, learned from that row before a write if not known yet; one whose
|
||||
row is sealed under a key not here is private, and a write into it is
|
||||
refused.
|
||||
- **Before a write decides on a key,** the device reads the records and the
|
||||
tree anew: a studyset missing from a tree read before, or moved since,
|
||||
would look readable.
|
||||
- **A device without the account's keys,** on an account that has them,
|
||||
writes no content at all: it opens no record, so every tree would look
|
||||
readable to it. That the account has keys is any sign, never taken back:
|
||||
the keys open here, the register's yes, key rows (remembered once seen),
|
||||
records or sealed tags and settings seen; the register's answer comes
|
||||
through the instance unsigned, so its no outweighs nothing and is asked
|
||||
again.
|
||||
- **Settings** keep the newest versions seen (each row's vector that no
|
||||
other seen is newer than): a row older than any of them, or than another
|
||||
read with it, isn't read, so withholding a newer row and handing back an
|
||||
older one rolls nothing back; rows folded away here are never read again.
|
||||
Count me folds off where any row says off, so an instance handing back a
|
||||
genuine older on can't switch it on (the person switches it on again at
|
||||
worst). The short reading's `HLKS` counter is gone.
|
||||
- **Publishing** sends exactly the tag names the sheet showed; names that
|
||||
changed in the sync before Publish are refused on the device.
|
||||
- **Finish** lists what it would take in, each edit with what it says, and
|
||||
takes in exactly those rows, each only while it still says what was
|
||||
shown.
|
||||
- **Rewrites:** a pending one keeps whether it may take in what nobody
|
||||
signed (only the person's own make-private of a readable tree may; never a
|
||||
sweep), and a sweep never takes over the person's own. A sweep reads only
|
||||
rows not under the key already, yielding now and then; a write waits only
|
||||
for a rewrite of its own tree, reading for none. A tree stays partly
|
||||
readable while rows a sweep can't move remain, which are looked at again
|
||||
once more keys open.
|
||||
- **A damaged `privacy.json`** is kept aside; once the keys open, the device
|
||||
goes on as a fresh one would, with every residual risk of a fresh device.
|
||||
|
||||
**The hostile-instance test** (`test/core/privacy/hostile_instance_test.dart`)
|
||||
holds the invariant: seeded runs interleave what the person does on a
|
||||
laptop, a phone and a device without the keys (studysets, topics, cards,
|
||||
notes, paths, steps, answers, PDFs, files, moves, making private or readable,
|
||||
tags, Count me, Finish, publishing, reading back) with what an instance that
|
||||
alters rows does to the local database, as a sync would bring it (changing
|
||||
any unsigned column, replaying older versions under their ids, pointing
|
||||
revisions and answers elsewhere, planting unsigned rows and signed rows
|
||||
copied from elsewhere, withholding, handing back or forging key records,
|
||||
linking tags, withholding files, folding away and handing back settings,
|
||||
answering the register no), each followed by the upkeep after a merge.
|
||||
After every step it checks, against a model kept from the person's actions
|
||||
alone, that no marker of content private when written is readable in any
|
||||
row the device keeps or file it stores unless the person made it readable,
|
||||
that a release sends only the tag names shown, and that Count me is on
|
||||
only where the person turned it on last, on the device they did it on (any
|
||||
other can be held at an earlier record, below). 50 seeds of 40 steps run
|
||||
with `tool/check.sh` (about a minute); `HOSTILE_SEEDS` and `HOSTILE_STEPS`
|
||||
run more. Suggesting from a copy isn't among its actions (a test of its own
|
||||
covers it).
|
||||
|
||||
Run on the code before the final reading (`6179c98`), 117 of 200 seeds
|
||||
broke the invariant: 73 by H2's class, content a device without the keys
|
||||
wrote readable (seeds 0, 2, 3, …); the rest by what is listed below
|
||||
(writes on a stale tree, a rollback of Count me, release names not shown).
|
||||
H1 itself wasn't reached by chance there, in 1,000 seeds without the device
|
||||
without the keys, nor on the final code with only the binding taken out:
|
||||
its chain is five particular steps long, and on the old code the more
|
||||
common faults end each seed first. Its own test fails on the old code.
|
||||
|
||||
Writing it found, beyond H1 and H2: a write deciding on a key from records
|
||||
and a tree read before; a topic the instance moved to the top level, its
|
||||
own row never read here; a studyset whose key record was withheld from a
|
||||
device that never saw it private; a studyset made readable by its own
|
||||
record taken as private once the instance moved it under a private one;
|
||||
settings rolled back by withholding a newer row; and two queries that
|
||||
failed at run time (`id IN` with nullable ids: a path's step revisions and
|
||||
answers couldn't be read). Each has a test of its own now.
|
||||
|
||||
Residual risks, as they stand:
|
||||
|
||||
- **Any device can be held at any earlier genuine record.** A fresh device
|
||||
shown an old readable version of a studyset, its records withheld, writes
|
||||
readable there; so does a device whose `privacy.json` was lost. A device
|
||||
the instance keeps from a later Count me off goes on reporting copies;
|
||||
the device that turned it off never does.
|
||||
- **First sealing of tags and settings** on each device trusts what is
|
||||
readable then.
|
||||
- **Old sealed content handed back** shows as it was (R2), except settings.
|
||||
- **Structure and rhythm** stay readable (D7).
|
||||
- **Tag links aren't signed:** the instance can link a private tag to an
|
||||
item of a studyset being published; the Publish sheet shows every name
|
||||
before it goes.
|
||||
|
|
|
|||
Loading…
Reference in a new issue