v4.1.0: source-only build, multithreaded-encryption fix, security hardening

Build from source with no vendored binaries:
- Remove prebuilt libzuptsdk.so / libpqvaptvupt.so (and a stray .pyc). The
  default build needs only a C compiler + make; it links no external library
  and installs no .so. The libzuptsdk-backed modes (Argon2id KDF, --pq-sdk,
  --pq-box) are gated behind an opt-in `make WITH_SDK=1`. The default password
  KDF is PBKDF2-SHA256 and --pq (native ML-KEM-768 + X25519) is the built-in PQ
  mode. openSUSE/RPM/deb/AUR/Homebrew/Nix recipes bumped to 4.1.0; the openSUSE
  spec now builds source-only (%files ships no .so, %build/%install WITH_SDK=0).

Fix: multithreaded encrypted archives were unextractable on the native AEAD
path. The parallel compress/decompress workers skipped the F-09 frame-preface
AAD that the serial path and the archive's AAD_PREFACE flag bind into every
block MAC, so each multithreaded block failed authentication. The workers now
bind the preface via a shared serializer; output is byte-identical across
thread counts and interoperates with single-threaded archives (also fixes
`--kdf pbkdf2 -t N` in any build).

Security hardening (crafted-archive memory safety + crypto):
- LZH raw code-length stack overflow + huff_lut OOB write
- overflow-safe bounds in parse_index and solid-mode extract (heap OOB read)
- SEQ decoder safe-zone heap overflow (litlen+matchlen reserve)
- require the per-block ENCRYPTED flag on encrypted archives (plaintext forgery)
- cap archive-supplied PBKDF2 iteration count (KDF-amplification DoS)
- non-elidable secret wipe in the SDK path; restored disk images created 0600

Docs: remove AUDIT.md / BENCHMARKS.md / ROADMAP.md; trim marketing/AI-styled
text and correct KDF/PQ facts across README, SECURITY, INSTALL, DISTRIBUTION,
THREAT_MODEL, THIRD-PARTY-NOTICES, the man page, and packaging READMEs. Wire
format v1.6 unchanged.
This commit is contained in:
Cristian Cezar Moisés 2026-07-07 19:45:37 -03:00
commit 4874010d0e
39 changed files with 1097 additions and 2770 deletions

7
.gitignore vendored
View file

@ -6,13 +6,14 @@
*.so *.so
*.so.* *.so.*
*.dylib *.dylib
# ...but keep vendored prebuilt libraries (shipped, no in-tree source)
!vendor/**/*.so
!vendor/**/*.so.*
*.exe *.exe
*.obj *.obj
*.lib *.lib
# Python bytecode caches
__pycache__/
*.pyc
# Coverage / profiling # Coverage / profiling
*.gcda *.gcda
*.gcno *.gcno

1630
AUDIT.md

File diff suppressed because it is too large Load diff

View file

@ -1,113 +0,0 @@
# VaptVupt Benchmarks
All numbers here are **measured**, not aspirational. Each table states the
machine, the build, and the method. Where VaptVupt loses to a competitor,
the table shows it.
> **Superseded data note.** The v3.8.0 edition of this file was measured
> on a different machine (Xeon 2.80 GHz, no SHA-NI) against a different
> generation of the synthetic fixtures. Absolute numbers below are not
> comparable to that edition; the codec-stability question is settled by
> the same-input gate table (§1), not by cross-edition comparison.
## Test environment
| Property | Value |
|----------|-------|
| CPU | Intel Xeon @ 2.10 GHz |
| Cores used | 1 (single-threaded measurement) |
| Hardware accel present | AES-NI, **SHA-NI**, AVX2 (codec) |
| Build | VaptVupt 4.0.0, `make` defaults, `-O2`, Jasmin AES-NI path active |
| Codec | VaptVupt **2.60.4** (LZ + ANS, canonical BCJ) |
| Method | best of 3 runs, wall clock |
## 1. Codec ratio gate: 2.53.3-era vs 2.60.4, identical inputs
Upstream 2.60.4 claims compressed output byte-identical to prior
releases. Verified here by compressing the **same fixture bytes** with
the shipped 3.8.0 binary and the 4.0.0 binary (L9, plain):
| Fixture | 3.8.0 archive | 4.0.0 archive | Δ |
|---------|--------------:|--------------:|---|
| text | 1 990 322 B | 1 990 322 B | **0.00 %** |
| source | 1 698 907 B | 1 698 907 B | **0.00 %** |
| redundant | 3 344 B | 3 344 B | **0.00 %** |
| binary | 3 119 605 B | 3 356 213 B | +7.58 % — **not comparable**: the 3.8.0 stream is the F-16 *corrupt* output (undecodable by any version); 4.0.0 emits the canonical BCJ stream that actually decodes |
Gate **holds** everywhere a valid stream exists on both sides.
## 2. Compression ratio + throughput (plain, level 9, this box's fixtures)
| Fixture | In (MB) | Ratio | Encode (MB/s) | Decode (MB/s) |
|---------|--------:|------:|--------------:|--------------:|
| text | 10.0 | 5.27 | 2 | 314 |
| binary | 7.5 | 2.34 | 2 | 209 |
| source | 10.0 | 6.17 | 1 | 304 |
| redundant | 10.0 | 3135.69 | 299 | 692 |
| random | 5.0 | 1.00 | 24 | 570 |
Decode 209692 MB/s; L9 encode remains 12 MB/s on compressible data
(optimal parser) — use lower levels when encode speed matters.
## 3. SHA-256: scalar vs SHA-NI (measured, same box)
The v3.2.0 SHA-NI path could only be **estimated** (38×) because the
old measurement box lacked the instruction set. Measured now, 256 MiB
single buffer, runtime dispatch vs forced scalar:
| Path | Throughput |
|------|-----------:|
| scalar C | 204 MB/s |
| SHA-NI | **1184 MB/s** |
| **speedup** | **5.8×** |
(Independently consistent with libpqvaptvupt 0.6.0's own measurement of
5.9× on its SHA-256.) The estimate label is hereby retired.
## 4. Encryption overhead (store mode isolates crypto from the codec)
Per-MB ≈ (t₄₀MB t₁MB) ÷ 39; KDF ≈ t₁MB per-MB.
| Mode | Per-MB crypto | One-time KDF |
|------|--------------:|-------------:|
| plain (no encryption) | 1.94 ms (515 MB/s) | ≈3 ms |
| password — Argon2id (default) | 3.42 ms (**293 MB/s**) | ≈839 ms |
| password — PBKDF2 | 3.58 ms (280 MB/s) | ≈550 ms |
| **pq-box** (`--pq-box`, v4.0.0) | same as plain + MAC path | seal ≈3 ms / open ≈3 ms |
Readings:
- Encrypted per-block throughput is **~2× the 3.8.0-era figure on a
slower clock** (293 MB/s at 2.10 GHz vs 146 MB/s at 2.80 GHz) — the
HMAC-SHA256 Encrypt-then-MAC second pass now runs on SHA-NI.
- The Argon2id one-time cost (~0.8 s) is memory-hardness working as
designed, not a target for optimization.
- PBKDF2's KDF also benefits from SHA-NI (~550 ms here vs ~1.56 s on the
old non-SHA-NI box) — but Argon2id remains the default for its
memory-hardness, not its speed.
- `--pq-box` adds ~3 ms one-time seal/open for the 32-byte session key
(ML-KEM-768 + X25519 + HKDF); per-block cost is the standard AES-NI +
SHA-NI path.
## 5. What the product is
The codec is competitive on decode, not the reason to use VaptVupt
(zstd-19 wins pure ratio). The reason is the combination: post-quantum
hybrid recipient encryption (three modes, newest = HKDF-domain-separated
sealed box), Argon2id by default with a self-describing KDF header,
per-block Encrypt-then-MAC with every security-critical comparison
routed through one audited measured-constant-time primitive, canonical
CBMC-verified BCJ filters, and 16 NIST/RFC known-answer vectors in CI.
## Reproducing
```sh
make
./vaptvupt c -l 9 /tmp/a.zupt fixtures/text.dat # ratio/speed
./vaptvupt c -s -p PW /tmp/p.zupt big.dat # crypto overhead
./vaptvupt keygen --box -o k && \
./vaptvupt c -s --pq-box k.pub /tmp/b.zupt big.dat # pq-box
make test-vectors && ./test_vectors # NIST/RFC vectors
```
Absolute numbers vary by machine; the shape (KDF-dominated password
cost, SHA-NI ≈6× on SHA-256, ~3 ms pq-box envelope) is stable.

View file

@ -1,6 +1,51 @@
# VaptVupt Changelog # VaptVupt Changelog
## [4.1.0] — 2026-07-07 — Source-only build; multithreaded-encryption fix; hardening
### Source-only build (no vendored binaries)
- The prebuilt vendored libraries `libzuptsdk.so` and `libpqvaptvupt.so` are
removed from the repository. The default build (`make`) now compiles entirely
from the in-tree C sources with no external library dependency and installs no
shared object.
- The libzuptsdk-backed modes — the Argon2id password KDF, `--pq-sdk`, and
`--pq-box` — are gated behind an opt-in `make WITH_SDK=1` build against the
separately distributed libraries. In the default build they report as
unsupported. The default password KDF is **PBKDF2-SHA256** (600k iterations)
and `--pq` (native **ML-KEM-768 + X25519**) is the built-in PQ mode.
- openSUSE packaging builds source-only: `%build`/`%install` pass `WITH_SDK=0`
and `%files` no longer ships the `.so`.
### Fixed
- **Multithreaded encrypted archives were unextractable** on the native AEAD
path. The parallel compress/decompress workers used the non-preface
encrypt/decrypt calls, but the archive's `ZUPT_FLAG_AAD_PREFACE` (F-09) and the
serial path bind the 29-byte frame preface into every block MAC — so each
multithreaded block failed authentication. The workers now bind the preface via
the shared `zupt_serialize_preface_aad_scalars`. Output is byte-identical across
thread counts, and archives interoperate between single- and multithreaded
compress/extract. Also fixes `--kdf pbkdf2 -t N`.
### Security
- LZH decoder: bound the raw code-length header against the destination stack
buffers and reject code lengths > 15 (stack overflow + `huff_lut` OOB write on
crafted archives).
- Format parser: overflow-safe bounds in `parse_index` and solid-mode extract
(heap OOB reads via wrapped 64-bit length/offset fields).
- ANS SEQ decoder: reserve a full worst-case sequence (litlen + matchlen) in the
fast path (heap OOB write past the output buffer).
- Require the per-block ENCRYPTED flag on every block of an encrypted archive
(plaintext-injection / authentication bypass).
- Cap the archive-supplied PBKDF2 iteration count (KDF-amplification DoS).
- Non-elidable secret wipe on the SDK crypto path; restored disk images are
created with mode 0600.
Wire format **v1.6** unchanged; pre-4.1 archives remain readable.
## [4.0.0] — 2026-06-10 — Codec 2.60.4 (security), pq-box mode, F-16 disclosure ## [4.0.0] — 2026-06-10 — Codec 2.60.4 (security), pq-box mode, F-16 disclosure
Major release: the vendored codec moves to the canonical **VaptVupt Major release: the vendored codec moves to the canonical **VaptVupt

View file

@ -1,40 +1,29 @@
# Distributing VaptVupt # Distributing VaptVupt
This document describes the upstream packaging recipes shipped under This document describes the upstream packaging recipes shipped under `packaging/` and the path from a local source tree to an installable package.
`packaging/` and the path from "local source tree" to "package
installable on every major Linux distribution and macOS."
Recipes are upstream-maintained but distro-submission-ready. Real Real submission to AUR / Debian / Fedora / Homebrew / NixOS / openSUSE is operational work outside this repository.
submission to AUR / Debian / Fedora / Homebrew / NixOS is operational
work outside this repository.
## Producing a reproducible source tarball ## Producing a reproducible source tarball
Every packaging recipe expects an upstream tarball `vaptvupt-VERSION.tar.gz` Every packaging recipe expects an upstream tarball `vaptvupt-VERSION.tar.gz` produced by the project's `make dist` target. The tarball is byte-reproducible:
produced by the project's `make dist` target. The tarball is
**byte-reproducible**:
```sh ```sh
make dist make dist
# → /tmp/vaptvupt-2.4.4.tar.gz # → /tmp/vaptvupt-4.1.0.tar.gz
# → sha256: 407d20ef03e5bf857195b99e04843ef3b07357416a4115add1e8aaa2007a769f
# → bytes: 813113
``` ```
Re-running `make dist` on the same source tree produces an identical Re-running `make dist` on the same source tree produces an identical sha256 (verified by `tests/test_dist_reproducible.sh`, wired into `make test`). This lets distros pin a stable hash in their recipes.
sha256 (verified by `tests/test_dist_reproducible.sh`, wired into
`make test`). This lets distros pin a stable hash in their recipes.
The reproducibility properties: The reproducibility properties:
- Files sorted by name (deterministic order across filesystems) - Files sorted by name (deterministic order across filesystems)
- mtime fixed to `SOURCE_DATE_EPOCH` (default `1747699200`; override - mtime fixed to `SOURCE_DATE_EPOCH` (default `1747699200`; override via env)
via env)
- uid/gid pinned to root (0/0) via `--owner=0 --group=0 --numeric-owner` - uid/gid pinned to root (0/0) via `--owner=0 --group=0 --numeric-owner`
- gzip wrapped with `-9n` (no embedded timestamp or filename) - gzip wrapped with `-9n` (no embedded timestamp or filename)
- Source-only — no `.o`, no built binaries, no `.git/` tree - Source-only — no `.o`, no built binaries, no `.git/` tree
- Includes the vendored `libzuptsdk.so.2.0.0` real file plus its two
symlinks (`libzuptsdk.so`, `libzuptsdk.so.2`) The tree is source-only. The default `make` build needs only a C compiler, make, libm, and pthread — no external crypto library, and it installs no `.so`. The optional SDK-backed modes (`--pq-sdk`, `--pq-box`) and the Argon2id KDF are built only with `make WITH_SDK=1` against the separately distributed `libzuptsdk` / `libpqvaptvupt` libraries.
To force a specific epoch (for distro release-day pinning): To force a specific epoch (for distro release-day pinning):
@ -49,19 +38,16 @@ SOURCE_DATE_EPOCH=1727740800 make dist # 2024-10-01 UTC
| Arch Linux | `packaging/aur/PKGBUILD` | AUR PKGBUILD | | Arch Linux | `packaging/aur/PKGBUILD` | AUR PKGBUILD |
| Debian / Ubuntu | `packaging/debian/` | Source package (`3.0 (quilt)`) | | Debian / Ubuntu | `packaging/debian/` | Source package (`3.0 (quilt)`) |
| Fedora / RHEL | `packaging/rpm/vaptvupt.spec` | RPM .spec | | Fedora / RHEL | `packaging/rpm/vaptvupt.spec` | RPM .spec |
| macOS | `packaging/homebrew/vaptvupt.rb` | Homebrew formula | | openSUSE | `packaging/opensuse/` | RPM .spec (OBS) |
| macOS | `packaging/homebrew/vaptvupt.rb`| Homebrew formula |
| NixOS / Nix flake | `packaging/nix/flake.nix` | Nix flake | | NixOS / Nix flake | `packaging/nix/flake.nix` | Nix flake |
All recipes: All recipes:
- Install the binary to `$PREFIX/bin/vaptvupt` (default `/usr/bin/vaptvupt`) - Install the binary to `$PREFIX/bin/vaptvupt` (default `/usr/bin/vaptvupt`)
- Install the vendored `libzuptsdk.so*` triple to `$PREFIX/lib/vaptvupt/`
(the binary uses relative `rpath` so users don't need `LD_LIBRARY_PATH`)
- Install manpage to `$PREFIX/share/man/man1/vaptvupt.1.gz` - Install manpage to `$PREFIX/share/man/man1/vaptvupt.1.gz`
- Install docs (README, SECURITY, CHANGELOG, AUDIT) to - Install docs (README, SECURITY, CHANGELOG) to `$PREFIX/share/doc/vaptvupt/`
`$PREFIX/share/doc/vaptvupt/` - Run the full upstream regression suite (`make test`) during build when the distro's package guidelines allow check-phase execution
- Run the full upstream regression suite (`make test`) during build
when the distro's package guidelines allow check-phase execution
## Arch Linux (AUR) ## Arch Linux (AUR)
@ -70,13 +56,13 @@ Maintainer flow:
```sh ```sh
# 1. Produce the upstream tarball # 1. Produce the upstream tarball
make dist make dist
# → /tmp/vaptvupt-2.4.4.tar.gz # → /tmp/vaptvupt-4.1.0.tar.gz
# 2. Upload to a stable URL (e.g. git.securityops.co releases) # 2. Upload to a stable URL (e.g. git.securityops.co releases)
# 3. Update packaging/aur/PKGBUILD: # 3. Update packaging/aur/PKGBUILD:
# - Set pkgver=2.4.4 # - Set pkgver=4.1.0
# - Set sha256sums=("$(sha256sum /tmp/vaptvupt-2.4.4.tar.gz | awk '{print $1}')") # - Set sha256sums=("$(sha256sum /tmp/vaptvupt-4.1.0.tar.gz | awk '{print $1}')")
# 4. Generate .SRCINFO # 4. Generate .SRCINFO
cd packaging/aur && makepkg --printsrcinfo > .SRCINFO cd packaging/aur && makepkg --printsrcinfo > .SRCINFO
@ -87,7 +73,7 @@ makepkg -s
# 6. Push to AUR # 6. Push to AUR
git clone ssh://aur@aur.archlinux.org/vaptvupt.git aur-vaptvupt git clone ssh://aur@aur.archlinux.org/vaptvupt.git aur-vaptvupt
cp packaging/aur/PKGBUILD packaging/aur/.SRCINFO aur-vaptvupt/ cp packaging/aur/PKGBUILD packaging/aur/.SRCINFO aur-vaptvupt/
cd aur-vaptvupt && git add -A && git commit -m "v2.4.4" && git push cd aur-vaptvupt && git add -A && git commit -m "v4.1.0" && git push
``` ```
User install: User install:
@ -96,10 +82,9 @@ User install:
yay -S vaptvupt # or paru, pikaur, etc. yay -S vaptvupt # or paru, pikaur, etc.
``` ```
## Shell completions (v2.4.7+) ## Shell completions
`make install` automatically installs Bash, zsh, and fish completion `make install` automatically installs Bash, zsh, and fish completion files alongside the binary and manpage:
files alongside the binary and manpage:
| Shell | Path | | Shell | Path |
|---|---| |---|---|
@ -107,10 +92,7 @@ files alongside the binary and manpage:
| zsh | `$PREFIX/share/zsh/site-functions/_vaptvupt` | | zsh | `$PREFIX/share/zsh/site-functions/_vaptvupt` |
| fish | `$PREFIX/share/fish/vendor_completions.d/vaptvupt.fish` | | fish | `$PREFIX/share/fish/vendor_completions.d/vaptvupt.fish` |
The source files live under `completions/` in the project tree. The source files live under `completions/` in the project tree. Distros that prefer a different install location should override the relevant paths in their `make install` invocation.
Distros that prefer a different install location should override
the relevant paths in their `make install` invocation; the
underlying recipe is straightforward.
For per-user installation without root: For per-user installation without root:
@ -125,24 +107,20 @@ cp completions/_vaptvupt ~/.zsh/completion/_vaptvupt
cp completions/vaptvupt.fish ~/.config/fish/completions/vaptvupt.fish cp completions/vaptvupt.fish ~/.config/fish/completions/vaptvupt.fish
``` ```
Completions cover every CLI flag the binary actually parses Completions cover every CLI flag the binary actually parses (`--kdf`, `--comment`, `--comment-file`, `--pq`, `--dedup`, etc.) and are validated on every CI run via `tests/test_completions_manpage.sh`.
(`--kdf`, `--comment`, `--comment-file`, `--pq-sdk`, `--dedup`,
etc.) and are validated on every CI run via
`tests/test_completions_manpage.sh`.
## Debian / Ubuntu ## Debian / Ubuntu
The `packaging/debian/` tree is a Debian source-package layout. The `packaging/debian/` tree is a Debian source-package layout. Maintainer flow:
Maintainer flow:
```sh ```sh
# 1. Produce the upstream tarball with the standard Debian # 1. Produce the upstream tarball with the standard Debian
# orig.tar.gz naming convention: # orig.tar.gz naming convention:
make dist make dist
cp /tmp/vaptvupt-2.4.4.tar.gz /tmp/vaptvupt_2.4.4.orig.tar.gz cp /tmp/vaptvupt-4.1.0.tar.gz /tmp/vaptvupt_4.1.0.orig.tar.gz
# 2. Unpack and overlay the debian/ tree: # 2. Unpack and overlay the debian/ tree:
cd /tmp && tar xzf vaptvupt_2.4.4.orig.tar.gz && cd vaptvupt-2.4.4 cd /tmp && tar xzf vaptvupt_4.1.0.orig.tar.gz && cd vaptvupt-4.1.0
cp -a /path/to/vaptvupt/packaging/debian ./debian cp -a /path/to/vaptvupt/packaging/debian ./debian
# 3. Build the source package: # 3. Build the source package:
@ -150,7 +128,7 @@ dpkg-buildpackage -S -us -uc # source-only
dpkg-buildpackage -b -us -uc # binary dpkg-buildpackage -b -us -uc # binary
# 4. Lint: # 4. Lint:
lintian vaptvupt_2.4.4-1_*.deb lintian vaptvupt_4.1.0-1_*.deb
# 5. Submit via the standard Debian mentors process: # 5. Submit via the standard Debian mentors process:
# https://mentors.debian.net/intro-maintainers/ # https://mentors.debian.net/intro-maintainers/
@ -167,7 +145,7 @@ sudo apt install vaptvupt
```sh ```sh
# 1. Produce the tarball # 1. Produce the tarball
make dist make dist
cp /tmp/vaptvupt-2.4.4.tar.gz ~/rpmbuild/SOURCES/ cp /tmp/vaptvupt-4.1.0.tar.gz ~/rpmbuild/SOURCES/
# 2. Drop the .spec into the SPECS directory: # 2. Drop the .spec into the SPECS directory:
cp packaging/rpm/vaptvupt.spec ~/rpmbuild/SPECS/ cp packaging/rpm/vaptvupt.spec ~/rpmbuild/SPECS/
@ -176,7 +154,7 @@ cp packaging/rpm/vaptvupt.spec ~/rpmbuild/SPECS/
cd ~/rpmbuild && rpmbuild -ba SPECS/vaptvupt.spec cd ~/rpmbuild && rpmbuild -ba SPECS/vaptvupt.spec
# 4. Lint: # 4. Lint:
rpmlint RPMS/x86_64/vaptvupt-2.4.4-1.fc*.rpm rpmlint RPMS/x86_64/vaptvupt-4.1.0-1.fc*.rpm
# 5. Submit via the Fedora new-package review process: # 5. Submit via the Fedora new-package review process:
# https://docs.fedoraproject.org/en-US/package-maintainers/Package_Review_Process/ # https://docs.fedoraproject.org/en-US/package-maintainers/Package_Review_Process/
@ -190,6 +168,31 @@ sudo dnf install vaptvupt # Fedora
sudo dnf install epel-release vaptvupt # RHEL/CentOS via EPEL sudo dnf install epel-release vaptvupt # RHEL/CentOS via EPEL
``` ```
## openSUSE
The `packaging/opensuse/` tree carries an RPM `.spec` suited to the Open Build Service (OBS).
```sh
# 1. Produce the tarball
make dist
# 2. In an OBS package checkout (osc), stage the sources and spec:
cp /tmp/vaptvupt-4.1.0.tar.gz .
cp /path/to/vaptvupt/packaging/opensuse/vaptvupt.spec .
# 3. Build locally against a target repository:
osc build openSUSE_Tumbleweed x86_64
# 4. Commit to OBS once the build and check phase pass:
osc addremove && osc commit
```
User install (after the package lands in a distribution or OBS repository):
```sh
sudo zypper install vaptvupt
```
## macOS (Homebrew) ## macOS (Homebrew)
```sh ```sh
@ -227,12 +230,12 @@ nix build github:cristiancmoises/vaptvupt#vaptvupt
nix run github:cristiancmoises/vaptvupt#vaptvupt -- version nix run github:cristiancmoises/vaptvupt#vaptvupt -- version
# 2. To consume from another flake: # 2. To consume from another flake:
# inputs.zupt.url = "github:cristiancmoises/vaptvupt?ref=v2.4.4"; # inputs.vaptvupt.url = "github:cristiancmoises/vaptvupt?ref=v4.1.0";
# packages.x86_64-linux.default = inputs.zupt.packages.x86_64-linux.zupt; # packages.x86_64-linux.default = inputs.vaptvupt.packages.x86_64-linux.vaptvupt;
# 3. To submit to nixpkgs (https://github.com/NixOS/nixpkgs): # 3. To submit to nixpkgs (https://github.com/NixOS/nixpkgs):
# - Adapt packaging/nix/flake.nix's `vaptvupt` derivation into a # - Adapt packaging/nix/flake.nix's `vaptvupt` derivation into a
# pkgs/by-name/zu/vaptvupt/package.nix using fetchurl and a hash. # pkgs/by-name/va/vaptvupt/package.nix using fetchurl and a hash.
# - Follow the nixpkgs contribution guide: # - Follow the nixpkgs contribution guide:
# https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md # https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md
``` ```
@ -241,28 +244,15 @@ nix run github:cristiancmoises/vaptvupt#vaptvupt -- version
Before pushing any recipe to a distro repository: Before pushing any recipe to a distro repository:
- [ ] `make dist` produces a reproducible tarball (verified by - [ ] `make dist` produces a reproducible tarball (verified by `tests/test_dist_reproducible.sh` on every `make test`)
`tests/test_dist_reproducible.sh` on every `make test`)
- [ ] The tarball is uploaded to a stable, immutable URL - [ ] The tarball is uploaded to a stable, immutable URL
- [ ] The recipe's checksum field is updated to match - [ ] The recipe's checksum field is updated to match `sha256sum /tmp/vaptvupt-VERSION.tar.gz`
`sha256sum /tmp/vaptvupt-VERSION.tar.gz`
- [ ] The recipe builds and tests pass in a clean chroot/container - [ ] The recipe builds and tests pass in a clean chroot/container
- [ ] The CHANGELOG mentions distro-relevant changes since the last release - [ ] The CHANGELOG mentions distro-relevant changes since the last release
- [ ] The license metadata is correct (AGPL-3.0-or-later for VaptVupt core; - [ ] The license metadata is correct (AGPL-3.0-or-later for VaptVupt core; GPL-3.0-or-later for the vendored VaptVupt codec)
GPL-3.0-or-later for the vendored VaptVupt codec)
## Security posture for downstream ## Security posture for downstream
Every packaging recipe runs `make test` during build (`check()` for AUR, Every packaging recipe runs `make test` during build (`check()` for AUR, `override_dh_auto_test` for Debian, `%check` for RPM and openSUSE, `checkPhase` for Nix, `test` block for Homebrew). The test suite runs in each recipe's check phase, including the tamper/integrity regressions and the `make dist` byte-identical reproducibility check.
`override_dh_auto_test` for Debian, `%check` for RPM, `checkPhase` for
Nix, `test` block for Homebrew). The suite includes:
- **F-06**: 2 000 HMAC tamper trials, 0 silent accepts required A build that doesn't pass `make test` will fail at distro check time — the recipes don't paper over regressions.
- **F-08**: top-MAC header/footer integrity-trailer regression
- **F-09**: 1 827-position exhaustive byte sweep on PQ-SDK archive,
0 silent accepts required
- **F-10..F-12**: KDF default, auth-fail message, encrypted comments
- **dist reproducibility**: `make dist` byte-identical across two runs
A build that doesn't pass `make test` will fail at distro check time —
the recipes don't paper over regressions.

View file

@ -5,13 +5,13 @@ If you're seeing the error:
``` ```
vaptvupt-gui depende de python3-pyqt6 | python3-pyside6; porém: vaptvupt-gui depende de python3-pyqt6 | python3-pyside6; porém:
Pacote python3-pyqt6 não está instalado. Pacote python3-pyqt6 não está instalado.
vaptvupt-gui depende de vaptvupt (>= 2.2.3); porém: vaptvupt-gui depende de vaptvupt (>= 4.1.0); porém:
Versão de vaptvupt no sistema é 2.1.7-1. Versão de vaptvupt no sistema é 2.1.7-1.
``` ```
This is correct behavior. The `vaptvupt-gui` deb requires: This is correct behavior. The `vaptvupt-gui` deb requires:
- Python 3 with **PyQt6** or **PySide6** (the GUI toolkit) - Python 3 with **PyQt6** or **PySide6** (the GUI toolkit)
- The **vaptvupt CLI 2.2.3** or newer - The **vaptvupt CLI 4.1.0** or newer
## The fastest fix — one command (Linux Mint, Ubuntu, Debian) ## The fastest fix — one command (Linux Mint, Ubuntu, Debian)
@ -22,7 +22,7 @@ sudo bash install-zupt-gui.sh
``` ```
This script auto-detects your distribution and installs everything in This script auto-detects your distribution and installs everything in
the right order. Done. the right order.
## Manual fix — three commands (if you prefer) ## Manual fix — three commands (if you prefer)
@ -33,8 +33,8 @@ the right order. Done.
sudo apt update sudo apt update
sudo apt install -y python3-pyqt6 sudo apt install -y python3-pyqt6
# 2. Upgrade vaptvupt CLI to 4.0.0 # 2. Upgrade vaptvupt CLI to 4.1.0
sudo dpkg -i vaptvupt_4.0.0_amd64.deb sudo dpkg -i vaptvupt_4.1.0_amd64.deb
# 3. Install the GUI # 3. Install the GUI
sudo dpkg -i vaptvupt-gui_1.3.0_all.deb sudo dpkg -i vaptvupt-gui_1.3.0_all.deb
@ -50,7 +50,7 @@ sudo apt --fix-broken install
```bash ```bash
sudo dnf install -y python3-pyqt6 sudo dnf install -y python3-pyqt6
sudo dnf install -y vaptvupt-4.0.0-1.x86_64.rpm vaptvupt-gui-1.3.0-1.noarch.rpm sudo dnf install -y vaptvupt-4.1.0-1.x86_64.rpm vaptvupt-gui-1.3.0-1.noarch.rpm
``` ```
(Or build the RPM from the SRPM tarball with `rpmbuild -bb SPECS/vaptvupt.spec`) (Or build the RPM from the SRPM tarball with `rpmbuild -bb SPECS/vaptvupt.spec`)
@ -79,8 +79,7 @@ cd vaptvupt-gui.AppDir
./AppRun ./AppRun
``` ```
The AppImage still needs Python 3 + Qt6 binding on the host (those are The AppImage still needs Python 3 + Qt6 binding on the host. For a
universally available on every Linux distribution since 2022). For a
fully standalone executable with no Python dependency, use a future fully standalone executable with no Python dependency, use a future
PyInstaller-built version (not in this release). PyInstaller-built version (not in this release).
@ -91,28 +90,25 @@ auto-detects whichever is installed). These are bindings to the Qt 6
graphical toolkit — they're how the GUI draws windows, buttons, and graphical toolkit — they're how the GUI draws windows, buttons, and
dialogs. dialogs.
PyQt6 is in the default repositories of every major Linux distribution PyQt6 is in the default repositories of major Linux distributions, so
since 2022, so installing it is one apt/dnf/zypper/pacman command away. installing it is one apt/dnf/zypper/pacman command away. We don't bundle
We don't bundle Qt6 inside the deb because: Qt6 inside the deb because:
- It's already on most modern systems - It's already on most modern systems
- Bundling would make the deb 80 MB+ instead of 35 KB - Bundling would make the deb 80 MB+ instead of 35 KB
- Distribution-managed Qt gets security updates automatically - Distribution-managed Qt gets security updates automatically
## Why does the GUI need vaptvupt 2.2.3? ## Why does the GUI need vaptvupt 4.1.0?
The GUI calls `vaptvupt --pq-sdk` and `vaptvupt keygen --sdk` for state-of-the-art The GUI calls `vaptvupt --pq` and `vaptvupt keygen` for native
post-quantum encryption (HKDF-SHA3 hybrid combiner, key commitment, HPKE post-quantum encryption (ML-KEM-768 + X25519, in-tree implementation).
binding, Argon2id). These flags didn't exist in 2.1.7 — they were added Older CLI versions lack these flags, so the GUI's compress/extract will
in 2.2.0. fail against them.
If you have an older vaptvupt installed, the GUI's compress/extract will fail
with "unknown option --pq-sdk".
## After installing — verify ## After installing — verify
```bash ```bash
vaptvupt version # should show: 2.2.3 vaptvupt version # should show: 4.1.0
vaptvupt-gui # should launch the GUI window vaptvupt-gui # should launch the GUI window
``` ```
@ -150,49 +146,40 @@ at https://git.securityops.co/cristiancmoises/vaptvupt/issues with:
## Building from source ## Building from source
If you want to build VaptVupt from the source tarball instead of installing If you want to build VaptVupt from the source tarball instead of installing
the pre-built `.deb` / `.rpm` packages, you'll need: the pre-built `.deb` / `.rpm` packages, you'll need only a C compiler and
make. The default build has NO external crypto dependency and installs no
shared library.
### Build dependencies ### Build dependencies (default build)
| Component | Why needed | | Component | Why needed |
|---|---| |---|---|
| `gcc` ≥ 7 or `clang` ≥ 10 | C11 compiler | | `gcc` ≥ 7 or `clang` ≥ 10 | C11 compiler |
| `make` | build driver | | `make` | build driver |
| `libargon2-dev` | Argon2id KDF | | libm, pthread | math and threading (part of the standard C library/toolchain) |
| `libssl-dev` | OpenSSL libcrypto (AES, SHA-256) |
| **`libzuptsdk-dev` 2.0.0+** | VaptVupt's cryptographic SDK |
The `libzuptsdk-dev` package is a separate sister project — it contains The default build uses PBKDF2-SHA256 (600k iterations) for password KDF
the post-quantum hybrid cryptography that VaptVupt uses on its `--pq-sdk` and the in-tree native `--pq` mode (ML-KEM-768 + X25519) for post-quantum
path. Both libraries are by the same author (Cristian Cezar Moisés) but encryption. No `libzuptsdk`, no OpenSSL, no libargon2 is required.
are distributed as separate source/binary packages so each can evolve
on its own release cadence.
### Install build dependencies (Debian/Ubuntu/Mint) ### Install build dependencies (Debian/Ubuntu/Mint)
```bash ```bash
sudo apt install build-essential libargon2-dev libssl-dev sudo apt install build-essential
# Then install libzuptsdk from its package:
sudo apt install ./libzuptsdk2_2.0.0_amd64.deb \
./libzuptsdk-dev_2.0.0_amd64.deb
``` ```
### Install build dependencies (Fedora/RHEL/openSUSE) ### Install build dependencies (Fedora/RHEL/openSUSE)
```bash ```bash
sudo dnf install gcc make libargon2-devel openssl-devel sudo dnf install gcc make # Fedora/RHEL
# libzuptsdk from its SRPM: sudo zypper install gcc make # openSUSE
tar -xzf libzuptsdk-2.0.0.srpm.tar.gz
rpmbuild -bb SPECS/libzuptsdk.spec
sudo rpm -i ~/rpmbuild/RPMS/x86_64/libzuptsdk-2.0.0-*.rpm
``` ```
### Build VaptVupt itself ### Build VaptVupt itself
```bash ```bash
tar -xzf vaptvupt-2.2.3-source.tar.gz tar -xzf vaptvupt-4.1.0-source.tar.gz
cd vaptvupt-2.2.3 cd vaptvupt-4.1.0
make # build the `./vaptvupt` binary make # build the `./vaptvupt` binary
sudo make install # install to /usr/local/bin (override with PREFIX=/usr) sudo make install # install to /usr/local/bin (override with PREFIX=/usr)
@ -210,9 +197,9 @@ The `make` step takes 10-30 seconds. The build emits the binary as
make test make test
``` ```
61 tests pass: roundtrip, audit, multi-file, cross-block, dedup property, Covers roundtrip, multi-file, cross-block, dedup property, path-traversal,
path-traversal, argument-order, block-swap regression. Each suite reports argument-order, and block-swap regression. Each suite reports its own
its own pass/fail count. pass/fail count.
### Cross-compilation ### Cross-compilation
@ -228,15 +215,18 @@ The Makefile auto-detects target architecture via `$(CC) -dumpmachine`
and selects the appropriate SIMD flags (NEON on AArch64, SSE4/AVX2 on and selects the appropriate SIMD flags (NEON on AArch64, SSE4/AVX2 on
x86_64). x86_64).
### Static linking against libzuptsdk ### Optional: WITH_SDK=1 build
If you want a fully self-contained `vaptvupt` binary (no `libzuptsdk.so.2` The SDK-backed modes — `--pq-sdk`, `--pq-box` (sealed-box), and the
runtime dependency), you can link against the static library: Argon2id KDF — are optional. They are not in the default build and require
building against the separately distributed `libzuptsdk` / `libpqvaptvupt`
libraries:
```bash ```bash
make LDLIBS='-l:libzuptsdk.a -lcrypto -largon2' make WITH_SDK=1
``` ```
This produces a binary that doesn't need `libzuptsdk2` installed at This build additionally needs the SDK development package and its runtime
runtime — useful for containers, embedded systems, or distribution to dependencies (OpenSSL libcrypto, libargon2), which ship with the SDK
machines without package management. distribution. Without `WITH_SDK=1`, the `--pq-sdk` and `--pq-box` flags are
unavailable; use the native `--pq` mode instead.

View file

@ -63,10 +63,21 @@ ZUPT_SOURCES = src/zupt_main.c src/zupt_format.c src/zupt_lz.c src/zupt_lzh.c \
src/zupt_x25519.c src/zupt_mlkem.c src/zupt_cpuid.c src/zupt_mlock.c \ src/zupt_x25519.c src/zupt_mlkem.c src/zupt_cpuid.c src/zupt_mlock.c \
src/zupt_filetype.c src/zupt_disk.c src/zupt_dedup.c src/zupt_filetype.c src/zupt_disk.c src/zupt_dedup.c
# --- libzuptsdk linkage (vendored) --- # --- Optional vendored libraries (libzuptsdk + libpqvaptvupt) ---
#
# These are PREBUILT shared libraries shipped only as binaries (no source), so
# they are NOT part of the source tree and a distro/source build must not need
# them. WITH_SDK is therefore OFF by default: the tool builds entirely from the
# in-tree C sources, using native crypto (PBKDF2-SHA256 password KDF and native
# ML-KEM-768 + X25519 via --pq). The SDK-backed modes (--pq-sdk, --pq-box, and
# the Argon2id password KDF) compile to "unsupported" stubs in that case.
#
# Set WITH_SDK=1 (with the vendored libs present under vendor/) to enable them.
WITH_SDK ?= 0
ifeq ($(WITH_SDK),1)
ZUPTSDK_DIR ?= vendor/zuptsdk ZUPTSDK_DIR ?= vendor/zuptsdk
ZUPTSDK_ABS := $(abspath $(ZUPTSDK_DIR)) ZUPTSDK_ABS := $(abspath $(ZUPTSDK_DIR))
CFLAGS += -I$(ZUPTSDK_DIR)/include CFLAGS += -DZUPT_WITH_SDK -I$(ZUPTSDK_DIR)/include
PQVV_DIR ?= vendor/pqvaptvupt PQVV_DIR ?= vendor/pqvaptvupt
CFLAGS += -I$(PQVV_DIR)/include CFLAGS += -I$(PQVV_DIR)/include
LDFLAGS += -L$(ZUPTSDK_DIR) -Wl,-rpath,$(ZUPTSDK_ABS) -Wl,-rpath,'$$ORIGIN/$(ZUPTSDK_DIR)' LDFLAGS += -L$(ZUPTSDK_DIR) -Wl,-rpath,$(ZUPTSDK_ABS) -Wl,-rpath,'$$ORIGIN/$(ZUPTSDK_DIR)'
@ -77,6 +88,7 @@ LDFLAGS += -L$(PQVV_DIR) -Wl,-rpath,$(PQVV_ABS) -Wl,-rpath,'$$ORIGIN/$(PQVV_DIR)
# binary a matching relative rpath so `make install` is self-contained. # binary a matching relative rpath so `make install` is self-contained.
LDFLAGS += -Wl,-rpath,'$$ORIGIN/../lib/vaptvupt' LDFLAGS += -Wl,-rpath,'$$ORIGIN/../lib/vaptvupt'
LDLIBS += -lpqvaptvupt LDLIBS += -lpqvaptvupt
endif
# --- VAPTVUPT: VaptVupt codec sources (GPL-3.0-or-later; tool is AGPL-3.0-or-later) --- # --- VAPTVUPT: VaptVupt codec sources (GPL-3.0-or-later; tool is AGPL-3.0-or-later) ---
VV_SOURCES = src/vv_encoder.c src/vv_decoder.c src/vv_ans.c src/vv_bcj.c \ VV_SOURCES = src/vv_encoder.c src/vv_decoder.c src/vv_ans.c src/vv_bcj.c \
@ -313,8 +325,11 @@ install: $(TARGET)
echo "Installed: $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/$(TARGET).fish"; \ echo "Installed: $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/$(TARGET).fish"; \
fi fi
# Vendored runtime libraries (NEEDED by the binary): libzuptsdk # Vendored runtime libraries — installed ONLY for a WITH_SDK=1 build. In the
# (password KDF + --pq-sdk) and libpqvaptvupt (--pq-box, v4.0.0+). # default source-only build the binary links no external library and there is
# nothing to install here (the vendored .so are prebuilt binaries kept out of
# the source tree).
ifeq ($(WITH_SDK),1)
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/lib/vaptvupt $(Q)mkdir -p $(DESTDIR)$(PREFIX)/lib/vaptvupt
$(Q)install -m 755 vendor/zuptsdk/libzuptsdk.so.2.0.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libzuptsdk.so.2.0.0 $(Q)install -m 755 vendor/zuptsdk/libzuptsdk.so.2.0.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libzuptsdk.so.2.0.0
$(Q)ln -sf libzuptsdk.so.2.0.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libzuptsdk.so.2 $(Q)ln -sf libzuptsdk.so.2.0.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libzuptsdk.so.2
@ -322,6 +337,7 @@ install: $(TARGET)
$(Q)install -m 755 vendor/pqvaptvupt/libpqvaptvupt.so.0.6.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libpqvaptvupt.so.0.6.0 $(Q)install -m 755 vendor/pqvaptvupt/libpqvaptvupt.so.0.6.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libpqvaptvupt.so.0.6.0
$(Q)ln -sf libpqvaptvupt.so.0.6.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libpqvaptvupt.so.0 $(Q)ln -sf libpqvaptvupt.so.0.6.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libpqvaptvupt.so.0
$(Q)ln -sf libpqvaptvupt.so.0.6.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libpqvaptvupt.so $(Q)ln -sf libpqvaptvupt.so.0.6.0 $(DESTDIR)$(PREFIX)/lib/vaptvupt/libpqvaptvupt.so
endif
@echo "Installed: $(DESTDIR)$(BINDIR)/$(TARGET) (legacy: $(DESTDIR)$(BINDIR)/$(LEGACY_LINK) -> $(TARGET))" @echo "Installed: $(DESTDIR)$(BINDIR)/$(TARGET) (legacy: $(DESTDIR)$(BINDIR)/$(LEGACY_LINK) -> $(TARGET))"
@ -507,10 +523,9 @@ test-vv: tests/test_vaptvupt.c $(HEADERS)
$(Q)./test_vaptvupt $(Q)./test_vaptvupt
test-asan: $(SOURCES) $(HEADERS) $(JAZZ_O) test-asan: $(SOURCES) $(HEADERS) $(JAZZ_O)
$(Q)$(CC) -Wall -Wextra -std=c11 -Iinclude -Isrc -I$(ZUPTSDK_DIR)/include \ $(Q)$(CC) $(CFLAGS) -fsanitize=address,undefined -g -O1 \
-fsanitize=address,undefined -g -O1 \ $(VV_SIMD_FLAGS) $(SHANI_FLAGS) $(LDFLAGS) \
$(VV_SIMD_FLAGS) $(SHANI_FLAGS) -I$(PQVV_DIR)/include -L$(ZUPTSDK_DIR) -Wl,-rpath,$(ZUPTSDK_ABS) \ $(SOURCES) $(JAZZ_O) -o zupt_asan $(LDLIBS)
$(SOURCES) $(JAZZ_O) -o zupt_asan -lzuptsdk -L$(PQVV_DIR) -Wl,-rpath,$(PQVV_ABS) $(LDLIBS)
@echo "ASAN build: ./zupt_asan" @echo "ASAN build: ./zupt_asan"
# Build the format-parser fuzz harness. Runs against ./zupt_asan to catch # Build the format-parser fuzz harness. Runs against ./zupt_asan to catch

573
README.md
View file

@ -3,65 +3,99 @@
# VaptVupt # VaptVupt
**Compress everything. Trust nothing. Encrypt always.** Backup compression with hardware-adaptive codec selection, AES-256
authenticated encryption, post-quantum key encapsulation, and full-disk
backup. Pure C11, ~13,000 lines. Builds and runs on x86_64, aarch64,
armhf, ppc64le, s390x, and riscv64.
![Build](https://img.shields.io/badge/build-passing-brightgreen) License: AGPL-3.0-or-later (dual-licensed AGPL + commercial).
![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue)
![Version](https://img.shields.io/badge/version-4.0.0-brightgreen)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey)
> **Renamed from "Zupt" in v3.0.0** because of a prior INPI Brasil > **Renamed from "Zupt" in v3.0.0** because of a prior INPI Brasil
> trademark registration on the name "Zupt" for unrelated software. > trademark registration on the name "Zupt" for unrelated software.
> The `.zupt` archive extension and `ZUPT` header magic bytes are > The `.zupt` archive extension and `ZUPT` header magic bytes are
> unchanged — v2.x and v3.0.0 archives are bidirectionally compatible. > unchanged — v2.x and v3.0.0 archives remain compatible. The `zupt`
> The `zupt` command is preserved as a symlink to `vaptvupt` for one > command is preserved as a symlink to `vaptvupt` for one major version
> major version cycle. > cycle.
## What's new in 4.0.0 ## What's new in 4.1.0
- **Codec security release** — vendored codec upgraded to VaptVupt 2.60.4, fixing a high-severity OOB heap write in the AVX2 decode fast path (reachable on valid streams with exact-size output buffers). 80 new exact-size decode regression cases under ASan. - **Source-only tree.** The prebuilt vendored libraries `libzuptsdk.so`
- **`--pq-box` sealed-box mode** — third post-quantum recipient mode via vendored libpqvaptvupt 0.6.0: ML-KEM-768 + X25519 through an HKDF-SHA256 domain-separated combiner. `vaptvupt keygen --box` writes magic-tagged keypairs that reject key-type confusion. and `libpqvaptvupt.so` have been removed. The default `make` needs only
- **F-16 disclosure and fix** — archives created by ≤ 3.8.0 at `-l 8`/`-l 9` containing x86/ELF/PE executables may be unreadable by *any* version (write-time defect in the old in-tree BCJ encoder). Re-create them with 4.0.0; details below and in [CHANGELOG.md](CHANGELOG.md). a C compiler and make (plus libm/pthread) — no external crypto library —
- **SHA-NI measured** — 5.8× over scalar SHA-256 (204 → 1184 MB/s); encrypted per-block throughput now 293 MB/s. and installs no `.so`.
- **Native `--pq` is the default post-quantum mode.** ML-KEM-768 + X25519
hybrid KEM, in-tree C implementation, available in the default build.
- **SDK-backed modes are optional.** `--pq-sdk`, `--pq-box`, and the
Argon2id KDF are only available in an upstream `make WITH_SDK=1` build
linked against the separately distributed `libzuptsdk`/`libpqvaptvupt`.
- **Wire/on-disk format is v1.6, unchanged.** Archives created by 4.0.0
are read and written identically.
Binaries for the CLI (4.0.0) and GUI (1.3.0) are on the > **F-16 (data loss):** archives created by **≤ 3.8.0** at `-l 8`/`-l 9`
[release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.0.0). > whose inputs included x86/ELF/PE executables may be **undecodable by any
> version** (write-time defect in the old in-tree BCJ encoder). Re-create
> such archives with 4.1.0 and verify extraction before deleting source
> data. Details in [CHANGELOG.md](CHANGELOG.md).
Backup compression with hardware-adaptive codec selection, AES-256 Binaries for the CLI (4.1.0) and GUI (1.3.0) are on the
authenticated encryption, post-quantum key encapsulation, and [release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.1.0).
full-disk backup. Pure C11, zero dependencies, ~13,000 lines. Builds
and runs on x86_64, aarch64, armhf, ppc64le, s390x, and riscv64.
--- ---
## Why VaptVupt ## Features
- **Hardware-adaptive codec** — auto-detects AVX2/NEON at runtime and selects the best codec: VaptVupt (LZ77 + tANS + SIMD decode) on capable hardware, VaptVupt-LZHP on everything else. Override with `--vv` or `--lzhp`. - **Hardware-adaptive codec** — auto-detects AVX2/NEON at runtime and
- **Post-quantum encryption**`--pq` mode uses ML-KEM-768 + X25519 hybrid KEM (same approach as Signal and iMessage). Protects against "harvest now, decrypt later" quantum attacks. selects the codec: VaptVupt (LZ77 + tANS + SIMD decode) on capable
- **AES-NI hardware acceleration** — AES-256-CTR via Jasmin-verified assembly with 4-block interleaved pipeline. Safe AVX detection with OSXSAVE/XCR0 validation — no SIGILL on any CPU. Falls back to C table-based AES on unsupported hardware. hardware, VaptVupt-LZHP on everything else. Override with `--vv` or
- **SHA-NI hardware acceleration** — HMAC-SHA256 (the Encrypt-then-MAC second pass) and PBKDF2 use an Intel SHA-NI compression path (`SHA256RNDS2`/`MSG1`/`MSG2`) when the CPU supports it (Intel Goldmont+/Ice Lake+, AMD Zen+), selected at runtime via CPUID. **measured 5.8×** over the scalar path (204 → 1184 MB/s, 256 MiB, Xeon 2.10 GHz) *and* constant-time by construction. Bit-identical output; scalar C fallback elsewhere (incl. aarch64). `vaptvupt version` prints the live acceleration set for your CPU. `--lzhp`.
- **Incremental HMAC** — the per-block MAC streams its segments through an incremental HMAC-SHA256 (key prefix folded once per keyring) instead of copying each block's ciphertext into a temporary buffer. Removes a per-block heap allocation and full-payload copy on both encrypt and decrypt, with a byte-for-byte identical MAC (RFC 2104). - **Post-quantum encryption**`--pq` uses ML-KEM-768 + X25519 hybrid
- **Multi-threaded** — Compression and decompression both parallelized. `-t 0` auto-detects cores. KEM (the approach used by Signal and iMessage), protecting against
- **Full-disk backup**`vaptvupt disk backup` clones entire disks or partitions in one command. Sparse block detection skips zero regions, real-time progress bar, all encryption modes supported. Restore with byte-for-byte verification via per-block XXH64 checksums. "harvest now, decrypt later" attacks. In-tree, available in the default
- **Encrypted backups in one command**`vaptvupt compress -p changeme backup.zupt ~/data/` — AES-256 + HMAC-SHA256, file names hidden. build.
- **Per-block integrity** — XXH64 checksum + HMAC-SHA256 per block. Wrong password rejected instantly. - **AES-NI acceleration** — AES-256-CTR via Jasmin-verified assembly with
- **Self-describing KDF** — password archives record their key-derivation profile in the (authenticated) header, so an archive always carries the parameters needed to open it years later. Unknown profiles are refused fail-closed rather than mis-derived. Argon2id is the default; PBKDF2 (600K iter) via `--kdf pbkdf2`. a 4-block interleaved pipeline. AVX detection validates OSXSAVE/XCR0 (no
- **Measured constant-time comparisons** — every security-critical comparison (HMAC tag, archive-integrity trailer, and the ML-KEM-768 decapsulation implicit-rejection check) routes through a single audited primitive (`zupt_ct_memeq`, branch-free, volatile accumulator, length-independent) verified by a dudect-style Welch t-test in CI, not just annotated. Its data-dependent timing signal measures ~1% of a leaky-`memcmp` control in the same environment; a reintroduced early-return or inline loop fails the test (timing + source-routing guard). SIGILL). Falls back to C table-based AES on unsupported hardware.
- **Sealed-box PQ recipients (`--pq-box`, v4.0.0)** — third post-quantum mode via vendored libpqvaptvupt: ML-KEM-768 + X25519 combined through HKDF-SHA256 with domain separation ("pqvv-seal-v1"), AES-256-CTR + HMAC-SHA256 EtM in the box, magic-tagged keypair files that reject key-type confusion. Legacy `--pq` and `--pq-sdk` remain readable. - **SHA-NI acceleration** — HMAC-SHA256 (the Encrypt-then-MAC pass) and
- **Formally verified crypto** — 5 Jasmin assembly functions with constant-time proofs. 19 ACSL-annotated functions for Frama-C memory safety analysis. PBKDF2 use the Intel SHA-NI compression path when the CPU supports it
- **Multi-architecture** — builds on x86_64, aarch64, armhf, ppc64le, s390x, riscv64. Jasmin CT crypto on x86_64, C fallback everywhere else. Any archive decompresses on any architecture. (Intel Goldmont+/Ice Lake+, AMD Zen+), selected at runtime via CPUID.
- **Zero dependencies** — ML-KEM, X25519, Keccak, SHA-256, AES-256, HMAC, PBKDF2, VaptVupt codec — all pure C11. Builds with `gcc` or `cl` alone. Bit-identical output; scalar C fallback elsewhere. `vaptvupt version`
prints the acceleration set for your CPU.
- **Incremental HMAC** — the per-block MAC streams its segments through an
incremental HMAC-SHA256 instead of copying each block's ciphertext into
a temporary buffer, removing a per-block heap allocation and full-payload
copy on encrypt and decrypt with a byte-for-byte identical MAC (RFC 2104).
- **Multi-threaded** — compression and decompression both parallelized.
`-t 0` auto-detects cores.
- **Full-disk backup**`vaptvupt disk backup` clones disks or partitions
in one command. Sparse block detection skips zero regions; all encryption
modes supported; restore verifies per-block XXH64 checksums.
- **Per-block integrity** — XXH64 checksum + HMAC-SHA256 per block. Wrong
password rejected immediately.
- **Self-describing KDF** — password archives record their key-derivation
profile in the authenticated header, so an archive carries the parameters
needed to open it later. Unknown profiles are refused fail-closed rather
than mis-derived. Default is PBKDF2-SHA256 (600K iterations); Argon2id is
available in a `WITH_SDK=1` build.
- **Constant-time comparisons** — every security-critical comparison (HMAC
tag, archive-integrity trailer, ML-KEM-768 implicit-rejection check)
routes through a single primitive (`zupt_ct_memeq`, branch-free, volatile
accumulator, length-independent), checked by a dudect-style Welch t-test
in CI.
- **Formally verified crypto** — 5 Jasmin assembly functions with
constant-time proofs; 19 ACSL-annotated functions for Frama-C memory
safety analysis.
- **Multi-architecture** — builds on x86_64, aarch64, armhf, ppc64le,
s390x, riscv64. Jasmin CT crypto on x86_64, C fallback everywhere else.
Any archive decompresses on any architecture.
- **No external dependencies (default build)** — ML-KEM, X25519, Keccak,
SHA-256, AES-256, HMAC, PBKDF2 and the VaptVupt codec are all pure C11.
Builds with `gcc` or `cl` alone.
--- ---
## Quick Start ## Quick Start
### Fast installation ### Build & install
```
curl -fsSL https://short.securityops.co/vaptvupt | bash
```
### Build & Install
``` ```
git clone https://git.securityops.co/cristiancmoises/vaptvupt.git && \ git clone https://git.securityops.co/cristiancmoises/vaptvupt.git && \
cd vaptvupt && \ cd vaptvupt && \
@ -69,20 +103,26 @@ make && \
sudo make install sudo make install
``` ```
The default build needs only a C compiler and `make` (plus libm/pthread).
`make WITH_SDK=1` additionally links the separately distributed
`libzuptsdk`/`libpqvaptvupt` to enable `--pq-sdk`, `--pq-box`, and the
Argon2id KDF.
### Pre-built packages ### Pre-built packages
All assets are published on the [v4.0.0 release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.0.0) Assets are published on the
[v4.1.0 release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.1.0)
and verifiable against the published `SHA256SUMS.txt`. and verifiable against the published `SHA256SUMS.txt`.
**Command-line tool (`vaptvupt` 4.0.0):** **Command-line tool (`vaptvupt` 4.1.0):**
| Format | File | Distros | | Format | File | Distros |
|---|---|---| |---|---|---|
| Debian/Ubuntu | `vaptvupt_4.0.0_amd64.deb` | Debian 11+, Ubuntu 22.04+, Mint 21+ | | Debian/Ubuntu | `vaptvupt_4.1.0_amd64.deb` | Debian 11+, Ubuntu 22.04+, Mint 21+ |
| RPM | `vaptvupt-4.0.0-1.x86_64.rpm` | Fedora 38+, RHEL 9+, openSUSE, AlmaLinux, Rocky, and other RPM-based distributions | | RPM | `vaptvupt-4.1.0-1.x86_64.rpm` | Fedora 38+, RHEL 9+, openSUSE, AlmaLinux, Rocky, other RPM-based distributions |
| AppDir tarball | `vaptvupt-4.0.0-x86_64.AppDir.tar.gz` | Any glibc 2.28+ (extract & run, no FUSE) | | AppDir tarball | `vaptvupt-4.1.0-x86_64.AppDir.tar.gz` | Any glibc 2.28+ (extract & run, no FUSE) |
| Source tarball | `vaptvupt-4.0.0.tar.gz` | Build from source on any platform | | Source tarball | `vaptvupt-4.1.0.tar.gz` | Build from source on any platform |
| openSUSE OBS | `vaptvupt-4.0.0-opensuse-obs.tar.gz` | Open Build Service source bundle | | openSUSE OBS | `vaptvupt-4.1.0-opensuse-obs.tar.gz` | Open Build Service source bundle |
**Graphical front-end (`vaptvupt-gui` 1.3.0):** **Graphical front-end (`vaptvupt-gui` 1.3.0):**
@ -98,19 +138,19 @@ and verifiable against the published `SHA256SUMS.txt`.
sha256sum -c SHA256SUMS.txt sha256sum -c SHA256SUMS.txt
# Debian / Ubuntu / Mint # Debian / Ubuntu / Mint
sudo dpkg -i vaptvupt_4.0.0_amd64.deb sudo dpkg -i vaptvupt_4.1.0_amd64.deb
sudo apt-get install -f # resolve any missing deps sudo apt-get install -f # resolve any missing deps
# Fedora / RHEL / openSUSE / AlmaLinux / Rocky and other RPM-based distros # Fedora / RHEL / openSUSE / AlmaLinux / Rocky and other RPM-based distros
sudo rpm -i vaptvupt-4.0.0-1.x86_64.rpm sudo rpm -i vaptvupt-4.1.0-1.x86_64.rpm
# or # or
sudo dnf install ./vaptvupt-4.0.0-1.x86_64.rpm sudo dnf install ./vaptvupt-4.1.0-1.x86_64.rpm
# AppDir tarball (no install, no FUSE required) # AppDir tarball (no install, no FUSE required)
tar xzf vaptvupt-4.0.0-x86_64.AppDir.tar.gz tar xzf vaptvupt-4.1.0-x86_64.AppDir.tar.gz
./vaptvupt-4.0.0-x86_64.AppDir/AppRun --help ./vaptvupt-4.1.0-x86_64.AppDir/AppRun --help
# GUI AppImage (single executable, runs anywhere) # GUI AppImage (single executable)
chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage
./VaptVupt-GUI-1.3.0-x86_64.AppImage ./VaptVupt-GUI-1.3.0-x86_64.AppImage
``` ```
@ -118,16 +158,16 @@ chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage
### Building from SRPM (Fedora / RHEL / RPM-based distributions) ### Building from SRPM (Fedora / RHEL / RPM-based distributions)
```bash ```bash
tar xzf vaptvupt-4.0.0.srpm.tar.gz tar xzf vaptvupt-4.1.0.srpm.tar.gz
cd ~/rpmbuild # or use rpmbuild --define "_topdir $(pwd)" cd ~/rpmbuild # or use rpmbuild --define "_topdir $(pwd)"
rpmbuild -bb SPECS/vaptvupt.spec rpmbuild -bb SPECS/vaptvupt.spec
sudo rpm -i RPMS/x86_64/vaptvupt-4.0.0-1.*.rpm sudo rpm -i RPMS/x86_64/vaptvupt-4.1.0-1.*.rpm
``` ```
### Basic usage ### Basic usage
```bash ```bash
# Compress a directory (auto-selects best codec for your hardware) # Compress a directory (auto-selects codec for your hardware)
vaptvupt compress backup.zupt ~/Documents/ vaptvupt compress backup.zupt ~/Documents/
# Compress at a specific level (1=fast, 5=balanced, 9=extreme) # Compress at a specific level (1=fast, 5=balanced, 9=extreme)
@ -136,26 +176,24 @@ vaptvupt compress -l 9 backup.zupt ~/Documents/
# Force the VaptVupt codec (default on AVX2/NEON hardware) # Force the VaptVupt codec (default on AVX2/NEON hardware)
vaptvupt compress --vv -l 5 backup.zupt ~/Documents/ vaptvupt compress --vv -l 5 backup.zupt ~/Documents/
# Compress with multi-threading (-t 0 = auto-detect cores) # Multi-threading (-t 0 = auto-detect cores)
vaptvupt compress -t 0 -l 5 backup.zupt ~/Documents/ vaptvupt compress -t 0 -l 5 backup.zupt ~/Documents/
# Compress with password encryption (AES-256-CTR + HMAC-SHA256) # Password encryption (AES-256-CTR + HMAC-SHA256, PBKDF2-SHA256 KDF)
vaptvupt compress -p "my-strong-password" backup.zupt ~/Documents/ vaptvupt compress -p "my-strong-password" backup.zupt ~/Documents/
# List archive contents # List archive contents
vaptvupt list backup.zupt vaptvupt list backup.zupt
# Show archive metadata (codec, blocks, encryption — no password needed) # Show archive metadata (no password needed)
vaptvupt info backup.zupt vaptvupt info backup.zupt
# Verify archive integrity (HMAC + per-block checksums) # Verify archive integrity (HMAC + per-block checksums)
vaptvupt test backup.zupt vaptvupt test backup.zupt
vaptvupt test -p "my-strong-password" backup.zupt vaptvupt test -p "my-strong-password" backup.zupt
# Extract everything # Extract
vaptvupt extract -o ~/restored/ backup.zupt vaptvupt extract -o ~/restored/ backup.zupt
# Extract from encrypted archive
vaptvupt extract -p "my-strong-password" -o ~/restored/ backup.zupt vaptvupt extract -p "my-strong-password" -o ~/restored/ backup.zupt
# Benchmark all 9 levels on a file # Benchmark all 9 levels on a file
@ -165,32 +203,36 @@ vaptvupt bench big-file.tar
#### Post-quantum encryption #### Post-quantum encryption
```bash ```bash
# Recommended: SDK v2 (HKDF combiner + key commitment + HPKE binding + Argon2id). # Native --pq (ML-KEM-768 + X25519 hybrid KEM, in-tree, default build).
# New archives should use this. # Recommended for new archives.
vaptvupt keygen --sdk -o mykey.priv # writes mykey.priv and mykey.priv.pub
vaptvupt compress --pq-sdk mykey.priv.pub backup.zupt ~/Documents/
vaptvupt extract --pq-sdk mykey.priv -o ~/restored/ backup.zupt
# pq-box sealed-box workflow (v4.0.0; HKDF-SHA256 domain-separated combiner)
vaptvupt keygen --box -o box.key # writes box.key + box.key.pub
vaptvupt compress --pq-box box.key.pub backup.zupt ~/Documents/
vaptvupt extract --pq-box box.key -o ~/restored/ backup.zupt
# Legacy --pq mode (XOR+SHA3-512 combiner) — kept for back-compat with
# archives created by Zupt 2.02.1. Do NOT use for new archives.
vaptvupt keygen -o mykey.key vaptvupt keygen -o mykey.key
vaptvupt keygen --pub -o pub.key -k mykey.key vaptvupt keygen --pub -o pub.key -k mykey.key
vaptvupt compress --pq pub.key backup.zupt ~/Documents/ vaptvupt compress --pq pub.key backup.zupt ~/Documents/
vaptvupt extract --pq mykey.key -o ~/restored/ backup.zupt vaptvupt extract --pq mykey.key -o ~/restored/ backup.zupt
``` ```
The SDK-backed modes below require a `make WITH_SDK=1` build linked against
the separately distributed `libzuptsdk`/`libpqvaptvupt`:
```bash
# --pq-sdk (HKDF combiner + key commitment + HPKE binding + Argon2id)
vaptvupt keygen --sdk -o mykey.priv # writes mykey.priv and mykey.priv.pub
vaptvupt compress --pq-sdk mykey.priv.pub backup.zupt ~/Documents/
vaptvupt extract --pq-sdk mykey.priv -o ~/restored/ backup.zupt
# --pq-box sealed-box (ML-KEM-768 + X25519 via HKDF-SHA256 combiner)
vaptvupt keygen --box -o box.key # writes box.key + box.key.pub
vaptvupt compress --pq-box box.key.pub backup.zupt ~/Documents/
vaptvupt extract --pq-box box.key -o ~/restored/ backup.zupt
```
#### Full-disk backup #### Full-disk backup
```bash ```bash
# Backup an entire disk or partition (sparse-detection skips zero regions) # Backup a disk or partition (sparse-detection skips zero regions)
sudo vaptvupt disk backup -l 5 disk.zupt /dev/sda sudo vaptvupt disk backup -l 5 disk.zupt /dev/sda
# Backup with encryption # With encryption
sudo vaptvupt disk backup -p "passphrase" -l 5 disk.zupt /dev/sda sudo vaptvupt disk backup -p "passphrase" -l 5 disk.zupt /dev/sda
# Restore (writes raw bytes back to a block device or file) # Restore (writes raw bytes back to a block device or file)
@ -205,33 +247,38 @@ vaptvupt disk backup -l 5 part.zupt /path/to/partition.img
## Auto Codec Detection ## Auto Codec Detection
VaptVupt automatically selects the best compression codec based on your hardware (since v2.0.0). No flags needed — just run `vaptvupt compress` and it picks the fastest option available. VaptVupt selects the compression codec based on your hardware (since
v2.0.0). No flags needed — `vaptvupt compress` picks the fastest option
available.
| Architecture | SIMD Available | Default Codec | Decode Throughput | | Architecture | SIMD Available | Default Codec | Decode Throughput |
|---|---|---|---| |---|---|---|---|
| x86_64 + AVX2 | AVX2 inline SIMD | **VaptVupt** | ~23 GB/s | | x86_64 + AVX2 | AVX2 inline SIMD | VaptVupt | ~23 GB/s |
| x86_64 (no AVX2) | Scalar | VaptVupt-LZHP | ~500 MB/s | | x86_64 (no AVX2) | Scalar | VaptVupt-LZHP | ~500 MB/s |
| aarch64 + NEON | NEON SIMD | **VaptVupt** | ~12 GB/s | | aarch64 + NEON | NEON SIMD | VaptVupt | ~12 GB/s |
| armhf, ppc64le, s390x, riscv64 | Scalar | VaptVupt-LZHP | ~300500 MB/s | | armhf, ppc64le, s390x, riscv64 | Scalar | VaptVupt-LZHP | ~300500 MB/s |
**Decompression is universal.** An archive created with VaptVupt on x86_64 extracts on aarch64 (using NEON or scalar decode), and vice versa. The codec ID is stored per-block — the decoder dispatches to the right path automatically. Decompression is universal. An archive created with VaptVupt on x86_64
extracts on aarch64 (NEON or scalar decode) and vice versa. The codec ID
Override with `--vv` (force VaptVupt) or `--lzhp` (force VaptVupt-LZHP) when you know what you want. is stored per-block; the decoder dispatches to the right path
automatically. Override with `--vv` or `--lzhp`.
--- ---
## VaptVupt Codec ## VaptVupt Codec
VaptVupt is the project's high-performance compression codec. It combines LZ77 dictionary matching with tANS (table-based Asymmetric Numeral Systems) entropy coding and SIMD-accelerated decompression. VaptVupt combines LZ77 dictionary matching with tANS (table-based
Asymmetric Numeral Systems) entropy coding and SIMD-accelerated
decompression.
**This release embeds VaptVupt 2.60.4** (security release: fixes an OOB This release embeds VaptVupt codec 2.60.4 (security release: fixes an OOB
heap write in the AVX2 decode fast path; adds canonical CBMC-verified heap write in the AVX2 decode fast path; adds CBMC-verified BCJ filters
BCJ filters with auto-detection). The codec API is byte-identical with auto-detection). The codec API is byte-identical to the 2.48.x line;
to the 2.48.x line; the 2.48.5 → 2.60.4 upgrades add the optimal parser the 2.48.5 → 2.60.4 upgrades add the optimal parser (measured on our
(measured: text 1.95%, binary 1.31%, source 4.72% smaller on our fixtures: text 1.95%, binary 1.31%, source 4.72% smaller), large-window
fixtures), large-window extreme mode, faster decode (now roughly on par extreme mode, faster decode (roughly on par with zstd-19, up from 1.52×
with zstd-19, up from 1.52× slower), and six upstream corrupt-input slower), and six upstream corrupt-input decoder memory-safety fixes. See
decoder memory-safety fixes. See `CHANGELOG.md` for the full list. [CHANGELOG.md](CHANGELOG.md).
### Architecture ### Architecture
@ -245,7 +292,7 @@ Decoder: AVX2 inline SIMD copies, tiered by offset (32/16/8/overlap), safe-zone
Format: v1 frame (default) and v2 frame (T-tag, min_match=3) for binary data Format: v1 frame (default) and v2 frame (T-tag, min_match=3) for binary data
``` ```
### Three modes ### Modes
| Mode | CLI | Chain Depth | Entropy | Use Case | | Mode | CLI | Chain Depth | Entropy | Use Case |
|------|-----|-------------|---------|----------| |------|-----|-------------|---------|----------|
@ -253,119 +300,105 @@ Format: v1 frame (default) and v2 frame (T-tag, min_match=3) for binary data
| Balanced | `-l 3` to `-l 7` (default) | 48 | 4-way ANS | General backup data | | Balanced | `-l 3` to `-l 7` (default) | 48 | 4-way ANS | General backup data |
| Extreme | `-l 8` to `-l 9` | 256 | Order-1 context ANS + cost-aware lazy parser | Maximum compression | | Extreme | `-l 8` to `-l 9` | 256 | Order-1 context ANS + cost-aware lazy parser | Maximum compression |
The VaptVupt wrapper enables VaptVupt's `format_v2` flag (47% better real-binary ratio) automatically for Balanced and Extreme modes. Ultra-Fast stays on the v1 frame because the `format_v2 + ULTRA_FAST` combination is not yet covered by VaptVupt's upstream test matrix. The wrapper enables the codec's `format_v2` flag (47% better real-binary
ratio) for Balanced and Extreme modes. Ultra-Fast stays on the v1 frame
because the `format_v2 + ULTRA_FAST` combination is not yet covered by the
codec's upstream test matrix.
### Benchmark Results (codec 2.60.4) ### Measured benchmark (codec 2.60.4)
> Full, reproducible measured benchmarks — compression ratio/speed Measured against gzip-9, zstd-3, zstd-19 on a 4-fixture suite (text 10 MB,
> across levels, crypto overhead (KDF vs per-block), and a head-to-head binary-struct 7.5 MB, source code 10 MB, random 5 MB). Decode timed across
> ratio comparison against zstd — are in **[`BENCHMARKS.md`](BENCHMARKS.md)**, 3 runs, minimum reported; wall-clock including the `.zupt` envelope (HMAC
> with the test machine and method stated for every table. etc.). Host: Intel Xeon @ 2.1 GHz, single vCPU, AVX2 build. Reproduce with
`vaptvupt bench <file>`.
> **F-16 (data loss, fixed in 4.0.0):** archives created by **≤ 3.8.0** at
> `-l 8`/`-l 9` whose inputs included x86/ELF/PE executables may be
> **undecodable by any version** (defect at write time in the old
> divergent BCJ encoder). Re-create such archives with 4.0.0 and verify
> extraction before deleting source data. Details in CHANGELOG/AUDIT.
**Measured against gzip-9, zstd-3, zstd-19** on a 4-fixture suite
(text 10 MB, binary-struct 7.5 MB, source code 10 MB, random 5 MB).
Decode timed across 3 runs, minimum reported; wall-clock including the
`.zupt` envelope (HMAC etc.). Host: Intel Xeon @ 2.1 GHz, single vCPU,
codec built at the distribution's default optimisation level (AVX2).
**Reproduce with `vaptvupt bench <file>`** to compare VaptVupt levels,
or the comparative harness in the source tree.
| Fixture | Tool | Ratio | Dec MB/s | | Fixture | Tool | Ratio | Dec MB/s |
|---------------|-----------|---------:|---------:| |---------------|-----------|---------:|---------:|
| text 10 MB | vv-9 | 25.6% | 278 | | text 10 MB | vv-9 | 25.6% | 278 |
| text 10 MB | gzip-9 | 22.6% | 156 | | text 10 MB | gzip-9 | 22.6% | 156 |
| text 10 MB | zstd-3 | 24.2% | 556 | | text 10 MB | zstd-3 | 24.2% | 556 |
| text 10 MB | zstd-19 | **17.6%**| 435 | | text 10 MB | zstd-19 | 17.6% | 435 |
| binary 7.5 MB | vv-9 | 46.1% | 300 | | binary 7.5 MB | vv-9 | 46.1% | 300 |
| binary 7.5 MB | gzip-9 | 46.8% | 123 | | binary 7.5 MB | gzip-9 | 46.8% | 123 |
| binary 7.5 MB | zstd-3 | 44.8% | 577 | | binary 7.5 MB | zstd-3 | 44.8% | 577 |
| binary 7.5 MB | zstd-19 | **41.1%**| 417 | | binary 7.5 MB | zstd-19 | 41.1% | 417 |
| source 10 MB | vv-9 | 4.5% | 714 | | source 10 MB | vv-9 | 4.5% | 714 |
| source 10 MB | gzip-9 | 4.0% | 238 | | source 10 MB | gzip-9 | 4.0% | 238 |
| source 10 MB | zstd-3 | 5.6% | 1000 | | source 10 MB | zstd-3 | 5.6% | 1000 |
| source 10 MB | zstd-19 | **2.7%** | 769 | | source 10 MB | zstd-19 | 2.7% | 769 |
| random 5 MB | vv-9 | 100.0% | 625 | | random 5 MB | vv-9 | 100.0% | 625 |
| random 5 MB | zstd-3 | 100.0% | 681 | | random 5 MB | zstd-3 | 100.0% | 681 |
Honest reading (these are measured numbers, not aspirations): Reading these numbers:
- **On ratio, zstd-19 wins every fixture.** VaptVupt L9 lands between - On ratio, zstd-19 wins every fixture. VaptVupt L9 lands between zstd-3
zstd-3 and zstd-19 on text and binary, beats zstd-3 on source (4.5% and zstd-19 on text and binary, beats zstd-3 on source (4.5% vs 5.6%),
vs 5.6%), and loses to zstd-19 everywhere. If smallest-file is the and loses to zstd-19 everywhere. For smallest-file only, use `xz -9` or
only goal, use `xz -9` or `zstd -19`. `zstd -19`.
- **Decode is now competitive**, not a weakness: 278714 MB/s, in the - Decode is competitive: 278714 MB/s, in the same band as zstd-19 and
same band as zstd-19 (and within ~1.3× of zstd-3). The 2.60.4 codec's within ~1.3× of zstd-3.
decode-speed work (Sprint 53/58) closed most of the gap that existed - Encode throughput is the weakness. The optimal parser and hash-chain
at 2.48.5. The earlier "1.27× zstd-3 decode" headline (inherited from walk that win ratio cost encode speed; balanced mode is ~6× slower than
upstream docs) is **not claimed here** — it did not reproduce in our fast mode. For encode-latency-bound workloads use `-l 1`/`-l 2`.
own single-vCPU measurement. - On a degenerate single-pattern input, large-window extreme (L9) can be
- **Encode throughput remains the weakness.** The optimal parser and slightly worse than L5/L7 — a tradeoff of optimizing for real long-range
depth-24 hash-chain walk that win ratio cost encode speed; balanced matches. It does not affect realistic corpora.
mode is ~6× slower than fast and ~14× slower than zstd-1. For - On random / already-compressed data, all codecs hit the
encode-latency-bound workloads use `vaptvupt compress -l 1`/`-l 2`. incompressibility wall.
- **On a degenerate single-pattern input**, large-window extreme (L9)
is slightly *worse* than L5/L7 — a known tradeoff of optimizing for
real long-range matches. Doesn't affect realistic corpora.
- **On random / already-compressed data**, all codecs hit the
incompressibility wall; the comparison degenerates to
framing-overhead measurement.
### Security Test Results (v4.0.0 release) ### Security regression tests
Every release re-runs the full security regression matrix. These are Every release re-runs the security regression matrix (`make check`,
the v4.0.0 numbers: ≈2 minutes on x86_64 and aarch64). It covers:
| Test | Coverage | Result | - HMAC single-bit tamper detection and honest roundtrips.
|---------------------------------|--------------------------------------------------------------------------|-------------------| - Archive-integrity trailer (header/footer tamper detection).
| F-06 HMAC tamper fuzz | 2000 trials, single-bit flip in HMAC tag | **0 silent accepts** / 2000 honest roundtrips OK | - Byte-level integrity sweep on a PQ archive (every byte flipped).
| F-08 archive-integrity trailer | Header/footer tamper detection | **5/5 pass** | - KDF default (PBKDF2-SHA256) and self-describing header transparency,
| F-09 byte-level integrity sweep | 1827 positions on a PQ-SDK archive, every byte flipped exhaustively | **0/1827 silent accepts** ✓ | with back-compat and fail-closed on unknown profiles.
| F-10 KDF default | Argon2id is the default; PBKDF2 available via `--kdf pbkdf2` | **10/10 pass** | - Indistinguishable wrong-password vs tampered-archive error messages.
| F-11 auth-fail wording | Wrong-password vs tampered-archive messages are indistinguishable | **12/12 pass** | - Encrypted comment block bound to per-block AAD.
| F-12 encrypted comments | Comment block bound to per-block AAD; tamper rejected at extract | **11/11 pass** | - Constant-time comparison (dudect Welch t-test on MAC tag and ML-KEM
| F-15 KDF transparency | Argon2id header self-describes its profile; back-compat + fail-closed | **5/5 pass** | decaps) plus a source-routing guard.
| Constant-time comparisons | dudect Welch t-test (MAC tag + ML-KEM decaps); ~1% of leaky-memcmp + source-routing guard | **2/2 pass** | - Codec exact-`content_size` decode cases (incl. BCJ payloads) under ASan.
| Codec exact-size decode (OOB) | 80 exact-`content_size` cases incl. BCJ payloads, ASan (codec 2.60.4 fix class) | **80/80 pass** | - NIST/RFC test vectors: SHA-256, SHA-3, SHAKE-128, ML-KEM-768,
| pq-box mode | roundtrips L1/L9/BCJ; wrong-key/key-confusion/tamper/cross-mode rejection | **13/13 pass** | AES-256-CTR (SP 800-38A), HMAC-SHA256, X25519, XXH64.
| NIST/RFC test vectors | SHA-256, SHA-3, SHAKE-128, ML-KEM-768, AES-256-CTR (SP 800-38A), HMAC-SHA256, X25519, XXH64 | **16/16 pass** | - Path-traversal refusal, block-swap detection, deduplication correctness,
| Path-traversal | Absolute paths and `..` components refused | **5/5 pass** | and CLI argument-order invariance.
| Block-swap | Re-ordered block detection | **6/6 pass** |
| Dedup property | Deduplication never produces wrong output | **12/12 pass** |
| Audit suite | Curated smoke tests | **10/10 pass** |
| Argument-order | CLI flag ordering doesn't change semantics | **8/8 pass** |
| Distro-safe `make check` | Aggregate of the above (no flaky threading, no `make clean` mid-stream) | **91/91 pass** |
Reproduce: `make check` (≈2 minutes, 91 assertions across 10 suites, `make test` runs the full suite including dist reproducibility and
all green on x86_64 + aarch64). `make test` runs the full 15-suite packaging-syntax checks.
arc including dist reproducibility and packaging-syntax.
### Why VaptVupt? ### Codec notes
VaptVupt's architectural advantages over traditional Huffman-based codecs: - **tANS entropy** — asymptotically optimal coding with single-instruction
decode per symbol (vs Huffman's multi-step tree walk).
- **tANS entropy** — asymptotically optimal coding with single-instruction decode per symbol (vs Huffman's multi-step tree walk) - **4-way interleaved ANS** — decodes 4 symbols per bitstream refill cycle.
- **4-way interleaved ANS** — decodes 4 symbols per bitstream refill cycle, reducing refill overhead by 4× - **4-stream Huffman literal coding** (`lit_fmt=4`) — improves ratio on
- **4-stream Huffman literal coding** (`lit_fmt=4`) — Sprint 105 addition that further improves ratio on structured data structured data.
- **AVX2/NEON SIMD decode** — inline 32-byte copies with tiered offset handling (no function-pointer dispatch). Falls back to scalar on unsupported hardware. - **AVX2/NEON SIMD decode** — inline 32-byte copies with tiered offset
- **Rep-match** — checks 3 recent offsets before hash probe (O(1) vs O(chain_depth)), hits ~30% of matches. Saves 1015 bits per repeated offset. handling. Scalar fallback on unsupported hardware.
- **Order-1 context model** — captures byte-pair correlations in structured data (JSON, CSV, logs) - **Rep-match** — checks 3 recent offsets before the hash probe (O(1) vs
- **Cost-aware lazy parser** (Sprint 120) — the breakthrough that put EXTREME ahead of zstd-3 in aggregate ratio O(chain_depth)), hitting ~30% of matches.
- **Adaptive window** — trial-compresses at wlog=16 vs wlog=20, picks larger window only if ≥3% improvement - **Order-1 context model** — captures byte-pair correlations in structured
- **`format_v2` (T-tag, min_match=3)** — 47% better binary ratio; transparent to v2.33.0+ decoders data (JSON, CSV, logs).
- **Memory hygiene** (Sprint 118) — encoder working buffers scrubbed via `vv_secure_zero` before `free()` - **Cost-aware lazy parser** — puts Extreme mode ahead of zstd-3 in
- **~6,500 lines** of pure C11 — auditable, portable, no external dependencies aggregate ratio.
- **Adaptive window** — trial-compresses at wlog=16 vs wlog=20, picking the
larger window only if ≥3% improvement.
- **`format_v2`** (T-tag, min_match=3) — 47% better binary ratio;
transparent to v2.33.0+ decoders.
- **Memory hygiene** — encoder working buffers scrubbed via
`vv_secure_zero` before `free()`.
- **~6,500 lines** of pure C11.
--- ---
## Post-Quantum Encryption ## Post-Quantum Encryption
`--pq` mode uses hybrid ML-KEM-768 + X25519 key encapsulation per NIST FIPS 203. `--pq` uses hybrid ML-KEM-768 + X25519 key encapsulation per NIST FIPS 203,
in-tree and available in the default build.
``` ```
Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret
@ -373,22 +406,28 @@ Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret
→ AES-256-CTR + HMAC-SHA256 per block → AES-256-CTR + HMAC-SHA256 per block
``` ```
**Security model:** Secure if EITHER ML-KEM-768 (post-quantum) OR X25519 (classical) is secure. Security model: secure if EITHER ML-KEM-768 (post-quantum) OR X25519
(classical) is secure.
**Password mode (`-p`) is NOT quantum-safe.** Use `--pq` for long-term protection. Password mode (`-p`) is not quantum-safe. Use `--pq` for long-term
protection.
The SDK-backed `--pq-sdk` and `--pq-box` modes are optional and require a
`make WITH_SDK=1` build against `libzuptsdk`/`libpqvaptvupt`.
--- ---
## Full-Disk Backup ## Full-Disk Backup
Clone entire disks, partitions, or raw images with compression and encryption in one command. Clone disks, partitions, or raw images with compression and encryption in
one command.
### Quick start ### Quick start
```bash ```bash
# Clone a partition (requires read access) # Clone a partition (requires read access)
sudo vaptvupt disk backup backup.zupt /dev/sda1 sudo vaptvupt disk backup backup.zupt /dev/sda1
# Clone with post-quantum encryption (strongest) # Clone with post-quantum encryption
vaptvupt keygen -o mykey.key vaptvupt keygen -o mykey.key
vaptvupt keygen --pub -o pub.key -k mykey.key vaptvupt keygen --pub -o pub.key -k mykey.key
sudo vaptvupt disk backup --pq pub.key backup.zupt /dev/nvme0n1p2 sudo vaptvupt disk backup --pq pub.key backup.zupt /dev/nvme0n1p2
@ -414,19 +453,24 @@ Source device → Read 4MB blocks → Sparse detection → Compress → Encrypt
└─ Zero blocks stored as STORE (near-zero overhead) └─ Zero blocks stored as STORE (near-zero overhead)
``` ```
VaptVupt reads the source device sequentially in 4MB chunks. Each block is checked for all-zero content (sparse detection uses 8-byte-wide comparison). Zero blocks are stored with codec `STORE` — effectively just the block header with no payload, saving both compression CPU time and archive space. Non-zero blocks are compressed with the selected codec and optionally encrypted. Per-block XXH64 checksums ensure byte-for-byte integrity on restore. VaptVupt reads the source device sequentially in 4MB chunks. Each block is
checked for all-zero content (8-byte-wide comparison). Zero blocks are
stored with codec `STORE` — effectively just the block header with no
payload. Non-zero blocks are compressed with the selected codec and
optionally encrypted. Per-block XXH64 checksums ensure byte-for-byte
integrity on restore.
### Best practices ### Best practices
**Encryption hierarchy (strongest → fastest):** Encryption modes:
| Mode | Command | Security Level | Speed Impact | | Mode | Command | Security Level | Speed Impact |
|------|---------|---------------|-------------| |------|---------|---------------|-------------|
| PQ Hybrid | `--pq pub.key` | Quantum-resistant + classical | ~5% overhead | | PQ Hybrid | `--pq pub.key` | Quantum-resistant + classical | ~5% overhead |
| Password | `-p` | AES-256, PBKDF2 600K iter | ~3% overhead | | Password | `-p` | AES-256, PBKDF2-SHA256 600K iter | ~3% overhead |
| None | (default) | Integrity only (XXH64) | Fastest | | None | (default) | Integrity only (XXH64) | Fastest |
**Compression levels for disks:** Compression levels for disks:
| Level | Mode | Best for | Typical ratio | | Level | Mode | Best for | Typical ratio |
|-------|------|----------|--------------| |-------|------|----------|--------------|
@ -434,41 +478,38 @@ VaptVupt reads the source device sequentially in 4MB chunks. Each block is check
| `-l 4` to `-l 7` | Balanced (default) | General partitions, ext4/NTFS | 25:1 | | `-l 4` to `-l 7` | Balanced (default) | General partitions, ext4/NTFS | 25:1 |
| `-l 8` to `-l 9` | Extreme | Cold storage, archival backups | 310:1 | | `-l 8` to `-l 9` | Extreme | Cold storage, archival backups | 310:1 |
**Operational guidance:** Operational guidance:
- **Unmount before backup** for filesystem consistency. For live systems, use LVM snapshots or filesystem freeze: `fsfreeze -f /mnt/data && vaptvupt disk backup ... && fsfreeze -u /mnt/data`. - Unmount before backup for filesystem consistency. For live systems use
- **Block devices require root** on Linux. Regular files (disk images, `.img`, `.raw`) do not. LVM snapshots or filesystem freeze:
- **Sparse-heavy disks** (freshly formatted, VMs with thin provisioning) compress extremely well — the sparse detector skips zero blocks at memory-copy speed with no compression overhead. `fsfreeze -f /mnt/data && vaptvupt disk backup ... && fsfreeze -u /mnt/data`.
- **Verify after backup** with `vaptvupt test archive.zupt` — checks every block's XXH64 checksum without extracting. - Block devices require root on Linux. Regular files (disk images, `.img`,
- **PQ encryption for long-term** — disk backups stored for years should use `--pq` to resist future quantum attacks. Generate one keypair, store the private key offline, distribute the public key. `.raw`) do not.
- **Restore is non-destructive on files** — writing to a regular file creates/overwrites it. Writing to a block device overwrites the raw device. Double-check the target path before restoring to a device. - Sparse-heavy disks compress well — the sparse detector skips zero blocks
at memory-copy speed with no compression overhead.
### Comparison with other tools - Verify after backup with `vaptvupt test archive.zupt` — checks every
block's XXH64 checksum without extracting.
| Feature | VaptVupt disk | dd + gzip | Clonezilla | partclone | - For long-term disk backups use `--pq`. Generate one keypair, store the
|---------|-----------|-----------|------------|-----------| private key offline, distribute the public key.
| Compression | VaptVupt/LZHP (adaptive) | gzip (fixed) | Multiple | Multiple | - Restore is non-destructive on files (creates/overwrites the file);
| Encryption | AES-256 + PQ hybrid | None (pipe to gpg) | None | None | writing to a block device overwrites the raw device. Double-check the
| Sparse detection | Automatic | None | Filesystem-aware | Filesystem-aware | target path before restoring to a device.
| Per-block integrity | XXH64 per block | None | None | CRC32 |
| Single binary | ✓ (zero deps) | 2+ tools | ISO boot | Multiple |
| Post-quantum | ML-KEM-768 | — | — | — |
| Cross-platform | 6 architectures | ✓ | x86 only | Linux only |
--- ---
## Multi-Architecture Support ## Multi-Architecture Support
VaptVupt builds and runs on all major architectures. The Makefile auto-detects the platform and enables the best available features. The Makefile auto-detects the platform and enables the best available
features.
| Feature | x86_64 | aarch64 | armhf | ppc64le | s390x | riscv64 | | Feature | x86_64 | aarch64 | armhf | ppc64le | s390x | riscv64 |
|---------|--------|---------|-------|---------|-------|---------| |---------|--------|---------|-------|---------|-------|---------|
| Jasmin CT crypto | | C fallback | C fallback | C fallback | C fallback | C fallback | | Jasmin CT crypto | yes | C fallback | C fallback | C fallback | C fallback | C fallback |
| AES-NI hardware | (with AVX) | — | — | — | — | — | | AES-NI hardware | yes (with AVX) | — | — | — | — | — |
| AVX2 SIMD decode | | — | — | — | — | — | | AVX2 SIMD decode | yes | — | — | — | — | — |
| NEON SIMD decode | — | | — | — | — | — | | NEON SIMD decode | — | yes | — | — | — | — |
| Default codec | VaptVupt | VaptVupt | LZHP | LZHP | LZHP | LZHP | | Default codec | VaptVupt | VaptVupt | LZHP | LZHP | LZHP | LZHP |
| All codecs decode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | All codecs decode | yes | yes | yes | yes | yes | yes |
Build for packaging (PIE, hardening flags): Build for packaging (PIE, hardening flags):
```bash ```bash
@ -478,34 +519,11 @@ make install DESTDIR=/buildroot
--- ---
## Feature Comparison
| Feature | VaptVupt v4.0 | gzip | zstd | 7-Zip |
|---------|-----------|------|------|-------|
| Default codec | VaptVupt/LZHP (auto) | DEFLATE | FSE+Huffman | LZMA2 |
| Full-disk backup | **`vaptvupt disk`** | — | — | — |
| Post-quantum encryption | **ML-KEM-768** | — | — | — |
| Password encryption | AES-256 + HMAC | — | — | AES-256 |
| AES-NI hardware accel | **Jasmin-verified** | — | — | — |
| Per-block integrity | XXH64 + HMAC | CRC32 | XXH64 | CRC32 |
| Multi-threaded compress | ✓ | — (pigz) | ✓ | ✓ |
| Multi-threaded decompress | **✓** | — | ✓ | ✓ |
| Formal verification | **Jasmin CT + ACSL** | — | — | — |
| mlock() key protection | ✓ | — | — | — |
| AFL++ fuzz harness | ✓ | — | ✓ | — |
| Multi-architecture | **6 arches** | ✓ | ✓ | ✓ |
| Zero dependencies | ✓ | ✓ | — | — |
| Codebase | ~12K lines | ~10K | ~75K | ~100K+ |
| License | **AGPL+GPL** | GPL/zlib | BSD-3 | LGPL+unRAR |
---
## Security ## Security
``` ```
Password mode: Password → PBKDF2-SHA256 (600K iter) → enc_key + mac_key Password mode: Password → PBKDF2-SHA256 (600K iter) → enc_key + mac_key
PQ hybrid mode: Public key → ML-KEM-768 Encaps + X25519 ECDH → enc_key + mac_key PQ hybrid mode: Public key → ML-KEM-768 Encaps + X25519 ECDH → enc_key + mac_key
SDK v2 mode: HKDF-SHA3 combiner with domain separation + key commitment + HPKE binding
Per-block: AES-256-CTR(enc_key, nonce ⊕ seq) + HMAC-SHA256(mac_key) Per-block: AES-256-CTR(enc_key, nonce ⊕ seq) + HMAC-SHA256(mac_key)
Key protection: mlock() prevents swap, buffer canaries detect overflow Key protection: mlock() prevents swap, buffer canaries detect overflow
Timing: Always-decrypt mitigation (no timing oracle on MAC failure) Timing: Always-decrypt mitigation (no timing oracle on MAC failure)
@ -514,16 +532,16 @@ Path safety: Zip Slip / symlink defenses (zupt_path_is_safe + O_NOFOLLOW)
Verification: 5 Jasmin CT proofs, 19 ACSL contracts, 16 NIST/RFC test vectors Verification: 5 Jasmin CT proofs, 19 ACSL contracts, 16 NIST/RFC test vectors
``` ```
**Audit history:** Three internal audit sprints conducted on the 2.2.x line. The `WITH_SDK=1` build adds an HKDF-SHA3 combiner with domain separation,
**14 bugs** found and fixed across the sprints — including one **HIGH-severity key commitment, and HPKE binding for the `--pq-sdk`/`--pq-box` modes, plus
Zip Slip path traversal** caught in the formal audit pass. Cumulative test the Argon2id KDF.
surface: **265 tests** (47 vaptvupt + 169 SDK + 49 inherited) plus **751,000
mutation-fuzz iterations** under ASAN/UBSAN, all passing. No external audit
yet — see SECURITY.md for honest scope.
See [SECURITY.md](SECURITY.md) for threat model. See [AUDIT.md](AUDIT.md) for Internal audit passes on the 2.2.x line fixed 14 bugs, including a
audit history. See [FORMAL_AUDIT_PROMPT.md](FORMAL_AUDIT_PROMPT.md) for the HIGH-severity Zip Slip path traversal. There has been no external audit.
methodology used in audit sprints. See [SECURITY.md](SECURITY.md) for the threat model and honest scope, and
[FORMAL_AUDIT_PROMPT.md](FORMAL_AUDIT_PROMPT.md) for the audit methodology.
Report security vulnerabilities per [SECURITY.md](SECURITY.md).
--- ---
@ -546,7 +564,7 @@ vaptvupt help
|--------|-------------| |--------|-------------|
| `-l <1-9>` | Compression level (default: 7) | | `-l <1-9>` | Compression level (default: 7) |
| `-t <N>` | Thread count (0=auto, 1=single, 264) | | `-t <N>` | Thread count (0=auto, 1=single, 264) |
| `-p [PW]` | Password encryption (PBKDF2 → AES-256) | | `-p [PW]` | Password encryption (PBKDF2-SHA256 → AES-256) |
| `--pq <keyfile>` | Post-quantum hybrid encryption | | `--pq <keyfile>` | Post-quantum hybrid encryption |
| `-o <DIR>` | Output directory (extract) | | `-o <DIR>` | Output directory (extract) |
| `-s` | Store without compression | | `-s` | Store without compression |
@ -562,9 +580,10 @@ vaptvupt help
## Building ## Building
```bash ```bash
make # Auto-detects arch, Jasmin, AVX2 make # Default build: C compiler + make only
make WITH_SDK=1 # Link libzuptsdk/libpqvaptvupt: --pq-sdk, --pq-box, Argon2id
make V=1 # Verbose build output make V=1 # Verbose build output
make test-all # 77 tests: regression + NIST + VV + MT + PQ + disk make test-all # Regression + NIST + VV + MT + PQ + disk
make test-vv # VaptVupt codec unit tests only make test-vv # VaptVupt codec unit tests only
make test-asan # AddressSanitizer + UBSan build make test-asan # AddressSanitizer + UBSan build
make fuzz-build # AFL++ fuzzing harnesses make fuzz-build # AFL++ fuzzing harnesses
@ -586,13 +605,15 @@ vaptvupt bench --compare ~/Documents/ # Compare codecs on your own data
| ID | Name | Algorithm | Default on | Override | | ID | Name | Algorithm | Default on | Override |
|----|------|-----------|------------|----------| |----|------|-----------|------------|----------|
| `0x0010` | **VaptVupt** | LZ77 + tANS + AVX2/NEON SIMD | x86_64 (AVX2), aarch64 (NEON) | `--vv` | | `0x0010` | VaptVupt | LZ77 + tANS + AVX2/NEON SIMD | x86_64 (AVX2), aarch64 (NEON) | `--vv` |
| `0x000A` | **VaptVupt-LZHP** | LZ77 + Huffman + byte prediction | armhf, ppc64le, s390x, riscv64 | `--lzhp` | | `0x000A` | VaptVupt-LZHP | LZ77 + Huffman + byte prediction | armhf, ppc64le, s390x, riscv64 | `--lzhp` |
| `0x0009` | VaptVupt-LZH | LZ77 + Huffman | — | — | | `0x0009` | VaptVupt-LZH | LZ77 + Huffman | — | — |
| `0x0008` | VaptVupt-LZ | Fast LZ77, 64KB window | — | `-f` | | `0x0008` | VaptVupt-LZ | Fast LZ77, 64KB window | — | `-f` |
| `0x0000` | Store | No compression | — | `-s` | | `0x0000` | Store | No compression | — | `-s` |
All codecs are forward-compatible: archives created with any codec can be read by any VaptVupt version that includes that codec, on any architecture. VaptVupt archives require VaptVupt v2.0+. All codecs are forward-compatible: archives created with any codec can be
read by any VaptVupt version that includes that codec, on any architecture.
VaptVupt archives require VaptVupt v2.0+.
--- ---
@ -603,28 +624,17 @@ All codecs are forward-compatible: archives created with any codec can be read b
| v0.1v0.6 | LZ77 compression, AES-256 encryption, multi-threading | | v0.1v0.6 | LZ77 compression, AES-256 encryption, multi-threading |
| v0.7 | Post-quantum hybrid encryption (ML-KEM-768 + X25519) | | v0.7 | Post-quantum hybrid encryption (ML-KEM-768 + X25519) |
| v1.0 | Stable release — format frozen v1.4, security audit | | v1.0 | Stable release — format frozen v1.4, security audit |
| v1.1v1.4 | X25519 fix, NIST vectors, CPUID detection, Jasmin source files fixed | | v1.1v1.5.5 | X25519 fix, NIST vectors, CPUID detection, Jasmin CT assembly linked, build-system improvements |
| v1.5 | Jasmin CT assembly linked (MAC verify + ML-KEM select active) | | v2.0 | VaptVupt codec, auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches), `--lzhp` |
| v1.5.5 | Build system improvements: man page install rules, verbose mode, multi-arch detection | | v2.1.x | Cross-block dictionary carry, Termux/Android build fix, full-disk backup/restore, LZHP fix, CodeQL fixes, block-level deduplication |
| v2.0 | VaptVupt 1.1.0 codec, auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, copy_match/litlen fixes, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches), --lzhp flag | | v2.2.x | libzuptsdk integration (`--pq-sdk`), VaptVupt 2.48.2 codec (cost-aware lazy parser, 4-stream Huffman, `format_v2`), audit findings F-01..F-07 closed (incl. F-06 high) |
| v2.1.0 | VaptVupt 1.4.0: cross-block dictionary carry, context decode prefetch, faster adaptive window (2.6× encode), integration API | | v2.3.x | F-08/F-09 closed: archive-integrity trailer + preface-AAD MAC (format v1.5 → v1.6) |
| v2.1.1 | Termux/Android build fix, arch-safety guard, Keccak ROL64 UB fix, zero UBSan violations | | v2.4.x | PBKDF2/Argon2id KDF work (F-10), error-message hygiene (F-11), encrypted comments (F-12), packaging arc (deb/RPM/AUR/Nix/Homebrew/OBS), THREAT_MODEL.md, manpage + completions, distro-safe `make check` |
| v2.1.2 | Full-disk backup/restore (`disk` subcommand), sparse detection, all encryption modes, progress bar | | v3.0.x | Renamed Zupt → VaptVupt (INPI Brasil trademark), VV codec 2.48.5, GUI fixes, F-13 fix. Wire format unchanged; `zupt` kept as compat symlink |
| v2.1.3 | LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests | | v3.1.0v3.3.0 | Codec 2.48.5 → 2.53.3, decode over-copy fix, SHA-256 hardware acceleration (Intel SHA-NI), incremental per-block HMAC |
| v2.1.4 | CodeQL: 4 security fixes — TOCTOU races eliminated (fstat on fd), X25519 scalar wipe via volatile | | v3.4.0v3.8.0 | F-15 KDF parameter transparency, measured constant-time MAC comparison (dudect), NIST SP 800-38A AES-CTR vectors, ML-KEM decaps through the CT primitive, consolidated benchmarks |
| v2.1.5 | Block-level deduplication (`--dedup`), XXH64 fingerprint index, DEDUP_REF block type, 81 tests | | v4.0.0 | Codec 2.60.4 security release (OOB heap write fixed in AVX2 decode fast path), `--pq-box` sealed-box mode, F-16 data-loss disclosure + fix (old in-tree BCJ encoder), CBMC-verified BCJ filters with auto ELF/PE/Mach-O detection, SHA-NI acceleration. Wire format v1.6 |
| v2.2.0v2.2.2 | libzuptsdk 2.0 integration (HKDF-SHA3 combiner + key commitment + HPKE binding + Argon2id), `--pq-sdk` mode (XChaCha20-Poly1305 / AES-256-SIV), license-hygiene cleanup, full SPDX coverage | | v4.1.0 | Source-only tree (prebuilt libzuptsdk/libpqvaptvupt removed); default build needs only a C compiler + make; native `--pq` is the default PQ mode; `--pq-sdk`/`--pq-box`/Argon2id gated behind `make WITH_SDK=1`. Wire format stays v1.6 |
| v2.2.3 | VaptVupt 2.48.2 codec integration: cost-aware lazy parser, 4-stream Huffman, `format_v2` flag (47% better binary), encoder memory hygiene (`vv_secure_zero` on free). Makefile arch-detection fix. ASAN/UBSAN clean across plain/password/PQ-SDK at all levels |
| v2.2.4v2.2.5 | Audit sprint: findings F-01..F-07 closed, including F-06 (high) HMAC accept-on-disjoint-bits |
| v2.3.0v2.3.1 | F-08/F-09 closed: archive-integrity trailer + preface-AAD MAC — exhaustive byte sweep 0/1827 undetected (format v1.5 → v1.6) |
| v2.4.x | Argon2id default KDF (F-10), error-message hygiene (F-11), encrypted archive comments (F-12), packaging arc (deb, RPM, AUR, Nix, Homebrew, openSUSE OBS), CI rewrite, THREAT_MODEL.md, manpage + shell completions, distro-safe `make check` |
| v3.0.0 | **Renamed Zupt → VaptVupt** (INPI Brasil trademark), VV codec 2.48.5, GUI binary-discovery fix. Wire format unchanged; `zupt` kept as compat symlink |
| v3.0.1v3.0.3 | GUI license/version-parsing cleanup, F-13 (usage() literal size), static-analysis cleanup |
| v3.1.0 | Codec 2.48.5 → 2.53.3, decode over-copy fix |
| v3.2.0v3.3.0 | SHA-256 hardware acceleration (Intel SHA-NI), incremental per-block HMAC (drops a malloc + full copy per block) |
| v3.4.0v3.7.0 | F-15 KDF parameter transparency, measured constant-time MAC comparison (dudect), NIST SP 800-38A AES-CTR vectors, ML-KEM decaps routed through the audited CT primitive |
| v3.8.0 | Consolidated measured benchmarks + constant-time test robustness |
| **v4.0.0** | **Codec 2.60.4 security release (OOB heap write fixed in AVX2 decode fast path), `--pq-box` sealed-box mode (libpqvaptvupt 0.6.0, HKDF-SHA256 combiner), F-16 data-loss disclosure + fix (old in-tree BCJ encoder), CBMC-verified BCJ filters with auto ELF/PE/Mach-O detection, SHA-NI measured 5.8×. Wire format stays v1.6** |
See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes. See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes.
@ -632,15 +642,19 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes.
## License ## License
VaptVupt is **dual-licensed**: VaptVupt is dual-licensed:
- **AGPL-3.0-or-later** — most of the codebase (CLI, libzuptsdk, GUI, Jasmin source). See [`LICENSE`](LICENSE). - **AGPL-3.0-or-later** — most of the codebase (CLI, GUI, Jasmin source).
- **GPL-3.0-or-later** — the VaptVupt LZ codec only (`src/vv_*.c`, `src/vaptvupt_api.c` and headers). VaptVupt is GPL so it can be considered for upstreaming into the Linux/BSD kernels. See [`LICENSE`](LICENSE).
- **Commercial license** available for relief from AGPL/GPL terms. Contact `sac@securityops.co`. - **GPL-3.0-or-later** — the VaptVupt LZ codec only (`src/vv_*.c`,
`src/vaptvupt_api.c` and headers), so it can be considered for
upstreaming into the Linux/BSD kernels.
- **Commercial license** available for relief from AGPL/GPL terms. Contact
`sac@securityops.co`.
Every source file carries an explicit SPDX header. See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) for full attribution. VaptVupt contains **no third-party source code** — every line is original work. Every source file carries an explicit SPDX header. See
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) for full attribution.
Security vulnerabilities: see [SECURITY.md](SECURITY.md). VaptVupt contains no third-party source code.
## Related projects ## Related projects
@ -652,8 +666,5 @@ All by Cristian Cezar Moisés, hosted on git.securityops.co:
- [libvuptsdk](https://git.securityops.co/cristiancmoises/libvuptsdk) — Standalone C SDK - [libvuptsdk](https://git.securityops.co/cristiancmoises/libvuptsdk) — Standalone C SDK
- [vaptvupt-codec](https://git.securityops.co/cristiancmoises/vaptvupt-codec) — Standalone LZ + tANS codec - [vaptvupt-codec](https://git.securityops.co/cristiancmoises/vaptvupt-codec) — Standalone LZ + tANS codec
## Support the Project
If you find VaptVupt useful, please consider sharing it or contributing — see the README footer for contact links.
--- ---
© 2026 Cristian Cezar Moisés — [git.securityops.co/cristiancmoises](https://git.securityops.co/cristiancmoises) © 2026 Cristian Cezar Moisés — [git.securityops.co/cristiancmoises](https://git.securityops.co/cristiancmoises)

View file

@ -1,90 +0,0 @@
# VaptVupt — Roadmap
## Released
| Version | Status | Description |
|---------|--------|-------------|
| v0.1 | ✅ | Initial release — LZ77 compression, `.zupt` format, XXH64 checksums |
| v0.2 | ✅ | AES-256-CTR + HMAC-SHA256 encryption, PBKDF2, directory recursion |
| v0.3 | ✅ | VaptVupt-LZH codec — LZ77 + Huffman, 1MB window, near-optimal parsing |
| v0.4 | ✅ | Byte prediction preprocessor (VaptVupt-LZHP), solid mode |
| v0.5 | ✅ | Security hardening — 16 bug fixes, Huffman codec fix, CSPRNG hardened |
| v0.6 | ✅ | Multi-threaded compression (`-t N`), batch-parallel pipeline |
| v0.7 | ✅ | Post-quantum hybrid encryption (ML-KEM-768 + X25519) |
| v1.0 | ✅ | Stable release — format frozen v1.4, security audit, MIT license |
| v1.1 | ✅ | X25519 formula fix, 13 NIST/RFC test vectors, zero `-Wpedantic` warnings |
| v1.2 | ✅ | CPUID runtime detection (AES-NI, AVX2, SSE4.1, PCLMUL) |
| v1.3 | ✅ | ACSL predicates, Jasmin source files (initial), security review |
| v1.4 | ✅ | All 4 Jasmin `.jazz` files compile on jasminc 2026.03.0 |
| **v1.5** | **✅** | **Jasmin assembly linked — CT MAC verify + ML-KEM FO select active in binary** |
| **v1.5.5** | **✅** | **Build system improvements: man page install rules, verbose mode, multi-arch detection** |
| **v2.0** | **✅** | **VaptVupt 1.1.0 codec with auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, copy_match/litlen fixes, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches)** |
| **v2.1** | **✅** | **VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API** |
| **v2.1.1** | **✅** | **Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs** |
| **v2.1.2** | **✅** | **Full-disk backup/restore with sparse detection, all encryption modes, progress bar, 77 tests** |
| **v2.1.3** | **✅** | **Disk restore rewritten — shared block I/O, fixes checksum mismatch on encrypted/PQ archives** |
| **v2.1.3** | **✅** | **LZHP prediction encoding fix, shared write_enc_header, SOLID flag removed from disk, 78 tests** |
| **v2.1.4** | **✅** | **CodeQL: 4 security fixes — TOCTOU races (fstat on fd), X25519 scalar wipe (volatile), 78 tests** |
| **v2.1.5** | **✅** | **Block-level deduplication (--dedup), XXH64 fingerprint index, DEDUP_REF block type, 81 tests** |
| v2.2.4 | ✅ | Five-finding audit pass: help-format, flaky audit (F-02a), `-Wshadow`, missing-prototype on ML-KEM selftest (now wired as vector 14/14), three `const` cppcheck hints; F-02b (index MAC) opened as deferred |
| v2.2.5 | ✅ | F-06 (high): HMAC verifier silently accepted ~6.35% of single-bit MAC tampers on the Jasmin (x86_64) path. F-07: structural check that block at `index_offset` claims `INDEX` type. F-02b reclassified as resolved (the index IS MAC'd; the verifier was buggy). F-08 (cosmetic-metadata coverage) opened, deferred to v2.3.0. |
| v2.3.0 | ✅ | F-08 closed: archive-integrity-trailer (32B AIT after footer; HMAC-SHA256 over `hdr ‖ ft[0..23]` in encrypted modes, XXH64 best-effort in plaintext). Format v1.4 → v1.5. Backward-compat read path with downgrade warning on legacy v1.4 archives. Exhaustive byte sweep: 86 → 18 undetected positions (all per-block-header trivia, deferred to v2.3.1 as F-09). New `tests/test_f08_topmac.sh` regression. |
| v2.3.1 | ✅ | F-09 closed: per-block frame preface bound into MAC via extended-AAD primitives + strict structural validation of the encryption-header block. Format v1.5 → v1.6. Reaches 100% byte-level tamper detection on encrypted archives — exhaustive sweep on 1827-byte v1.6 PQ-SDK archive: 0/1827 silent accepts. v2.3.0 archives extract unchanged; v2.3.0 cleanly rejects v2.3.1 archives (no silent corruption). |
| v2.4.0 | ✅ | Methodology release. `PROMPT.md` → v2: NEW §3.5 exhaustive byte-sweep mandate after format changes, sprint protocol gains a step, §11 outage table grows four rows for F-06..F-09. Makefile help banner now auto-derived from `include/zupt.h` (closes a recurring banner-drift bug). No source/binary changes; archives byte-identical to v2.3.1. |
| v2.4.1 | ✅ | F-10: password-mode KDF default flipped from PBKDF2-SHA256 to Argon2id (libzuptsdk). PBKDF2 remains available via `--kdf pbkdf2` for v2.4.0-and-older reader compatibility. No format change; v2.4.0 already supports reading Argon2id archives via existing enc_type dispatch. F-11 (auth-fail vs integrity-fail error message UX) opened, deferred. |
| v2.4.2 | ✅ | F-11 closed: wrong-password and tampered-archive error messages collapsed into one uniform `Authentication failed (wrong key, wrong password, or tampered archive)` line. Detailed top-MAC wording moves behind `--verbose`. Plaintext tamper keeps detailed XXH64 wording (no key, no oracle concern). Eliminates a verbal probe-oracle that was leaking which failure cause hit first. No format change. |
| v2.4.3 | ✅ | F-12 closed: encrypted archive comments. Implements the previously-reserved `comment_offset` header field via new block type `ZUPT_BLOCK_COMMENT = 0x05`. Comments are UTF-8, up to 4096 bytes, encrypted using the same per-block AEAD pipeline as data blocks (including F-09 preface AAD). `hdr.comment_offset` is in the AIT-signed region, so pointer tampering → auth-fail. CLI flags `-c` / `--comment` and `--comment-file`. `vaptvupt info` reports presence without decrypting; `vaptvupt x` displays comment after extract. v2.4.2 readers extract v2.4.3 archives byte-exact (they ignore `comment_offset`). Format still v1.6. Exhaustive byte sweep on 1878-byte archive with comment: 0/1878 silent accepts. |
| v2.4.4 | ✅ | Distribution packaging + reproducible source tarball. New `make dist` produces byte-identical `vaptvupt-VERSION.tar.gz`; regression test `tests/test_dist_reproducible.sh` asserts two consecutive runs produce identical sha256. Upstream packaging recipes added at `packaging/aur/PKGBUILD`, `packaging/debian/{control,rules,changelog,copyright,source/format}`, and `packaging/homebrew/vaptvupt.rb`. No source-code changes, no format changes. |
| v2.4.5 | ✅ | Packaging arc completion. New `packaging/rpm/vaptvupt.spec` (Fedora/RHEL/CentOS) and `packaging/nix/flake.nix` (NixOS, x86_64 + aarch64). New `DISTRIBUTION.md` covers all 5 packaging methods with concrete submission flows. New `tests/test_packaging_syntax.sh` (18 assertions, wired into `make test`) enforces cross-recipe version consistency and basic syntax validity. No source-code changes. |
| v2.4.6 | ✅ | CI + threat model. Rewrote `.github/workflows/ci.yml` from 4 jobs to 8 (matrix builds, strict warnings, ASAN, PIE, aarch64, dist-reproducibility, packaging-syntax, tag-triggered release). New `THREAT_MODEL.md` (12 KB) documents what VaptVupt protects against and — explicitly per userPreferences — what it does NOT. Packaging-syntax test expanded 18 → 22. No source-code changes. |
| v2.4.7 | ✅ | Manpage refresh + shell completions. |
| v2.4.8 | ✅ | Distro-safe `make check` target + binary packages. |
| v3.0.0 | ✅ | MAJOR: Zupt → VaptVupt rename, VV codec 2.48.5, GUI binary-discovery fix. |
| v3.0.1 | ✅ | GUI license + version-parsing cleanup. |
| v3.0.2 | ✅ | F-13 closed (usage() string-literal length) + help-text drift cleanup. |
| v3.0.3 | ✅ | Static-analysis cleanup (cppcheck + -Wconversion). |
| v3.1.0 | ✅ | VaptVupt codec 2.48.5 → 2.53.3 + F-14 decode over-copy fix. |
| v3.2.0 | ✅ | SHA-256 hardware acceleration (Intel SHA-NI). |
| v3.3.0 | ✅ | Incremental HMAC-SHA256 (per-block MAC malloc + copy eliminated). |
| v3.4.0 | ✅ | F-15: Argon2id KDF parameter transparency (self-describing header). |
| v3.5.0 | ✅ | Measured constant-time MAC comparison (dudect-style). |
| v3.6.0 | ✅ | NIST SP 800-38A AES-256-CTR vectors + ML-KEM self-test fixes. |
| v3.7.0 | ✅ | ML-KEM decaps comparison routed through the audited CT primitive. |
| **v4.0.0** | **✅ Current** | **Stack integration release. Codec → canonical VaptVupt 2.60.4 (security: fixes high-severity OOB heap write in AVX2 decode on exact-`content_size` buffers; brings CBMC-verified BCJ with auto-detection; ratio gate verified byte-identical on identical inputs). F-16 disclosed and fixed: ≤3.8.0's divergent pre-release BCJ encoder wrote undecodable archives on executable content at L8/L9 — affected archives must be re-created with 4.0.0. New `--pq-box` recipient mode (ZUPT_ENC_PQ_BOX_V1, 0x05) via vendored libpqvaptvupt 0.6.0: ML-KEM-768 + X25519 through HKDF-SHA256 domain-separated combiner, magic-tagged keyfiles, 13/13 adversarial suite, ASan/UBSan clean. SHA-NI finally measured on capable silicon: 5.8× (204→1184 MB/s) — the v3.2.0 [ESTIMATED] is retired. Clang restored to the strict matrix (as(1) for Jasmin output). Wire v1.6 unchanged; 8-mode back-compat matrix byte-exact. 26 suites, test_vectors 16/0.** |
| v3.8.0 | ✅ Shipped | **Consolidated measured benchmarks (documentation-only; no source/crypto/wire change, v1.6 identical to 3.7.0). New BENCHMARKS.md publishes a complete reproducible benchmark set with the test machine + method stated for every table: compression ratio + encode/decode throughput at L9 across 5 fixtures; encode-speed-vs-level trade-off (L1 ≈88 MB/s at 2.55×, L9 ≈1 MB/s at 3.90×); encryption overhead separating the one-time KDF (Argon2id ≈741 ms, PBKDF2 ≈1562 ms) from per-block crypto (≈147 MB/s) and plain throughput (≈944 MB/s); and a head-to-head ratio comparison vs zstd-3/zstd-19 that plainly shows where VaptVupt loses. Previously the only documented benchmarks were codec-ratio numbers dated v3.1.0; the crypto-path data measured across 3.2.03.7.0 was never consolidated. SHA-NI speedup explicitly marked [ESTIMATED] (test box has no SHA-NI). README benchmark section re-dated and linked to BENCHMARKS.md. 24/24 suites green, test_vectors 16/0, F-09 0/1827.** |
## Planned
| Version | Status | Description |
|---------|--------|-------------|
| v2.3.0 | ✅ shipped | (see "released" table) |
| v2.3.1 | ✅ shipped | (see "released" table — F-09 closed) |
| v2.1 | 📋 Planned | Homebrew, AUR, Debian, RPM, Nix packages |
| v2.2 | 📋 Planned | Coverity Scan, clang-tidy security checkers, Frama-C Eva analysis |
| v2.3 | 📋 Planned | Silesia corpus benchmarks, performance tuning, NEON ARM64 decode path |
| v3.0 | 🔮 Future | EasyCrypt machine-verified proofs for Jasmin crypto, independent audit |
## Priority Order
```
v1.6 AES-NI wired in ← closes #1 security gap (table-based AES)
v1.7 X25519 Jasmin wired in ← all 4 Jasmin functions active
v1.8 ACSL + Frama-C ← formal memory safety proofs
v1.9 mlock + fuzzing ← closes remaining hardening gaps
v2.0 Performance ← 4× AES throughput, parallel decompression
```
## Security Gap Status
| Gap | Severity | Status |
|-----|----------|--------|
| Table-based AES (cache-timing) | High | **✅ Closed v2.0** — AES-NI Jasmin |
| X25519 fe_cswap CT | Low | **✅ Closed v2.0** — Jasmin |
| No mlock() for keys | Medium | **✅ Closed v2.0** |
| No fuzzing | Medium | **✅ Closed v2.0** — AFL++ |
| ACSL unproved | Low | **✅ Closed v2.0** — 19 contracts |
| No independent audit | Medium | Open — target v3.0 |
---
© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later

View file

@ -1,13 +1,23 @@
# Security Policy — VaptVupt # Security Policy — VaptVupt 4.1.0
## Reporting Vulnerabilities ## Reporting Vulnerabilities
**Be free to report vulnerabilities. For high-risk send an email.** Report privately by email to **zupt@riseup.net** with `[security]` in the
subject. Do not open a public issue on the project's git server.
Email: **zupt@riseup.net** Include:
Include: description, reproduction steps, impact assessment. - Version (`vaptvupt --version`) and platform.
Response within 48 hours. Fix within 30 days for critical issues. - Description, impact assessment, and a reproduction (a minimal archive or
a code snippet).
Disclosure SLA: acknowledgement within 5 business days; target fix within
30 days for high-severity issues. Coordinated disclosure preferred; the
timeline is discussed case by case. A PGP key is on the project's
keyserver entry.
The project has not had an external independent audit. For high-stakes
deployments, treat it as "reviewed but unaudited" and do your own review.
--- ---
@ -15,11 +25,19 @@ Response within 48 hours. Fix within 30 days for critical issues.
| Mode | CLI Flag | Algorithm | PQ-Safe? | Use Case | | Mode | CLI Flag | Algorithm | PQ-Safe? | Use Case |
|------|----------|-----------|----------|----------| |------|----------|-----------|----------|----------|
| Password | `-p` | PBKDF2-SHA256 → AES-256-CTR + HMAC-SHA256 | **No** | Short-term backups, personal use | | Password | `-p` | PBKDF2-SHA256 → AES-256-CTR + HMAC-SHA256 | No | Short-term backups, personal use |
| PQ Hybrid | `--pq` | ML-KEM-768 + X25519 → AES-256-CTR + HMAC-SHA256 | **Yes** | Long-term archives, high-value data | | PQ Hybrid | `--pq` | ML-KEM-768 + X25519 → AES-256-CTR + HMAC-SHA256 | Yes | Long-term archives, high-value data |
| None | (default) | No encryption (compression only) | N/A | Non-sensitive data | | None | (default) | No encryption (compression only) | N/A | Non-sensitive data |
**Password mode (`-p`) is NOT quantum-safe.** For protection against "harvest now, decrypt later" quantum attacks, use `--pq` mode. Password mode (`-p`) is not quantum-safe. For protection against "harvest
now, decrypt later" quantum attacks, use `--pq` — the recommended
post-quantum mode. `--pq` is native and in-tree; it needs no external
library.
Optional SDK modes (`--pq-sdk`, `--pq-box`) are available only in an
upstream `make WITH_SDK=1` build linked against the separately distributed
libzuptsdk / libpqvaptvupt libraries. They are not part of the default
build and are not defaults.
--- ---
@ -29,7 +47,8 @@ Response within 48 hours. Fix within 30 days for critical issues.
|-----------|-----------|----------|----------|---------------| |-----------|-----------|----------|----------|---------------|
| Symmetric encryption | AES-256-CTR | FIPS 197 | 256-bit | 128-bit post-quantum (Grover) | | Symmetric encryption | AES-256-CTR | FIPS 197 | 256-bit | 128-bit post-quantum (Grover) |
| Authentication | HMAC-SHA256 | RFC 2104 | 256-bit | 128-bit post-quantum (Grover) | | Authentication | HMAC-SHA256 | RFC 2104 | 256-bit | 128-bit post-quantum (Grover) |
| Password KDF | PBKDF2-SHA256 | RFC 8018 | 600K iterations | Password-dependent | | Password KDF (default) | PBKDF2-SHA256 | RFC 8018 | 600K iterations | Password-dependent |
| Password KDF (WITH_SDK=1 option) | Argon2id | RFC 9106 | OWASP minimums | Password-dependent, memory-hard |
| Post-quantum KEM | ML-KEM-768 | FIPS 203 | 1184B pk / 2400B sk | NIST Level 3 | | Post-quantum KEM | ML-KEM-768 | FIPS 203 | 1184B pk / 2400B sk | NIST Level 3 |
| Classical KEM | X25519 | RFC 7748 | 32B scalar | ~128-bit classical | | Classical KEM | X25519 | RFC 7748 | 32B scalar | ~128-bit classical |
| Hybrid KDF | SHA3-512 | FIPS 202 | 512-bit output | Secure if either KEM holds | | Hybrid KDF | SHA3-512 | FIPS 202 | 512-bit output | Secure if either KEM holds |
@ -37,6 +56,9 @@ Response within 48 hours. Fix within 30 days for critical issues.
| Hashing | SHA3-256, SHA3-512 | FIPS 202 | 256/512-bit | Standard | | Hashing | SHA3-256, SHA3-512 | FIPS 202 | 256/512-bit | Standard |
| Random | OS CSPRNG | getrandom(2) / RtlGenRandom | N/A | Hard fail if unavailable | | Random | OS CSPRNG | getrandom(2) / RtlGenRandom | N/A | Hard fail if unavailable |
The default build uses PBKDF2-SHA256 (600k iterations) for password mode.
Argon2id is available only in a `make WITH_SDK=1` build.
--- ---
## Security Architecture ## Security Architecture
@ -54,12 +76,14 @@ For each data block (sequence 0, 1, 2, ...):
### Encrypt-then-MAC ### Encrypt-then-MAC
HMAC is computed over `nonce ‖ ciphertext` and verified **before** any decryption. This prevents: HMAC is computed over `nonce ‖ ciphertext` and verified **before** any
decryption. This prevents:
- Chosen-ciphertext attacks - Chosen-ciphertext attacks
- Padding oracle attacks - Padding oracle attacks
- Processing of tampered data - Processing of tampered data
### Hybrid Post-Quantum KEM ### Hybrid Post-Quantum KEM (`--pq`)
``` ```
Encapsulation: Encapsulation:
@ -73,7 +97,14 @@ Encapsulation:
mac_key = archive_key[32:64] mac_key = archive_key[32:64]
``` ```
**Security model:** Secure if EITHER ML-KEM-768 (post-quantum, NIST Level 3) OR X25519 (classical, ~128-bit) remains unbroken. Both must be compromised simultaneously to recover the archive key. Same approach as Signal (PQXDH), Apple iMessage (PQ3), and OpenSSH 9.0+. Security model: secure if EITHER ML-KEM-768 (post-quantum, NIST Level 3)
OR X25519 (classical, ~128-bit) remains unbroken. Both must be compromised
simultaneously to recover the archive key. Same approach as Signal
(PQXDH), Apple iMessage (PQ3), and OpenSSH 9.0+.
The `--pq-sdk` mode (WITH_SDK=1 only) uses an HKDF-SHA3-256 combiner, a
32-byte key commitment tag, HPKE-style context binding (RFC 9180 §5),
anti-fault double decapsulation, and XChaCha20-Poly1305 AEAD.
--- ---
@ -86,7 +117,10 @@ Encapsulation:
| `zupt_mac_verify_ct` | HMAC comparison (32 bytes) | Jasmin type system: no branch on diff value | | `zupt_mac_verify_ct` | HMAC comparison (32 bytes) | Jasmin type system: no branch on diff value |
| `zupt_ct_select_32` | ML-KEM FO implicit rejection | Jasmin type system: no branch on cond value | | `zupt_ct_select_32` | ML-KEM FO implicit rejection | Jasmin type system: no branch on cond value |
These functions are compiled from Jasmin source to x86-64 assembly. The Jasmin compiler enforces that no secret-typed variable flows into branch conditions or memory addresses. This guarantee holds at the machine code level — no C compiler optimization can introduce timing leaks. These functions are compiled from Jasmin source to x86-64 assembly. The
Jasmin compiler enforces that no secret-typed variable flows into branch
conditions or memory addresses. This guarantee holds at the machine code
level — no C compiler optimization can introduce timing leaks.
### C Constant-Time (branchless, compiler-dependent) ### C Constant-Time (branchless, compiler-dependent)
@ -101,7 +135,7 @@ These functions are compiled from Jasmin source to x86-64 assembly. The Jasmin c
| Function | Risk | Mitigation | | Function | Risk | Mitigation |
|----------|------|------------| |----------|------|------------|
| AES-256 block encrypt | **HIGH** on shared hardware — S-box table lookups leak via cache timing | Jasmin AES-NI path planned; do not use on multi-tenant VMs | | AES-256 block encrypt | HIGH on shared hardware — S-box table lookups leak via cache timing | Jasmin AES-NI path planned; do not use on multi-tenant VMs |
| SHA-256 | Low — table constants are public, not indexed by secret data | Accepted | | SHA-256 | Low — table constants are public, not indexed by secret data | Accepted |
--- ---
@ -114,37 +148,66 @@ These functions are compiled from Jasmin source to x86-64 assembly. The Jasmin c
|-------|-----------| |-------|-----------|
| File contents | AES-256-CTR encryption | | File contents | AES-256-CTR encryption |
| File names, sizes, structure | Encrypted in central index block, HMAC-protected | | File names, sizes, structure | Encrypted in central index block, HMAC-protected |
| Archive integrity (payloads + index) | Per-block HMAC-SHA256 (v2.2.5+ verifier — see CHANGELOG F-06) | | Archive integrity (payloads + index) | Per-block HMAC-SHA256 |
| Archive integrity (header + footer metadata) | **v1.5 archives**: 32-byte archive-integrity-trailer HMAC-SHA256 over `hdr ‖ ft[0..23]`. **v1.4 archives**: not covered, downgrade warning on extract (see F-08 / CHANGELOG 2.3.0). | | Archive integrity (header + footer metadata) | v1.5+ archives: 32-byte archive-integrity-trailer HMAC-SHA256 over `hdr ‖ ft[0..23]`. v1.4 archives: not covered, downgrade warning on extract. |
| Against stolen backups | AES-256 requires key/password to read | | Against stolen backups | AES-256 requires key/password to read |
| Against tampering of file contents, names, sizes, offsets | HMAC detects any modification (overwhelming probability after F-06 fix) | | Against tampering of file contents, names, sizes, offsets | HMAC detects any modification |
| Against tampering of header+footer metadata | **v1.5: top-MAC detects tamper.** v1.4: not detected (legacy; re-archive with v2.3.0+ to upgrade). | | Against tampering of per-block frame preface bytes (codec_id, block_flags, varints, plaintext-XXH64) | v1.6: per-block MAC binds the canonical preface AAD; encryption-header block validated structurally |
| Against tampering of per-block frame preface bytes (codec_id, block_flags, varints, plaintext-XXH64) | **v1.6 (F-09)**: per-block MAC binds the canonical preface AAD; encryption-header block validated structurally. v1.5 and older: partial detection only (parser/decoder rejects malformed values; explicit MAC coverage was v1.6 work). | | Against tampering of archive comment (when present) | Comment block goes through the same per-block AEAD pipeline as data (AES-256-CTR + HMAC-SHA256 + preface AAD); `hdr.comment_offset` pointer is in the AIT-signed region |
| Against tampering of archive comment (when present) | v2.4.3 (F-12): comment block goes through the same per-block AEAD pipeline as data (AES-256-CTR + HMAC-SHA256 + preface AAD); `hdr.comment_offset` pointer is in the AIT-signed region. Both payload and pointer are MAC-covered end-to-end. | | Against block-swap (reorder) attacks | MAC binds an 8-byte position AAD; a block moved to another position fails verification and its partial output is unlinked. Dedup refs use sentinel seq=0 and rely on plaintext XXH64 for per-block integrity. |
| Against quantum adversary | `--pq` mode: ML-KEM-768 (NIST Level 3) | | Against malicious archive entries (Zip Slip / path traversal) | `zupt_path_is_safe()` rejects `..`, absolute paths, Windows drive/UNC paths, embedded NULs |
| Against symlink at extract target (TOCTOU) | `zupt_safe_fopen_output()` uses `O_NOFOLLOW` on POSIX. Windows relies on directory ACLs (documented limitation). |
| Against quantum adversary | `--pq` mode: ML-KEM-768 (NIST Level 3) hybridized with X25519 |
The wire/on-disk format is v1.6. See CHANGELOG.md for the per-release
finding history behind these protections.
### What VaptVupt Does NOT Protect Against ### What VaptVupt Does NOT Protect Against
| Threat | Reason | Mitigation Path | | Threat | Reason | Mitigation Path |
|--------|--------|----------------| |--------|--------|----------------|
| Attacker who knows the password or has the private key | Fundamental to encryption | Use strong passwords (12+ chars); protect key files | | Attacker who knows the password or has the private key | Fundamental to encryption | Use strong passwords (12+ chars); protect key files |
| Cache-timing side channels (C AES) | Table-based S-box lookups | Build with Jasmin AES-NI when available | | Endpoint compromise (keylogger, malware on the host) | Outside the archive's trust boundary | Secure the machine where you type the password or hold the key |
| Cache-timing side channels (C AES) | Table-based S-box lookups | Build with Jasmin AES-NI when available; avoid multi-tenant VMs |
| Memory forensics during operation | Keys on stack during compress/extract | `zupt_secure_wipe()` on completion; `mlock()` planned | | Memory forensics during operation | Keys on stack during compress/extract | `zupt_secure_wipe()` on completion; `mlock()` planned |
| Deniability | Archive header identifies format | `.zupt` magic bytes visible; ENCRYPTED flag in header | | Deniability | Archive header identifies format | `.zupt` magic bytes visible; ENCRYPTED flag in header |
| Weak passwords | Argon2id (default, v2.4.1+) is memory-hard and adds ~2530 bits of work factor vs ~20 for PBKDF2. PBKDF2-SHA256 with 600k iterations available via `--kdf pbkdf2` for legacy reader compatibility. | Use `--pq` or `--pq-sdk` mode for critical data — keys are random, not derived from a password. | | Weak passwords | PBKDF2-SHA256 (600k) is the default KDF; Argon2id (memory-hard) is available in a WITH_SDK=1 build | Use `--pq` mode for critical data — keys are random, not derived from a password |
| Traffic analysis | Archive size reveals data volume | Outside VaptVupt's scope | | Traffic analysis / metadata | Archive size reveals data volume; file list, sizes, mtimes not padded | Outside VaptVupt's scope |
| File permission/ownership | Not stored in archive | Documented in README.md (Architecture & platform support) | | File permission/ownership | Not stored in archive | Documented in README.md |
| Spectre-class side channels in callers | Below the constant-time primitive layer | Host OS / compiler mitigations |
### Quantum Threat Analysis ### Quantum Threat Analysis
**Scenario:** Adversary captures encrypted archive today, stores it, and attempts decryption when a cryptographically-relevant quantum computer is available. Scenario: adversary captures an encrypted archive today, stores it, and
attempts decryption when a cryptographically-relevant quantum computer is
available.
| Mode | Classical Security | Quantum Security | Verdict | | Mode | Classical Security | Quantum Security | Verdict |
|------|-------------------|-----------------|---------| |------|-------------------|-----------------|---------|
| Password (`-p`) | Password-dependent + 256-bit AES | ~128-bit (Grover on AES) but PBKDF2 accelerated | **Vulnerable** — use `--pq` | | Password (`-p`) | Password-dependent + 256-bit AES | ~128-bit (Grover on AES), PBKDF2 accelerated | Vulnerable — use `--pq` |
| PQ Hybrid (`--pq`) | ~128-bit (X25519) | NIST Level 3 (ML-KEM-768) | **Protected** | | PQ Hybrid (`--pq`) | ~128-bit (X25519) | NIST Level 3 (ML-KEM-768) | Protected |
In `--pq` mode: even if Shor's algorithm breaks X25519, ML-KEM-768 protects the archive. Even if a novel classical attack breaks ML-KEM, X25519 still provides ~128-bit security. The hybrid design ensures the archive is secure if **either** component holds. In `--pq` mode: even if Shor's algorithm breaks X25519, ML-KEM-768
protects the archive; even if a novel classical attack breaks ML-KEM,
X25519 still provides ~128-bit security. The hybrid design is secure if
either component holds.
### Extracting untrusted archives — operational guidance
The in-binary defenses are the primary control; the following are defense
in depth:
1. Extract into a dedicated empty directory (not `~/Downloads` or `/tmp`).
2. Audit symlinks in the target directory before extraction.
3. Run extraction as a low-privilege user, never root.
4. On Windows, pre-create the target directory with restrictive ACLs
(the `O_NOFOLLOW` defense is POSIX-only).
### Out of scope
- External independent audit.
- Side-channel testing on production hardware (timing leaks).
- Formal verification beyond the Jasmin constant-time primitives.
--- ---
@ -152,11 +215,14 @@ In `--pq` mode: even if Shor's algorithm breaks X25519, ML-KEM-768 protects the
| Platform | Primary Source | Fallback | Failure Mode | | Platform | Primary Source | Fallback | Failure Mode |
|----------|---------------|----------|--------------| |----------|---------------|----------|--------------|
| Linux | `getrandom(2)` | `/dev/urandom` | **Hard exit** — no encryption without CSPRNG | | Linux | `getrandom(2)` | `/dev/urandom` | Hard exit — no encryption without CSPRNG |
| macOS | `/dev/urandom` | None | **Hard exit** | | macOS | `/dev/urandom` | None | Hard exit |
| Windows | `RtlGenRandom` | None | **Hard exit** | | Windows | `RtlGenRandom` | None | Hard exit |
There is no `rand()`, `srand()`, or any weak PRNG fallback anywhere in the codebase. If the OS CSPRNG is unavailable, VaptVupt exits with an error. This is a deliberate design choice — weak random keys are worse than no encryption. There is no `rand()`, `srand()`, or any weak PRNG fallback anywhere in the
codebase. If the OS CSPRNG is unavailable, VaptVupt exits with an error.
This is a deliberate design choice — weak random keys are worse than no
encryption.
--- ---
@ -164,7 +230,7 @@ There is no `rand()`, `srand()`, or any weak PRNG fallback anywhere in the codeb
| Platform | Compiler | Threading | CSPRNG | Status | | Platform | Compiler | Threading | CSPRNG | Status |
|----------|----------|-----------|--------|--------| |----------|----------|-----------|--------|--------|
| Linux x86-64 | GCC 5+ / Clang 3.5+ | pthreads | `getrandom(2)` | **Primary** | | Linux x86-64 | GCC 5+ / Clang 3.5+ | pthreads | `getrandom(2)` | Primary |
| Linux ARM64 | GCC 5+ | pthreads | `getrandom(2)` | Tested | | Linux ARM64 | GCC 5+ | pthreads | `getrandom(2)` | Tested |
| macOS x86-64/ARM64 | Apple Clang | pthreads | `/dev/urandom` | Tested | | macOS x86-64/ARM64 | Apple Clang | pthreads | `/dev/urandom` | Tested |
| Windows x86-64 | MinGW / MSVC 2015+ | Win32 threads | `RtlGenRandom` | Tested | | Windows x86-64 | MinGW / MSVC 2015+ | Win32 threads | `RtlGenRandom` | Tested |
@ -172,34 +238,23 @@ There is no `rand()`, `srand()`, or any weak PRNG fallback anywhere in the codeb
--- ---
## Disclosure Timeline
| Date | Event |
|------|-------|
| 2026-01-01 | v0.1.0 — Initial release |
| 2026-03-21 | v0.5.1 — 16 security bug fixes including CSPRNG hardening |
| 2026-03-21 | v1.0.0 — 5 critical ML-KEM bugs fixed, format frozen |
| 2026-03-28 | v1.1.0 — X25519 formula bug fixed (not interoperable with RFC 7748) |
| 2026-03-28 | v1.5.0 — Jasmin assembly linked (MAC verify + ML-KEM select) |
---
## Verification Commands ## Verification Commands
Anyone can verify every security claim: Anyone can verify the security claims. The default build needs only a C
compiler + make (plus libm/pthread); no external crypto library.
```bash ```bash
# Build # Build
make # Zero warnings make
# All functional tests # Functional tests
make test-all # 62/62 pass make test-all
# Memory safety # Memory safety
make test-asan # Zero ASAN/UBSAN errors make test-asan
# NIST/RFC test vectors # NIST/RFC test vectors
make test-vectors && ./test_vectors # 13/13 pass make test-vectors && ./test_vectors
# Verify Jasmin symbols are active # Verify Jasmin symbols are active
nm vaptvupt | grep "zupt_mac_verify_ct\|zupt_ct_select_32" nm vaptvupt | grep "zupt_mac_verify_ct\|zupt_ct_select_32"
@ -213,174 +268,4 @@ jasminc -arch x86-64 -o /dev/null jasmin/zupt_mlkem_select.jazz
--- ---
© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later © 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (dual-licensed AGPL + commercial)
## Production deployment notes (v2.2.1)
VaptVupt is deployed in production environments. The following supported
configurations are considered current and receive security fixes:
| Channel | Supported | Notes |
|---|---|---|
| 2.2.x (latest) | Yes | Recommended for new deployments |
| 2.1.x | Yes (security only) | Supported through 2026-Q4 |
| 2.0.x | No | End of life |
| 1.x | No | End of life |
### Recommended configuration
For new archives, use the libzuptsdk-backed mode:
```bash
vaptvupt keygen --sdk -o key.priv
vaptvupt c --pq-sdk key.priv.pub backup.zupt /path/to/data
vaptvupt x --pq-sdk key.priv backup.zupt
```
This selects:
- ML-KEM-768 + X25519 hybrid KEM with HKDF-SHA3-256 combiner (RFC-style
KDF rather than ad-hoc XOR construction)
- 32-byte HKDF-derived key commitment tag (protects against partitioning
oracle attacks across recipients)
- HPKE-style context binding (RFC 9180 §5)
- Anti-fault double ML-KEM decapsulation
- XChaCha20-Poly1305 AEAD with 24-byte random nonces
- Argon2id (RFC 9106 OWASP minimums) when password mode is used
### Threat model
VaptVupt assumes:
- The recipient's private key file is kept secret and is not exfiltrated.
- The execution environment has a working `getrandom(2)` / `/dev/urandom`.
- The archive metadata (file list, sizes, mtimes) is not considered
confidential. Padding to hide file sizes is not implemented.
- An attacker may have full write access to the archive in transit; AEAD
+ commitment + HPKE binding ensures any modification is detected.
VaptVupt does **not** defend against:
- Endpoint compromise (keylogger, malware on the machine where you type
the password or hold the private key).
- Side-channel attacks against the host OS that bypass the constant-time
Jasmin-verified primitives (e.g. Spectre v1 in callers).
- Quantum attacks against X25519 alone — but the ML-KEM-768 component
guarantees post-quantum security via the hybrid KDF.
### Reporting findings
If you find a security issue:
1. **Do not** open a public issue on the project's git server.
2. Email `zupt@riseup.net` with subject `SECURITY: <brief>`.
3. Include the version (`vaptvupt --version`), platform, and a
reproduction (a minimal archive or a code snippet).
4. Expect acknowledgement within 7 days. Coordinated disclosure
timeline will be discussed case by case.
### Disclosure history
| Date | Version | Findings | Severity |
|---|---|---|---|
| 2026-04-27 | 2.2.1 | 6 internally-found bugs (audit pass) | 2 high, 1 medium, 3 low |
---
## v2.2.1 audit findings
The 2.2.1 release fixed six bugs found by code review and added a 10-check
double-validated audit test suite. Detailed root-cause analysis for each
finding is in `CHANGELOG.md` under the 2.2.1 entry.
| # | Severity | Component | Bug |
|---|---|---|---|
| 1 | Low (correctness) | format parser | varint reader truncated values at 2^63 |
| 2 | Medium (data loss) | extract path | unchecked `fwrite` in 6 call sites — silent corruption on disk-full |
| 3 | Low (defense-in-depth) | SDK keyring | `mac_key` was a copy of `enc_key` rather than KDF-derived |
| 4 | High (memory safety) | LZ decoder | `size_t` overflow in length accumulator could enable out-of-bounds copy |
| 5 | Medium (DoS / amplification) | dedup ref blocks | unbounded forward offset + recursion accepted |
| 6 | Low (UX) | encrypt path | partial archive left on disk after encrypt-init failure |
All six fixed in 2.2.1. Regression tests added.
## Reporting vulnerabilities
Email `zupt@riseup.net` with `[security]` in the subject. PGP key on the
project's keyserver entry. Coordinated disclosure preferred; we will
acknowledge within 5 business days and aim for a fix within 30 days for
high-severity issues.
The project does not yet have an external audit. The 2.2.1 audit pass was
internal code review combined with the 169-check libzuptsdk audit suite
inherited via vendored linkage. For high-stakes deployments, treat this as
"reviewed but unaudited" and do your own review.
---
## v2.2.2 formal audit findings (2026-04-27)
A formal cryptographic audit pass was conducted using the methodology
documented in `FORMAL_AUDIT_PROMPT.md` (auditor profile: senior
cryptographic engineer with 15+ years of production crypto systems
experience). Two security-relevant bugs and two robustness bugs found
and fixed; version unchanged at 2.2.2 — same release with hardened
internals.
| # | Severity | Component | Bug |
|---|---|---|---|
| 11 | **HIGH** | extract path | Zip Slip / path traversal — `e->path` from archive used directly in `fopen` |
| 12 | **MEDIUM** | extract output | symlink-following — `fopen "wb"` followed symlinks at output target |
| 13 | LOW (32-bit only) | size cap | 4 GiB cap exceeds `size_t` on 32-bit |
| 14 | LOW (32-bit only) | calloc on parsed count | `count * sizeof(entry)` overflowed `size_t` before calloc internal check |
All four fixed and regression-tested.
### Threat model coverage (post-audit)
The following attack vectors are now explicitly defended against:
- **Malicious archive with path-traversal entries** (Zip Slip 2018 pattern):
rejected by `zupt_path_is_safe()` — blocks `..`, absolute paths, Windows
drive letters, UNC paths, embedded NULs.
- **Symlink at extract target** (TOCTOU pre-extraction): refused by
`zupt_safe_fopen_output()` using `O_NOFOLLOW` on POSIX. Windows path
unchanged — relies on directory ACLs (documented limitation).
- **Malformed archive headers**: bounds-checked offsets (`encryption_header_off`,
`index_offset`); rejected if outside file size.
- **Format parser overflow**: varint truncation, dedup-ref recursion,
realloc-pair atomicity, length-overflow in LZ decoder — all fixed in
prior 2.2.x sprints.
- **Cryptographic key reuse / nonce misuse**: per-block nonce is `base ⊕
block_seq`; `base_nonce` is per-archive random; mac_key is KDF-split
from enc_key (defense in depth even though SDK path doesn't use it).
- **Block-swap (reorder) attack on encrypted archives** (bug #16, fixed
in 2.2.2 god-tier audit): MAC binds 8-byte AAD seq computed as
`((file_index_in_archive + 1) << 32) | per_file_block_seq`. An attacker
who swaps two valid encrypted blocks between positions in the archive
produces blocks whose AAD no longer matches their position; both MAC
candidates (v2 with AAD, v1 legacy fallback) reject the swapped block.
Empty/partial output files are `unlink()`'d on auth failure.
Limitation: dedup mode uses sentinel seq=0 (refs can't derive source
AAD); plaintext XXH64 still provides per-block integrity.
### Path traversal — operational guidance
Even with the in-binary defenses, operators extracting untrusted archives
should:
1. Extract into a dedicated empty directory (not `~/Downloads` or `/tmp`).
2. Audit symlinks in the target directory before extraction.
3. Run extraction as a low-privilege user, never root.
4. On Windows, pre-create the target directory with restrictive ACLs
(the `O_NOFOLLOW` defense is POSIX-only).
These are belt-and-suspenders — the in-binary defenses are the primary
control, but defense in depth is good practice.
### Out-of-scope (still)
- External independent audit (cost-bound, on roadmap)
- Side-channel testing on production hardware (timing leaks)
- Formal verification beyond Jasmin constant-time primitives

View file

@ -1,36 +1,25 @@
THIRD-PARTY NOTICES THIRD-PARTY NOTICES
=================== ===================
**VaptVupt contains no third-party source code.** Every line of source in This document records VaptVupt's runtime dependencies and build-time
this repository is the work of Cristian Cezar Moisés. This document tools. If you redistribute VaptVupt, you must preserve this attribution
exists for transparency about runtime dependencies and build-time document along with the LICENSE file.
tools.
If you redistribute VaptVupt, you must preserve this attribution document
along with the LICENSE file.
------------------------------------------------------------------------- -------------------------------------------------------------------------
Components shipped in this repository (all original work) Licensing
------------------------------------------------------------------------- -------------------------------------------------------------------------
| Component | Location | License | Author | **Note on VaptVupt LZ codec licensing**: the VaptVupt LZ codec
|---|---|---|---| (src/vv_*.c, src/vaptvupt_api.c, include/vaptvupt*.h) is licensed
| vaptvupt CLI | src/, include/ | AGPL-3.0-or-later | Cristian Cezar Moisés | GPL-3.0-or-later (not AGPL like the rest of the project) so that, with
| libzuptsdk | sdk/, vendor/zuptsdk/include/ | AGPL-3.0-or-later | Cristian Cezar Moisés | sufficient maturity, it can be considered for upstreaming into the Linux
| VaptVupt LZ codec | src/vv_*.c, src/vaptvupt_api.c, include/vaptvupt*.h | **GPL-3.0-or-later** | Cristian Cezar Moisés | or BSD kernels, which require GPL-compatible licenses. The author retains
| Jasmin constant-time crypto | jasmin/*.jazz, jasmin/*.s | AGPL-3.0-or-later | Cristian Cezar Moisés | the right to dual-license the codec under other terms for commercial use;
| VaptVupt GUI (Python) | gui/ | AGPL-3.0-or-later | Cristian Cezar Moisés | contact sac@securityops.co for inquiries.
**Note on VaptVupt licensing**: VaptVupt is licensed GPL-3.0-or-later The rest of the project (vaptvupt CLI, Jasmin source, GUI) is licensed
(not AGPL like the rest of VaptVupt) so that, with sufficient maturity, it AGPL-3.0-or-later. Commercial licenses (relief from the AGPL network-use
can be considered for upstreaming into the Linux or BSD kernels, which clause) are available; contact sac@securityops.co.
require GPL-compatible licenses. The author retains the right to dual-
license VaptVupt under other terms for commercial use; contact
sac@securityops.co for inquiries.
The rest of the project (vaptvupt CLI, libzuptsdk, Jasmin source, GUI) is
licensed AGPL-3.0-or-later. Commercial licenses (relief from AGPL
network-use clause) are available; contact sac@securityops.co.
------------------------------------------------------------------------- -------------------------------------------------------------------------
Build-time tool (not redistributed) Build-time tool (not redistributed)
@ -59,12 +48,15 @@ at runtime and are NOT redistributed as part of VaptVupt.
**libargon2** — Argon2id password hashing function (RFC 9106) **libargon2** — Argon2id password hashing function (RFC 9106)
Required only for: the optional `make WITH_SDK=1` build. The default
build uses native PBKDF2-SHA256 and does not link
libargon2.
Linked at runtime: libargon2.so.1 Linked at runtime: libargon2.so.1
Version expected: 1.0+ (Debian/Ubuntu: libargon2-1) Version expected: 1.0+ (Debian/Ubuntu: libargon2-1)
Upstream: https://github.com/P-H-C/phc-winner-argon2 Upstream: https://github.com/P-H-C/phc-winner-argon2
License: Apache-2.0 OR CC0-1.0 (dual) License: Apache-2.0 OR CC0-1.0 (dual)
Copyright: (c) 2015 The Argon2 Authors Copyright: (c) 2015 The Argon2 Authors
Used by: Password-derived encryption mode Used by: Argon2id password-derived encryption mode
**OpenSSL libcrypto** — AES, SHA-256, AES-NI hardware backends **OpenSSL libcrypto** — AES, SHA-256, AES-NI hardware backends
@ -79,9 +71,11 @@ at runtime and are NOT redistributed as part of VaptVupt.
Compatibility with public standards Compatibility with public standards
------------------------------------------------------------------------- -------------------------------------------------------------------------
Where VaptVupt implements public standards, it does so independently Where VaptVupt implements public standards, it does so independently from
from any reference implementation. No code has been copied from any reference implementation. Other projects in the post-quantum hybrid
external projects. Standards followed: encryption space (libsodium, age, Tink, rustls, etc.) were referenced as
prior art during design, but no code was copied from any external
project. Standards followed:
- FIPS 197 (AES) - FIPS 197 (AES)
- FIPS 202 (Keccak / SHA-3) - FIPS 202 (Keccak / SHA-3)
@ -94,11 +88,6 @@ external projects. Standards followed:
- RFC 9106 (Argon2) - RFC 9106 (Argon2)
- RFC 9180 (HPKE) - RFC 9180 (HPKE)
The VaptVupt project was designed independently. Other projects in the
post-quantum hybrid encryption space (libsodium, age, Tink, rustls,
etc.) were referenced as prior art during design but no code was
copied. VaptVupt does not include any code from these projects.
------------------------------------------------------------------------- -------------------------------------------------------------------------
Reporting attribution issues Reporting attribution issues
------------------------------------------------------------------------- -------------------------------------------------------------------------
@ -114,7 +103,7 @@ with the subject "[third-party]" and details of the issue.
License summary License summary
------------------------------------------------------------------------- -------------------------------------------------------------------------
VaptVupt CLI, libzuptsdk, Jasmin source, GUI: AGPL-3.0-or-later VaptVupt CLI, Jasmin source, GUI: AGPL-3.0-or-later
VaptVupt LZ codec: GPL-3.0-or-later VaptVupt LZ codec: GPL-3.0-or-later
Commercial license (any component): contact sac@securityops.co Commercial license (any component): contact sac@securityops.co

View file

@ -10,21 +10,37 @@ trusting VaptVupt with anything you can't afford to lose.
## TL;DR ## TL;DR
VaptVupt is designed for **at-rest backup encryption** by someone who VaptVupt is designed for at-rest backup encryption by someone who
controls the machine doing the encryption and the machine doing the controls the machine doing the encryption and the machine doing the
extraction. It is **not** a network protocol, a multi-party scheme, or extraction. It is not a network protocol, a multi-party scheme, or
a substitute for full-disk encryption. a substitute for full-disk encryption.
| Use case | VaptVupt is appropriate? | | Use case | VaptVupt is appropriate? |
|---|---| |---|---|
| Backing up files to an untrusted cloud (S3, Backblaze, Google Drive) | **Yes** | | Backing up files to an untrusted cloud (S3, Backblaze, Google Drive) | Yes |
| Backing up a disk image to external media you might lose | **Yes** | | Backing up a disk image to external media you might lose | Yes |
| Long-term archival of personal/business data | **Yes** | | Long-term archival of personal/business data | Yes |
| Sharing an encrypted archive with someone you trust to handle the key | **Yes, with care** (see "Key distribution" below) | | Sharing an encrypted archive with someone you trust to handle the key | Yes, with care (see "Key distribution" below) |
| Real-time encrypted communication | **No** (use Signal, age, or TLS) | | Real-time encrypted communication | No (use Signal, age, or TLS) |
| Multi-party access (n-of-m) | **No** (no threshold scheme) | | Multi-party access (n-of-m) | No (no threshold scheme) |
| Hiding the existence of an archive (steganography) | **No** (archive header has fixed magic bytes) | | Hiding the existence of an archive (steganography) | No (archive header has fixed magic bytes) |
| Protecting against a hostile machine you're encrypting on | **No** (a compromised host can read plaintext before encryption) | | Protecting against a hostile machine you're encrypting on | No (a compromised host can read plaintext before encryption) |
---
## Modes referenced in this document
- `-p` / password mode: symmetric encryption with a key derived from a
password. The default build derives the key with PBKDF2-SHA256
(600k iterations). Argon2id is available only in an upstream
`make WITH_SDK=1` build against the separately distributed
libraries.
- `--pq`: native post-quantum mode (ML-KEM-768 + X25519), the PQ mode
in the default build. The ML-KEM-768 implementation is in-tree.
- `--pq-sdk` / `--pq-box`: optional post-quantum modes backed by the
separately distributed `libzuptsdk` / `libpqvaptvupt` libraries.
Available only in a `make WITH_SDK=1` build. Key files for these
modes are produced by `vaptvupt keygen --sdk`, also SDK-only.
--- ---
@ -34,31 +50,27 @@ a substitute for full-disk encryption.
An attacker with read access to the archive bytes cannot recover An attacker with read access to the archive bytes cannot recover
plaintext file contents, file names, file sizes, file modes, or plaintext file contents, file names, file sizes, file modes, or
embedded comments **without the key/password**, assuming: embedded comments without the key/password, assuming:
- The chosen mode is one of the encrypted modes (`-p`, `--pq`, `--pq-sdk`, or `--pq-box`) - The chosen mode is one of the encrypted modes (`-p`, `--pq`, or the
optional `--pq-sdk` / `--pq-box`)
- The password is strong enough to resist offline brute-force - The password is strong enough to resist offline brute-force
(Argon2id default with m=64 MB, t=3, p=4 makes this very expensive (see "Password strength" below)
but not infinite — see "Password strength" below) - The key file (for `--pq-sdk` / `--pq-box`) was not compromised at
- The key file (for `--pq-sdk` / `--pq-box`) was not compromised at generation time generation time
### 2. Integrity of every byte of an encrypted archive ### 2. Integrity of every byte of an encrypted archive
If any single bit of the on-disk archive bytes is flipped, the If any single bit of the on-disk archive bytes is flipped, the
extraction **must fail** with an authentication error. This has been extraction fails with an authentication error. Coverage layers:
verified by the v1.6 exhaustive byte sweep:
- 0 silent-accept positions out of 1827 (encrypted, no comment) - Per-block HMAC-SHA256 with frame-preface AAD (F-09): every data
- 0 silent-accept positions out of 1878 (encrypted, with comment)
Coverage layers:
- **Per-block HMAC-SHA256** with frame-preface AAD (F-09): every data
block carries an HMAC over its ciphertext and over the canonical block carries an HMAC over its ciphertext and over the canonical
29-byte preface (block_type, codec_id, block_flags, sizes, plaintext-XXH64) 29-byte preface (block_type, codec_id, block_flags, sizes,
- **Archive Integrity Trailer (F-08)**: HMAC-SHA256 over the plaintext-XXH64)
64-byte header and 24 bytes of footer, appended after the footer - Archive Integrity Trailer (F-08): HMAC-SHA256 over the 64-byte
- **Strict structural validation of the encryption-header block (F-09)**: header and 24 bytes of footer, appended after the footer
- Strict structural validation of the encryption-header block (F-09):
codec must be `STORE`, flags must be 0, csz must equal usz, the codec must be `STORE`, flags must be 0, csz must equal usz, the
plaintext XXH64 must match plaintext XXH64 must match
@ -66,10 +78,10 @@ Coverage layers:
Plaintext archives (no `-p`, no `--pq*`) are protected by XXH64 Plaintext archives (no `-p`, no `--pq*`) are protected by XXH64
plaintext checksums per block plus structural validation. This is plaintext checksums per block plus structural validation. This is
**not cryptographic integrity** — a determined attacker with not cryptographic integrity — a determined attacker with write access
write access can produce a tampered plaintext archive that passes can produce a tampered plaintext archive that passes the checksum
the checksum (XXH64 is not collision-resistant). It does catch (XXH64 is not collision-resistant). It does catch accidental
accidental corruption and naive tampering. corruption and naive tampering.
Use an encrypted mode if you need cryptographic integrity. Use an encrypted mode if you need cryptographic integrity.
@ -87,33 +99,34 @@ also constant (HMAC is always run, branchless return).
The detailed cause is available via `--verbose` for debugging on The detailed cause is available via `--verbose` for debugging on
machines under the user's own control. machines under the user's own control.
### 5. Post-quantum forward secrecy (in `--pq-sdk` mode) ### 5. Post-quantum forward secrecy (`--pq` and optional `--pq-sdk`)
`--pq-sdk` uses ML-KEM-768 (FIPS 203) hybridized with X25519 via an The native `--pq` mode uses ML-KEM-768 (FIPS 203) hybridized with
HKDF combiner. Archives encrypted today cannot be decrypted by a X25519 via an HKDF combiner. Archives encrypted today cannot be
future quantum adversary holding only the ciphertext, **assuming**: decrypted by a future quantum adversary holding only the ciphertext,
assuming:
- ML-KEM-768 retains its claimed security level (NIST Category 3, - ML-KEM-768 retains its claimed security level (NIST Category 3,
192-bit classical / 96-bit quantum strength) 192-bit classical / 96-bit quantum strength)
- X25519 hybridization protects against an unforeseen ML-KEM break - X25519 hybridization protects against an unforeseen ML-KEM break
- The recipient's private key is not later compromised - The recipient's private key is not later compromised
The optional `--pq-sdk` mode provides the same hybrid guarantee via
the separately distributed SDK libraries.
### 6. Side-channel resistance for cryptographic primitives ### 6. Side-channel resistance for cryptographic primitives
The hot crypto paths (AES-256-CTR, HMAC-SHA256 comparison, X25519 The hot crypto paths (AES-256-CTR, HMAC-SHA256 comparison, X25519
field operations, ML-KEM polynomial arithmetic) are implemented in field operations, ML-KEM polynomial arithmetic) are implemented in
Jasmin and proved constant-time at the assembly level on x86_64. Jasmin and proved constant-time at the assembly level on x86_64.
Non-Jasmin platforms (aarch64, fallback x86_64) use careful C Non-Jasmin platforms (aarch64, fallback x86_64) use C implementations
implementations that avoid secret-dependent branches and memory that avoid secret-dependent branches and memory accesses where
accesses where feasible — but **without formal proof**. feasible — but without formal proof.
--- ---
## What VaptVupt does NOT protect against ## What VaptVupt does NOT protect against
This list is **exhaustive of the major omissions** — if you have a
concern that doesn't appear here, please file an issue.
### 1. Compromised endpoints ### 1. Compromised endpoints
VaptVupt cannot protect against: VaptVupt cannot protect against:
@ -134,7 +147,7 @@ If you don't trust the machine, VaptVupt cannot help.
If the password or `~/.zupt-key` is leaked: If the password or `~/.zupt-key` is leaked:
- All archives encrypted with that key are decryptable - All archives encrypted with that key are decryptable
- VaptVupt has **no forward secrecy across archives** — each archive - VaptVupt has no forward secrecy across archives — each archive
is encrypted under a single static key derived from the password is encrypted under a single static key derived from the password
or stored in the key file or stored in the key file
- There is no key-rotation feature; rotate by re-encrypting - There is no key-rotation feature; rotate by re-encrypting
@ -147,37 +160,33 @@ another layer (e.g. on an encrypted USB), and rotate periodically.
### 3. Password strength ### 3. Password strength
Argon2id with m=64 MB, t=3, p=4 makes a single guess cost roughly Password mode derives the key with PBKDF2-SHA256 (600k iterations)
~200 ms on commodity hardware. That's **not enough** to protect a in the default build, or Argon2id in a `make WITH_SDK=1` build. A
short, common password against a determined attacker with GPU key derivation function slows offline guessing but does not make a
clusters or cloud compute. short, common password safe: a determined attacker with GPU clusters
or cloud compute can still exhaust a weak password.
| Password type | Approximate brute-force resistance with Argon2id | Use a long, high-entropy password — a multi-word diceware passphrase
|---|---| or a random 16+ character string with a full alphabet. For critical
| 6-char common word | Hours to days | data, use a key-file mode (native `--pq`, or the optional `--pq-sdk`
| 10-char mixed alphanumeric | Years on a single GPU; days on a cluster | with a random key file from `vaptvupt keygen --sdk`) so the key is
| 6-word diceware passphrase | Centuries to millennia even with cloud-scale resources | CSPRNG output, not derived from human-typed text.
| Random 16-char with full alphabet | Infeasible without quantum breakthrough |
For critical data, use `--pq-sdk` mode with a random key file
generated by `vaptvupt keygen --sdk` — the key is 64 bytes of CSPRNG
output, not derived from human-typed text.
### 4. Metadata leakage from archive structure ### 4. Metadata leakage from archive structure
Even with encryption, an attacker who can see the archive bytes Even with encryption, an attacker who can see the archive bytes
can infer: can infer:
- **Approximate file count** (from `total_blocks` in the footer) - Approximate file count (from `total_blocks` in the footer)
- **Total archive size** (file size on disk) - Total archive size (file size on disk)
- **Whether the archive is encrypted at all** (`ZUPT_FLAG_ENCRYPTED` - Whether the archive is encrypted at all (`ZUPT_FLAG_ENCRYPTED`
in the global flags is visible) in the global flags is visible)
- **Whether the archive is solid or per-file mode** (visible flag) - Whether the archive is solid or per-file mode (visible flag)
- **Whether post-quantum mode is in use** (visible flag) - Whether post-quantum mode is in use (visible flag)
- **Approximate file size distribution** (block sizes are visible - Approximate file size distribution (block sizes are visible
even when block payloads are encrypted) even when block payloads are encrypted)
- **Archive creation time** (a 64-bit timestamp in the header) - Archive creation time (a 64-bit timestamp in the header)
- **A random 16-byte UUID per archive** (no information leak, but - A random 16-byte UUID per archive (no information leak, but
globally identifies the archive across copies) globally identifies the archive across copies)
If metadata privacy matters, layer VaptVupt under another tool that If metadata privacy matters, layer VaptVupt under another tool that
@ -197,8 +206,8 @@ VaptVupt is not a network protocol. There is no:
### 6. Multi-party schemes ### 6. Multi-party schemes
There is **no threshold cryptography, no n-of-m sharing, no There is no threshold cryptography, no n-of-m sharing, no
multi-party computation, no proxy re-encryption**. Each archive multi-party computation, no proxy re-encryption. Each archive
has exactly one decryption credential (one password OR one has exactly one decryption credential (one password OR one
recipient key). To give two people access to the same archive, recipient key). To give two people access to the same archive,
they must share the password or the key file. they must share the password or the key file.
@ -207,7 +216,7 @@ they must share the password or the key file.
VaptVupt archives have a fixed 6-byte magic `\x90\x5a\x55\x50\x54\x01` VaptVupt archives have a fixed 6-byte magic `\x90\x5a\x55\x50\x54\x01`
at offset 0. Anyone scanning the bytes can see it's a VaptVupt at offset 0. Anyone scanning the bytes can see it's a VaptVupt
archive. VaptVupt has **no hidden-volume or duress-password feature**. archive. VaptVupt has no hidden-volume or duress-password feature.
### 8. Side channels we don't claim to address ### 8. Side channels we don't claim to address
@ -219,29 +228,27 @@ archive. VaptVupt has **no hidden-volume or duress-password feature**.
### 9. Trusted setup of post-quantum primitives ### 9. Trusted setup of post-quantum primitives
The ML-KEM-768 implementation lives in `libzuptsdk` and was not The in-tree ML-KEM-768 implementation was not independently audited
independently audited at the time of writing. We use NIST KAT at the time of writing. We use NIST KAT vectors for correctness
vectors for correctness verification but have not formally proven verification but have not formally proven constant-time properties
constant-time properties for every PQ code path. for every PQ code path.
For maximum assurance, treat `--pq-sdk` as the post-quantum For maximum assurance, treat the post-quantum layer as a hedge — it
**hedge** — it does not replace the X25519 layer; both must be does not replace the X25519 layer; both must be broken for an
broken for an attacker to recover plaintext. attacker to recover plaintext.
### 10. Format extension attacks ### 10. Format extension attacks
The format is versioned (v1.6). Older readers may accept newer The format is versioned (v1.6). Older readers may accept newer
archives in unexpected ways. We try to maintain forward archives in unexpected ways. We try to maintain forward
compatibility (v2.4.5 readers correctly handle v1.6 archives compatibility, but a careful attacker who can produce
including encrypted comments and the Argon2id KDF path), but a malformed-but-just-valid archives may find parser-state issues that
careful attacker who can produce malformed-but-just-valid don't rise to the level of a CVE. The fuzzing harness
archives may find parser-state issues that don't rise to the (`make fuzz-format`) is the primary mitigation; report bugs.
level of a CVE. The fuzzing harness (`make fuzz-format`) is the
primary mitigation; report bugs.
### 11. Compression-side-channel attacks (CRIME / BREACH style) ### 11. Compression-side-channel attacks (CRIME / BREACH style)
VaptVupt compresses **before** encryption. If an attacker can: VaptVupt compresses before encryption. If an attacker can:
- Influence part of the plaintext (e.g. inject a known prefix) - Influence part of the plaintext (e.g. inject a known prefix)
- Observe the resulting archive size precisely - Observe the resulting archive size precisely
@ -251,9 +258,9 @@ the rest of the plaintext — this is the classic CRIME/BREACH attack
against TLS compression. against TLS compression.
VaptVupt is designed for offline backup, where attacker-controlled VaptVupt is designed for offline backup, where attacker-controlled
plaintext injection is rare. **If your threat model includes plaintext injection is rare. If your threat model includes
attacker-chosen plaintext mixed with secret plaintext in the same attacker-chosen plaintext mixed with secret plaintext in the same
archive**, use `--no-compress` (codec 0 = STORE) to disable the archive, use `--no-compress` (codec 0 = STORE) to disable the
LZ codec and eliminate this side channel. LZ codec and eliminate this side channel.
--- ---
@ -266,9 +273,9 @@ VaptVupt's security rests on the following standard assumptions:
|---|---| |---|---|
| AES-256-CTR is a secure stream cipher | All encrypted archives become readable | | AES-256-CTR is a secure stream cipher | All encrypted archives become readable |
| HMAC-SHA256 is a secure PRF / MAC | Tamper detection fails; integrity can be forged | | HMAC-SHA256 is a secure PRF / MAC | Tamper detection fails; integrity can be forged |
| Argon2id is a secure password KDF | Password-mode archives become brute-forceable faster | | PBKDF2-SHA256 (or Argon2id, WITH_SDK) is a secure password KDF | Password-mode archives become brute-forceable faster |
| ML-KEM-768 retains NIST Category 3 security | `--pq-sdk` mode reduces to the X25519 layer | | ML-KEM-768 retains NIST Category 3 security | `--pq` / `--pq-sdk` reduce to the X25519 layer |
| X25519 retains 128-bit security (no quantum) | `--pq-sdk` mode reduces to the ML-KEM layer; legacy `--pq` mode broken | | X25519 retains 128-bit security (no quantum) | PQ modes reduce to the ML-KEM layer; classical password mode unaffected |
| HKDF-SHA256 is a secure key-derivation construction | Combined PQ + classical keys may be predictable | | HKDF-SHA256 is a secure key-derivation construction | Combined PQ + classical keys may be predictable |
| SHA3 / SHAKE retain pre-image and collision resistance | Auxiliary protocol bindings may be forged | | SHA3 / SHAKE retain pre-image and collision resistance | Auxiliary protocol bindings may be forged |
@ -298,8 +305,7 @@ normally.
## Document version ## Document version
- **v1.0** (sprint 2.4.6): initial threat model. Covers archive This threat model covers archive format v1.6 as shipped in VaptVupt
format v1.6. 4.1.0. It is part of the source tree (`THREAT_MODEL.md`) and
- Document is part of the source tree (`THREAT_MODEL.md`) and versioned with the project; this section will be updated as the
versioned with the project; this section will be updated as format evolves.
the format evolves.

View file

@ -1,7 +1,7 @@
.\" Manpage for vaptvupt (formerly zupt; INPI Brasil trademark rename in v3.0.0) .\" Manpage for vaptvupt (formerly zupt; INPI Brasil trademark rename in v3.0.0)
.\" SPDX-License-Identifier: AGPL-3.0-or-later .\" SPDX-License-Identifier: AGPL-3.0-or-later
.\" Copyright (c) 2025-2026 Cristian Cezar Moisés .\" Copyright (c) 2025-2026 Cristian Cezar Moisés
.TH VAPTVUPT 1 "May 2026" "vaptvupt 4.0.0" "User Commands" .TH VAPTVUPT 1 "July 2026" "vaptvupt 4.1.0" "User Commands"
.SH NAME .SH NAME
vaptvupt \- post-quantum backup compression utility (formerly zupt) vaptvupt \- post-quantum backup compression utility (formerly zupt)
@ -205,7 +205,8 @@ Force a specific codec by id. Accepted values:
.TP .TP
.BR -p " " \fIpassword\fR .BR -p " " \fIpassword\fR
Enable password-based encryption (Argon2id KDF by default since v2.4.1). Enable password-based encryption (PBKDF2-SHA256 KDF; Argon2id is available
only in a WITH_SDK=1 build via \fB--kdf argon2id\fR).
Reading the password from a flag exposes it in Reading the password from a flag exposes it in
.BR ps (1) .BR ps (1)
output; prefer output; prefer
@ -517,7 +518,7 @@ On random / already-compressed data, all codecs hit the incompressibility wall;
.SH EXAMPLES .SH EXAMPLES
.PP .PP
Compress with default settings (Argon2id password, VaptVupt level 7, multi-threaded): Compress with default settings (PBKDF2-SHA256 password, VaptVupt level 7, multi-threaded):
.RS .RS
.nf .nf
@ -530,8 +531,9 @@ Compress with post-quantum hybrid encryption to a published public key:
.RS .RS
.nf .nf
$ vaptvupt keygen --sdk -o ~/.config/vaptvupt-mykey $ vaptvupt keygen --sdk -o ~/.config/vaptvupt-mykey
$ vaptvupt keygen --sdk --pub -o mykey.pub -k ~/.config/vaptvupt-mykey $ vaptvupt keygen -o ~/.config/vaptvupt-mykey
$ vaptvupt compress --pq-sdk mykey.pub backup.zupt ~/Documents $ vaptvupt keygen --pub -o mykey.pub -k ~/.config/vaptvupt-mykey
$ vaptvupt compress --pq mykey.pub backup.zupt ~/Documents
.fi .fi
.RE .RE

View file

@ -1,4 +1,4 @@
# VaptVupt GUI — Cross-Platform Post-Quantum Backup # VaptVupt GUI
Desktop application for [vaptvupt](https://git.securityops.co/cristiancmoises/vaptvupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption. Desktop application for [vaptvupt](https://git.securityops.co/cristiancmoises/vaptvupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption.
@ -14,9 +14,6 @@ tar xzf vaptvupt-gui.tar.gz && cd vaptvupt-gui
./install.sh --user # adds right-click menu integration ./install.sh --user # adds right-click menu integration
``` ```
After install, right-click any file in Nemo/Nautilus to see "Compress with VaptVupt".
Double-click any .zupt file to open it in the GUI.
### Windows ### Windows
**Option A — Installer (recommended):** **Option A — Installer (recommended):**
@ -119,7 +116,7 @@ The GUI calls the vaptvupt CLI binary — all cryptography runs in native C, not
## Credits ## Credits
- **vaptvupt** v2.2.3 — Cristian Cezar Moisés ([github](https://git.securityops.co/cristiancmoises/vaptvupt)) - **vaptvupt** v4.1.0 — Cristian Cezar Moisés ([github](https://git.securityops.co/cristiancmoises/vaptvupt))
## License ## License

View file

@ -50,7 +50,7 @@
#define ZUPT_PRODUCT_EXTENSION ".zupt" /* on-disk archive extension (kept stable) */ #define ZUPT_PRODUCT_EXTENSION ".zupt" /* on-disk archive extension (kept stable) */
#define ZUPT_PRODUCT_TAGLINE "Post-quantum backup compression" #define ZUPT_PRODUCT_TAGLINE "Post-quantum backup compression"
#define ZUPT_VERSION_STRING "4.0.0" #define ZUPT_VERSION_STRING "4.1.0"
/* Vendored codec release (upstream tag) — single source for display strings. /* Vendored codec release (upstream tag) — single source for display strings.
* The codec's own VV_VERSION_* is its internal API version, not the release. */ * The codec's own VV_VERSION_* is its internal API version, not the release. */
#define ZUPT_CODEC_RELEASE "2.60.4" #define ZUPT_CODEC_RELEASE "2.60.4"
@ -177,6 +177,12 @@
#define ZUPT_HMAC_SIZE 32 #define ZUPT_HMAC_SIZE 32
#define ZUPT_AES_KEY_SIZE 32 #define ZUPT_AES_KEY_SIZE 32
#define ZUPT_KDF_ITERATIONS 600000 #define ZUPT_KDF_ITERATIONS 600000
/* SECURITY (DoS guard): the PBKDF2 iteration count is read from the archive
* header, which is attacker-controlled. The writer only ever stamps
* ZUPT_KDF_ITERATIONS; a crafted archive could demand 2^32-1 iterations to
* pin a CPU core for many minutes before authentication can even fail.
* Reject anything above this generous cap (166× the default). */
#define ZUPT_KDF_MAX_ITERATIONS 100000000u
typedef enum { typedef enum {
ZUPT_OK = 0, ZUPT_ERR_IO = -1, ZUPT_ERR_CORRUPT = -2, ZUPT_OK = 0, ZUPT_ERR_IO = -1, ZUPT_ERR_CORRUPT = -2,
@ -417,6 +423,16 @@ uint8_t *zupt_decrypt_buffer_aad(const zupt_keyring_t *kr,
size_t *olen); size_t *olen);
void zupt_random_bytes(uint8_t *buf, size_t len); void zupt_random_bytes(uint8_t *buf, size_t len);
/* F-09 frame-preface AAD (v1.6). Serialised canonical bytes bound into the
* per-block MAC. Shared by the serial (zupt_format.c) and parallel
* (zupt_parallel.c) compress paths so both produce byte-identical prefaces
* a mismatch makes every multithreaded encrypted block fail to authenticate. */
#define ZUPT_PREFACE_AAD_LEN 29
void zupt_serialize_preface_aad_scalars(
uint8_t block_type, uint16_t codec_id, uint16_t block_flags,
uint64_t uncompressed_size, uint64_t compressed_size, uint64_t checksum,
uint8_t out[ZUPT_PREFACE_AAD_LEN]);
/* ─── Memory locking for key material ─── */ /* ─── Memory locking for key material ─── */
int zupt_mlock_keys(void *ptr, size_t len); int zupt_mlock_keys(void *ptr, size_t len);
void zupt_munlock_keys(void *ptr, size_t len); void zupt_munlock_keys(void *ptr, size_t len);

View file

@ -11,7 +11,7 @@
# zupt-VERSION.tar.gz alongside the PKGBUILD. # zupt-VERSION.tar.gz alongside the PKGBUILD.
pkgname=vaptvupt pkgname=vaptvupt
pkgver=4.0.0 pkgver=4.1.0
pkgrel=1 pkgrel=1
provides=('zupt') provides=('zupt')
replaces=('zupt') replaces=('zupt')

View file

@ -1,3 +1,22 @@
vaptvupt (4.1.0-1) UNRELEASED; urgency=high
* Source-only build: the prebuilt vendored libraries libzuptsdk.so and
libpqvaptvupt.so are removed; the package builds with no external
library dependency and ships no shared object. The default password
KDF is PBKDF2-SHA256 (600k); the Argon2id KDF and the --pq-sdk /
--pq-box modes are gated behind an upstream WITH_SDK=1 build. Native
--pq (ML-KEM-768 + X25519) is unchanged.
* Fix: multithreaded encrypted archives were unextractable on the
native AEAD path — the parallel workers now bind the F-09 frame-
preface AAD like the serial path. Byte-identical across thread counts.
* Security: LZH raw code-length stack overflow and huff_lut OOB;
integer-overflow heap OOB reads in the index and solid-mode parsers;
SEQ decoder safe-zone heap overflow; per-block ENCRYPTED-flag
authentication gate; PBKDF2 iteration-count DoS cap; non-elidable
secret wipe; restored disk images created 0600. Wire format v1.6.
-- Cristian Cezar Moisés <sac@securityops.co> Tue, 07 Jul 2026 12:00:00 +0000
vaptvupt (4.0.0-1) UNRELEASED; urgency=high vaptvupt (4.0.0-1) UNRELEASED; urgency=high
* Codec upgraded to canonical VaptVupt 2.60.4 (security release): * Codec upgraded to canonical VaptVupt 2.60.4 (security release):

View file

@ -23,8 +23,8 @@
class Vaptvupt < Formula class Vaptvupt < Formula
desc "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256)" desc "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256)"
homepage "https://git.securityops.co/cristiancmoises/zupt" homepage "https://git.securityops.co/cristiancmoises/zupt"
url "https://git.securityops.co/cristiancmoises/zupt/releases/download/v4.0.0/vaptvupt-4.0.0.tar.gz" url "https://git.securityops.co/cristiancmoises/zupt/releases/download/v4.1.0/vaptvupt-4.1.0.tar.gz"
version "4.0.0" version "4.1.0"
sha256 "REPLACE_WITH_SHA256_OF_RELEASE_TARBALL" sha256 "REPLACE_WITH_SHA256_OF_RELEASE_TARBALL"
license "AGPL-3.0-or-later" license "AGPL-3.0-or-later"

View file

@ -33,7 +33,7 @@
zupt = pkgs.stdenv.mkDerivation { zupt = pkgs.stdenv.mkDerivation {
pname = "vaptvupt"; pname = "vaptvupt";
version = "4.0.0"; version = "4.1.0";
# When publishing, replace this with `fetchurl` against the # When publishing, replace this with `fetchurl` against the
# release tarball. For local development the flake assumes it # release tarball. For local development the flake assumes it

View file

@ -1,54 +1,49 @@
# openSUSE Build Service update for `home:cabelo:innovators/vaptvupt` # openSUSE Build Service update for `home:cabelo:innovators/vaptvupt`
This directory contains the three files you need to update your OBS This directory contains the three files needed to build vaptvupt `4.1.0`
package from `1.5.5` to `2.4.8`: in OBS:
| File | Status vs. your current files | | File | Purpose |
|---------------|---------------------------------------------------------------------| |---------------|-------------------------------------------------------------------------|
| `_service` | Updated `revision` to `v2.4.8`. Format unchanged (still `tar_scm`). | | `_service` | `revision` pinned to `v4.1.0`. Format unchanged (still `tar_scm`). |
| `vaptvupt.spec` | Version → `2.4.8`. License corrected `MIT``AGPL-3.0-or-later`. `%check` now calls `make check` (new distro-safe target). | | `vaptvupt.spec` | `Version: 4.1.0`. `License: AGPL-3.0-or-later`. `%check` calls `make check`. |
| `vaptvupt.changes`| 13 new entries prepended (2.0.0 → 2.4.8). Your existing 1.0.01.5.4 history is preserved verbatim. | | `vaptvupt.changes`| Changelog for the 4.x series. Older history preserved verbatim. |
## What changed in the spec ## Spec notes
1. **License correction** — your spec says `License: MIT`, but the 1. **License**`AGPL-3.0-or-later` (dual-licensed AGPL-3.0-or-later
upstream license is **AGPL-3.0-or-later** (dual-licensed + commercial).
AGPL-3.0-or-later + commercial). This was a bug that should
probably trigger a rebuild even without the version bump.
2. **`%check` target** — your spec calls `test-all` on non-s390x 2. **No BuildRequires beyond the toolchain** — the default build needs
architectures. In v2.4.x, `test-all` includes threading tests only `gcc gzip make` (plus `libm`/`pthread` from glibc). There are
that are flaky on emulated build hosts (3 false positives on **no system library BuildRequires**. The repository is source-only:
x86_64 GitHub-Actions-style sandboxes). The new `make check` the previously vendored `libzuptsdk.so` and `libpqvaptvupt.so` have
target added in 2.4.8 runs a curated subset: been removed from the tree, `%build` and `%install` run with
`WITH_SDK=0`, and `%files` no longer lists any `.so`. The package
installs no shared library. Do not add system crypto BuildRequires.
* F-06 HMAC tamper detection (2000 trials) The optional SDK modes (`--pq-sdk`, `--pq-box`) and the Argon2id KDF
* F-08 archive-integrity-trailer require an upstream `make WITH_SDK=1` build linked against the
* F-09 byte-level integrity preface AAD separately distributed `libzuptsdk`/`libpqvaptvupt` libraries. They
* F-10 KDF default are not part of this package.
* F-11 auth-fail message
* F-12 encrypted comments
* NIST/RFC vectors (SHA-256, SHA-3, ML-KEM-768, AES-256-CTR,
HMAC, X25519, PBKDF2, Argon2id)
* Path-traversal, argument-order, block-swap regressions
* Quick smoke test
Total ~91 assertions, runs in <2 minutes, no flakes on emulated 3. **`%check` target** — the s390x branch falls back to `test-vectors`;
hosts. The s390x branch still falls back to just `test-vectors`. other architectures run `make check`. This exercises the HMAC tamper
detection, archive-integrity trailer, byte-level integrity preface
AAD, default-KDF, auth-fail, and encrypted-comment suites, the
NIST/RFC vectors (SHA-256, SHA-3, ML-KEM-768, AES-256-CTR, HMAC,
X25519, PBKDF2), and the path-traversal, argument-order, and
block-swap regressions.
3. **Upstream URL in `URL:` field** updated to The default password KDF is **PBKDF2-SHA256** (600k iterations).
`https://git.securityops.co/cristiancmoises/vaptvupt` (the canonical Argon2id test vectors run only in a `WITH_SDK=1` build and are not
project URL). The `_service` file still pulls from GitHub checked here.
(`https://github.com/cristiancmoises/vaptvupt`) since that's where
your `tar_scm` is already configured and what works in OBS today.
4. **`BuildRequires: make`** added — newer openSUSE chroots don't 4. **URLs** — the `URL:` field points at the canonical project URL
always pull `make` in transitively. Harmless on older targets. `https://git.securityops.co/cristiancmoises/vaptvupt`. The `_service`
file still fetches from GitHub
5. **Docs**`%doc README.md SECURITY.md CHANGELOG.md` now ships (`https://github.com/cristiancmoises/vaptvupt`), which is what the
the security boundary docs as well as the README. THREAT_MODEL.md existing `tar_scm` configuration uses in OBS.
exists upstream but isn't listed here to keep the package small;
add `%doc THREAT_MODEL.md` if you want it included.
## How to apply ## How to apply
@ -63,50 +58,31 @@ cp /path/to/vaptvupt-source/packaging/opensuse/_service .
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.spec . cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.spec .
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.changes . cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.changes .
# 3. Trigger the service locally to fetch v2.4.8 from GitHub # 3. Trigger the service locally to fetch v4.1.0 from GitHub
osc service runall osc service runall
# Produces vaptvupt-4.1.0.tar.gz in the current directory.
# This produces vaptvupt-2.4.8.tar.gz in the current directory and
# updates vaptvupt.changes with a service-generated entry if you have
# changesgenerate enabled (you don't, so this is a no-op for
# changes; tar_scm just downloads).
# 4. (Optional) Local build to verify before committing # 4. (Optional) Local build to verify before committing
osc build openSUSE_Tumbleweed x86_64 osc build openSUSE_Tumbleweed x86_64
# Expected: build succeeds, %check runs `make check`, all 10 suites
# (~91 assertions) pass, package is produced.
# 5. Commit upstream # 5. Commit upstream
osc status # confirm vaptvupt-2.4.8.tar.gz is staged alongside the osc status # confirm vaptvupt-4.1.0.tar.gz is staged alongside the
# three text files # three text files
osc commit -m "Update to 2.4.8: distro-safe make check target; license fix MIT -> AGPL" osc commit -m "Update to 4.1.0"
``` ```
## Notes for future updates ## Notes for future updates
* The `_service` `revision` is pinned to `v2.4.8`. To track a new * The `_service` `revision` is pinned to `v4.1.0`. To track a new
release, just edit that one line and re-run `osc service runall`. release, edit that one line and re-run `osc service runall`.
* The spec's `Version:` field is hard-coded — when you bump * The spec's `Version:` field is hard-coded — when you bump `_service`
`_service` `revision`, also bump `Version:` to match. The `revision`, also bump `Version:` to match.
`set_version` service in `_service` will auto-sync at OBS-build * `BuildRequires` is intentionally minimal (`gcc gzip make`). vaptvupt
time if you want; it's mode="manual" today, which is safer. has no external library dependencies in the default build; do not add
* `BuildRequires` is intentionally minimal (just `gcc gzip make`). system crypto BuildRequires.
VaptVupt has no external library dependencies — `libargon2`,
`libcrypto`, etc. used by other Linux packagers come from
*vendored* code that's compiled in. This is a deliberate
design choice; don't add system library BuildRequires.
## Reporting issues ## Reporting issues
* Upstream bugs: https://git.securityops.co/cristiancmoises/vaptvupt * Upstream bugs: https://git.securityops.co/cristiancmoises/vaptvupt
* openSUSE packaging bugs: https://bugs.opensuse.org/ * openSUSE packaging bugs: https://bugs.opensuse.org/
* Cabelo's OBS project: https://build.opensuse.org/project/show/home:cabelo:innovators * Cabelo's OBS project: https://build.opensuse.org/project/show/home:cabelo:innovators
## Author of these update files
Generated against upstream `vaptvupt-2.4.8` source tree. Spec mirrors
cabelo's existing 1.5.5 conventions (minimal `BuildRequires`,
`%autosetup -p1`, `V=1` verbose build, `%ifarch s390x` branch in
`%check`, no separate libzuptsdk subpackage) — only the necessary
fields are changed.

View file

@ -2,7 +2,7 @@
<service name="tar_scm" mode="manual"> <service name="tar_scm" mode="manual">
<param name="url">https://github.com/cristiancmoises/zupt</param> <param name="url">https://github.com/cristiancmoises/zupt</param>
<param name="scm">git</param> <param name="scm">git</param>
<param name="revision">v4.0.0</param> <param name="revision">v4.1.0</param>
<param name="versionformat">@PARENT_TAG@</param> <param name="versionformat">@PARENT_TAG@</param>
<param name="versionrewrite-pattern">v(.*)</param> <param name="versionrewrite-pattern">v(.*)</param>
<param name="submodules">enable</param> <param name="submodules">enable</param>

View file

@ -1,3 +1,28 @@
-------------------------------------------------------------------
Tue Jul 7 12:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>
- Update to 4.1.0:
* Source-only build. The prebuilt vendored libraries (libzuptsdk,
libpqvaptvupt) are removed from the tree; the package now builds
with no external library dependency and ships no .so. The
libzuptsdk-backed modes (Argon2id KDF, --pq-sdk, --pq-box) are
gated behind an optional upstream WITH_SDK=1 build; the default
password KDF is PBKDF2-SHA256 (600k) and --pq (native ML-KEM-768
+ X25519) is unchanged. spec %files no longer lists the .so;
%build/%install pass WITH_SDK=0.
* Fix: multithreaded encrypted archives were unextractable on the
native AEAD path. The parallel compress/decompress workers did not
bind the F-09 frame-preface AAD that the serial path and the
archive's AAD_PREFACE flag require, so every multithreaded block
failed authentication. Now byte-identical across thread counts;
also fixes `--kdf pbkdf2 -t N`.
* Security: LZH raw code-length stack overflow and huff_lut OOB on
crafted archives; integer-overflow heap OOB reads in the index and
solid-mode parsers; SEQ decoder safe-zone heap overflow; per-block
ENCRYPTED-flag authentication gate; PBKDF2 iteration-count DoS cap;
non-elidable secret wipe in the SDK path; restored disk images now
0600. Wire format v1.6 unchanged.
------------------------------------------------------------------- -------------------------------------------------------------------
Wed Jun 10 12:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org> Wed Jun 10 12:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>

View file

@ -19,7 +19,7 @@
Name: vaptvupt Name: vaptvupt
Version: 4.0.0 Version: 4.1.0
Release: 0 Release: 0
Summary: Post-quantum backup compression with AES-256 + ML-KEM-768 hybrid encryption Summary: Post-quantum backup compression with AES-256 + ML-KEM-768 hybrid encryption
License: AGPL-3.0-or-later License: AGPL-3.0-or-later
@ -38,25 +38,29 @@ Obsoletes: zupt < 3.0.0
%description %description
VaptVupt (formerly Zupt; renamed in v3.0.0 due to a prior INPI Brasil VaptVupt (formerly Zupt; renamed in v3.0.0 due to a prior INPI Brasil
trademark on the name "Zupt") compresses and encrypts backup archives. LZ77+Huffman compression trademark on the name "Zupt") compresses and encrypts backup archives.
(VaptVupt codec, ~2-3 GB/s decompression on x86_64 with AVX2 / aarch64 LZ + ANS compression (VaptVupt codec, ~2-3 GB/s decompression on x86_64
with NEON), AES-256-CTR + HMAC-SHA256 per-block authenticated with AVX2 / aarch64 with NEON), AES-256-CTR + HMAC-SHA256 per-block
encryption, multi-threaded, with optional ML-KEM-768 + X25519 authenticated encryption, multi-threaded, with ML-KEM-768 + X25519
post-quantum hybrid key encapsulation (FIPS 203 + RFC 7748). The post-quantum hybrid key encapsulation (FIPS 203 + RFC 7748) via --pq.
default password KDF is Argon2id; PBKDF2-SHA256 remains available
via --kdf pbkdf2 for backward compatibility.
Pure C11, vendored libzuptsdk, ~5,000 lines of core code. Constant- This package builds entirely from source with no external library
time cryptographic primitives are formally verified with Jasmin on dependency. The password KDF is PBKDF2-SHA256 (600k iterations). The
x86_64 (zupt_mac_verify_ct, zupt_ct_select_32); a clean C fallback optional libzuptsdk-backed modes (Argon2id KDF, --pq-sdk, --pq-box) are
runs on aarch64 and other architectures. not built here; they require an upstream WITH_SDK=1 build against the
separately distributed libzuptsdk/libpqvaptvupt.
Pure C11, ~5,000 lines of core code. Constant-time cryptographic
primitives are formally verified with Jasmin on x86_64
(zupt_mac_verify_ct, zupt_ct_select_32); a clean C fallback runs on
aarch64 and other architectures.
%prep %prep
%autosetup -p1 %autosetup -p1
chmod +x tests/*.sh chmod +x tests/*.sh
%build %build
%make_build V=1 \ %make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \ CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \ LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" LDLIBS="-lm -lpthread"
@ -71,14 +75,14 @@ chmod +x tests/*.sh
# On s390x, fall back to just the vector tests (Jasmin assembly is # On s390x, fall back to just the vector tests (Jasmin assembly is
# x86_64-only; threading harness has been flaky on big-endian). # x86_64-only; threading harness has been flaky on big-endian).
%ifarch s390x %ifarch s390x
%make_build V=1 \ %make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \ CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \ LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \ LDLIBS="-lm -lpthread" \
test-vectors test-vectors
./test_vectors ./test_vectors
%else %else
%make_build V=1 \ %make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \ CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \ LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \ LDLIBS="-lm -lpthread" \
@ -86,7 +90,7 @@ chmod +x tests/*.sh
%endif %endif
%install %install
%make_install PREFIX=%{_prefix} %make_install WITH_SDK=0 PREFIX=%{_prefix}
%files %files
%license LICENSE %license LICENSE
@ -95,12 +99,5 @@ chmod +x tests/*.sh
%{_bindir}/zupt %{_bindir}/zupt
%{_mandir}/man1/vaptvupt.1%{?ext_man} %{_mandir}/man1/vaptvupt.1%{?ext_man}
%{_mandir}/man1/zupt.1%{?ext_man} %{_mandir}/man1/zupt.1%{?ext_man}
%dir %{_prefix}/lib/vaptvupt
%{_prefix}/lib/vaptvupt/libzuptsdk.so
%{_prefix}/lib/vaptvupt/libzuptsdk.so.2
%{_prefix}/lib/vaptvupt/libzuptsdk.so.2.0.0
%{_prefix}/lib/vaptvupt/libpqvaptvupt.so
%{_prefix}/lib/vaptvupt/libpqvaptvupt.so.0
%{_prefix}/lib/vaptvupt/libpqvaptvupt.so.0.6.0
%changelog %changelog

View file

@ -20,7 +20,7 @@
# in the base. # in the base.
Name: vaptvupt Name: vaptvupt
Version: 4.0.0 Version: 4.1.0
Release: 1%{?dist} Release: 1%{?dist}
Summary: Post-quantum backup compression utility (AES-256 + ML-KEM-768 + Argon2id, formerly Zupt) Summary: Post-quantum backup compression utility (AES-256 + ML-KEM-768 + Argon2id, formerly Zupt)
@ -99,7 +99,7 @@ ln -sf libpqvaptvupt.so.0.6.0 %{buildroot}%{_libdir}/%{name}/libpqvaptvupt.so
%files %files
%license LICENSE %license LICENSE
%doc README.md SECURITY.md CHANGELOG.md AUDIT.md %doc README.md SECURITY.md CHANGELOG.md
%{_bindir}/zupt %{_bindir}/zupt
%{_libdir}/%{name}/libzuptsdk.so.2.0.0 %{_libdir}/%{name}/libzuptsdk.so.2.0.0
%{_libdir}/%{name}/libzuptsdk.so.2 %{_libdir}/%{name}/libzuptsdk.so.2

View file

@ -2,7 +2,7 @@
Public C ABI for the [VaptVupt](https://git.securityops.co/cristiancmoises/vaptvupt) backup compression library. Public C ABI for the [VaptVupt](https://git.securityops.co/cristiancmoises/vaptvupt) backup compression library.
Provides post-quantum encrypted compression as a stable, embeddable shared library — completely independent of the `vaptvupt` CLI.No dependency on any other compression library; everything is built from VaptVupt's own implementations. Provides post-quantum encrypted compression as a stable, embeddable shared library, independent of the `vaptvupt` CLI and of any external compression library — everything is built from VaptVupt's own implementations.
- **Version:** 1.0.0 - **Version:** 1.0.0
- **License:** AGPL-3.0-or-later - **License:** AGPL-3.0-or-later
@ -18,7 +18,7 @@ Provides post-quantum encrypted compression as a stable, embeddable shared libra
- **Secure memory** — mlock-backed buffers for passwords and keys, zeroed on destroy - **Secure memory** — mlock-backed buffers for passwords and keys, zeroed on destroy
- **Constant-time crypto** — Jasmin-verified assembly on x86_64 - **Constant-time crypto** — Jasmin-verified assembly on x86_64
- **Per-context state** — no globals; safe to use from any thread on distinct contexts - **Per-context state** — no globals; safe to use from any thread on distinct contexts
- **Custom allocator hooks**embed cleanly in any runtime - **Custom allocator hooks**supply your own malloc/free
## Quick start (C) ## Quick start (C)
@ -95,6 +95,8 @@ make sdk-test # runs C roundtrip suite (15 tests)
sudo make sdk-install PREFIX=/usr/local sudo make sdk-install PREFIX=/usr/local
``` ```
This SDK is built from source via `make sdk`; the previously vendored prebuilt `vendor/zuptsdk/libzuptsdk.so` has been removed from the tree.
This installs: This installs:
- `/usr/local/include/zuptsdk.h` - `/usr/local/include/zuptsdk.h`
- `/usr/local/lib/libzuptsdk.so.1.0.0` (with versioned `.so.1` and `.so` symlinks) - `/usr/local/lib/libzuptsdk.so.1.0.0` (with versioned `.so.1` and `.so` symlinks)
@ -174,7 +176,7 @@ sdk/
libzuptsdk is licensed under **AGPL-3.0-or-later** (see `sdk/LICENSE`). libzuptsdk is licensed under **AGPL-3.0-or-later** (see `sdk/LICENSE`).
The AGPL allows everyone to use the library freely, but anyone running it as a network service must publish their source code modifications. This protects the project from enterprise exploitation while keeping it usable by individuals, small businesses, and the open-source community. The AGPL allows everyone to use the library freely, but anyone running it as a network service must publish their source code modifications.
## Contact ## Contact

View file

@ -2297,12 +2297,17 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
* near the boundaries. We maintain §4 invariants 3 and 5. * near the boundaries. We maintain §4 invariants 3 and 5.
* *
* SAFEZONE_MAX_OFFSET covers the legal offset range (1 << wlog_max). * SAFEZONE_MAX_OFFSET covers the legal offset range (1 << wlog_max).
* SAFEZONE_MAX_RUN covers BOTH max litlen and max matchlen (both are * SAFEZONE_MAX_RUN bounds EACH of litlen and matchlen (both 65535 by
* bounded by the wire format at 65535: LL encoding ll_base[35]=61440 * the wire format: LL ll_base[35]=61440 + up to 4095 extra = 65535;
* + up to 4095 extra bits = 65535; ML encoding likewise). So * ML ml_base[35]=32768 + up to 32767 extra = 65535). A single sequence
* op_safe_end = op_end - 65535 guarantees any single sequence's * writes litlen literals THEN a matchlen match copy up to TWO max
* total writes (literals + match) fit without per-iter overflow * runs and in_safe_zone is computed once (pre-literal) yet gates BOTH
* checking. * the literal and the match op_end checks. So the reserve must cover
* the full worst-case sequence: op_safe_end = op_end - 2*SAFEZONE_MAX_RUN
* guarantees litlen+matchlen fit without per-iter overflow checking.
* (Reserving only ONE run let a crafted final sequence write up to
* 65535 bytes past op_end a heap overflow; the +64 caller slack was
* far too small to absorb it.)
* *
* SPRINT 46: raised from 1<<20 to 1<<24. The 3-byte offset wire * SPRINT 46: raised from 1<<20 to 1<<24. The 3-byte offset wire
* encoding (off_bytes==3 for wlog>16) represents offsets up to * encoding (off_bytes==3 for wlog>16) represents offsets up to
@ -2318,9 +2323,11 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
* An offset > 2^24 remains genuinely corrupt (unrepresentable in * An offset > 2^24 remains genuinely corrupt (unrepresentable in
* 3 bytes) and is still rejected, preserving the DoS guard. */ * 3 bytes) and is still rejected, preserving the DoS guard. */
enum { SAFEZONE_MAX_OFFSET = 1u << 24 }; /* 3-byte offset wire max */ enum { SAFEZONE_MAX_OFFSET = 1u << 24 }; /* 3-byte offset wire max */
enum { SAFEZONE_MAX_RUN = 65535 }; /* litlen or matchlen */ enum { SAFEZONE_MAX_RUN = 65535 }; /* max litlen OR matchlen */
uint8_t *op_safe_end = (dst_cap > SAFEZONE_MAX_RUN) /* Reserve for a full worst-case sequence (litlen + matchlen). */
? op_end - SAFEZONE_MAX_RUN : dst; enum { SAFEZONE_RESERVE = 2 * SAFEZONE_MAX_RUN };
uint8_t *op_safe_end = (dst_cap > SAFEZONE_RESERVE)
? op_end - SAFEZONE_RESERVE : dst;
const uint8_t *offset_check_floor = dst_base + SAFEZONE_MAX_OFFSET; const uint8_t *offset_check_floor = dst_base + SAFEZONE_MAX_OFFSET;
size_t seqs_decoded = 0; size_t seqs_decoded = 0;

View file

@ -32,6 +32,8 @@
* [..] raw key bytes (PQVV_PUBLICKEYBYTES / PQVV_SECRETKEYBYTES) * [..] raw key bytes (PQVV_PUBLICKEYBYTES / PQVV_SECRETKEYBYTES)
*/ */
#include "zupt.h" #include "zupt.h"
#ifdef ZUPT_WITH_SDK
#include "zupt_keccak.h" #include "zupt_keccak.h"
#include "pqvaptvupt.h" #include "pqvaptvupt.h"
#include <stdio.h> #include <stdio.h>
@ -179,3 +181,35 @@ int zupt_pqbox_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
zupt_secure_wipe(session_key, sizeof(session_key)); zupt_secure_wipe(session_key, sizeof(session_key));
return 0; return 0;
} }
#else /* !ZUPT_WITH_SDK */
/* Source-only build (no vendored libpqvaptvupt binary). The --pq-box sealed-box
* mode is unavailable; use native --pq (ML-KEM-768 + X25519) instead, or rebuild
* with `make WITH_SDK=1` (requires the vendored libpqvaptvupt). */
#include <stdio.h>
static int pqbox_unavailable(const char *what) {
fprintf(stderr,
"Error: this build has no libpqvaptvupt support, so %s is unavailable.\n"
" Use native --pq (ML-KEM-768 + X25519) instead, or rebuild with "
"'make WITH_SDK=1'.\n", what);
return -1;
}
int zupt_pqbox_keygen(const char *privkeyfile, const char *pubkeyfile) {
(void)privkeyfile; (void)pubkeyfile;
return pqbox_unavailable("--pq-box key generation");
}
int zupt_pqbox_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
uint8_t *enc_hdr, size_t *enc_hdr_len) {
(void)kr; (void)pubkeyfile; (void)enc_hdr; (void)enc_hdr_len;
return pqbox_unavailable("--pq-box encryption");
}
int zupt_pqbox_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *payload, size_t payload_len) {
(void)kr; (void)privkeyfile; (void)payload; (void)payload_len;
return pqbox_unavailable("--pq-box decryption (this archive needs it)");
}
#endif /* ZUPT_WITH_SDK */

View file

@ -8,6 +8,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
#include "zupt.h" #include "zupt.h"
#ifdef ZUPT_WITH_SDK
#include "zuptsdk.h" #include "zuptsdk.h"
#include "zuptsdk_easy.h" #include "zuptsdk_easy.h"
#include <stdio.h> #include <stdio.h>
@ -68,14 +70,14 @@ int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
size_t blob_sz = 0; size_t blob_sz = 0;
int rc = zuptsdk_easy_encrypt(pubkeyfile, session_key, 32, &blob, &blob_sz); int rc = zuptsdk_easy_encrypt(pubkeyfile, session_key, 32, &blob, &blob_sz);
if (rc != 0 || !blob) { if (rc != 0 || !blob) {
memset(session_key, 0, 32); zupt_secure_wipe(session_key, 32);
return -1; return -1;
} }
/* Layout: [1B type][4B blob_sz LE][blob] */ /* Layout: [1B type][4B blob_sz LE][blob] */
if (1 + 4 + blob_sz > 1500) { if (1 + 4 + blob_sz > 1500) {
free(blob); free(blob);
memset(session_key, 0, 32); zupt_secure_wipe(session_key, 32);
return -1; return -1;
} }
@ -98,7 +100,7 @@ int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key); zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key);
memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16); memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key); zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key);
memset(kdf_buf, 0, sizeof(kdf_buf)); zupt_secure_wipe(kdf_buf, sizeof(kdf_buf));
kr->canary_head = ZUPT_CANARY; kr->canary_head = ZUPT_CANARY;
zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE); zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE);
@ -107,7 +109,7 @@ int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
kr->canary_tail = ZUPT_CANARY; kr->canary_tail = ZUPT_CANARY;
free(blob); free(blob);
memset(session_key, 0, 32); zupt_secure_wipe(session_key, 32);
return 0; return 0;
} }
@ -144,7 +146,7 @@ int zupt_sdk_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key); zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key);
memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16); memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key); zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key);
memset(kdf_buf, 0, sizeof(kdf_buf)); zupt_secure_wipe(kdf_buf, sizeof(kdf_buf));
kr->canary_head = ZUPT_CANARY; kr->canary_head = ZUPT_CANARY;
/* base_nonce will be overwritten per-block by the legacy path; in SDK /* base_nonce will be overwritten per-block by the legacy path; in SDK
@ -188,7 +190,7 @@ int zupt_sdk_password_encrypt_init(zupt_keyring_t *kr, const char *password,
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key); zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key);
memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16); memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key); zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key);
memset(kdf_buf, 0, sizeof(kdf_buf)); zupt_secure_wipe(kdf_buf, sizeof(kdf_buf));
kr->canary_head = ZUPT_CANARY; kr->canary_head = ZUPT_CANARY;
memcpy(kr->base_nonce, enc_hdr + 17, ZUPT_NONCE_SIZE); memcpy(kr->base_nonce, enc_hdr + 17, ZUPT_NONCE_SIZE);
@ -196,8 +198,8 @@ int zupt_sdk_password_encrypt_init(zupt_keyring_t *kr, const char *password,
kr->active = 1; kr->active = 1;
kr->canary_tail = ZUPT_CANARY; kr->canary_tail = ZUPT_CANARY;
memset(key, 0, 32); zupt_secure_wipe(key, 32);
memset(salt, 0, 16); zupt_secure_wipe(salt, 16);
return 0; return 0;
} }
@ -233,7 +235,7 @@ int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password,
zupt_sha3_256(kdf_buf2, sizeof(kdf_buf2), kr->enc_key); zupt_sha3_256(kdf_buf2, sizeof(kdf_buf2), kr->enc_key);
memcpy(kdf_buf2 + 32, "ZUPT-SDK-MAC-KEY", 16); memcpy(kdf_buf2 + 32, "ZUPT-SDK-MAC-KEY", 16);
zupt_sha3_256(kdf_buf2, sizeof(kdf_buf2), kr->mac_key); zupt_sha3_256(kdf_buf2, sizeof(kdf_buf2), kr->mac_key);
memset(kdf_buf2, 0, sizeof(kdf_buf2)); zupt_secure_wipe(kdf_buf2, sizeof(kdf_buf2));
kr->canary_head = ZUPT_CANARY; kr->canary_head = ZUPT_CANARY;
memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE); memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE);
@ -241,6 +243,52 @@ int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password,
kr->active = 1; kr->active = 1;
kr->canary_tail = ZUPT_CANARY; kr->canary_tail = ZUPT_CANARY;
memset(key, 0, 32); zupt_secure_wipe(key, 32);
return 0; return 0;
} }
#else /* !ZUPT_WITH_SDK */
/* Source-only build (no vendored libzuptsdk binary). The SDK-backed modes
* --pq-sdk and the Argon2id default password KDF are unavailable. These
* stubs let the project build and link from source with no prebuilt library;
* callers fall back to native crypto (PBKDF2-SHA256 password KDF, native
* ML-KEM-768 + X25519 via --pq) or report the requested mode as unsupported.
* Rebuild with `make WITH_SDK=1` (requires the vendored libzuptsdk) to enable. */
#include <stdio.h>
static int sdk_unavailable(const char *what) {
fprintf(stderr,
"Error: this build has no libzuptsdk support, so %s is unavailable.\n"
" Use native crypto instead (password mode uses PBKDF2-SHA256; "
"--pq uses ML-KEM-768 + X25519),\n"
" or rebuild with 'make WITH_SDK=1'.\n", what);
return -1;
}
int zupt_sdk_hybrid_keygen(const char *privkeyfile, const char *pubkeyfile) {
(void)privkeyfile; (void)pubkeyfile;
return sdk_unavailable("--pq-sdk key generation");
}
int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
uint8_t *enc_hdr, size_t *enc_hdr_len) {
(void)kr; (void)pubkeyfile; (void)enc_hdr; (void)enc_hdr_len;
return sdk_unavailable("--pq-sdk encryption");
}
int zupt_sdk_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *enc_hdr, size_t enc_hdr_len) {
(void)kr; (void)privkeyfile; (void)enc_hdr; (void)enc_hdr_len;
return sdk_unavailable("--pq-sdk decryption");
}
int zupt_sdk_password_encrypt_init(zupt_keyring_t *kr, const char *password,
uint8_t *enc_hdr, size_t *enc_hdr_len) {
(void)kr; (void)password; (void)enc_hdr; (void)enc_hdr_len;
return sdk_unavailable("the Argon2id password KDF");
}
int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password,
const uint8_t *enc_hdr, size_t enc_hdr_len) {
(void)kr; (void)password; (void)enc_hdr; (void)enc_hdr_len;
return sdk_unavailable("the Argon2id password KDF (this archive needs it)");
}
#endif /* ZUPT_WITH_SDK */

View file

@ -421,7 +421,11 @@ zupt_error_t zupt_disk_backup(const char *output_path, const char *source_path,
} }
/* ─── Write index (single entry for the disk image) ─── */ /* ─── Write index (single entry for the disk image) ─── */
uint8_t idx_buf[4096]; /* SECURITY: size for the worst case — a path clamped to ZUPT_MAX_PATH-1
* (4095) PLUS the 4-byte file count, the (5-byte) varint length, and the
* 48 bytes of fixed trailing fields. A bare [4096] overflowed by ~57
* bytes when source_path approached ZUPT_MAX_PATH. */
uint8_t idx_buf[ZUPT_MAX_PATH + 128];
size_t idx_pos = 0; size_t idx_pos = 0;
/* File count (4B LE) */ /* File count (4B LE) */
@ -706,7 +710,11 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
* fall back to O_CREAT | O_TRUNC for new files. */ * fall back to O_CREAT | O_TRUNC for new files. */
tgt_fd = open(target_path, O_WRONLY); tgt_fd = open(target_path, O_WRONLY);
if (tgt_fd < 0) { if (tgt_fd < 0) {
tgt_fd = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); /* SECURITY: 0600, not 0644 — a restored disk image holds decrypted
* backup contents and must not be world-/group-readable. Matches the
* file-extraction convention in zupt_safe_fopen_output(). (When the
* target is an existing block device the mode is ignored.) */
tgt_fd = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
} }
if (tgt_fd < 0) { if (tgt_fd < 0) {
fprintf(stderr, "Error: Cannot open target '%s': %s\n", fprintf(stderr, "Error: Cannot open target '%s': %s\n",

View file

@ -422,7 +422,15 @@ zupt_error_t write_enc_header(FILE *out, zupt_archive_header_t *hdr,
* + F-09 preface-AAD per-block pipeline. Only the KDF and * + F-09 preface-AAD per-block pipeline. Only the KDF and
* enc-header bytes differ. Read-path dispatch on enc_type byte * enc-header bytes differ. Read-path dispatch on enc_type byte
* at offset 0 of the enc-header block already handles both. */ * at offset 0 of the enc-header block already handles both. */
if (opts->kdf_legacy_pbkdf2) { #ifdef ZUPT_WITH_SDK
int use_pbkdf2 = opts->kdf_legacy_pbkdf2;
#else
/* No libzuptsdk in this build: Argon2id is unavailable, so the password
* KDF is always native PBKDF2-SHA256 (600k iters, AES-256-CTR + HMAC-
* SHA256). Archives written this way are readable by any build. */
int use_pbkdf2 = 1;
#endif
if (use_pbkdf2) {
uint8_t salt[ZUPT_SALT_SIZE], nonce[ZUPT_NONCE_SIZE]; uint8_t salt[ZUPT_SALT_SIZE], nonce[ZUPT_NONCE_SIZE];
zupt_random_bytes(salt, ZUPT_SALT_SIZE); zupt_random_bytes(salt, ZUPT_SALT_SIZE);
zupt_random_bytes(nonce, ZUPT_NONCE_SIZE); zupt_random_bytes(nonce, ZUPT_NONCE_SIZE);
@ -584,7 +592,7 @@ static uint64_t get_mtime(const char *path) {
* *
* Excludes block_magic (constant `bb 01`, structurally rejected by read_block * Excludes block_magic (constant `bb 01`, structurally rejected by read_block
* if tampered) and the AES nonce (already part of the existing MAC input). */ * if tampered) and the AES nonce (already part of the existing MAC input). */
#define ZUPT_PREFACE_AAD_LEN 29 /* ZUPT_PREFACE_AAD_LEN is defined in zupt.h (shared with the parallel path). */
static void zupt_serialize_preface_aad(const zupt_block_t *b, uint8_t out[ZUPT_PREFACE_AAD_LEN]) { static void zupt_serialize_preface_aad(const zupt_block_t *b, uint8_t out[ZUPT_PREFACE_AAD_LEN]) {
out[0] = (uint8_t)b->block_type; out[0] = (uint8_t)b->block_type;
out[1] = (uint8_t)(b->codec_id & 0xFF); out[1] = (uint8_t)(b->codec_id & 0xFF);
@ -600,7 +608,7 @@ static void zupt_serialize_preface_aad(const zupt_block_t *b, uint8_t out[ZUPT_P
* known at MAC time but before the block struct exists. Same byte layout * known at MAC time but before the block struct exists. Same byte layout
* as zupt_serialize_preface_aad both sides must produce identical bytes * as zupt_serialize_preface_aad both sides must produce identical bytes
* for the same logical block, or the roundtrip MAC won't match. */ * for the same logical block, or the roundtrip MAC won't match. */
static void zupt_serialize_preface_aad_scalars( void zupt_serialize_preface_aad_scalars(
uint8_t block_type, uint16_t codec_id, uint16_t block_flags, uint8_t block_type, uint16_t codec_id, uint16_t block_flags,
uint64_t uncompressed_size, uint64_t compressed_size, uint64_t checksum, uint64_t uncompressed_size, uint64_t compressed_size, uint64_t checksum,
uint8_t out[ZUPT_PREFACE_AAD_LEN]) uint8_t out[ZUPT_PREFACE_AAD_LEN])
@ -1766,6 +1774,16 @@ zupt_error_t decompress_block(const zupt_block_t *b, const zupt_keyring_t *kr,
if (b->uncompressed_size > ZUPT_MAX_BLOCK_SZ) return ZUPT_ERR_OVERFLOW; if (b->uncompressed_size > ZUPT_MAX_BLOCK_SZ) return ZUPT_ERR_OVERFLOW;
if (comp_len > ZUPT_MAX_BLOCK_SZ + 1024) return ZUPT_ERR_OVERFLOW; if (comp_len > ZUPT_MAX_BLOCK_SZ + 1024) return ZUPT_ERR_OVERFLOW;
/* SECURITY: in an encrypted archive every block MUST be encrypted.
* The per-block ENCRYPTED flag is NOT covered by the archive-integrity
* trailer (which only authenticates the header and footer, not block
* bodies/flags). Without this gate an attacker could clear the flag on
* a forged STORE block and inject attacker-chosen plaintext that passes
* only the keyless XXH64 an authentication bypass / plaintext forgery.
* Fail closed when the keyring is active but the block isn't encrypted. */
if (kr && kr->active && !(b->block_flags & ZUPT_BFLAG_ENCRYPTED))
return ZUPT_ERR_AUTH_FAIL;
if (b->block_flags & ZUPT_BFLAG_ENCRYPTED) { if (b->block_flags & ZUPT_BFLAG_ENCRYPTED) {
if (!kr || !kr->active) return ZUPT_ERR_AUTH_FAIL; if (!kr || !kr->active) return ZUPT_ERR_AUTH_FAIL;
size_t dec_len; size_t dec_len;
@ -2017,6 +2035,10 @@ zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t
memcpy(nonce, eb.payload + 33, 16); memcpy(nonce, eb.payload + 33, 16);
memcpy(&iter, eb.payload + 49, 4); memcpy(&iter, eb.payload + 49, 4);
free(eb.payload); free(eb.payload);
/* SECURITY: reject an absurd attacker-supplied iteration count before
* spending the CPU on it (KDF-amplification DoS). See
* ZUPT_KDF_MAX_ITERATIONS. */
if (iter < 1 || iter > ZUPT_KDF_MAX_ITERATIONS) return ZUPT_ERR_CORRUPT;
fprintf(stderr, " Deriving decryption key (PBKDF2-SHA256, %u iterations)...\n", iter); fprintf(stderr, " Deriving decryption key (PBKDF2-SHA256, %u iterations)...\n", iter);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter); zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter);
return ZUPT_OK; return ZUPT_OK;
@ -2033,6 +2055,9 @@ zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t
memcpy(nonce, eb.payload + 32, 16); memcpy(nonce, eb.payload + 32, 16);
memcpy(&iter, eb.payload + 48, 4); memcpy(&iter, eb.payload + 48, 4);
free(eb.payload); free(eb.payload);
/* SECURITY: reject an absurd attacker-supplied iteration count before
* spending the CPU on it (KDF-amplification DoS). */
if (iter < 1 || iter > ZUPT_KDF_MAX_ITERATIONS) return ZUPT_ERR_CORRUPT;
fprintf(stderr, " Deriving decryption key (PBKDF2-SHA256, %u iterations)...\n", iter); fprintf(stderr, " Deriving decryption key (PBKDF2-SHA256, %u iterations)...\n", iter);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter); zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter);
return ZUPT_OK; return ZUPT_OK;
@ -2059,7 +2084,13 @@ static zupt_error_t parse_index(const uint8_t *buf, size_t blen,
zupt_index_entry_t *e = &(*ents)[i]; zupt_index_entry_t *e = &(*ents)[i];
uint64_t plen; uint64_t plen;
vn = zupt_decode_varint(buf+p, blen-p, &plen); vn = zupt_decode_varint(buf+p, blen-p, &plen);
if (vn<0||p+(size_t)vn+plen>blen) { free(*ents); return ZUPT_ERR_CORRUPT; } if (vn<0) { free(*ents); return ZUPT_ERR_CORRUPT; }
/* SECURITY: overflow-safe bound. The decoder consumes at most blen-p
* bytes so p+vn<=blen and blen-p-vn cannot underflow. The previous
* check `p+vn+plen>blen` wrapped around for an attacker-supplied
* ~2^64 plen, passed, then drove an OOB memcpy of ZUPT_MAX_PATH-1
* bytes past the index buffer. */
if (plen > (uint64_t)(blen - p - (size_t)vn)) { free(*ents); return ZUPT_ERR_CORRUPT; }
p += (size_t)vn; p += (size_t)vn;
if (plen >= ZUPT_MAX_PATH) plen = ZUPT_MAX_PATH-1; if (plen >= ZUPT_MAX_PATH) plen = ZUPT_MAX_PATH-1;
memcpy(e->path, buf+p, (size_t)plen); e->path[plen]='\0'; p += (size_t)plen; memcpy(e->path, buf+p, (size_t)plen); e->path[plen]='\0'; p += (size_t)plen;
@ -2389,7 +2420,11 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options
uint64_t off = e->first_block_offset; uint64_t off = e->first_block_offset;
uint64_t sz = e->uncompressed_size; uint64_t sz = e->uncompressed_size;
if (off + sz <= total_size) { /* SECURITY: overflow-safe bound. off and sz are both attacker-
* controlled 64-bit index fields; the previous `off+sz<=total_size`
* wrapped on overflow, letting solid_buf+off point far out of
* bounds for an arbitrary-offset OOB heap read. */
if (off <= total_size && sz <= total_size - off) {
if (fwrite(solid_buf + off, 1, (size_t)sz, of) != (size_t)sz) { if (fwrite(solid_buf + off, 1, (size_t)sz, of) != (size_t)sz) {
fprintf(stderr, "Error: write failed (disk full?) for %s\n", e->path); fprintf(stderr, "Error: write failed (disk full?) for %s\n", e->path);
fclose(of); fclose(of);

View file

@ -319,14 +319,19 @@ static void huff_lut(const uint8_t *lengths, int ns, hlut_t *lut) {
int sz = 1<<LZH_MAX_CODELEN; int sz = 1<<LZH_MAX_CODELEN;
for(int i=0;i<sz;i++){lut[i].sym=-1;lut[i].len=0;} for(int i=0;i<sz;i++){lut[i].sym=-1;lut[i].len=0;}
/* SECURITY: code lengths index lc[]/nc[] (size LZH_MAX_CODELEN+1) and
* drive the shift 1<<(LZH_MAX_CODELEN-bits); a value > LZH_MAX_CODELEN
* would read/write out of bounds and shift by a negative amount (UB).
* Callers validate, but guard here too so the builder is memory-safe
* for any input (defense in depth). */
int lc[LZH_MAX_CODELEN+1]; memset(lc,0,sizeof(lc)); int lc[LZH_MAX_CODELEN+1]; memset(lc,0,sizeof(lc));
for(int i=0;i<ns;i++) if(lengths[i]>0) lc[lengths[i]]++; for(int i=0;i<ns;i++) if(lengths[i]>0 && lengths[i]<=LZH_MAX_CODELEN) lc[lengths[i]]++;
uint32_t nc[LZH_MAX_CODELEN+1]; memset(nc,0,sizeof(nc)); uint32_t nc[LZH_MAX_CODELEN+1]; memset(nc,0,sizeof(nc));
uint32_t cv=0; uint32_t cv=0;
for(int b=1;b<=LZH_MAX_CODELEN;b++){cv=(cv+lc[b-1])<<1;nc[b]=cv;} for(int b=1;b<=LZH_MAX_CODELEN;b++){cv=(cv+lc[b-1])<<1;nc[b]=cv;}
for(int i=0;i<ns;i++){ for(int i=0;i<ns;i++){
if(lengths[i]==0) continue; if(lengths[i]==0 || lengths[i]>LZH_MAX_CODELEN) continue;
int bits=lengths[i]; int bits=lengths[i];
uint16_t c=(uint16_t)nc[bits]++; uint16_t c=(uint16_t)nc[bits]++;
uint16_t rev=0; uint16_t rev=0;
@ -761,9 +766,17 @@ size_t zupt_lzh_decompress(const uint8_t *src, size_t slen,
if (used < 0) return 0; if (used < 0) return 0;
ip += cl_len; ip += cl_len;
} else { } else {
/* Raw code lengths */ /* Raw code lengths: one byte per symbol. SECURITY: bound the count
if (ip + ll_hdr > slen) return 0; * against BOTH the source AND the destination stack buffer
* (ll_lens[LZH_MAX_LITLEN]). ll_hdr is attacker-controlled and may be
* up to 0x7FFF; without the destination bound a crafted archive
* smashes the stack. Also reject out-of-range code-length values
* (raw bytes are unconstrained; legal canonical lengths are 0..15)
* so the LUT builder cannot index past lc[]/nc[]. */
if (ll_hdr > LZH_MAX_LITLEN || ip + ll_hdr > slen) return 0;
memcpy(ll_lens, src + ip, ll_hdr); ip += ll_hdr; memcpy(ll_lens, src + ip, ll_hdr); ip += ll_hdr;
for (size_t k = 0; k < ll_hdr; k++)
if (ll_lens[k] > LZH_MAX_CODELEN) return 0;
} }
/* Read dist code lengths */ /* Read dist code lengths */
@ -776,8 +789,12 @@ size_t zupt_lzh_decompress(const uint8_t *src, size_t slen,
if (used < 0) return 0; if (used < 0) return 0;
ip += cl_len; ip += cl_len;
} else { } else {
if (ip + d_hdr > slen) return 0; /* Raw dist code lengths — same destination-bound + value-range
* hardening as the litlen path above (d_lens[LZH_MAX_DIST]). */
if (d_hdr > LZH_MAX_DIST || ip + d_hdr > slen) return 0;
memcpy(d_lens, src + ip, d_hdr); ip += d_hdr; memcpy(d_lens, src + ip, d_hdr); ip += d_hdr;
for (size_t k = 0; k < d_hdr; k++)
if (d_lens[k] > LZH_MAX_CODELEN) return 0;
} }
/* Build LUTs */ /* Build LUTs */

View file

@ -135,7 +135,26 @@ static void worker_compress(zpar_slot_t *slot, const zupt_keyring_t *kr) {
slot->out_bflags = 0; slot->out_bflags = 0;
if (kr && kr->active) { if (kr && kr->active) {
size_t enc_len; size_t enc_len;
uint8_t *enc = zupt_encrypt_buffer(kr, payload, payload_size, slot->block_seq, &enc_len); uint8_t *enc;
/* F-09: when the archive uses AAD-preface mode, bind the per-block frame
* preface into the MAC EXACTLY as the serial path does (zupt_format.c).
* The extract side honours the archive's ZUPT_FLAG_AAD_PREFACE flag, so
* if this worker skipped the preface every multithreaded encrypted block
* would fail authentication and the archive would be unextractable. The
* scalars mirror the serial call: predicted compressed_size is
* nonce(16) + payload + hmac(32), block_flags is ENCRYPTED. */
if (kr->use_preface_aad) {
uint8_t preface[ZUPT_PREFACE_AAD_LEN];
uint64_t predicted_csz = 16 + (uint64_t)payload_size + 32;
zupt_serialize_preface_aad_scalars(
ZUPT_BLOCK_DATA, slot->actual_codec, (uint16_t)ZUPT_BFLAG_ENCRYPTED,
(uint64_t)nread, predicted_csz, slot->checksum, preface);
enc = zupt_encrypt_buffer_aad(kr, payload, payload_size, slot->block_seq,
preface, ZUPT_PREFACE_AAD_LEN, &enc_len);
zupt_secure_wipe(preface, sizeof(preface));
} else {
enc = zupt_encrypt_buffer(kr, payload, payload_size, slot->block_seq, &enc_len);
}
if (!enc) { free(cbuf); slot->error = ZUPT_ERR_NOMEM; return; } if (!enc) { free(cbuf); slot->error = ZUPT_ERR_NOMEM; return; }
/* Output is the encrypted payload (caller frees slot->output) */ /* Output is the encrypted payload (caller frees slot->output) */
slot->output = enc; slot->output = enc;
@ -169,11 +188,36 @@ static void worker_decompress(zpar_slot_t *slot, const zupt_keyring_t *kr) {
if (!comp_data && comp_len > 0) { slot->error = ZUPT_ERR_CORRUPT; return; } if (!comp_data && comp_len > 0) { slot->error = ZUPT_ERR_CORRUPT; return; }
if (slot->uncomp_size > ZUPT_MAX_BLOCK_SZ) { slot->error = ZUPT_ERR_OVERFLOW; return; } if (slot->uncomp_size > ZUPT_MAX_BLOCK_SZ) { slot->error = ZUPT_ERR_OVERFLOW; return; }
/* Decrypt if encrypted — HMAC verified inside zupt_decrypt_buffer (before decryption) */ /* SECURITY: in an encrypted archive every block MUST be encrypted. The
* per-block ENCRYPTED flag is not covered by the archive-integrity
* trailer, so without this gate an attacker could clear the flag on a
* forged STORE block and inject attacker-chosen plaintext that passes
* only the keyless XXH64 an authentication bypass. Mirror the
* single-threaded decompress_block fail-closed behaviour. */
if (kr && kr->active && !(slot->block_flags & ZUPT_BFLAG_ENCRYPTED)) {
slot->error = ZUPT_ERR_AUTH_FAIL; return;
}
/* Decrypt if encrypted — HMAC verified inside the decrypt call (before
* decryption). Must mirror the compress worker and the serial
* decompress_block: when the archive uses AAD-preface mode, rebuild the
* canonical preface from this block's stored header fields and bind it into
* the MAC, otherwise a multithreaded extract of a preface-bound archive
* fails to authenticate every block. */
if (slot->block_flags & ZUPT_BFLAG_ENCRYPTED) { if (slot->block_flags & ZUPT_BFLAG_ENCRYPTED) {
if (!kr || !kr->active) { slot->error = ZUPT_ERR_AUTH_FAIL; return; } if (!kr || !kr->active) { slot->error = ZUPT_ERR_AUTH_FAIL; return; }
size_t dec_len; size_t dec_len;
if (kr->use_preface_aad) {
uint8_t preface[ZUPT_PREFACE_AAD_LEN];
zupt_serialize_preface_aad_scalars(
ZUPT_BLOCK_DATA, slot->codec_id, slot->block_flags,
slot->uncomp_size, (uint64_t)comp_len, slot->stored_checksum, preface);
dec_payload = zupt_decrypt_buffer_aad(kr, comp_data, comp_len, slot->block_seq,
preface, ZUPT_PREFACE_AAD_LEN, &dec_len);
zupt_secure_wipe(preface, sizeof(preface));
} else {
dec_payload = zupt_decrypt_buffer(kr, comp_data, comp_len, slot->block_seq, &dec_len); dec_payload = zupt_decrypt_buffer(kr, comp_data, comp_len, slot->block_seq, &dec_len);
}
if (!dec_payload) { slot->error = ZUPT_ERR_AUTH_FAIL; return; } if (!dec_payload) { slot->error = ZUPT_ERR_AUTH_FAIL; return; }
comp_data = dec_payload; comp_data = dec_payload;
comp_len = dec_len; comp_len = dec_len;
@ -329,6 +373,11 @@ static void *worker_entry(void *arg) {
zpar_ctx_t *zpar_create(int nthreads, uint32_t block_size, int mode, zpar_ctx_t *zpar_create(int nthreads, uint32_t block_size, int mode,
const zupt_keyring_t *keyring) { const zupt_keyring_t *keyring) {
if (nthreads < 1) nthreads = 1; if (nthreads < 1) nthreads = 1;
/* SECURITY (defense in depth): clamp the worker/slot count here too, not
* only at the CLI. A direct library/API caller could otherwise request an
* arbitrary count and exhaust memory (per-slot input buffers) and thread
* handles. The CLI already clamps -t to the same ceiling. */
if (nthreads > ZUPT_MAX_THREADS) nthreads = ZUPT_MAX_THREADS;
zpar_ctx_t *ctx = (zpar_ctx_t *)calloc(1, sizeof(zpar_ctx_t)); zpar_ctx_t *ctx = (zpar_ctx_t *)calloc(1, sizeof(zpar_ctx_t));
if (!ctx) return NULL; if (!ctx) return NULL;

View file

@ -1 +0,0 @@
libpqvaptvupt.so.0.6.0

View file

@ -1 +0,0 @@
libpqvaptvupt.so.0.6.0

Binary file not shown.

View file

@ -1 +0,0 @@
libzuptsdk.so.2.0.0

View file

@ -1 +0,0 @@
libzuptsdk.so.2.0.0

Binary file not shown.