Search selfstore
v1.8.21

Encrypted backups in the browser, done right

What client-side encryption actually protects against, why AES-256-GCM over Argon2id, and why a format spec beats a vendor promise.

“Your data is encrypted” is the least informative sentence in software. TLS in transit? At rest on the vendor’s disks, with the vendor holding the keys? The threat models differ so much that the same words describe opposite architectures. This article states one architecture precisely: browser apps that encrypt before the data leaves the device, as selfstore implements it, and what that does and does not protect.

The claim worth making

When a backup file leaves the user’s device already encrypted, under a key derived from a password only the user knows, the storage provider holds opaque bytes. Google, the Nextcloud admin, whoever finds the USB stick: they learn the file’s size, its creation date and nothing else. There is no server-side key to subpoena, mismanage or leak, because there is no server in the loop at all.

That is a different claim from “we encrypt your data”, and it is checkable, which is the point of this article’s last section.

The primitives, and why these

AES-256-GCM for the payload. Authenticated encryption is not optional: a backup that decrypts to silently corrupted state is worse than one that fails. GCM gives confidentiality and integrity in one pass; a wrong password, a flipped ciphertext byte or tampered parameters all fail the same way: loudly, with no partially valid read.

Argon2id to turn a human password into a key. Passwords have to assume offline brute force, so the derivation must be expensive in the attacker’s favorite currency, memory: 46 MiB and 3 passes by default. Two details matter more than the defaults, though:

Two operational details, honestly told: JavaScript cannot zeroize strings, so the password itself lives at the platform’s mercy (derived keys are non-extractable WebCrypto keys, which is what the platform can promise). And Argon2id at real cost would freeze a UI thread, so it runs in a dedicated worker, falling back silently to the main thread where workers are not available, with byte-identical output.

The file is a ZIP on purpose

An encrypted selfstore backup is still a valid ZIP archive: a cleartext meta.json (app name, dates, KDF parameters, IV), the ciphertext, and a human-readable note. A person who finds the file in 2036 can tell what it is and which app reads it, without any secret. What they cannot do is read the data.

The unencrypted variant opens in any archive tool: a JSON manifest plus the user’s files. No mystery blobs in either mode: the difference between the two is exactly one property, confidentiality, not inspectability of the container.

Verifiability beats promises

Everything above is an implementation’s word. What makes it trustworthy is that the format is specified independently of the library: SPEC.md defines the container, the header fields and the crypto envelope; canonical test vectors pin the bytes; and a ~120-line Python reference reader, sharing zero code with the TypeScript, reads real backups. If the library disappeared tomorrow, the files remain readable from the spec alone, and any claim in this article can be checked against an artifact rather than taken on faith.

One last honest note, because trust also means stating limits: none of this defends against malicious code running inside your app’s origin. XSS owns the session, including the data already decrypted in memory and the device key that unseals the local cache; the threat model spells out those non-goals. Client-side encryption protects the data at rest and in the cloud, and that is precisely the claim, no more, no less.


Try selfstore in five minutes, or read the other notes.