# Security notes What vault protects, how, and where the protection stops. See `docs/COMPATIBILITY.md` for the exact formats. ## What is protected - **Contents, names, sizes, modes and times** of stored files, against anyone who reads the vault directory without the passphrase: everything is inside Fury files sealed to a hybrid FrodoKEM-640 + ML-KEM-768 key, so the file key stays secret unless both KEMs are broken. - **Structure**: records are one fixed size; objects are at most 4 MiB and padded to power-of-two buckets; object names derive from a secret per-file seed, so nothing on disk links a file's objects; every ciphertext and shard directory has its timestamps pinned to the epoch; all 256 shards exist from the start; `init` adds filler sealed to a discarded key. - **Integrity**: every byte that is acted on is authenticated (header MAC, STREAM tags with a final-chunk flag, no trailing data); an object starts with its own ID, so objects cannot be swapped, reordered or replayed under another record; `get` publishes output only after the whole file, padding and end of file included, has authenticated. - **Deniability**: the key file always has two equal slots, one possibly random; both are always tried; decoy and real files are indistinguishable from filler to the other passphrase. ## What is not protected - The number and total size of ciphertexts, and when the directory changes, are visible to whoever stores it. - A passphrase is only as strong as it is: Argon2id (3 passes, 64 MiB, 4 lanes) slows guessing, it does not prevent it. - Anyone who can run code as you while the vault is unlocked (or watch you type the passphrase) can read everything. - Nothing here hides that you use vault. ## Secrets in memory Passphrases, seeds, private keys, key-encryption keys and plaintext buffers are held in owned types (`SecretArray`, `SecretVec`, `LockedBuf`) that are not `Clone`, `Copy`, `Debug` or serialisable, have fixed allocations that never reallocate, and are cleared with `zeroize` when dropped — on success, on error and while unwinding from a panic (release builds unwind). What cannot be cleared, so is not claimed: - copies the compiler leaves in registers or on the stack; - copies inside third-party key objects: `chacha20poly1305` cipher states (the key schedule), `ml-kem`'s `Seed` passed by value (the decapsulation key itself is zeroized on drop with the crate's `zeroize` feature), `hmac`/`hkdf` internal states, `argon2` internals other than its working memory (which vault allocates and clears itself); - the passphrase in `$VAULT_PASSPHRASE` (it is in the process environment for the process lifetime) and in the terminal driver's buffers; - anything after `abort`, `kill -9`, a power cut, or once swapped out; - file data in the OS page cache, and plaintext temporary files on disk (below). None of this is erasure from physical memory. ## The agent `vault unlock` re-executes the binary as `vault __agent` with `VAULT_PASSPHRASE` and `VAULT_DECOY_PASSPHRASE` removed from its environment, no secret in its arguments, its working directory `/` and standard error closed. It hands over the 34-byte suite ID and seed through a pipe. Before reading it, the agent: 1. sets the core-dump size limit to zero; 2. on Linux, sets `PR_SET_DUMPABLE` to 0 (no core dumps, no same-user `ptrace`, `/proc/PID/mem` or `/proc/PID/environ`) and checks it took; 3. maps a private page, `mlock`s it and, on Linux, marks it `MADV_DONTDUMP`, all checked. Any failure is reported to `unlock` and the agent exits without the key; there is no fallback to unprotected memory. On other Unix systems (macOS, the BSDs, illumos) steps 1 and 3 apply; there is no portable equivalent of `PR_SET_DUMPABLE`, so another process of the same user may be able to read the agent's memory with a debugger. The socket lives in a directory that must be owned by you with mode exactly 0700 (refused otherwise); the socket is 0600, checked before readiness. Each request has a two-second deadline; the expiry deadline is enforced with a timed `poll`, so it does not depend on another client arriving, and a key is never handed out after expiry. On exit the agent clears the page and removes the socket only if it is still its own. `unlock` waits at most ten seconds for the agent to report and kills it otherwise. Windows: the agent is not supported (`unlock` says so); every command asks for the passphrase. ## Filesystem safety - Output is published with an atomic no-replace primitive (`renameat2(RENAME_NOREPLACE)`, `renameatx_np(RENAME_EXCL)`, `link`+`unlink`, or `MoveFileExW` without replace), so an existing file, symlink (dangling or not) or one created concurrently is never replaced; only vault's own temporary file is removed. There is no check-then-rename fallback. - The temporary output file is mode 0600 until the content has fully authenticated; only then are the recorded mode and times applied. - `get -r` opens each directory below the chosen destination with `O_NOFOLLOW|O_DIRECTORY` relative to its parent, so a symlink planted inside the destination cannot redirect files elsewhere. On Windows, which has no `openat`, each component is checked not to be a link or junction just before use; a concurrent swap between check and use is not excluded. - Writes follow crash-safe order with explicit `fsync` of files and directories (Unix): objects before their record; a new record before the removal of the one it replaces; a record's removal before its objects'. A failure after a record is already in place never deletes its objects. Windows has no directory fsync through the APIs used; NTFS journals metadata. - The vault lock uses the same POSIX `fcntl` locks as the Go version, so mixed Go and Rust processes exclude each other. Only a read-only filesystem lets a reader proceed without the lock. ## Plaintext on disk - `vault add URL` and `vault mirror` stream non-HTML downloads to a private (0600) temporary file in `$TMPDIR` before storing them, because the size must be known first; it is removed on every normal path, including errors. HTML and CSS are rewritten in memory. `vault absorb` stores tar entries straight from the stream with no temporary file. - `vault get` writes plaintext where you ask; `vault migrate` deletes the originals with `unlink`, which is not secure deletion. ## Network Downloads verify TLS certificates (rustls with the platform verifier; it is never disabled), follow at most 10 redirects (http and https only; for `mirror`, only to the same host), and are bounded by connect, response and whole-request deadlines and a byte budget counted after decompression. See `vault(1)` for the defaults and flags. ## Cryptographic implementations FrodoKEM-640-SHAKE is a port of CIRCL v1.6.4's implementation (constant-time in the same places: no secret-dependent branches or indices; the re-encryption check selects the rejection key with a mask). It reproduces CIRCL's NIST KAT digest. ML-KEM-768 (`ml-kem`), ChaCha20-Poly1305 and XChaCha20-Poly1305 (`chacha20poly1305`), Argon2id (`argon2`), HKDF, HMAC, SHA-2 and SHA-3 are RustCrypto crates; TLS is rustls with `ring`. A Rust rewrite is not stronger cryptography: the algorithms and parameters are unchanged. ## Reporting Please report vulnerabilities privately to andy@dixon.cx.