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.
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 toEntry 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 |
|
unsigned, no hashes |
v2 |
|
blob digest |
v3 |
|
file vs directory entry |
v4 |
|
plaintext digest, so |
v5 |
|
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¶
id16-character lowercase hex string, generated from 8 bytes of cryptographic randomness (
std/sysrand).pathTarget file path. Paths under
$HOMEare stored with~/prefix for portability across machines. In root-relative mode, paths are stored relative to the configured root. Absolute paths outside$HOMEare stored as-is.hash(v2+) SHA-256 hex digest of the encrypted blob. Verified on
unseal,getandcheck.kind(v3+)
fileordir.contentHash(v4+) SHA-256 of the plaintext. Lets
statusand the incrementalsealdecide 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 |
|
age |
x25519 identity |
|
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:
The manifest is decrypted and its GPG signature is verified
Each blob’s SHA-256 hash is compared against the manifest
Blobs are decrypted to temporary files
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
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.