build.sh builds the Rust targets with the Go-era names, refuses an OUT that is /, $HOME or the tree, checks static linking and names the Go targets Rust cannot build. packaging/package.sh builds and inspects packages without signing or publishing; repo.sh reuses it. Seven fuzz targets (one found a URL-parser panic, fixed here with a regression test), cargo-deny policy, CI for lint, tests on Linux/macOS/Windows, cross builds with qemu runs, supply chain and fuzzing. Compatibility specification and security notes. Co-Authored-By: Claude <noreply@anthropic.com>
147 lines
7.2 KiB
Markdown
147 lines
7.2 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, 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.
|