# Security model Source: https://selfstore.dev/docs/security What selfstore protects, in layers, and where each layer stops - encrypted backups, an at-rest local cache, the optional cacheLock, and the one ceiling no browser app escapes. Honest boundaries, not marketing. 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](https://github.com/selfstoredev/selfstore/blob/main/THREAT-MODEL.md), and the backup format is [independently specified](https://selfstore.dev/docs/format) 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.
Your password is stretched by Argon2id into a key-encryption key, which wraps a random per-backup data key; that data key AES-256-GCM-encrypts your snapshot. The backup file holds a cleartext header plus the encrypted key and ciphertext. Your password never stored, never leaves the device Argon2id, memory-hard (~46 MiB) Key-encryption key one per password, derived on the fly wraps a fresh random data key Data key random, one per backup AES-256-GCM Header app name, date cleartext Wrapped data key Argon2id params per file encrypted Ciphertext AES-256-GCM(your snapshot) encrypted
The backup file (a .zip on your home): a tiny cleartext header, then everything else sealed. Whoever holds the file, without the password, holds noise.
**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](https://selfstore.dev/docs/sensitive) 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](https://github.com/selfstoredev/selfstore/blob/main/SECURITY.md). Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt