Security model
selfstore is a security tool, so it states its limits as plainly as its guarantees. This page is the summary; the full analysis - assets, trust boundaries, thirteen named threats and explicit non-goals - is THREAT-MODEL.md, and the backup format is independently specified with test vectors.
The shape of the model
There is no selfstore server, so there is no central store to breach. The trust boundaries are simple:
- Your device is trusted. Your data is readable on the machine you use it on - that is the point of local-first.
- The home is untrusted for confidentiality. A Drive admin, a WebDAV host, an S3 operator: with a password they only ever hold ciphertext.
- The network is untrusted. Backups travel as ciphertext; credentials are refused over plain http.
- Your app’s origin is trusted, and is the ceiling (see the end).
The layers
1. The backup, end to end. With a password, a backup is AES-256-GCM over a
random data key, itself wrapped per password with Argon2id (memory-hard, ~46
MiB). Whoever holds the file sees ciphertext plus a small cleartext header (app
name, date) that is never secret and never authenticated. A wrong password, a
flipped byte or an altered parameter all fail as DECRYPT_FAILED - there is no
partially-valid read.
2. The local cache, at rest. The IndexedDB working copy is not plaintext: its collections and file blobs are AES-256-GCM-sealed under a non-extractable per-device key. This defeats casual inspection, partial exfiltration and disk forensics of the store. Its limit is honest: the key sits in the same database, so a copy of the whole browser profile carries a usable key. Only the small sync bookkeeping stays in the clear.
3. cacheLock - beat a profile copy.
For the top tier, seal the cache under a key held in
memory only - derived from a password, or an app-supplied key such as a
passkey PRF result - never written to disk. A copied profile then carries no
usable key. The unavoidable cost is one unlock per session: a secret that could
be derived without asking could be derived by an attacker with the profile too.
Branch it on your app’s existing login and the UX is unchanged. See
sensitive apps for the setup.
The ceiling: your origin
None of the layers changes the one limit no browser app escapes. Code running in your origin - through XSS, a compromised dependency, a poisoned CDN - can read the decrypted data and the in-memory password, whatever the cache lock is set to. selfstore is defence-in-depth (secrets out of web storage, ciphertext off the device), not a sandbox against the app it runs inside. This is the number-one non-goal, stated up front. Mitigate it where it lives:
- A strict Content-Security-Policy; no third-party scripts.
- Subresource Integrity and a reproducible build.
- Ship only the entries you use, to keep the attack surface small.
Other stated limits
- Rollback. Without a server, selfstore cannot prove a home served the latest backup, only that committed local edits are never erased by an older one. Mitigate by encrypting and using a home only the user controls.
- Metadata. An encrypted backup still reveals its app name, date and rough size; record ids and clocks travel inside the encrypted envelope.
- Password recovery. There is none. Lose the password, lose the encrypted backup. That is a promise, not a gap.
Reporting
Security reports go to the process in SECURITY.md.