At‑rest encryption + per‑client scope

by

Following up on the launch thread, few of you asked same good question:

transcript is immutable and redaction happens at share boundary, but doesn't local index still hold plaintext secrets at rest, so any agent/MCP client with read access (or synced backup) can pull old key straight out of search?


it is a logical deduction. scritty's model is not "your disk is the trust boundary." instead there's opt in at‑rest encryption on the local store one switch, pointed at a passphrase you hold (env var or OS keychain, never written to config) and the whole local store is encrypted, both layers, not just the transcript:

  1. relational transcript + keyword index (SQLCipher): page‑level AES‑256, each page authenticated with an HMAC so tampering is detectable, key stretched from the passphrase via PBKDF2.

  2. vector index: XChaCha20‑Poly1305 authenticated encryption, key derived via Argon2id, KDF params + salt bound as AAD. same passphrase as layer one so an old key buried in history isn't readable by another process, MCP client, or synced backup without it. fail‑closed -- passphrase absent > the store refuses to open, never a silent fallback to plaintext.

  3. scope gating: per client. MCP access is by scoped token; hand an agent a token bound to a specific session (or set), and it can't widen to the whole corpus. cross‑corpus reads (e.g. corpus‑wide semantic search) are refused for that token, not silently honored. "one agent asking for everything" only works if you explicitly mint it admin scope.

Bottom line is trust boundary is passphrase + token scope, not filesystem. redaction stays separate share‑boundary concern; encryption is what protects local copy.

46 views

Add a comment

Replies

Best

this is a genuinely solid writeup, most products just say "encrypted at rest" and stop there. one thing i didn't see covered: passphrase rotation. if i suspect a passphrase leaked, is rotating it a fast re-wrap of the derived keys, or does it mean decrypting and re-encrypting the whole local store, transcript + vector index, page by page. for anyone with a long-lived scritty history that gap between "i should rotate" and "rotation actually finishes" matters, since the old passphrase is still valid the whole time it's running

  straight answer: rotation is full re-encrypt, not a fast re-wrap and it was a deliberate design choice.

there's no envelope/DEK layer. passphrase derives key directly in both layers -- SQLCipher stretches it to the page key via PBKDF2, vector index derives its XChaCha20-Poly1305 key via Argon2id.

new passphrase = new derived key = every page and every sealed blob gets re-encrypted

for you those interested it means O(history), not O(1)


now the why behind this design choice meaning not a DEK re-wrap. so yes, for a leaked passphrase it'd be faster but not safer. let's say someone had the passphrase and already copied the store, no rotation scheme un-leaks that copy meaning they decrypt it offline and with a DEK the underlying data key never changes on rotation. so a leaked data key is unrotatable without full re-encrypt anyway whereas direct derive means once rotation finishes that old passphrase is dead everywhere, no wrapped-key blob left around.

on the window you mentioned, meaning the gap while it runs: rotation re-seals into temp files and atomically swaps, so there's never a plaintext window, and it's crash-safe (an interrupted rotate rolls forward or back on next open, never split-key store). one command rotates whole local store under new passphrase and that is:

transcript + keyword index + vector index

the caveat is yes, old passphrase stays valid until swap completes, but for a brief moment on hopefully what is a rare, deliberate, offline op.

I would like clarity on how revoked tokens immediately lose access without requiring another restart.

 there's no per connection cached grant so nothing outlives the token. auth hashes bearer (sha256) and checks it against live in memory registry on every request. revoking removes hash durably, under a cross-process file lock, persisted to disk.


two cases:

• revoke from running server itself (`revoke_token` tool): it mutates shared registry in place, so very next request carrying that token misses lookup i.e., refused so no restart.
• revoke out of process (CLI, or second instance): the server polls registry file's mtime every 500ms and reloads on change, so an external revoke propagates in under a second. still no restart.


fail closed throughout - unknown/missing/revoked hash all get same generic 401. only caveat worth stating plainly: it's per request, so a call already in flight will finish; revocation kicks in starting on next request, and cross process propagation is bounded by that ~500ms poll, not literally instantaneous.