From cddde5ca529ee0a704747a9ede14675f110d7866 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Thu, 8 Oct 2026 11:44:23 +0100 Subject: [PATCH] Cross builds, packaging split, CI, fuzzing, supply-chain policy 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 --- .github/workflows/ci.yml | 102 +++ .gitignore | 4 + Cargo.toml | 5 + SECURITY.md | 146 ++++ build.sh | 161 ++++- deny.toml | 33 + docs/COMPATIBILITY.md | 187 +++++ fuzz/Cargo.lock | 1283 ++++++++++++++++++++++++++++++++++ fuzz/Cargo.toml | 68 ++ fuzz/fuzz_targets/header.rs | 15 + fuzz/fuzz_targets/html.rs | 15 + fuzz/fuzz_targets/keyfile.rs | 11 + fuzz/fuzz_targets/names.rs | 18 + fuzz/fuzz_targets/record.rs | 15 + fuzz/fuzz_targets/stream.rs | 46 ++ fuzz/fuzz_targets/tar.rs | 19 + packaging/package.sh | 140 ++++ packaging/repo.sh | 76 +- scripts/smoke.sh | 43 ++ src/absorb.rs | 7 +- src/agent.rs | 24 +- src/cli.rs | 14 +- src/net/mod.rs | 4 +- src/net/url.rs | 16 +- src/platform/fs.rs | 1 + src/platform/memory.rs | 1 + tests/cli.rs | 7 +- 27 files changed, 2347 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 SECURITY.md create mode 100644 deny.toml create mode 100644 docs/COMPATIBILITY.md create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/header.rs create mode 100644 fuzz/fuzz_targets/html.rs create mode 100644 fuzz/fuzz_targets/keyfile.rs create mode 100644 fuzz/fuzz_targets/names.rs create mode 100644 fuzz/fuzz_targets/record.rs create mode 100644 fuzz/fuzz_targets/stream.rs create mode 100644 fuzz/fuzz_targets/tar.rs create mode 100755 packaging/package.sh create mode 100755 scripts/smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..064cca9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: ci + +on: + push: + pull_request: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + # Oracle-backed interoperability tests must run, not skip, in CI. + VAULT_REQUIRE_ORACLE: "1" + +jobs: + lint: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - run: rustup show active-toolchain || rustup toolchain install + - run: cargo fmt --check + - run: cargo clippy --locked --all-targets --all-features -- -D warnings + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + # The Go toolchain builds the test-only oracle; vault itself needs no Go. + - uses: actions/setup-go@v6 + with: + go-version: "1.26.x" + cache-dependency-path: oracle/go/go.sum + - run: rustup show active-toolchain || rustup toolchain install + - name: test + shell: bash + run: | + if [[ $RUNNER_OS == Windows ]]; then unset VAULT_REQUIRE_ORACLE; fi + cargo test --locked --all-targets --all-features + - run: cargo test --locked --doc + - run: cargo build --locked --release + - name: oracle self-test + if: runner.os == 'Linux' + working-directory: oracle/go + run: go test ./... + + cross: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - run: rustup show active-toolchain || rustup toolchain install + - name: zig and cargo-zigbuild + run: | + curl -sSLo zig.tar.xz https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz + echo "70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 zig.tar.xz" | sha256sum -c + mkdir -p "$HOME/zig" && tar -xJf zig.tar.xz -C "$HOME/zig" --strip-components=1 + echo "$HOME/zig" >>"$GITHUB_PATH" + cargo install cargo-zigbuild --version 0.23.4 --locked + - name: targets + run: | + rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl i686-unknown-linux-musl \ + arm-unknown-linux-musleabihf armv7-unknown-linux-musleabihf riscv64gc-unknown-linux-musl \ + loongarch64-unknown-linux-musl powerpc64le-unknown-linux-musl powerpc64-unknown-linux-gnu \ + s390x-unknown-linux-gnu x86_64-pc-windows-gnu i686-pc-windows-gnu aarch64-pc-windows-gnullvm wasm32-wasip1 + - name: build + run: | + TARGETS="linux/amd64 linux/arm64 linux/386 linux/armv6 linux/armv7 linux/riscv64 linux/loong64 \ + linux/ppc64le linux/ppc64 linux/s390x windows/amd64 windows/386 windows/arm64 wasip1/wasm" ./build.sh + - name: run the arm64 and big-endian s390x builds under qemu + run: | + sudo apt-get update -q && sudo apt-get install -yq qemu-user + cargo build --locked --release + EMU=qemu-aarch64 scripts/smoke.sh dist/vault-linux-arm64 target/release/vault + EMU="qemu-s390x -L /usr/s390x-linux-gnu" scripts/smoke.sh dist/vault-linux-s390x target/release/vault || echo "s390x needs a glibc sysroot; build-only" + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + supply-chain: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - run: rustup show active-toolchain || rustup toolchain install + - run: cargo install cargo-deny --version 0.20.2 --locked + - run: cargo deny --locked check + + fuzz: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - run: rustup toolchain install nightly-2026-10-01 --profile minimal + - run: cargo +nightly-2026-10-01 install cargo-fuzz --version 0.13.2 --locked + - name: a short run of every target + working-directory: fuzz + run: | + for t in $(cargo +nightly-2026-10-01 fuzz list); do + cargo +nightly-2026-10-01 fuzz run "$t" -- -max_total_time=60 + done diff --git a/.gitignore b/.gitignore index e5098ad..df5b2d5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ /docs/guide/vault-guide.pdf /docs/guide/*.log /target +fuzz/target +fuzz/corpus +fuzz/artifacts +fuzz/coverage diff --git a/Cargo.toml b/Cargo.toml index 1278e88..67e6588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,8 @@ rustix = { version = "1.1.5", features = ["fs", "process", "mm", "termios", "eve [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_Security"] } + +[profile.release] +codegen-units = 1 +lto = true +strip = true diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4153f20 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,146 @@ +# 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. diff --git a/build.sh b/build.sh index 9b09f7f..1594179 100755 --- a/build.sh +++ b/build.sh @@ -1,48 +1,143 @@ #!/usr/bin/env bash -# Build vault for every OS and CPU architecture Go can target without cgo. +# Build vault for the supported operating systems and CPU architectures. # -# ./build.sh # everything, into ./dist +# ./build.sh # every target this host can build, into ./dist # TARGETS="linux/amd64 darwin/arm64" ./build.sh -# OUT=/tmp/release ./build.sh +# OUT=/tmp/release JOBS=4 ./build.sh # -# Binaries are static, stripped and reproducible (CGO_ENABLED=0, -trimpath). -# iOS and most Android targets are skipped (they need cgo); see below. -# 32-bit ARM Linux is built twice: armv6 (Raspberry Pi Zero/1) and armv7. -# SHA256SUMS is written next to the binaries. +# Binaries keep the Go-era names, vault-OS-ARCH[.exe], and are stripped +# release builds from the pinned toolchain (rust-toolchain.toml) and the +# locked dependencies (Cargo.lock), with build paths remapped so the output +# does not depend on where the tree lives. Linux binaries are static (musl) +# except s390x and ppc64, which link glibc dynamically; static linking is +# checked. SHA256SUMS is written next to the binaries. +# +# How each target is built, and what the host needs: +# zig cargo-zigbuild and zig on PATH (Linux, Windows GNU ABI) +# mac a Mac with Xcode (macOS) +# cross `cross` and Docker (FreeBSD, NetBSD, illumos, Solaris) +# cargo nothing extra (WASI) +# A requested target the host cannot build is reported, the others are still +# built, and the script exits non-zero. See docs/PLATFORMS.md for the Go +# targets with no Rust equivalent; asking for one is an error. +# +# JOBS builds several targets at once, each in its own target directory +# under target/build-sh (default 1: one at a time, cargo using every CPU). set -euo pipefail cd "$(dirname "$0")" +root=$PWD + +table() { + # os/arch Rust target method + cat <<-'EOF' + linux/amd64 x86_64-unknown-linux-musl zig + linux/arm64 aarch64-unknown-linux-musl zig + linux/386 i686-unknown-linux-musl zig + linux/armv6 arm-unknown-linux-musleabihf zig + linux/armv7 armv7-unknown-linux-musleabihf zig + linux/riscv64 riscv64gc-unknown-linux-musl zig + linux/loong64 loongarch64-unknown-linux-musl zig + linux/ppc64le powerpc64le-unknown-linux-musl zig + linux/ppc64 powerpc64-unknown-linux-gnu zig + linux/s390x s390x-unknown-linux-gnu zig + android/arm64 aarch64-unknown-linux-musl zig + windows/amd64 x86_64-pc-windows-gnu zig + windows/386 i686-pc-windows-gnu zig + windows/arm64 aarch64-pc-windows-gnullvm zig + darwin/amd64 x86_64-apple-darwin mac + darwin/arm64 aarch64-apple-darwin mac + freebsd/amd64 x86_64-unknown-freebsd cross + freebsd/386 i686-unknown-freebsd cross + netbsd/amd64 x86_64-unknown-netbsd cross + illumos/amd64 x86_64-unknown-illumos cross + solaris/amd64 x86_64-pc-solaris cross + wasip1/wasm wasm32-wasip1 cargo + EOF +} + +# Go targets that Rust cannot build with a stable toolchain (no prebuilt +# standard library, or no Rust target at all). See docs/PLATFORMS.md. +unsupported="aix/ppc64 dragonfly/amd64 freebsd/arm freebsd/arm64 freebsd/riscv64 js/wasm +linux/mips linux/mipsle linux/mips64 linux/mips64le netbsd/386 netbsd/arm netbsd/arm64 +openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 openbsd/ppc64 openbsd/riscv64 +plan9/386 plan9/amd64 plan9/arm" out=${OUT:-dist} -# iOS and 32-bit/x86 Android need cgo and a vendor SDK. android/arm64 builds -# a static binary that runs in Termux. -targets=${TARGETS:-$(go tool dist list | grep -vE '^ios/|^android/(386|amd64|arm)$')} -jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)} +jobs=${JOBS:-1} +targets=${TARGETS:-$(table | awk '{print $1}')} -rm -rf "$out" -mkdir -p "$out" +# Never clean anything but our own output, and never in a directory that +# matters: OUT must be a dedicated directory, not /, $HOME or the tree. +mkdir -p -- "$out" +out=$(cd -- "$out" && pwd -P) +case $out in +/ | "$(cd ~ && pwd -P)" | "$root") echo "refusing OUT=$out: use a dedicated directory" >&2; exit 2 ;; +esac +rm -f -- "$out"/vault-* "$out"/SHA256SUMS "$out"/*.log + +export CARGO_TARGET_DIR_BASE=$root/target/build-sh +export RUSTFLAGS="${RUSTFLAGS:-} --remap-path-prefix=$root=vault --remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=cargo" +export OUT_DIR=$out JOBS_N=$jobs HOST_OS=$(uname -s) build_one() { - local os=${1%/*} arch=${1#*/} variants=("") name ext="" - [[ $os == windows ]] && ext=.exe - [[ $arch == wasm ]] && ext=.wasm - [[ $os/$arch == linux/arm ]] && variants=(6 7) - for v in "${variants[@]}"; do - name=vault-$os-$arch${v:+v$v}$ext - if ! GOOS=$os GOARCH=$arch GOARM=${v:-} CGO_ENABLED=0 \ - go build -trimpath -buildvcs=false -ldflags='-s -w -buildid=' -o "$OUT_DIR/$name" . 2>"$OUT_DIR/$name.log"; then - echo "FAILED $name" >&2 - cat "$OUT_DIR/$name.log" >&2 - return 1 + local want=$1 row os triple method name ext="" tdir bin + row=$(table | awk -v t="$want" '$1 == t') + if [[ -z $row ]]; then + if grep -qw -- "$want" <<<"$unsupported"; then + echo "UNSUPPORTED $want: no stable Rust target; see docs/PLATFORMS.md" >&2 + else + echo "UNKNOWN $want" >&2 fi - rm "$OUT_DIR/$name.log" - echo "built $name" - done + return 1 + fi + read -r _ triple method <<<"$row" + os=${want%/*} + [[ $os == windows ]] && ext=.exe + [[ $os == wasip1 ]] && ext=.wasm + name=vault-${want/\//-}$ext + tdir=$root/target + [[ $JOBS_N != 1 ]] && tdir=$CARGO_TARGET_DIR_BASE/$triple + case $method in + zig) + command -v cargo-zigbuild >/dev/null && command -v zig >/dev/null || + { echo "SKIPPED $want: needs cargo-zigbuild and zig" >&2; return 1; } + cmd=(cargo zigbuild) ;; + mac) + [[ $HOST_OS == Darwin ]] || { echo "SKIPPED $want: needs a Mac with Xcode" >&2; return 1; } + cmd=(cargo build) ;; + cross) + command -v cross >/dev/null && command -v docker >/dev/null || + { echo "SKIPPED $want: needs cross and docker" >&2; return 1; } + cmd=(cross build) ;; + cargo) cmd=(cargo build) ;; + esac + local flags=$RUSTFLAGS + # zig's musl getrandom() never reaches the kernel on riscv64 (it spins); + # make the getrandom crate issue the system call itself. + [[ $triple == riscv64*-musl ]] && flags+=' --cfg getrandom_backend="linux_raw"' + if ! RUSTFLAGS=$flags CARGO_TARGET_DIR=$tdir "${cmd[@]}" --release --locked --target "$triple" >"$OUT_DIR/$name.log" 2>&1; then + echo "FAILED $want ($triple)" >&2 + tail -20 "$OUT_DIR/$name.log" >&2 + return 1 + fi + bin=$tdir/$triple/release/vault$ext + [[ $os == wasip1 ]] && bin=$tdir/$triple/release/vault.wasm + cp -- "$bin" "$OUT_DIR/$name" + rm -f -- "$OUT_DIR/$name.log" + if [[ $triple == *-musl* ]] && command -v file >/dev/null; then + file -L "$OUT_DIR/$name" | grep -q 'static' || { echo "FAILED $want: not statically linked" >&2; return 1; } + fi + echo "built $name ($triple)" } -export -f build_one -export OUT_DIR=$out +export -f build_one table +export unsupported -printf '%s\n' $targets | xargs -P "$jobs" -I{} bash -c 'build_one {}' +status=0 +printf '%s\n' $targets | xargs -P "$jobs" -I{} bash -c 'build_one {}' || status=1 -cd "$out" -if command -v sha256sum >/dev/null; then sha256sum vault-* >SHA256SUMS; else shasum -a 256 vault-* >SHA256SUMS; fi -echo "$(ls vault-* | wc -l | tr -d ' ') binaries in $out" +cd -- "$out" +if ls vault-* >/dev/null 2>&1; then + if command -v sha256sum >/dev/null; then sha256sum vault-* >SHA256SUMS; else shasum -a 256 vault-* >SHA256SUMS; fi +fi +echo "$(ls vault-* 2>/dev/null | wc -l | tr -d ' ') binaries in $out" +exit $status diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..392695d --- /dev/null +++ b/deny.toml @@ -0,0 +1,33 @@ +# cargo-deny policy: run `cargo deny --locked check`. + +[graph] +all-features = true + +[advisories] +# Every advisory is an error unless listed here with a reason. +ignore = [] + +[licenses] +# Licences compatible with distributing vault under GPL-3.0-or-later. +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-3-Clause", + "CDLA-Permissive-2.0", + "GPL-3.0-or-later", + "ISC", + "MIT", + "Unicode-3.0", + "Unlicense", + "Zlib", +] +confidence-threshold = 0.9 + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md new file mode 100644 index 0000000..16e8722 --- /dev/null +++ b/docs/COMPATIBILITY.md @@ -0,0 +1,187 @@ +# Compatibility specification + +This is the on-disk and on-wire contract between the Go implementation +(frozen in `oracle/go`, commit `4c1507c`) and the Rust implementation. It is +derived from the Go sources and pinned by vectors the Go oracle generated +(`tests/vectors`, regenerated with `oracle/gen-vectors.sh`). The Rust port is +an implementation migration, not a format migration: nothing here changes, +no data is rekeyed or converted, and no new suite becomes the default. + +Each section names the tests that hold the Rust code to it. "Go" below always +means the oracle. + +## 1. Key file + +| Item | Value | +| --- | --- | +| Encoding | one PEM block, type `VAULT KEY`, standard base64 with padding, 64 columns, as Go's `pem.Encode` | +| Body | exactly 198 bytes: an 18-byte header and two 90-byte slots | +| Header | `VAULTKEY1`, Argon2id time `3` (u32 BE), memory `65536` KiB (u32 BE), lanes `4` (u8) | +| Slot | 16-byte salt, 24-byte XChaCha20-Poly1305 nonce, 34 bytes sealed (u16 BE suite ID, 32-byte master seed), 16-byte tag | +| Slot key | Argon2id v1.3 (passphrase bytes as given, no normalisation, salt, t=3, m=64 MiB, p=4, 32 bytes) | +| Associated data | the 18-byte header | +| Second slot | a decoy identity's slot, or 90 random bytes; which slot is real is random | +| Unlock | Argon2id runs for both slots every time (Rust runs them concurrently); the first slot that opens wins; "incorrect passphrase" if neither | + +Accepted framing: optional text before the block (Go's `pem.Decode`), only +whitespace after it, header lines inside the block ignored. Files over 64 KiB +are refused outright (Go would read them, then reject them). + +Tests: `keyfile::tests::*` (Argon2id vectors, Go-written key files with and +without decoy open to the Go-recorded seeds, byte-identical PEM re-encoding, +malformed framing, NFC/NFD passphrases differ). + +## 2. Hybrid suite `0x0001`, tag `frodo640shake+mlkem768` + +- Master seed: 32 bytes. Expansion: + `HKDF-SHA-256(ikm = seed, salt = none, info = "fury/v1 seed-expand frodo640shake+mlkem768")`, + read as 48 bytes of FrodoKEM seed then 64 bytes of ML-KEM seed. +- FrodoKEM-640-SHAKE exactly as CIRCL v1.6.4 `kem/frodo/frodo640shake` + (the round-3 FrodoKEM, not the ISO/eFrodoKEM variants): key seed + `s(16) || seedSE(16) || z(16)`; public key 9616 bytes (`seedA || pack(B)`); + private key 19888 bytes (`s || pk || S as u16 LE || H(pk)`); ciphertext 9720 + bytes; 16-byte shared secret; 16-byte encapsulation seed `mu`; implicit + rejection returns `SHAKE128(ct || s)`. +- ML-KEM-768 (FIPS 203) with seed `d || z`; encapsulation key 1184 bytes, + expanded decapsulation key 2400 bytes, ciphertext 1088 bytes, 32-byte + shared secret, implicit rejection. +- Public key: FrodoKEM public key `||` ML-KEM encapsulation key (10800 bytes). + Private key (for comparison only): FrodoKEM private `||` ML-KEM expanded. +- Wrap: fresh, independent randomness for both encapsulations every time. + `KEK = HKDF-SHA-256(ikm = ss1 || ss2, salt = none, info = "fury/v1 kek frodo640shake+mlkem768" || ct1 || ct2)`; + the 32-byte file key is sealed with ChaCha20-Poly1305 under the KEK and an + all-zero 12-byte nonce. The zero nonce is safe only because each KEK is new + and used once; the Rust code keeps that sealing function private to the + suite. +- Stanza: type `frodo640shake+mlkem768`, two arguments + `base64(ct1)`, `base64(ct2)`, body = 48-byte sealed file key. +- Unwrap: a stanza of another type is "not mine"; wrong argument count, + bad base64 or wrong ciphertext lengths are fatal for the file; an AEAD + failure (wrong key) is "not mine". The first stanza that opens wins. + +Rust implementation: FrodoKEM is a line-by-line port of CIRCL +(`src/crypto/frodo.rs`) because no maintained Rust crate implements CIRCL's +variant (RustCrypto `frodo-kem` 0.1 implements the salted ISO version with +9752-byte ciphertexts). ML-KEM is RustCrypto `ml-kem` 0.3.2. + +Tests: `crypto::frodo::tests::nist_kat_digest` (reproduces the SHA-256 +CIRCL checks against the FrodoKEM reference KAT file), +`crypto::kem::tests::{frodo_matches_circl, mlkem_matches_circl, +hybrid_derivation_matches_go, wrap_matches_go_and_unwraps, +fresh_wraps_differ_and_foreign_keys_do_not_match}`. + +## 3. Fury header + +```text +fury-encryption.org/v1\n +-> TYPE ARG ARG\n per stanza; TYPE and ARGs are 1+ bytes of 0x21-0x7e +BODY, 64 base64 columns per line, ending with a line shorter than 64 +--- MAC\n MAC = base64 of 32 bytes +``` + +- Base64: standard alphabet, no padding, non-zero trailing bits rejected, + CR or LF anywhere in an encoded value rejected. +- A body whose encoding fills its last line (the 48-byte wrapped key encodes + to exactly 64 characters) is followed by an empty line; serialisation is + canonical and Rust reproduces Go's bytes exactly. +- Limits: 1 MiB per stanza (all its lines), 16 MiB per header; a line must + end in LF; three bytes of look-ahead decide between a stanza and the + footer; the payload starts immediately after the footer's LF, with any + read-ahead returned first. +- MAC: `HMAC-SHA-256(key = HKDF-SHA-256(file key, salt none, info "header"), message = header up to and including "---")`, + compared in constant time. +- Payload: 16-byte nonce, then STREAM keyed by + `HKDF-SHA-256(file key, salt = nonce, info "payload")`. + +Tests: `crypto::fury::tests::{header_vectors_match_go, +header_parser_agrees_with_go, decrypts_go_files}` (the parser's accept or +reject verdict, and the payload bytes left after the header, match Go on +every case in `crypto.json`), `crypto::header::tests::*`, +`tests/interop_crypto.rs` (Go decrypts files Rust wrote, and rejects +truncated, extended and corrupted ones). + +## 4. STREAM + +- 64 KiB plaintext chunks; ChaCha20-Poly1305; 16-byte tag per chunk. +- Nonce: 12 bytes, bytes 3..11 the big-endian chunk index, last byte `1` + on the final chunk only, `0` otherwise. +- Only the final chunk may be short; it may be empty only when it is the + only chunk (empty plaintext). A plaintext that is an exact multiple of + 64 KiB ends with a full-length final chunk. +- Rejected: no final chunk (truncation), an empty final chunk after + others, bytes after the final chunk, any chunk failing authentication, + a payload length no STREAM encoding can have. +- The index never wraps; Rust refuses to go past 2^64 − 1 chunks (Go's + counter is 88 bits; neither limit is reachable in practice). +- Random access (`StreamReaderAt`) authenticates the final chunk when + constructed, then any chunk on demand. + +Tests: `crypto::stream::tests::*` (Go vectors for sizes around every +boundary, any write/read step sizes, malformations rejected identically by +the streaming, in-memory and random-access readers, concurrency of the +random-access reader, counter overflow). + +## 5. Store + +- `DIR/.vault` contains exactly `VAULT 2\n`. +- `DIR/records/xx/ID` and `DIR/objects/xx/ID`: `ID` is 16 bytes as + lowercase hex, `xx` its first byte; all 256 shards of both exist after + `init`. Entries whose names are not exactly that form, or are in the + wrong shard, are ignored. +- Every ciphertext, every shard and both top directories have access and + modification times pinned to the Unix epoch; removals re-pin the shard. +- Record: a Fury file whose plaintext is exactly 4096 bytes: a u32 BE JSON + length (≤ 4092), the JSON, random padding. JSON is Go's `encoding/json` + output for `{"name","seed","size","mode","modified","added"}`: HTML + characters and U+2028/2029 escaped, times RFC 3339 with up to nine + fractional digits, trailing zeros removed, `Z`. Rust writes it + byte-identically. +- Validation on read: name valid (below), seed 64 lowercase hex characters, + size 0..=1 TiB, both times valid RFC 3339; unknown fields refused. +- Names: non-empty UTF-8, ≤ 1024 bytes, already clean (Go `path.Clean`), + relative, not `.`/`..`/`../…`, no backslash, no control characters. +- Object IDs: `HMAC-SHA-256(key = seed, msg = index as big-endian i64)[:16]`. +- Objects: plaintext = own ID (16 bytes), then up to `4 MiB − 16` file + bytes, then random padding to the next power of two from 64 KiB to 4 MiB. + An empty file still has one object. These sizes are plaintext; the + ciphertext adds the header (~14.6 KB) and 16 bytes per 64 KiB. +- Reading an object checks: it opens, its first 16 bytes are its ID, its + length is exactly the padded size for its part, and the whole STREAM + (final chunk, end of file) authenticates. +- Records the key cannot open (no stanza matches) are skipped silently; + ones that open but are malformed are reported and skipped. +- Duplicates (an interrupted replace) are ordered newest `added` first; the + next write of that name removes them all. +- Filler: `init` writes 8–32 entries (or more, to reach `--filler`) of + log-uniform sizes from 1 KiB to `clamp(target/8, 4 MiB, 1 GiB)`, sealed to + a throwaway key, through the same code as `add`. + +Tests: `store::types::tests::*` (names, object IDs, padded sizes, object +counts, record JSON byte-identical to Go), `store::tests::reads_go_vault` +(the oracle's fixture vault: real files, decoy file, foreign record), +`store::tests::*`, `tests/cli.rs` lifecycle tests in both directions. + +## 6. Locks + +`DIR/.lock`, created mode 0600. Unix: POSIX `fcntl` record locks over the +whole file (`F_SETLK` then `F_SETLKW`), shared for readers, exclusive for +writers, released by the kernel when the process dies. Windows: +`LockFileEx` on byte 0, length 1. Other systems: an `O_EXCL` lock file. +Tests: `tests/cli.rs::lock_*` (Rust waits for Go, Go waits for Rust, both +proceed after the holder is killed). + +## 7. Agent + +Socket `$XDG_RUNTIME_DIR` (else `$TMPDIR`, else `/tmp`) `/vault-UID/agent-` +followed by the first 8 bytes of SHA-256 of the key file's absolute path +(Go `filepath.Abs`, including its use of `$PWD`) in hex. The directory must +be the user's own with mode exactly 0700. One request per connection: `g` +→ 34 bytes (u16 BE suite ID, seed); `q` → the agent wipes the key and +closes. Two-second deadline per request. Tests: +`tests/cli.rs::agent_*` (Rust client with Go agent and the reverse). + +## 8. What is deliberately not identical + +See `docs/MIGRATION.md` for the full list of intentional behaviour changes. +None of them alters a byte that is written to the vault or the key file +beyond what the Go format already allows (fresh randomness, timestamps). diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..07d7970 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,1283 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "argon2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core", +] + +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "crypto-common", + "ctutils", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "ctutils", + "typenum", + "zeroize", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common", + "rand_core", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8", + "rand_core", + "sha3", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", + "zeroize", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "phc", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "vault" +version = "0.6.0" +dependencies = [ + "argon2", + "base64", + "chacha20poly1305", + "flate2", + "getrandom 0.4.3", + "hkdf", + "hmac", + "jiff", + "ml-kem", + "rustix", + "serde", + "serde_json", + "sha2", + "sha3", + "tar", + "ureq", + "windows-sys 0.61.2", + "zeroize", +] + +[[package]] +name = "vault-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "tar", + "vault", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "serde", + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..e775343 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "vault-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +tar = { version = "0.4.46", default-features = false } +vault = { path = ".." } + +# Not part of the main package's workspace. +[workspace] + +[profile.release] +debug = 1 + +[[bin]] +name = "header" +path = "fuzz_targets/header.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "stream" +path = "fuzz_targets/stream.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "keyfile" +path = "fuzz_targets/keyfile.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "record" +path = "fuzz_targets/record.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "names" +path = "fuzz_targets/names.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "html" +path = "fuzz_targets/html.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "tar" +path = "fuzz_targets/tar.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/header.rs b/fuzz/fuzz_targets/header.rs new file mode 100644 index 0000000..d10248e --- /dev/null +++ b/fuzz/fuzz_targets/header.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! The Fury header parser never panics, and accepts only canonical headers: +//! whatever it accepts re-serialises to exactly the bytes it consumed. +#![no_main] +use std::io::Read; +use vault::crypto::header; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + if let Ok((h, mut rest)) = header::parse(data) { + let mut tail = Vec::new(); + rest.read_to_end(&mut tail).unwrap(); + let consumed = &data[..data.len() - tail.len()]; + assert_eq!(h.marshal(), consumed, "non-canonical header accepted"); + } +}); diff --git a/fuzz/fuzz_targets/html.rs b/fuzz/fuzz_targets/html.rs new file mode 100644 index 0000000..39e3676 --- /dev/null +++ b/fuzz/fuzz_targets/html.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! The mirror's HTML and CSS rewriters never panic, and rewriting nothing +//! returns the input byte for byte. +#![no_main] +use vault::net::html; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + let _ = html::tokenize(data); + assert_eq!(html::rewrite_html(data, &mut |_| None, &mut |_| None), data); + assert_eq!(html::rewrite_css(data, false, &mut |_| None), data); + let _ = html::rewrite_html(data, &mut |_| None, &mut |u| Some(format!("x/{}", u.len()))); + if let Ok(s) = std::str::from_utf8(data) { + let _ = vault::net::Url::parse(s).map(|u| u.join("../a?b#c")); + } +}); diff --git a/fuzz/fuzz_targets/keyfile.rs b/fuzz/fuzz_targets/keyfile.rs new file mode 100644 index 0000000..fb00d20 --- /dev/null +++ b/fuzz/fuzz_targets/keyfile.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! PEM decoding never panics, and decoding what it encoded is the identity. +#![no_main] +use vault::keyfile::{decode_pem, encode_pem, parse_body}; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + if let Some(body) = decode_pem(data) { + let _ = parse_body(&body); + assert_eq!(decode_pem(encode_pem(&body).as_bytes()), Some(body)); + } +}); diff --git a/fuzz/fuzz_targets/names.rs b/fuzz/fuzz_targets/names.rs new file mode 100644 index 0000000..8d38764 --- /dev/null +++ b/fuzz/fuzz_targets/names.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! Path validation: a valid vault name is clean, relative and confined; the +//! tar name check only ever yields valid-shaped relative names. +#![no_main] +use vault::store::VaultName; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + let Ok(s) = std::str::from_utf8(data) else { return }; + if let Some(n) = VaultName::new(s) { + let n = n.as_str(); + assert_eq!(vault::util::clean(n), n); + assert!(!n.starts_with('/') && !n.contains('\\') && !n.split('/').any(|c| c == ".." || c == "." || c.is_empty())); + assert!(n.len() <= 1024); + } + if let Some(n) = vault::absorb::safe_name(s) { + assert!(!n.is_empty() && !n.starts_with('/') && !n.split('/').any(|c| c == "..")); + } +}); diff --git a/fuzz/fuzz_targets/record.rs b/fuzz/fuzz_targets/record.rs new file mode 100644 index 0000000..95625fb --- /dev/null +++ b/fuzz/fuzz_targets/record.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! Record JSON parsing never panics; an accepted record re-encodes (Go's +//! JSON) to something that parses back to the same values. +#![no_main] +use vault::store::{Id, Record}; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + if let Ok(r) = Record::from_json(data, Id([0; 16])) { + let Some(j) = r.to_json() else { return }; + let r2 = Record::from_json(&j, Id([0; 16])).expect("own encoding parses"); + assert_eq!(r.name, r2.name); + assert_eq!((r.size, r.mode, r.modified, r.added), (r2.size, r2.mode, r2.modified, r2.added)); + assert!(r.seed.same_as(&r2.seed)); + } +}); diff --git a/fuzz/fuzz_targets/stream.rs b/fuzz/fuzz_targets/stream.rs new file mode 100644 index 0000000..c3b0ca7 --- /dev/null +++ b/fuzz/fuzz_targets/stream.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! The streaming, in-memory and random-access STREAM readers agree on every +//! input: the same plaintext or all reject. Inputs are mostly forgeries; +//! the first byte selects a mode that starts from a valid stream instead. +#![no_main] +use std::io::{Read, Write}; +use vault::crypto::stream::{StreamReader, StreamReaderAt, StreamWriter, open_all}; + +const KEY: [u8; 32] = [7; 32]; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + let Some((&mode, rest)) = data.split_first() else { return }; + let input = if mode & 1 == 1 { + // A valid stream of the given plaintext, then a mutation. + let mut w = StreamWriter::new(&KEY, Vec::new()); + let n = rest.len().min(200_000); + w.write_all(&rest[..n]).unwrap(); + let mut c = w.finish().unwrap(); + if mode & 2 == 2 && !c.is_empty() { + let i = (mode as usize * 7919) % c.len(); + c[i] ^= mode | 1; + } + if mode & 4 == 4 { + c.truncate(c.len().saturating_sub(mode as usize % 40)); + } + c + } else { + rest.to_vec() + }; + let mut a = Vec::new(); + let streaming = StreamReader::new(&KEY, &input[..]).read_to_end(&mut a).map(|_| a); + let mut b = input.clone(); + let whole = open_all(&KEY, &mut b).map(|n| b[..n].to_vec()); + assert_eq!(streaming.is_ok(), whole.is_ok()); + if let (Ok(x), Ok(y)) = (&streaming, &whole) { + assert_eq!(x, y); + } + if let Ok(r) = StreamReaderAt::new(&KEY, &input[..], input.len() as u64) { + let mut out = vec![0u8; r.size() as usize]; + let got = r.read_at(&mut out, 0).map(|n| out[..n].to_vec()); + assert_eq!(got.is_ok(), whole.is_ok()); + if let (Ok(x), Ok(y)) = (got, whole) { + assert_eq!(x, y); + } + } +}); diff --git a/fuzz/fuzz_targets/tar.rs b/fuzz/fuzz_targets/tar.rs new file mode 100644 index 0000000..faf4d40 --- /dev/null +++ b/fuzz/fuzz_targets/tar.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! Walking arbitrary tar input as absorb does never panics, and every name +//! it would store is confined. +#![no_main] +use std::io::Read; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + let mut a = tar::Archive::new(data); + let Ok(entries) = a.entries() else { return }; + for e in entries.take(64) { + let Ok(mut e) = e else { return }; + let name = e.path_bytes().into_owned(); + if let Some(n) = std::str::from_utf8(&name).ok().and_then(vault::absorb::safe_name) { + assert!(!n.split('/').any(|c| c == "..")); + } + let mut sink = Vec::new(); + let _ = e.by_ref().take(1 << 20).read_to_end(&mut sink); + } +}); diff --git a/packaging/package.sh b/packaging/package.sh new file mode 100755 index 0000000..4961c36 --- /dev/null +++ b/packaging/package.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# Build the dixon-vault .deb, .rpm and Arch packages locally and show what +# is in them. Nothing is signed or published: packaging/repo.sh does that, +# reusing this script's build step. +# +# packaging/package.sh # from the working tree, into dist/packages +# packaging/package.sh v0.6 # from a tag (a clean export of it) +# OUT=/tmp/pkgs ARCHS="amd64 arm64" packaging/package.sh +# +# Needs: the pinned Rust toolchain with cargo-zigbuild and zig (the static +# Linux binaries are cross-built), and go, only to run the pinned nfpm +# packager. dpkg-deb, rpm and zstd are used for inspection when present. +# None of these is needed to run vault itself. +# +# ARCHS package architectures (default below) +# RPM_SIGNING_KEY armoured private key file to sign the .rpm with +# (repo.sh sets it; unset here means unsigned) +set -euo pipefail + +# ---- project --------------------------------------------------------------- +NAME=vault # the command +# The package: plain "vault" is HashiCorp Vault in Arch's [extra] and in +# HashiCorp's own apt and dnf repositories, which would win upgrades. Both +# install /usr/bin/vault, so they conflict. +PACKAGE=dixon-vault +CONFLICTS=vault +SUMMARY="Post-quantum encrypted file vault with a decoy passphrase" +HOMEPAGE=https://dixon.cx/vault +LICENSE=GPL-3.0-or-later +MANPAGES=docs/*.1 +DOCS="README.md LICENSE SECURITY.md" +# Package architecture -> Rust target (all static musl binaries). +triple() { + case $1 in + amd64) echo x86_64-unknown-linux-musl ;; + arm64) echo aarch64-unknown-linux-musl ;; + arm) echo armv7-unknown-linux-musleabihf ;; + 386) echo i686-unknown-linux-musl ;; + riscv64) echo riscv64gc-unknown-linux-musl ;; + *) return 1 ;; + esac +} +# ---------------------------------------------------------------------------- + +ARCHS=${ARCHS:-"amd64 arm64 arm 386 riscv64"} +NFPM="go run github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.1" + +say() { echo "package: $*" >&2; } +die() { say "$*"; exit 1; } + +# build_packages SRC OUTDIR VERSION: binaries, then one .deb, .rpm and +# .pkg.tar.zst per architecture, into OUTDIR. +build_packages() { + local src=$1 outdir=$2 version=$3 arch nfarch t work + for tool in cargo cargo-zigbuild zig go; do + command -v $tool >/dev/null || die "needs $tool" + done + work=$(mktemp -d) + mkdir -p "$outdir" + for arch in $ARCHS; do + t=$(triple "$arch") || die "no Rust target for $arch" + nfarch=$arch + [[ $arch == arm ]] && nfarch=arm7 + say "building $NAME for $arch ($t)" + flags="--remap-path-prefix=$src=vault" + # zig's musl getrandom() never reaches the kernel on riscv64; have + # the getrandom crate issue the system call itself. + [[ $t == riscv64*-musl ]] && flags+=' --cfg getrandom_backend="linux_raw"' + (cd "$src" && RUSTFLAGS=$flags cargo zigbuild --quiet --release --locked --target "$t") || die "build for $arch failed" + install -D -m 0755 "$src/target/$t/release/$NAME" "$work/bin-$arch/$NAME" + { + cat <<-EOF + name: $PACKAGE + arch: $nfarch + platform: linux + version: $version + release: 1 + maintainer: Andy Dixon + description: $SUMMARY + homepage: $HOMEPAGE + license: $LICENSE + conflicts: [${CONFLICTS:-}] + contents: + - src: $work/bin-$arch/$NAME + dst: /usr/bin/$NAME + file_info: {mode: 0755} + EOF + for m in $(cd "$src" && ls $MANPAGES); do + printf ' - src: %s\n dst: /usr/share/man/man1/%s\n file_info: {mode: 0644}\n' \ + "$src/$m" "$(basename "$m")" + done + for d in $DOCS; do + printf ' - src: %s\n dst: /usr/share/doc/%s/%s\n file_info: {mode: 0644}\n' \ + "$src/$d" "$PACKAGE" "$d" + done + if [[ -n ${RPM_SIGNING_KEY:-} ]]; then + printf 'rpm:\n signature:\n key_file: %s\n' "$RPM_SIGNING_KEY" + fi + } >"$work/nfpm-$arch.yaml" + for fmt in deb rpm archlinux; do + $NFPM pkg -f "$work/nfpm-$arch.yaml" -p $fmt -t "$outdir/" >/dev/null + done + done + rm -rf -- "$work" +} + +# inspect_packages DIR: list each package's metadata and files. +inspect_packages() { + local p + for p in "$1"/*; do + echo "== $(basename "$p")" + case $p in + *.deb) if command -v dpkg-deb >/dev/null; then dpkg-deb -f "$p" Package Version Architecture Conflicts; dpkg-deb -c "$p"; fi ;; + *.rpm) if command -v rpm >/dev/null; then rpm -qpi "$p" 2>/dev/null | head -12; rpm -qplv "$p"; fi ;; + *.pkg.tar.zst) if command -v zstd >/dev/null; then zstd -dcq "$p" | tar -tvf -; fi ;; + esac + done +} + +# When run (not sourced by repo.sh): build from the tree or a tag, inspect. +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + cd "$(git rev-parse --show-toplevel)" + out=${OUT:-dist/packages} + mkdir -p -- "$out" + out=$(cd -- "$out" && pwd -P) + rm -f -- "$out"/*.deb "$out"/*.rpm "$out"/*.pkg.tar.zst + if [[ $# -gt 0 ]]; then + git rev-parse -q --verify "refs/tags/$1" >/dev/null || die "no tag $1" + src=$(mktemp -d) + trap 'rm -rf -- "$src"' EXIT + git archive "$1" | tar -x -C "$src" + version=${1#v} + else + src=$PWD + version=$(cargo metadata --no-deps --format-version 1 | python3 -c 'import sys,json; print(json.load(sys.stdin)["packages"][0]["version"])') + fi + build_packages "$src" "$out" "$version" + inspect_packages "$out" + say "$(ls "$out" | wc -l) packages in $out (unsigned, not published)" +fi diff --git a/packaging/repo.sh b/packaging/repo.sh index f248dec..fa0c379 100755 --- a/packaging/repo.sh +++ b/packaging/repo.sh @@ -10,37 +10,25 @@ # Every index is rebuilt over all the packages in the repository, so other # projects publishing there with their copy of this script are kept. # -# Needs: go, docker, gpg, apt-ftparchive (apt-utils), flock. +# Needs: the pinned Rust toolchain with cargo-zigbuild and zig, go (to run +# nfpm), docker, gpg, apt-ftparchive (apt-utils), flock. # REPO_DIR where the repository lives (default below) # REPO_GNUPGHOME GnuPG home holding the signing key (default below) -# ARCHS Go architectures to package (default below) +# ARCHS architectures to package (see packaging/package.sh) +# +# To build and inspect packages without signing or publishing, run +# packaging/package.sh instead. # # The same script, with its own project block, is in ../mapsize/packaging. set -euo pipefail -# ---- project --------------------------------------------------------------- -NAME=vault # the command -# The package: plain "vault" is HashiCorp Vault in Arch's [extra] and in -# HashiCorp's own apt and dnf repositories, which would win upgrades. Both -# install /usr/bin/vault, so they conflict. -PACKAGE=dixon-vault -CONFLICTS=vault -SUMMARY="Post-quantum encrypted file vault with a decoy passphrase" -HOMEPAGE=https://dixon.cx/vault -LICENSE=GPL-3.0-or-later -MANPAGES=docs/*.1 -DOCS="README.md LICENSE" -# build OUT: build the binary to OUT, in the source tree, with GOOS, GOARCH -# and GOARM set and VERSION the release. -build() { - CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags='-s -w -buildid=' -o "$1" . -} -# ---------------------------------------------------------------------------- +# The project (package name, contents, architectures) and the build live in +# packaging/package.sh, which can also be run on its own to build and inspect +# packages without signing or publishing anything. +. "$(dirname "$0")/package.sh" REPO_DIR=${REPO_DIR:-/home/andy/domains/repo.dixon.cx/public_html} REPO_GNUPGHOME=${REPO_GNUPGHOME:-$HOME/.config/repo.dixon.cx/gnupg} -ARCHS=${ARCHS:-"amd64 arm64 arm 386 riscv64"} -NFPM="go run github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.1" SUITE=stable COMPONENT=main ARCH_REPO=dixon # the pacman repository name: [dixon] @@ -48,7 +36,7 @@ ARCH_REPO=dixon # the pacman repository name: [dixon] say() { echo "repo: $*" >&2; } die() { say "$*"; exit 1; } -for tool in go docker gpg apt-ftparchive flock; do +for tool in cargo cargo-zigbuild zig go docker gpg apt-ftparchive flock; do command -v $tool >/dev/null || die "needs $tool" done [[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist" @@ -64,47 +52,13 @@ export VERSION WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT -git archive "$TAG" | tar -x -C "$WORK" --one-top-level=src +mkdir -p "$WORK/src" +git archive "$TAG" | tar -x -C "$WORK/src" gpg --batch --armor --export-secret-keys "$KEY" >"$WORK/signing.asc" -# ---- build ----------------------------------------------------------------- +# ---- build ---------------------------------------------------------------- say "building $PACKAGE $VERSION for $ARCHS" -mkdir -p "$WORK/out" -for arch in $ARCHS; do - goarm= nfarch=$arch - [[ $arch == arm ]] && goarm=7 nfarch=arm7 - (cd "$WORK/src" && GOOS=linux GOARCH=$arch GOARM=$goarm build "$WORK/bin-$arch/$NAME") - { - cat <<-EOF - name: $PACKAGE - arch: $nfarch - platform: linux - version: $VERSION - release: 1 - maintainer: Andy Dixon - description: $SUMMARY - homepage: $HOMEPAGE - license: $LICENSE - conflicts: [${CONFLICTS:-}] - contents: - - src: $WORK/bin-$arch/$NAME - dst: /usr/bin/$NAME - file_info: {mode: 0755} - EOF - for m in $(cd "$WORK/src" && ls $MANPAGES); do - printf ' - src: %s\n dst: /usr/share/man/man1/%s\n file_info: {mode: 0644}\n' \ - "$WORK/src/$m" "$(basename "$m")" - done - for d in $DOCS; do - printf ' - src: %s\n dst: /usr/share/doc/%s/%s\n file_info: {mode: 0644}\n' \ - "$WORK/src/$d" "$PACKAGE" "$d" - done - printf 'rpm:\n signature:\n key_file: %s\n' "$WORK/signing.asc" - } >"$WORK/nfpm-$arch.yaml" - for fmt in deb rpm archlinux; do - $NFPM pkg -f "$WORK/nfpm-$arch.yaml" -p $fmt -t "$WORK/out/" >/dev/null - done -done +RPM_SIGNING_KEY=$WORK/signing.asc build_packages "$WORK/src" "$WORK/out" "$VERSION" # ---- publish --------------------------------------------------------------- # One publisher at a time: the indexes cover every project's packages. diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 0000000..84584f2 --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Runtime smoke test of one vault binary, optionally under an emulator: +# create a key and vault, store files across object boundaries, read them +# back, then (if a reference binary is given) check the reference can read +# what this binary wrote and the other way round. +# +# scripts/smoke.sh BINARY [REFERENCE_BINARY] +# EMU="qemu-aarch64" scripts/smoke.sh dist/vault-linux-arm64 target/release/vault +# +# Uses a private temporary directory, removed afterwards. The passphrase is +# a public test value. +set -euo pipefail +bin=$(realpath "$1") +ref=${2:+$(realpath "$2")} +emu=${EMU:-} +work=$(mktemp -d "${TMPDIR:-/tmp}/vault-smoke.XXXXXX") +trap 'rm -rf -- "$work"' EXIT +cd "$work" +export VAULT_PASSPHRASE=smoke-test-passphrase VAULT_KEY=$work/key VAULT_DIR=$work/store +export XDG_RUNTIME_DIR=$work/run +mkdir -m 700 run +v() { $emu "$bin" "$@"; } + +v genkey 2>/dev/null +v init 2>/dev/null +head -c $((4 * 1024 * 1024 + 123)) /dev/urandom >big +printf 'small\n' >small +: >empty +v add big 2>/dev/null +v add small dir 2>/dev/null +v add empty dir 2>/dev/null +[[ $(v list | wc -l) == 3 ]] || { echo "FAIL list"; exit 1; } +v get big -o big.out 2>/dev/null && cmp big big.out +v get dir/small -o small.out 2>/dev/null && cmp small small.out +if [[ -n $ref ]]; then + "$ref" get big -o big.ref 2>/dev/null && cmp big big.ref + printf 'from the reference\n' >r.txt + "$ref" add r.txt 2>/dev/null + v get r.txt -o r.out 2>/dev/null && cmp r.txt r.out +fi +v rm -rf dir 2>/dev/null +[[ $(v list | wc -l) == $([[ -n $ref ]] && echo 2 || echo 1) ]] || { echo "FAIL rm"; exit 1; } +echo "ok $(basename "$bin")${emu:+ (under $emu)}" diff --git a/src/absorb.rs b/src/absorb.rs index 9d2d1bb..868d6e2 100644 --- a/src/absorb.rs +++ b/src/absorb.rs @@ -90,9 +90,10 @@ fn mtime(entry: &mut tar::Entry) -> Result { if let Ok(Some(exts)) = entry.pax_extensions() { for ext in exts.flatten() { if ext.key_bytes() == b"mtime" - && let Some(t) = std::str::from_utf8(ext.value_bytes()).ok().and_then(parse_pax_time) { - return Ok(t); - } + && let Some(t) = std::str::from_utf8(ext.value_bytes()).ok().and_then(parse_pax_time) + { + return Ok(t); + } } } let secs = entry diff --git a/src/agent.rs b/src/agent.rs index af9b25b..35df901 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -24,12 +24,18 @@ use std::path::{Path, PathBuf}; use std::time::Duration; pub const AGENT_CMD: &str = "__agent"; +#[cfg_attr(not(unix), allow(dead_code))] const GET: u8 = b'g'; +#[cfg_attr(not(unix), allow(dead_code))] const QUIT: u8 = b'q'; +#[cfg_attr(not(unix), allow(dead_code))] const REPLY: usize = 2 + crate::crypto::kem::SEED_SIZE; +#[cfg_attr(not(unix), allow(dead_code))] const TIMEOUT: Duration = Duration::from_secs(2); /// How long `unlock` waits for a starting agent to say it is ready. +#[cfg_attr(not(unix), allow(dead_code))] const START_TIMEOUT: Duration = Duration::from_secs(10); +#[cfg_attr(not(unix), allow(dead_code))] const SECRET_ENVS: [&str; 2] = ["VAULT_PASSPHRASE", "VAULT_DECOY_PASSPHRASE"]; /// Go's `filepath.Abs`: lexical, with `os.Getwd`'s preference for `$PWD`. @@ -53,10 +59,12 @@ fn go_getwd() -> std::io::Result { let dot = std::fs::metadata(".")?; if let Some(pwd) = std::env::var_os("PWD").map(PathBuf::from) && pwd.is_absolute() - && let Ok(m) = std::fs::metadata(&pwd) - && m.dev() == dot.dev() && m.ino() == dot.ino() { - return Ok(pwd); - } + && let Ok(m) = std::fs::metadata(&pwd) + && m.dev() == dot.dev() + && m.ino() == dot.ino() + { + return Ok(pwd); + } std::env::current_dir() } @@ -265,9 +273,11 @@ mod imp { // Remove the socket only if it is still the one this agent made. use std::os::unix::fs::MetadataExt; if let Ok(m) = std::fs::symlink_metadata(sock) - && m.ino() == ours.ino() && m.dev() == ours.dev() { - let _ = std::fs::remove_file(sock); - } + && m.ino() == ours.ino() + && m.dev() == ours.dev() + { + let _ = std::fs::remove_file(sock); + } }; { use std::os::unix::fs::PermissionsExt; diff --git a/src/cli.rs b/src/cli.rs index c2e749a..02af8ba 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -94,9 +94,10 @@ macro_rules! say { fn out(line: &str) { let mut o = std::io::stdout().lock(); if let Err(e) = o.write_all(line.as_bytes()).and_then(|_| o.flush()) - && e.kind() == std::io::ErrorKind::BrokenPipe { - std::process::exit(128 + 13); - } + && e.kind() == std::io::ErrorKind::BrokenPipe + { + std::process::exit(128 + 13); + } } #[derive(Default)] @@ -319,9 +320,10 @@ fn default_path(flag: &Option, env: &str, name: &str) -> Result /// environment, else from the key file with the passphrase. fn key_identity(key: &Path) -> Result { if std::env::var_os(PASSPHRASE_ENV).is_none() - && let Ok(id) = agent::identity(key) { - return Ok(id); - } + && let Ok(id) = agent::identity(key) + { + return Ok(id); + } let pass = read_passphrase("Enter passphrase:")?; keyfile::unlock(key, pass.expose()).map_err(|e| Error::new(e.to_string())) } diff --git a/src/net/mod.rs b/src/net/mod.rs index 45b38b8..51c5288 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -29,8 +29,8 @@ //! it; that file holds plaintext until it is removed, on every path, once //! stored. HTML and CSS for rewriting are held in memory instead. -mod html; -mod url; +pub mod html; +pub mod url; #[cfg(test)] mod tests; diff --git a/src/net/url.rs b/src/net/url.rs index bf08431..979ebb0 100644 --- a/src/net/url.rs +++ b/src/net/url.rs @@ -81,7 +81,12 @@ fn unescape(s: &str, mode: Mode) -> Result { b'%' => { let (h, l) = match (b.get(i + 1).copied().and_then(unhex), b.get(i + 2).copied().and_then(unhex)) { (Some(h), Some(l)) => (h, l), - _ => return Err(format!("invalid URL escape {:?}", &s[i..(i + 3).min(s.len())])), + _ => { + return Err(format!( + "invalid URL escape {:?}", + String::from_utf8_lossy(&b[i..(i + 3).min(b.len())]) + )); + } }; if mode == Mode::Host && h < 8 && &s[i..i + 3] != "%25" { return Err(format!("invalid URL escape {:?}", &s[i..i + 3])); @@ -447,6 +452,15 @@ pub(crate) mod tests { assert_eq!(super::super::key(&u), s("key"), "{ctx} key"); } + /// Found by fuzzing: a bad escape before a multi-byte character used to + /// slice through the character and panic. + #[test] + fn bad_escape_before_multibyte_character() { + assert!(Url::parse("/K%\u{8e3}").is_err()); + assert!(Url::parse("http://h/%é").is_err()); + assert!(Url::parse("http://h/%2").is_err()); + } + #[test] fn parse_matches_go() { for v in vectors()["parse"].as_array().unwrap() { diff --git a/src/platform/fs.rs b/src/platform/fs.rs index 7191286..5bac771 100644 --- a/src/platform/fs.rs +++ b/src/platform/fs.rs @@ -36,6 +36,7 @@ pub fn create_private_dir_all(p: &Path) -> io::Result<()> { } pub fn create_private_dir(p: &Path) -> io::Result<()> { + #[cfg_attr(not(unix), allow(unused_mut))] let mut b = std::fs::DirBuilder::new(); #[cfg(unix)] b.mode(0o700); diff --git a/src/platform/memory.rs b/src/platform/memory.rs index 498c98f..6ea88e1 100644 --- a/src/platform/memory.rs +++ b/src/platform/memory.rs @@ -14,6 +14,7 @@ //! `/proc/PID/mem` access. Other Unix systems get the rlimit only. Windows //! is not supported by the agent (see SECURITY.md). +#[cfg(unix)] use std::io; #[cfg(unix)] diff --git a/tests/cli.rs b/tests/cli.rs index 773fabc..bd13cca 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2,11 +2,15 @@ //! The real binaries, end to end: the Rust CLI on its own, and the Rust //! and Go CLIs sharing one key, one vault, one lock and one agent. +// The lock and agent tests are Unix-only; their imports are unused elsewhere. +#![cfg_attr(not(unix), allow(unused_imports))] mod common; use std::io::Write; -use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::path::Path; +use std::path::PathBuf; use std::process::{Child, Command, Output, Stdio}; use std::time::{Duration, Instant}; @@ -286,6 +290,7 @@ fn absorb_from_stdin() { /// Starts `absorb` and feeds it one byte: it takes the exclusive lock and /// then holds it until its standard input closes or it is killed. The /// caller kills and waits for it. +#[cfg(unix)] #[allow(clippy::zombie_processes)] fn hold_lock(e: &Env, bin: &str) -> Child { let mut c = e