Manifest Format

1 Overview

The manifest maps opaque blob IDs to target file paths. It is stored encrypted and GPG-signed at .vault/manifest.gpg and decrypted on every operation.

../_images/manifest_flow.svg

2 Format

2.1 v5 (current)

# vault-manifest-v5
# vault-seal-key 9f2b...  (sha256 over backend, recipient, identity, signer)
a1b2c3d4e5f67890    ~/.secret/api_key.txt   e3b0c442...     file    7d865e95...
fedcba9876543210    ~/.config/app/credentials.json  a7ffc6f8...     file    2c26b46b...
  • First line: version header

  • # vault-seal-key <sha256> records what the blobs were encrypted to

  • Entry lines: <id>\t<path>\t<blob-sha256>\t<kind>\t<plaintext-sha256>

  • Blank lines and lines starting with # are ignored

The seal key exists so seal can skip a file whose plaintext is unchanged without going stale. Comparing content alone would be wrong the moment the recipient, backend or signer moves: unchanged plaintext still owes a fresh blob under a new key. Any change to the recorded key re-encrypts everything, and a manifest with no recorded key (v4 and earlier) seals in full once to establish one.

2.2 Earlier versions

Version

Line shape

Added

v1

id\tpath

unsigned, no hashes

v2

id\tpath\thash

blob digest

v3

id\tpath\thash\tkind

file vs directory entry

v4

+ contentHash

plaintext digest, so status needs no decryption

v5

+ seal-key header

incremental seal that survives key rotation

All are still readable. unseal rejects v1 by default because it carries no blob hashes; pass --allow-unsigned, then seal to upgrade.

2.3 Fields

id

16-character lowercase hex string, generated from 8 bytes of cryptographic randomness (std/sysrand).

path

Target file path. Paths under $HOME are stored with ~/ prefix for portability across machines. In root-relative mode, paths are stored relative to the configured root. Absolute paths outside $HOME are stored as-is.

hash

(v2+) SHA-256 hex digest of the encrypted blob. Verified on unseal, get and check.

kind

(v3+) file or dir.

contentHash

(v4+) SHA-256 of the plaintext. Lets status and the incremental seal decide what changed without decrypting anything.

3 Blob files

Each manifest entry has a corresponding .vault/<id>.gpg (or .age) file holding the encrypted contents. --set-filename "" keeps the original filename out of GPG metadata.

3.1 Envelope keys (v6)

Blobs are encrypted under a per-file data key, and the data keys live together in .vault/keys.gpg (or .age), itself encrypted to the recipients.

Before v6 each blob was encrypted straight to the recipient. Both backends do envelope encryption internally, but that wrapping lives inside the blob, so the recipient was baked into every payload and adding one re-encrypted the whole vault. Hoisting the key means rotate rewrites one small file and leaves every payload alone.

Backend

Data key

Payload

gpg

256-bit passphrase

gpg --symmetric

age

x25519 identity

age -r <recipient>

A vault may hold both formats while a migration drains. The key file is the discriminator: an entry with a data key is v6, one without is encrypted to the recipient directly. The two cannot disagree, because a key is only ever written by the encryption that used it.

A v6 payload carries no in-band signature, since it is symmetric. Authenticity comes from the signed manifest and the per-blob digest, which is the same model the age backend has always used.

4 Integrity model

The signed manifest is the integrity anchor. On unseal:

  1. The manifest is decrypted and its GPG signature is verified

  2. Each blob’s SHA-256 hash is compared against the manifest

  3. Blobs are decrypted to temporary files

  4. Each blob’s GPG signature is verified, where one exists in band: v5 gpg blobs carry one, age blobs and v6 enveloped blobs do not

  5. Only after all checks pass are files moved to their final paths

nimvault check runs step 2 on its own, over every entry, without decrypting and without key material.

This prevents forgery (attacker encrypts a malicious blob to your public key), ciphertext swapping (attacker renames blobs), and manifest replacement (attacker substitutes a v1 unsigned manifest).

5 Directory layout

.vault/
  config            # recipient configuration (plaintext)
  manifest.gpg      # signed + encrypted manifest
  keys.gpg          # per-file data keys, encrypted to the recipients (v6)
  a1b2c3d4e5f67890.gpg  # signed + encrypted blob
  fedcba9876543210.gpg  # signed + encrypted blob

Only .vault/ is committed to git. The plaintext files live at their target paths outside the repository.