FSOR — Forward-Secrecy Output Reset — is the server-side at-rest layer
beneath the end-to-end Q-Ratchet. It wraps message content written through the REST send path in a per-conversation envelope whose key is derived at runtime and lives only in process memory; sealed-sender, group-fan-out and WebSocket-spooled rows are stored as raw end-to-end ciphertext, and SELECT count(*) FROM messages WHERE fsor_epoch IS NULL audits the gap. On destroy, the envelope epoch advances: bytes remaining in any backup, WAL, autovacuum page or replica become envelope-locked against anyone holding those bytes alone.
Why FSOR exists
A hard DELETE removes the live row and nothing else. Residue
survives in the write-ahead log, autovacuum heap pages, the cache layer,
file-store blobs, backup snapshots, and replica WALs. FSOR narrows that
gap for one lane: a row written through the REST send path sits in an
envelope whose wrap key is derived at use and never stored, so once its
epoch is retired the bytes that survive are useless to anyone without
the operator master. Rows from the other lanes are deleted and never
wrapped: their residue rests on the end-to-end layer alone. FSOR wraps
no file either, and any server-side encryption a file’s store adds
is under a key the operator holds, so against the operator a file rests
on its end-to-end seal alone.
The server-blindness contract
The wrap key cannot decrypt the user's plaintext. It can only peel the outer at-rest envelope, exposing the same end-to-end ciphertext that would have been on disk before this layer existed.
The wrap key derives at runtime, via HKDF-SHA-512 per
(conversation, epoch), from a master the relay reads from its
environment; the wrap key itself is never persisted. The database stores
only the epoch number and an opaque SHA-384 commitment that lets an
auditor prove a row binds the right epoch without exposing the key. Even
a leaked master reveals only ciphertext still protected by
ML-KEM-1024 + X25519 +
ChaCha20-Poly1305. FSOR is strictly defensive: it can never
make confidentiality worse than the inner layer, only better.
The at-rest envelope
wrap_key(conv, epoch) = HKDF-SHA-512(master,
info = "QERYX-FSOR-WRAP-v1" || conv_id || epoch_be8, 32)
wrapped = nonce(12) || ChaCha20-Poly1305(
key = wrap_key, aad = "QERYX-FSOR-ENVELOPE-v1" || conv_id || epoch_be8,
plaintext = E2E_ciphertext)
On the recipient's acknowledgment the row is retired and hard-deleted by the sweep; the destruction receipt, signed under QERYX-FSOR-DESTROY-v1, is the record that it happened. A group message, sealed to each member by default, and a message read with Stealth Mode on or read receipts off are retired without a receipt the relay can match; they wait, like undelivered messages, for the 30-day horizon, operator-tunable between 1 and 90 days, then irrevocable purge.
The destruction flow
- The conversation's active epoch is marked retired in the ledger, and
unwrap_at_restrefuses that epoch from then on. Wrap keys are derived per use and zeroized after each use, so there is no stored key to destroy — and none to leak. A fresh epoch row with a new commitment is inserted. Once the destruction is complete and no message row can still address those epochs, the whole ledger for that conversation is removed, so a destroyed conversation leaves no standing record naming its two parties. -
A Merkle tree is built over the destroyed messages and its root signed with
ML-DSA-87under the contextQERYX-FSOR-ROOT-v1, committing the operator to the set of rows the destruction enumerated. - Row deletion fires a database trigger whose listener deletes every file-store blob linked to the row in the message-file table — a catch-net for future code paths. No chat send writes that link, so a photo, video, file or group voice note is not deleted with its message: its blob lives to its own expiry, 45 days after upload by default, unless its sender deletes it first. The cache holds no per-conversation entry, so there is nothing for that arm to reach; we say so rather than count it.
After step 1, WAL and backup residue for the retired epoch is permanently unreadable to anyone holding those bytes alone. The operator's own master can still re-derive the wrap key, which is why master rotation — a runbook step, recommended monthly — is the arm that closes this layer against a compelled operator too.
Auditor verification
A destruction record is signed with ML-DSA-87 under the context QERYX-FSOR-ROOT-v1, over the SHA-384 of the conversation identifier, the new epoch, the leaf count and the Merkle root, in that order. The exact byte layout, the tree shape and the signature framing the code emits are published alongside the operator key at /fsor/audit/operator-key; verify against those, not against a reconstruction.
The Merkle root commits to which messages were destroyed; a user still holding ciphertext can re-derive leaves and prove inclusion or exclusion. If verification passes, the operator is cryptographically committed to the destruction.
What FSOR does not promise
FSOR retires an epoch rather than destroying a stored key: against anyone without the operator master, what that epoch wrapped stays unreadable. Every surface it does not reach is listed here.
- Other lanes and files. Only rows written through the REST send path are wrapped. Sealed-sender, group fan-out and WebSocket rows are deleted and never wrapped, so their residue in a backup is the end-to-end ciphertext alone. FSOR wraps no file, and any server-side encryption a file’s store adds is under a key the operator holds, so against the operator a file rests on its end-to-end seal alone.
- The soft-delete grace window. Until the sweep deletes a delivered row, ten to twenty-five minutes after the acknowledgment, the wrapped envelope is recoverable with operator cooperation. Past the window, recovery by anyone without the erasure master requires breaking the end-to-end layer; a holder of the master gets back to that same end-to-end ciphertext and no further.
- Live operator coercion. An operator under live legal compulsion could be forced to shadow-copy inbound ciphertext before FSOR wraps it. The WebSocket path retires a row only on the recipient device's own receipt or retire call, and a sweep deletes it later — a push wakes a device and retires nothing — but this remains a policy-and-audit control, not a cryptographic one — and we say so.
- A leaked master. Rows written under a leaked master are unwrappable by the leaker — down to the pre-FSOR baseline of end-to-end ciphertext at rest — until the next rotation restores the guarantee for new rows.
References
-
QERYX Protocol Specification §8 —
docs/SPEC/06-fsor-server-erasure.md; implementationbackend/src/services/fsor/mod.rsandbackend/src/services/fsor/receipt_emitter.rs. - RFC 5869 — HKDF; RFC 8439 — ChaCha20-Poly1305.
- NIST FIPS 204 — ML-DSA (destruction-root signatures).
- What a Subpoena Yields — the plain-language consequence of this construction.