# The backup format Source: https://selfstore.dev/docs/format A documented, independently specified ZIP layout with canonical test vectors and a Python reference reader - no lock-in, verifiably. Every local-first library says your data is yours. The question that separates the claim from the fact is simple: **if this library disappeared tonight, could somebody else read your file tomorrow?** For selfstore the answer is yes, and it is checked rather than promised. The format is specified independently of this implementation in [SPEC.md](https://github.com/selfstoredev/selfstore/blob/main/SPEC.md); a roughly 120-line [Python reference reader](https://github.com/selfstoredev/selfstore/tree/main/spec) opens real backups with no JavaScript anywhere; and canonical test vectors are committed next to it, so a second implementation can prove it agrees instead of hoping. Two properties do most of the work here, and both were deliberate. **The spec is short enough to read in one sitting.** That is not a stylistic preference. A format nobody finishes reading gets one implementation, and one implementation is indistinguishable from lock-in. The layout below is the whole of it: a ZIP, a cleartext header, and one encrypted entry. **A second implementation exists, in another language, run on every change.** A spec with a single implementation describes that implementation, whatever it claims. The Python reader is the disagreement detector: when the library and the document drift, one of them fails the vectors and CI says which. ## The layout Both modes are genuine ZIP archives: ```text unencrypted: meta.json + selfstore.json + files/* (browsable in any ZIP tool) encrypted: meta.json + data.enc + LISEZMOI.txt / README (still a valid ZIP) ``` - `meta.json` is the cleartext header: `format`, `app`, `appVersion`, `schemaVersion`, `createdAt` and `encryption`. `format` is the **container generation**, and there are three - 1 for a plain archive, 2 for group mode, 3 for the authenticated password envelope, which is what a password-protected backup writes. - A generation 3 header adds the slot table `keys[]` and the payload `iv`. There is no top-level KDF: **the key derivation lives per slot**, because each secret that opens the file wraps the same data key under its own salt and cost. - `selfstore.json` holds your named collections; `files/*` your binary files. - `data.enc` is the inner ZIP, encrypted whole with AES-256-GCM. - The readme rides **inside** the archive so a person who finds the file in ten years knows what it is and what app to feed it to. In generation 3 the exact `meta.json` bytes are the payload's additional authenticated data, so altering **any** header field - the slot table above all - fails the tag. In group mode the header is covered by the author's signature instead. The nuance worth carrying into your own code: the header is authenticated for a reader who already holds the key, which means it is not trustworthy at the moment you are tempted to use it. Show it to a human, never branch a security decision on it. The store's own bookkeeping (schema version, merge metadata) travels in a dedicated `sync.json` entry, so your collections stay pristine. ## The forward guarantee A generation N file is **always** a ZIP whose `meta.json` carries `format: N`. Any reader, however old, can therefore at least identify a newer file and refuse it honestly with `UNSUPPORTED_VERSION`. A cipher or KDF the reader does not know is refused the same way, never as a misleading `DECRYPT_FAILED`. Old readers fail **truthfully**, which is the property that makes long-lived archives trustworthy. ## Crypto parameters travel with the file Encryption is AES-256-GCM over an Argon2id-derived key (46 MiB, 3 passes by default). The parameters are stored per file, so backups written under older defaults keep decrypting forever, and they are **bounded on read** (memory up to 1 GiB, up to 10 passes, up to 4 lanes): a hostile file cannot declare absurd parameters and melt the reading machine. Two guards protect the reading side in general: any archive entry declaring more than 512 MiB, or an archive totalling more than 1 GiB, is refused before inflation (`TOO_LARGE`, the zip-bomb guard). ## What is and is not authenticated GCM authenticates the ciphertext and the crypto parameters: a flipped byte or altered KDF settings fail decryption outright. The cosmetic header fields (`app`, `appVersion`, `createdAt`) are cleartext and **not** authenticated; show them to humans, never base a security decision on them. This split is deliberate and documented in the [threat model](https://github.com/selfstoredev/selfstore/blob/main/THREAT-MODEL.md). ## The conformance kit The spec exists so that other software can read these files, and [`spec/`](https://github.com/selfstoredev/selfstore/tree/main/spec) is everything needed to do it: | | | | --- | --- | | `SPEC.md` | the normative description, at the repository root | | `selfstore_reader.py` | the independent reader, ~120 lines, no JavaScript | | `vectors/` | canonical backups: plain, encrypted, multi-password envelope, external-key envelope, group | | `verify_vectors.py` | runs the reader against every vector and exits non-zero on any mismatch | | [`CRYPTO-RATIONALE.md`](https://github.com/selfstoredev/selfstore/blob/main/CRYPTO-RATIONALE.md) | why each cryptographic decision was made, the alternatives rejected, and where its author would attack it first | Read the vectors and you read selfstore backups: ```sh git clone https://github.com/selfstoredev/selfstore cd selfstore/spec pip install argon2-cffi cryptography python3 verify_vectors.py # the conformance run python3 selfstore_reader.py backup.zip # or point it at your own file ``` The vectors are pinned: a file written by 1.0.0 still reads today, and the suite proves it on every run rather than asserting it in a release note. That is the mechanism behind the stability promise - the number on the package is not what you are trusting, the file is. Data rescue tools, migration scripts, a CLI in Go or Rust, an import path in a competing application: all legitimate, all intended. A format you can leave is worth more than a library you cannot. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt