docs(privacy): private studysets as built (ADR 0019)

Learn's AGENTS.md says how private studysets work (keys, the envelope,
locked rows, rewriting a tree) and what changed for the connector,
publishing, uploads and the learner count; ADR 0019 records what was built
otherwise than decided or not yet (a private studyset's files); the
halkyon_app README describes the content cipher and device keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 21:50:29 +02:00
commit dcdc9c1542
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 41 additions and 5 deletions

File diff suppressed because one or more lines are too long

View file

@ -114,3 +114,29 @@ millisecond), its table and the columns it changed.
rule 6's way back is taken. rule 6's way back is taken.
- Making a large library private takes a sync of everything in it. - Making a large library private takes a sync of everything in it.
- Nobody, the operator included, can restore a private studyset whose keys are lost. - Nobody, the operator included, can restore a private studyset whose keys are lost.
## As built (21 Oct 2026)
- **Not yet:** a private studyset's files (D5) are still uploaded readable,
and an asset row's file name with them. The instance takes encrypted
uploads already (`startUpload`/`finishUpload`'s `encrypted`: no PDF check,
4 KiB more room); the app has yet to seal a file with its studyset's key
before hashing it, which every place adding a file (the editors, the PDF
import, copies, archives) must name the studyset for.
- **Count me** stays in the person's settings, which follow them to every
device, and the device mirrors it to the instance after each sync
(`counted_learner`), reporting the copies only while it is on: the
instance keeps the setting, as D2 asks, but the person's choice is still
made where it was.
- **Tag names:** the instance refuses a name sent for a tag that isn't the
person's and a release whose encrypted tag has none; names of the
person's tags the release doesn't carry are ignored rather than refused,
as the device sends all its tags' names.
- **Locked rows:** a studyset whose key doesn't open shows as locked; its
other rows read as blank, which the lists leave out; a tag or settings
row that doesn't open is left out entirely, so nothing folds or deletes
it on a device without the keys.
- **Identity binding:** each payload is sealed for its table, row id and
column, and signed by the account; the signature goes before the
ciphertext in the payload column.

View file

@ -68,6 +68,15 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
hands it to the screens, which then offer no Add, and the start screen hands it to the screens, which then offer no Add, and the start screen
offers an account to create in place of the app's sentence on adding offers an account to create in place of the app's sentence on adding
GitHub; `gitHubProblemText` says why a GitHub sign-in was refused. GitHub; `gitHubProblemText` says why a GitHub sign-in was refused.
- **Content sealed with data keys** (`lib/src/privacy/`, ADR 0019):
`ContentCipher` (`SodiumContentCipher`, on the isolate it is used on, as an
app seals and opens rows one by one; `FakeContentCipher` in tests) makes a
store's data keys, sealed to the account and signed by it, opens them, and
seals and opens a row's content for the row it is stored in (its table, id
and column), the payload holding the signature before the ciphertext. An
anonymous person's device makes key pairs of its own
(`AccountCrypto.newDeviceKeys`), which `makePassword` keeps as the account's
once they set a password.
- **A password and TOTP** (`lib/src/password/`, ADR 0017): `PasswordRepository` - **A password and TOTP** (`lib/src/password/`, ADR 0017): `PasswordRepository`
on a `PasswordGateway` (`ServerpodPasswordGateway`), an `AccountCrypto` on a `PasswordGateway` (`ServerpodPasswordGateway`), an `AccountCrypto`
(`SodiumAccountCrypto`: halkyon_keys and SRP's client half, each step on an (`SodiumAccountCrypto`: halkyon_keys and SRP's client half, each step on an