# vault container _(no submodules)_ ## Purpose `container.ts` is the drive-backed encrypted blob layer that sits between the orchestration `Vault` class and the underlying `DriveLike` adapter. It handles the on-disk format — `[manifest header][AEAD nonce][AEAD ciphertext+tag]` — by combining `manifest/manifest.ts` (header packing) with `@suaveplan/crypto-cipher`'s XChaCha20-Poly1305 (body authentication). The container is the single boundary at which vault state crosses between in-memory plaintext and on-disk ciphertext. `loadContainer` reads the blob, parses the manifest, and AEAD-decrypts the body using the manifest as AAD; `saveContainer` reverses the process and writes back atomically. Tampering with any byte of the manifest or ciphertext fails the AEAD tag check, raising `VAULT.DECRYPT_FAILED`. ## Features - `createEmptyContainer(input)` — build a fresh state + manifest for first-time vault creation. - `loadContainer(drive, path, vaultKey)` — read encrypted blob from drive and decrypt it. - `saveContainer(input)` — refresh the manifest's `lastModifiedAt`, encrypt, and atomically write the blob. - `encryptContainer(input)` / `decryptContainer(blob, vaultKey)` — pure byte-level helpers without drive I/O (used by tests and integration). - `serialiseState(state)` / `deserialiseState(bytes)` — round-trip the in-memory item / history / ledger state through UTF-8 JSON. - All drive-side failures map to `VAULT.DRIVE.FAILURE`; AEAD / encoding failures map to `VAULT.DECRYPT_FAILED`. ## Usage: Basic Example ```ts import { createEmptyContainer, encryptContainer, decryptContainer } from "@suaveplan/vault/container/container"; const { manifest, state } = createEmptyContainer({ vaultId: "personal", argon2idMemoryKB: 65536, argon2idIterations: 3, argon2idParallelism: 4, now: Date.now(), }); const blob = encryptContainer({ manifest, state, vaultKey }); const round = decryptContainer(blob, vaultKey); // round.manifest.formatVersion === manifest.formatVersion // round.state.schemaVersion === state.schemaVersion ``` ## Usage: Advanced Example ```ts import { loadContainer, saveContainer } from "@suaveplan/vault/container/container"; // On unlock: read + decrypt + return state. const { manifest, state } = await loadContainer(drive, "/personal.vault", vaultKey); // ... mutate state via items module ... // On flush: refresh manifest + encrypt + atomic write. const updated = await saveContainer({ drive, path: "/personal.vault", manifest, state, vaultKey, now: Date.now(), }); ``` ## API Reference - `createEmptyContainer(input): ContainerLoadResult` — pristine state at head schema. - `serialiseState(state): Uint8Array` / `deserialiseState(bytes): VaultContainerState` — JSON round-trip. - `encryptContainer({ manifest, state, vaultKey, rng? }): Uint8Array` — pack header, encrypt body, concatenate. - `decryptContainer(blob, vaultKey): ContainerLoadResult` — slice, unpack, decrypt, deserialise. - `loadContainer(drive, path, vaultKey): Promise` — drive read + decrypt. - `saveContainer({ drive, path, manifest, state, vaultKey, now, rng? }): Promise` — refresh manifest, encrypt, drive write; returns the refreshed manifest. ## Implementation Notes - The blob layout is fixed: header at offset 0 (size `MANIFEST_BYTE_SIZE`), nonce at offset `MANIFEST_BYTE_SIZE` (24 bytes), ciphertext after that. Any change to the layout requires a manifest format-version bump. - The AEAD AAD is the **packed manifest header bytes**, not the parsed manifest object — this binds the ciphertext to the exact byte sequence the reader will use to choose the AEAD algorithm and KDF parameters. An attacker who flips a single bit in the header sees an authentication failure rather than a silent algorithm downgrade. - Drive `write` is contracted to be atomic in `DriveLike`; the container layer relies on this for crash safety. A partial write would corrupt the blob; truthfully-atomic adapters (POSIX `rename`, S3 `PutObject`, IndexedDB transactions) all satisfy the contract. - `serialiseState` collects items as an array (sorted into Map order on the way out) and re-builds the Map on `deserialiseState`. This keeps the on-disk JSON shape stable across runtimes that may not preserve Map iteration order through structured-clone. - The minimum blob length check in `decryptContainer` (`MANIFEST_BYTE_SIZE + NONCE_LENGTH + 16`) is the smallest possible valid blob: header + nonce + 16-byte Poly1305 tag with empty plaintext. Anything shorter is malformed by definition and fails fast with `VAULT.DECRYPT_FAILED`.