# Encryption and backups Source: https://selfstore.dev/docs/backups End-to-end encryption in one call, portable backup files the user owns, and the fluent backup-file API that needs no store. A backup is one snapshot serialized to a portable ZIP, optionally encrypted. With a store you rarely touch the file layer directly; without one, the fluent API drives the same engine. ## From the store: one call each ```ts await store.protect('a passphrase the user chose'); // end-to-end from here on await store.unprotect(); // reversible await store.downloadBackup(); // a portable .zip (encrypted while protected) const blob = await store.exportBackup(); // the same file, yours to route await store.importBackup(pickedFile); // read one back INTO the store ``` While `protect()` is on, every byte that leaves the device, to a Drive, a WebDAV server, a downloaded file, is AES-256-GCM ciphertext. The local working copy is sealed at rest too, under a non-extractable per-device key, so nothing readable sits on disk. `importBackup` replaces the store's data with the file's (removals propagate like edits) and throws `PASSWORD_REQUIRED` / `DECRYPT_FAILED` up front. ## Backup FILES without a store `backup`, `restore` and `changePassword` are pure of any storage, they take and return blobs, so an import/export feature is just calling them. They stay at the package root. The write chain is **staged**: an illegal order does not compile. ```ts import { backup, restore, changePassword } from 'selfstore'; // Write const blob = await backup({ collections: { notes }, files: [] }) .as('my-app', '1.2.0') // backup() only offers .as(); name the app first .encryptedWith(password) // omit for a plain, browsable ZIP .withReadme('Import this file into MyApp with your password.') .toBlob(); // or .toBytes(), or .toDisk('my-backup.zip') // Read const meta = await restore(file).meta(); // cleartext header, no password if (await restore(file).isEncrypted()) { /* ask the user */ } const snap = await restore(file).withPassword(password).read(); // Add, rotate or remove a file's password const rekeyed = await changePassword(blob, { from: 'old', to: 'new' }); ``` `meta()` never needs a password: the header (app, versions, dates, encryption parameters) is cleartext by design, so you can show "backup of my-app from March 3rd, encrypted" **before** asking for anything. ## The crypto, precisely - **Cipher:** AES-256-GCM. A wrong password, a flipped ciphertext byte or altered parameters all fail decryption. There is no partially valid read. - **Key derivation:** Argon2id, memory-hard, 46 MiB and 3 passes by default, stored **per file** (old backups keep decrypting when defaults improve) and bounded on read (up to 1 GiB / 10 passes) so a hostile file cannot melt the machine. - **Where it runs:** a dedicated Web Worker when the platform allows, falling back silently to the main thread with byte-identical results. The UI never janks on a password. `DECRYPT_FAILED` means "wrong password **or** tampered file", indistinguishable by design; present both possibilities. An empty or omitted password means "not encrypted", in both directions. ## Common mistakes - **Numeric record ids.** The simple store throws at `put()`; the fluent API will happily write them, but they will not sync when imported into a store. - **Guessing at errors.** Branch on `err.code`, never parse messages. See the [error reference](https://selfstore.dev/docs/errors). - **Reserved names.** `backup()` refuses collections starting with `__`. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt