# Encrypted Pods: an implementer's guide This guide orients a developer who needs to read or write an encrypted Cascade Pod. It is not normative. The format is specified in the [Pod Encryption Specification](../spec/pod-encryption.md) (version 1.1, Draft), and where this guide and that document differ, the specification is right. ## What an encrypted Pod is An encrypted Pod is an ordinary Cascade Pod ([Pod Structure Specification](../spec/pod-structure.md)) whose files are sealed one by one. Nothing about the layout changes: the same directories, the same file names, the same type indexes. Only the bytes inside the files change. - **One data key per Pod.** A random 256-bit data key seals every sealed file with AES-256-GCM. Each sealed file is exactly `nonce(12) || ciphertext || tag(16)`, with no magic number and no associated data (specification sections 2 and 3.1). - **The data key is wrapped.** A key derived from a passphrase with Argon2id seals the data key. The sealed copy is a *wrap*, and a Pod can hold more than one wrap of the same data key (section 1.1). - **The header is plaintext.** The wraps, and the parameters needed to derive each key, live in `settings/encryption.json`. A Pod is encrypted if and only if something is present at that path (section 4.1). - **Three files stay plaintext by design:** the header, `README.md`, and `provenance/egress-log.jsonl`. Every other regular file in an encrypted Pod is sealed, whatever its name or extension (section 3.2). Writers produce header version `1.1`, in which each wrap carries its own key derivation parameters. Readers must also accept version `1.0` (sections 4.3 and 4.4). ## What to read, in order 1. **Sections 2 and 3** of the [specification](../spec/pod-encryption.md): the primitives, and exactly which files are sealed. Most interoperability failures start here, so state every Argon2id parameter explicitly rather than relying on a library default (section 2). 2. **Section 5**, if you read Pods. The header is attacker-controlled input. A reader validates the whole header, including every limit in section 5.3, before it derives any key, and reports one of five outcomes (section 5.4). A header that is refused must never be reported as an incorrect passphrase, and a Pod that did not open must never be reported as a Pod with no records. 3. **Sections 6 and 7**, if you write Pods: the writer invariants, re-wrapping a passphrase, and the re-key, which is never done in place. 4. **Section 8**, for everyone: symbolic links are never followed inside a Pod, and only regular files are resources. 5. **Section 9**, before you describe the feature to anyone (see [What it does not protect](#what-it-does-not-protect) below). ## Conformance fixtures and vectors The cross-implementation fixtures live in the public [`conformance`](https://github.com/the-cascade-protocol/conformance) repository under `pod-encryption/` (specification section 10): | Kind | What it checks | |---|---| | positive fixtures | headers and sealed files written by two independent implementations, in versions `1.0` and `1.1`, plus one whole encrypted Pod. A conforming reader opens every one to the stated plaintext. | | negative vectors | headers every reader must refuse before deriving any key, each with its expected outcome (`malformed`, `unsupported-version` or `cannot-open`). | | acceptance vectors | headers every reader must open: the rules that tell a reader what not to refuse. | | file system vectors | layouts built at test time (links, FIFOs, devices), since a repository cannot carry them portably. | `pod-encryption/vectors.json` lists every fixture and vector with its expected outcome and the key to try. The passphrases in that directory are published test values that protect nothing but the fixtures. Never use one for a real Pod. To check a header vector by hand: make an empty directory, write the vector's header bytes to `settings/encryption.json` inside it, and open that directory as a Pod with the vector's `tryWith` key. ## Running the harness `scripts/check_pod_encryption.py` in the conformance repository runs every fixture and vector against an implementation through its command line. It needs Python 3 and nothing outside the standard library. Its one adapter today is for the `cascade` CLI: ```bash git clone https://github.com/the-cascade-protocol/conformance.git cd conformance python3 scripts/check_pod_encryption.py --cascade cascade # the CLI installed on PATH python3 scripts/check_pod_encryption.py --cascade "node /path/to/cascade-cli/dist/index.js" ``` The result is ratcheted against `pod-encryption/KNOWN_FAILURES.json`: a failure that is not listed fails the run, and a listed failure that starts passing also fails the run. Exit 0 means nothing moved, exit 1 means the ratchet moved, exit 2 means the harness could not run. An implementation without a command-line adapter can still use the vectors: load `vectors.json` in its own test suite and assert the stated outcome for each entry. ## What it does not protect The specification is explicit about its limits, and an implementation must not claim more: - **Swapped and rolled-back files are accepted** (section 9.4). Sealed files carry no associated data, so anyone who can write to the Pod folder can copy one sealed file over another, or put back an older sealed copy of a file, without any secret, and every conforming reader accepts the result. An encrypted Pod is not tamper-evident against someone who can write to its folder, and it is not rollback-proof. The design for binding each sealed file to its path is decision [D-SEAL-1](https://github.com/the-cascade-protocol/spec/blob/main/decisions/2026-09-26-sealed-resource-binding.md) in the specification repository; it is not yet part of the format. - **Names, structure and sizes are visible** (sections 3.2 and 9.2). File and directory names, the directory structure, file sizes, timestamps and the whole header can be read without a secret. - **The header is an offline guessing target** (section 9.3). The Argon2id parameters raise the cost of each guess; they cannot make a weak passphrase strong. - **Removing a wrap is not revocation** (section 6.6). Only a re-key cuts off someone who has held the data key, and only for the Pod it is applied to. A copy made earlier still opens with the secrets that opened it then (sections 7.4 and 9.7). ## A reference implementation The `cascade` CLI implements the format: `cascade pod init --encrypt`, `cascade pod encrypt`, `cascade pod decrypt`, and `cascade pod passphrase set` (with `--rotate-dek` for a re-key). Its operational documentation, covering commands, the passphrase environment variables, exit codes and error messages, is [`docs/pod-encryption.md`](https://github.com/the-cascade-protocol/cascade-cli/blob/main/docs/pod-encryption.md) in the `cascade-cli` repository. ## See also - [Pod Encryption Specification](../spec/pod-encryption.md) - [Pod Structure Specification](../spec/pod-structure.md) - [Conformance repository](https://github.com/the-cascade-protocol/conformance)