docs(adr): private studysets (ADR 0019)
The developer's decisions of 21 Oct: private studysets ship with build 22; a data key per private studyset and a personal key for tags and settings, sealed to the account and kept in a synced table; files encrypted too; a release's tag names and the copies counted come from the device; the shape, size and rhythm of studying stay readable and are listed; existing studysets get one offer; accounts without keys at the register wait for a password. ADR 0002 and ADR 0017 point to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
cccb692ff0
commit
074d05ab4e
3 changed files with 120 additions and 1 deletions
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
- **Status:** accepted
|
||||
- **Date:** 2026-09-29
|
||||
- **Amended by:** [ADR 0019](0019-private-studysets.md), which takes the way back below:
|
||||
private studysets are end-to-end encrypted.
|
||||
- **Amends:** [ADR 0001](0001-offline-sync.md), whose context assumed that private and
|
||||
shared data would be end-to-end encrypted later.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
recovery codes and a recovery phrase; Sign in with SimpleLogin later, as an extra way in.
|
||||
- **Builds on:** ADR 0002 (the content envelope: `keyId`, `nonce`, `payload`), ADR 0010
|
||||
(email sign-in), ADR 0016 (the register).
|
||||
- **Followed by:** ADR 0018 (the agenda), the first data encrypted with these keys.
|
||||
- **Followed by:** ADR 0019 (private studysets), the first data encrypted with these keys;
|
||||
ADR 0018 (the agenda).
|
||||
|
||||
## Context
|
||||
|
||||
|
|
|
|||
116
docs/adr/0019-private-studysets.md
Normal file
116
docs/adr/0019-private-studysets.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# ADR 0019: Private studysets
|
||||
|
||||
- **Status:** decided 21 Oct 2026, by the developer (D1–D8 below).
|
||||
- **Amends:** ADR 0002, by the way back it left open: a private studyset's content is no
|
||||
longer readable by the instance.
|
||||
- **Builds on:** ADR 0017 (the account's keys; D5, private per studyset and by default),
|
||||
ADR 0004 (tags, settings, files), ADR 0005 (publishing, Explore's copies, the learner
|
||||
count).
|
||||
- **Followed by:** ADR 0018 (the agenda), whose store uses the same keys.
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0017 gave every account key pairs that only its devices can open, and decided (D5)
|
||||
that a studyset, with everything inside it, is private or not, new ones private. ADR 0002
|
||||
had kept `keyId` and `nonce` in the envelope for exactly this, with the server's features
|
||||
skipping what they can't read.
|
||||
|
||||
What a studyset holds is spread over Learn's synced tables: the studyset's own payload;
|
||||
its cards', notes' and steps' revisions; its learning paths and PDFs; the answers given in
|
||||
its paths (a plain `answer` column); review and reading logs; the links to tags; and the
|
||||
files its content names, kept per space by their hash. Tags and settings belong to the
|
||||
person rather than to a studyset, yet hold content too: tag names (Jellyfin's show names
|
||||
among them), each studyset's study tags, its Jellyfin ties.
|
||||
|
||||
The server reads payloads in four places: the Claude connector, the learner count (each
|
||||
person's `countLearning` and the publication a copy came from), publishing (the tree, its
|
||||
revisions, its files and its tags' names), and feedback (threads about others'
|
||||
publications, readable by design). It checks that a file over 20 MB is a PDF. Every
|
||||
change the sync engine records carries its time (the writing device's clock, to the
|
||||
millisecond), its table and the columns it changed.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Privacy belongs to a top-level studyset** and holds for everything inside it. A
|
||||
topic moved into another tree takes on that tree's privacy, its rows encrypted anew on
|
||||
the device.
|
||||
2. **Keys.** A private studyset has a data key of its own (ADR 0017's `DataKey`), and
|
||||
every person has one personal key besides, for their tags and settings. Each is sealed
|
||||
to the account's X25519 key and kept in a new synced table in the person's space
|
||||
(`content_key`), so keys arrive with the data, work offline and go with the space
|
||||
(D6). An anonymous person's keys are sealed to the device's own key pairs (ADR 0017,
|
||||
9). A person whose account has no keys at the register (signed in with GitHub, no
|
||||
password yet) gets nothing private until they set a password, which the app leads to
|
||||
(D8); then the library makes the offer of 7.
|
||||
3. **What is encrypted.** With the studyset's key: the payloads of the studyset, of its
|
||||
cards', notes' and steps' revisions, of its learning paths and PDFs, and of the asset
|
||||
rows of its files; the answers in its paths (`learning_path_answer` gains `keyId` and
|
||||
`nonce`); and the files themselves (D5), stored under the hash of what is encrypted, so
|
||||
the same picture in two studysets is stored twice. With the personal key: the payloads
|
||||
of tags and of the settings. Each is ADR 0017's sealed content: XChaCha20-Poly1305 with
|
||||
the key's ID and the row (table, id, column) as additional data, so the instance can't
|
||||
move a payload to another row, and the author's Ed25519 signature, checked before
|
||||
anything is decrypted, which the payload column carries before the ciphertext. The
|
||||
plaintext inside is the payload as before, its `schemaVersion` with it.
|
||||
4. **What stays readable** (D7): the studyset tree and positions; when a studyset changed,
|
||||
was starred, archived or deleted; which card was rated how, when, for how long and in
|
||||
which mode; the PDF pages reached; path answers' outcomes; file sizes; and, from the
|
||||
sync engine, when each row changed, how many rows each table holds and how they hang
|
||||
together. The privacy page lists it: the instance sees a private studyset's shape, size
|
||||
and rhythm, never its content.
|
||||
5. **The server's features.**
|
||||
- Publishing refuses a private studyset and says how to open it up. A non-private
|
||||
release's tag names come from the author's device with the call (D2); the server
|
||||
checks they name exactly the tags its items carry.
|
||||
- The connector leaves private studysets out and tells Claude so.
|
||||
- The learner count: while a person's Count me is on, their device reports which
|
||||
publications their copies come from (D3), kept server-side and replaced with each
|
||||
report; the review log still gives the days studied. Count me itself moves into a
|
||||
setting the server keeps (D2), as the settings' payload is encrypted.
|
||||
- Feedback is unchanged: it is about others' publications, written to be read.
|
||||
- Dictionary lookups are unchanged: searches, a studyset's corrections (by its cards'
|
||||
entry ids) and the Watch screen's lines (unless the device holds the dictionaries)
|
||||
still go to the instance, as the privacy page says.
|
||||
- A file's upload says whether it is encrypted, and only a readable one is checked to
|
||||
be a PDF; the quotas count both alike.
|
||||
6. **Changing it.** Making a studyset private encrypts each of its rows in place (a
|
||||
payload update, as blanking is) and its files anew; making it non-private decrypts
|
||||
them likewise. The readable copies in the instance's backups stay until the backups
|
||||
expire; the app says so where it asks.
|
||||
7. **What exists already** stays as it is (D4): the library offers once to make every
|
||||
studyset private, and each studyset's menu changes it later. New studysets are
|
||||
private, copies from Explore too.
|
||||
8. **Keys lost** (an account reset without the recovery phrase): private studysets
|
||||
whose keys no longer open show as locked, with Delete alone.
|
||||
9. **Exports** decrypt on the device: an archive is readable wherever it is kept, which
|
||||
the export sheet says.
|
||||
10. **Released with build 22** (D1), whose `minimumAppBuild` already locks older apps out,
|
||||
so the new synced table and columns cost no second lockout.
|
||||
|
||||
## Decisions (21 Oct 2026, the developer's)
|
||||
|
||||
- **D1** Ship with build 22: one lockout of older apps for passwords and private
|
||||
studysets alike.
|
||||
- **D2** Tags and settings are encrypted with a personal key; a release's tag names come
|
||||
from the author's device; Count me becomes a setting the server keeps.
|
||||
- **D3** Copies are counted by the device reporting their publications, only while Count
|
||||
me is on.
|
||||
- **D4** Existing studysets stay as they are, with a single offer to make them all private.
|
||||
- **D5** A private studyset's files are encrypted too.
|
||||
- **D6** Data keys live in a synced table in the person's space.
|
||||
- **D7** The shape, size and rhythm stay readable and are listed; encrypting the values
|
||||
(ratings, durations, positions, pages) would hide how well, not when or how much, as the
|
||||
sync engine records every change's time.
|
||||
- **D8** An account without keys at the register keeps its studysets readable until it
|
||||
sets a password.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Build 22 brings a migration of synced tables (`content_key`; `keyId` and `nonce` on
|
||||
`learning_path_answer`), explained with its SQL when made.
|
||||
- Every payload codec reads and writes through the keys; a row whose key the device lacks
|
||||
is shown as locked, never half-read.
|
||||
- ADR 0002's rule 4 (server features read payloads) now holds for what isn't private;
|
||||
rule 6's way back is taken.
|
||||
- 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.
|
||||
Loading…
Reference in a new issue