Files
vault/README.md
T
Andy DixonandClaude 53968f8822 Docs for the Rust version; benchmarks; build.sh fixes
README, CLAUDE.md, man pages, the user guide and its screenshot script
now describe the Rust build and the intentional behaviour changes, which
docs/MIGRATION.md lists. Go and Rust micro-benchmarks for the same
operations, and scripts/bench.sh for end-to-end comparison. build.sh
exports its tree root to the per-target jobs and keeps logs out of the
output directory; riscv64 musl builds use getrandom's raw syscall
backend, since zig's musl getrandom never reaches the kernel there.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-08 11:50:55 +01:00

179 lines
8.7 KiB
Markdown

# vault
A post-quantum encrypted file vault in a single binary.
Files are encrypted with a hybrid FrodoKEM-640 + ML-KEM-768 key encapsulation
and ChaCha20-Poly1305. The vault directory holds only randomly named
ciphertexts. Names, sizes, modes and timestamps live inside encrypted
catalogue records of one fixed size. File bodies are cut into 4 MiB objects,
the last padded to a power-of-two bucket, so every object is 64 KiB to 4 MiB
whatever the file's size. Object names derive from a secret seed in the
record, so nothing on disk links a file's pieces. Records and objects are
spread over 256 subfolders (`records/3f/3fa9…`) to keep directories small.
## Install
On Debian, Ubuntu, Fedora, RHEL, openSUSE and Arch, install the
`dixon-vault` package from the signed repository at repo.dixon.cx; see
https://dixon.cx/repo for the two or three commands. (The package isn't
called `vault` because that name is HashiCorp Vault's.) Every platform also
has a single binary at https://download.dixon.cx/vault/.
Releases are packaged and published with `packaging/repo.sh [TAG]`.
## Build
vault is written in Rust (the toolchain is pinned in `rust-toolchain.toml`;
`rustup` installs it on first use). Releases up to 0.5 were Go; the Go code
lives on, frozen, in `oracle/go` as the test reference. Nothing at run time
needs Go.
```sh
cargo build --release --locked # produces target/release/vault
cargo test --locked # unit, CLI and Go-interoperability tests
./build.sh # every target this host can build, into ./dist
TARGETS="linux/amd64 windows/amd64" ./build.sh
```
`build.sh` cross-compiles with `cargo-zigbuild` and `zig` (Linux, statically
linked with musl, and Windows), on a Mac for macOS, and with `cross` and
Docker for the BSDs, illumos and Solaris. It names binaries as before
(`vault-linux-arm64`, `vault-windows-amd64.exe`, …), builds 32-bit ARM Linux
for both armv6 and armv7, and writes `dist/SHA256SUMS`. `docs/PLATFORMS.md`
lists every target, how it is built and tested, and the Go targets Rust
cannot build.
The interoperability tests build the Go oracle when `go` is installed and
are skipped (and say so) otherwise; set `VAULT_REQUIRE_ORACLE=1` to make a
missing oracle an error, as CI does. `oracle/gen-vectors.sh` regenerates the
Go test vectors. Packages are built and inspected locally, unsigned, with
`packaging/package.sh`, and published with `packaging/repo.sh [TAG]`.
Further reading: `SECURITY.md` (what is and is not protected, memory and
filesystem guarantees), `docs/COMPATIBILITY.md` (the formats),
`docs/MIGRATION.md` (what changed from the Go version) and
`docs/BENCHMARKS.md`.
## User guide
A full illustrated guide lives in `docs/guide`. Build the PDF with
`docs/guide/build-pdf.sh`, which fetches Tectonic if it isn't installed.
`docs/guide/shots.py` regenerates its screenshots from the real binary.
## Man pages
`docs/*.1` holds `vault(1)` and one page per command (`vault-add(1)`,
`vault-mirror(1)` and so on). Install them with
`cp docs/*.1 ~/.local/share/man/man1/`.
## Use
```sh
vault genkey # create ~/.vault/key, protected by a passphrase
vault init # create ~/.vault/store
vault init --filler 2G # the same, with at least 2 GiB of filler
vault add report.pdf # stored as report.pdf
vault add report.pdf work/2026 # stored as work/2026/report.pdf
vault add ./photos # every regular file under photos/
vault add https://example.com/a.pdf # download and store a URL
vault migrate ./photos # like add, then delete the originals
vault mirror https://example.com/ # crawl a site into example.com/, links rewritten
tar -cz photos | vault absorb backup # every file in a tar (or .tar.gz) under backup/
vault absorb backup < photos.tar.gz # same, from an existing archive
vault ls # the top level: folders (ending in /) and files
vault ls work # one level down, inside work/
vault list # every file, all the way down, full names
vault list work # everything under work/
vault get work/2026/report.pdf # writes ./report.pdf
vault get report.pdf -o copy.pdf
vault get -r work # a whole folder, into ./work
vault mv report.pdf old/report.pdf
vault mv report.pdf old/ # the same: into a folder, like mv(1)
vault mv work archive/work # folders move too
vault cp -r work work-backup # instant; copies share their encrypted data
vault rm old/report.pdf
vault rm -r old # a whole folder; lists it and asks first
vault rm -rf old # the same, without asking
vault unlock # remember the key for 5 minutes (--for 30m)
vault unlock --idle # ... or until 5 minutes go by unused
vault lock # forget it now
```
Commands that change the vault take an exclusive lock on `DIR/.lock`, and
commands that read it take a shared one. A second process waits, and says
so, until the first finishes. The operating system releases the lock if a
process dies; the locks are the same ones the Go version used, so old and
new versions exclude each other. WebAssembly (WASI) has no file locks, so
there the lock file itself is the lock. After a crash there you remove it
by hand; the error message names it.
Adding a name that already exists replaces it. `get` never replaces an
existing file (or symlink) and writes nothing until the whole file has
authenticated. `add URL` and `mirror` have size and time limits; see
`vault(1)`.
`--key PATH` and `--vault DIR` work with every command. They default to
`$VAULT_KEY` and `$VAULT_DIR`, then to `~/.vault/key` and `~/.vault/store`.
The passphrase is read from the terminal, or from `$VAULT_PASSPHRASE` for
scripts.
`vault unlock` asks once, then starts a helper process, like `ssh-agent`,
that holds the unlocked key in memory until a hard deadline (5 minutes by
default; with `--idle`, 5 minutes since a command last used it) or
`vault lock`. Until then commands skip the prompt, unless
`$VAULT_PASSPHRASE` is set, which always wins. The key reaches the helper
over a pipe and never touches the disk; the helper's memory is locked
against swapping, it cannot be core-dumped (or, on Linux, debugged), and it
answers only on a socket in a directory private to your user. While it is
unlocked, anything running as you can use the vault, decoy or not, so lock
before handing the machine over. If the helper cannot protect its memory it
refuses the key. `unlock` is not available on Windows.
## Decoy passphrase
`vault genkey` offers an optional second passphrase. In scripts, set
`$VAULT_DECOY_PASSPHRASE`. Each passphrase unlocks its own key, so a command
run with the decoy passphrase lists, adds and retrieves only decoy files. The
real files never appear.
```sh
vault genkey # set a passphrase, then a decoy passphrase
vault add plans.pdf # with the real passphrase
vault add shopping.txt # with the decoy passphrase
vault list # shows only what that passphrase stored
```
Two measures keep the real files deniable:
- The key file always holds two slots of identical size in random order. An
unused slot is random bytes, so the file does not show whether a decoy
exists or which slot is real.
- `init` fills every vault with 8 to 32 random entries that nobody can
decrypt. Anyone holding only the decoy passphrase sees unreadable entries in
every vault, so hidden real files look the same as filler.
A decoy can only be set when the key is created. Someone who watches the
vault over time can still see it grow when real files are added.
Default filler is only a few tens of megabytes, so someone holding the decoy
passphrase can see when far more data than that is unopenable. To hide more,
create the vault with `vault init --filler SIZE` (`500M`, `2G`, `1T`), at
least as large as what you will hide. Filler entries are written by the
same code as real files, with a spread of small and multi-chunk sizes, so
they look like a real collection. Filler can only be added at `init`;
added later it would stand out as a burst of new entries.
## Limits
- The storage host can see how many records and objects exist, so roughly
how much is stored, and when the vault changes. Chunking hides a file's
size in any single snapshot, but objects written together can be grouped
by anyone watching the folder or a sync service's upload log.
- Padding still costs space: small files round up to a 64 KiB-or-larger
bucket and a big file's last piece to its bucket, at most 4 MiB per file.
- The key file is the only way in. Losing it or its passphrase loses the data.
## Licence
GPL-3.0-or-later. The cryptographic core is derived from Fury.