| 1 | # Document body sealing implementation spec |
| 2 | |
| 3 | Status: approved for implementation by Claude Fable 5.1 |
| 4 | (`claude-fable-5-1`), session |
| 5 | `8aa40554-38b1-4597-9cb8-439ab594e268`. The final rollout amendment and |
| 6 | approval are recorded in Amadan Docs discussion 39. Whole-branch review, |
| 7 | exact-head CI, release integration, canary and deployment evidence are |
| 8 | separate gates. |
| 9 | |
| 10 | ## Scope and encoding |
| 11 | |
| 12 | Seal `doc_updates.update_bin` and `doc_snapshots.state`. Titles and other |
| 13 | metadata are outside this slice. Historical plaintext backups remain |
| 14 | preserved and are not claimed encrypted by this change. |
| 15 | |
| 16 | Use a separate `DOCS_CONTENT_KEY`: strict standard-base64 encoding of 32 |
| 17 | random bytes. Require it in production before opening the database, even |
| 18 | before activation. Only explicit development mode may use the fixed public |
| 19 | development key; maintenance always validates as production. |
| 20 | |
| 21 | Encoding 1 uses AES-256-GCM with a random 12-byte nonce prepended to the |
| 22 | ciphertext and tag. Associated data binds the fixed content domain, row |
| 23 | kind, immutable document UUID, sequence and snapshot ABI version. Allocate |
| 24 | the sequence inside the writer transaction before sealing the update. |
| 25 | Never send ciphertext to the CRDT core or sync clients. Decrypt and |
| 26 | validate all response rows before writing HTTP success headers. |
| 27 | |
| 28 | The additive migration creates encoding columns defaulting to 0 and a |
| 29 | singleton activation marker containing encoding, key fingerprint, |
| 30 | authenticated keycheck and scrubbed bookkeeping. Keycheck binds a fixed |
| 31 | sentinel and remains meaningful for an empty database. Wrong-key errors |
| 32 | are distinct from authentication failures and never include content bytes. |
| 33 | |
| 34 | ## Runtime states |
| 35 | |
| 36 | A database without the marker is in legacy compatibility mode. Refuse |
| 37 | startup if it already contains nonzero encoding rows. Log an Error on |
| 38 | every legacy boot with structured encoding and key-fingerprint fields. |
| 39 | Legacy reads accept only encoding 0. Both update and snapshot inserts |
| 40 | re-read the marker through the same writer transaction as the insert and |
| 41 | refuse if activation has occurred. A process never switches modes live. |
| 42 | |
| 43 | A database with the marker verifies its keycheck and refuses startup if |
| 44 | an EXISTS query finds any update or snapshot with encoding other than 1. |
| 45 | Activated reads reject other encodings and authenticate every body used. |
| 46 | An unusable snapshot may be abandoned for the retained update log; it is |
| 47 | an accelerator and is never the only copy of document history. |
| 48 | |
| 49 | ## Explicit maintenance |
| 50 | |
| 51 | Only the `docs-seal` subcommand invokes conversion or scrubbing. It requires |
| 52 | an existing regular database path and the expected key fingerprint. Read |
| 53 | the key only from `DOCS_CONTENT_KEY`. Reuse the exact server `BootSchema`. |
| 54 | Never resolve serving options, bind a listener or start replication. |
| 55 | There is no deactivate or marker-removal operation. |
| 56 | |
| 57 | Conversion is one transaction. Process bounded batches, bind bodies to |
| 58 | their stored document UUID, verify already-sealed rows and reject unknown |
| 59 | encodings or orphaned rows. On first conversion write the keycheck marker |
| 60 | atomically with the converted rows. On explicit repair of an activated |
| 61 | mixed database, convert remaining encoding-0 rows while leaving the marker, |
| 62 | fingerprint and keycheck unchanged. Clear only scrubbed bookkeeping when |
| 63 | new legacy bytes have been converted. Any failure rolls back all changes. |
| 64 | |
| 65 | Scrubbing is an explicit operation after conversion. The operator stops |
| 66 | the server and replication first. Set scrubbed false, use a pinned writer |
| 67 | connection, check the complete TRUNCATE-checkpoint result, VACUUM, check a |
| 68 | second TRUNCATE checkpoint, and set scrubbed true only on success. A busy |
| 69 | checkpoint is failure. Never put conversion or VACUUM on normal startup. |
| 70 | Every restored copy needs another explicit scrub; its stored scrubbed bit |
| 71 | is not evidence about the restored file's pages or WAL. |
| 72 | |
| 73 | ## Rollout constraints |
| 74 | |
| 75 | Deploy and smoke-test the compatibility-capable release with the real |
| 76 | production key before activation. Retain that exact release as the rollback |
| 77 | target. Verify key fingerprint and backup/key recovery before conversion. |
| 78 | Stop serving and replication for maintenance, then restart the same |
| 79 | compatibility-capable release after activation. Never roll back to an older |
| 80 | plaintext-only binary once sealed rows exist. Preserve historical backups. |
| 81 | A restored pre-conversion backup is visibly legacy and needs activation; |
| 82 | it must never be silently presented as encrypted. |
| 83 | |
| 84 | ## Required evidence |
| 85 | |
| 86 | Use actual Yjs fixtures and real SQLite. Cover authenticated row identity, |
| 87 | wrong keys and tampering; encrypted HTTP read/write and snapshots; stale |
| 88 | legacy reads and writes; both tables' mixed-state startup checks; explicit |
| 89 | maintenance inputs; atomic repair and marker preservation; raw database |
| 90 | and WAL canaries; incomplete checkpoints; process-exit recovery; and an |
| 91 | independent restored-copy rehearsal. Observe meaningful tests fail when |
| 92 | their safeguards are removed, then restore the code. Full repository CI |
| 93 | and an independent complete-diff review remain required before merge. |
| 94 | |