FSOR — Forward-Secrecy Output Reset — is the server-side at-rest layer beneath the end-to-end Q-Ratchet. It wraps every stored ciphertext in a per-conversation envelope whose key is derived at runtime and lives only in process memory. On destroy, the envelope epoch advances and the old key is zeroized: bytes remaining in any backup, WAL, autovacuum page, cache, or replica become envelope-locked under a key that no longer exists anywhere.
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 closes that
gap cryptographically instead of chasing every copy: the bytes that
survive are useless without a key that was never persisted.
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 from a master held only in an environment
variable — never persisted, never logged — via
HKDF-SHA-512 per (conversation, epoch). 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" || epoch_be8 || conv_id, 32)
wrapped = nonce(12) || ChaCha20-Poly1305(
key = wrap_key, aad = "QERYX-FSOR-ENVELOPE-v1",
plaintext = E2E_ciphertext)
On first successful delivery the stored ciphertext is nulled in place and the row becomes a tombstone: a re-fetch answers 410 Gone with the erasure timestamp. Undelivered messages ride a soft-delete grace, then a bounded offline horizon (default 7 days), then irrevocable purge.
The destruction flow
- The conversation's active epoch retires; its wrap key is zeroized in process memory.
- A fresh epoch row with a new commitment is appended — the ledger is append-only.
-
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 exactly what was destroyed. - Row deletion fires a database trigger that fans the purge out to the cache layer and the file store — a catch-net that holds even for future code paths that forget to purge directly.
After step 1, WAL and backup residue for the retired epoch is permanently un-unwrappable. Rotating the master itself (an operator runbook item, recommended monthly) additionally locks every previously retired epoch against a later master leak.
Auditor verification
A user who receives a destruction notice holds everything needed to check it:
ML-DSA-87.VerifyWithCtx(
pk = pinned operator key,
ctx = "QERYX-FSOR-ROOT-v1",
msg = proof_root || epoch_before || epoch_after || leaf_count,
sig = proof_root_signature) == OK
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 destroys a key, and destroying a key destroys what that key wrapped. Every surface it does not reach is listed here.
- The soft-delete grace window. Until ten minutes after delivery, the wrapped envelope is recoverable with operator cooperation. Past the window, recovery requires breaking the inner end-to-end layer and the wrap key both.
- Live operator coercion. An operator under live legal compulsion could be forced to shadow-copy inbound ciphertext before FSOR wraps it. The WebSocket fanout path erases server-side as soon as push delivery succeeds, 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.rsand the destruction-ledger migrations. - 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.