Search selfstore
v1.8.21

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.

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 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.