docs/guide/img is re-shot from the 0.6 binary. TLS roots come from the operating system by default; build.sh cross-builds macOS on Linux without that feature (no Apple SDK), trusting the bundled Mozilla roots. Lock tests wait for the reader to report it is blocked rather than assuming how long unlocking takes on a busy machine. Co-Authored-By: Claude <noreply@anthropic.com>
158 lines
8.0 KiB
Markdown
158 lines
8.0 KiB
Markdown
# 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 (straight from the pipe into the locked page, with no
|
|
intermediate buffer), 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.
|
|
- `migrate` checks an original is still the file it stored (device, inode,
|
|
size, time; on Windows only size and time) and then deletes it by name; a
|
|
replacement in the instant between the check and the deletion is not
|
|
excluded. Paths inside the vault directory are refused outright.
|
|
- A filesystem with neither `RENAME_NOREPLACE` nor hard links (some FUSE
|
|
mounts, FAT) cannot publish without risking a replace, so `get` fails
|
|
there with a message saying so rather than fall back.
|
|
- 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 0600
|
|
temporary file in `$TMPDIR` before storing them, because the size must be
|
|
known first. On Unix the file is unlinked as soon as it is created, so it
|
|
has no name and disappears even if vault is killed; on other systems it
|
|
is a named file 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 operating system's trust
|
|
store; macOS binaries cross-built on Linux use the bundled Mozilla roots
|
|
instead, see `docs/PLATFORMS.md`; verification 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.
|