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.*
*.dylib
# ...but keep vendored prebuilt libraries (shipped, no in-tree source)
!vendor/**/*.so
!vendor/**/*.so.*
*.exe
*.obj
*.lib
# Python bytecode caches
__pycache__/
*.pyc
# Coverage / profiling
*.gcda
*.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
## [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
Major release: the vendored codec moves to the canonical **VaptVupt

View file

@ -1,40 +1,29 @@
# Distributing VaptVupt
This document describes the upstream packaging recipes shipped under
`packaging/` and the path from "local source tree" to "package
installable on every major Linux distribution and macOS."
This document describes the upstream packaging recipes shipped under `packaging/` and the path from a local source tree to an installable package.
Recipes are upstream-maintained but distro-submission-ready. Real
submission to AUR / Debian / Fedora / Homebrew / NixOS is operational
work outside this repository.
Real submission to AUR / Debian / Fedora / Homebrew / NixOS / openSUSE is operational work outside this repository.
## Producing a reproducible source tarball
Every packaging recipe expects an upstream tarball `vaptvupt-VERSION.tar.gz`
produced by the project's `make dist` target. The tarball is
**byte-reproducible**:
Every packaging recipe expects an upstream tarball `vaptvupt-VERSION.tar.gz` produced by the project's `make dist` target. The tarball is byte-reproducible:
```sh
make dist
# → /tmp/vaptvupt-2.4.4.tar.gz
# → sha256: 407d20ef03e5bf857195b99e04843ef3b07357416a4115add1e8aaa2007a769f
# → bytes: 813113
# → /tmp/vaptvupt-4.1.0.tar.gz
```
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.
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.
The reproducibility properties:
- Files sorted by name (deterministic order across filesystems)
- mtime fixed to `SOURCE_DATE_EPOCH` (default `1747699200`; override
via env)
- mtime fixed to `SOURCE_DATE_EPOCH` (default `1747699200`; override via env)
- uid/gid pinned to root (0/0) via `--owner=0 --group=0 --numeric-owner`
- gzip wrapped with `-9n` (no embedded timestamp or filename)
- 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):
@ -48,20 +37,17 @@ SOURCE_DATE_EPOCH=1727740800 make dist # 2024-10-01 UTC
|-------------------|---------------------------------|----------------|
| Arch Linux | `packaging/aur/PKGBUILD` | AUR PKGBUILD |
| Debian / Ubuntu | `packaging/debian/` | Source package (`3.0 (quilt)`) |
| Fedora / RHEL | `packaging/rpm/vaptvupt.spec` | RPM .spec |
| macOS | `packaging/homebrew/vaptvupt.rb` | Homebrew formula |
| Fedora / RHEL | `packaging/rpm/vaptvupt.spec` | RPM .spec |
| openSUSE | `packaging/opensuse/` | RPM .spec (OBS) |
| macOS | `packaging/homebrew/vaptvupt.rb`| Homebrew formula |
| NixOS / Nix flake | `packaging/nix/flake.nix` | Nix flake |
All recipes:
- 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 docs (README, SECURITY, CHANGELOG, AUDIT) to
`$PREFIX/share/doc/vaptvupt/`
- Run the full upstream regression suite (`make test`) during build
when the distro's package guidelines allow check-phase execution
- Install docs (README, SECURITY, CHANGELOG) to `$PREFIX/share/doc/vaptvupt/`
- Run the full upstream regression suite (`make test`) during build when the distro's package guidelines allow check-phase execution
## Arch Linux (AUR)
@ -70,13 +56,13 @@ Maintainer flow:
```sh
# 1. Produce the upstream tarball
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)
# 3. Update packaging/aur/PKGBUILD:
# - Set pkgver=2.4.4
# - Set sha256sums=("$(sha256sum /tmp/vaptvupt-2.4.4.tar.gz | awk '{print $1}')")
# - Set pkgver=4.1.0
# - Set sha256sums=("$(sha256sum /tmp/vaptvupt-4.1.0.tar.gz | awk '{print $1}')")
# 4. Generate .SRCINFO
cd packaging/aur && makepkg --printsrcinfo > .SRCINFO
@ -87,7 +73,7 @@ makepkg -s
# 6. Push to AUR
git clone ssh://aur@aur.archlinux.org/vaptvupt.git 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:
@ -96,10 +82,9 @@ User install:
yay -S vaptvupt # or paru, pikaur, etc.
```
## Shell completions (v2.4.7+)
## Shell completions
`make install` automatically installs Bash, zsh, and fish completion
files alongside the binary and manpage:
`make install` automatically installs Bash, zsh, and fish completion files alongside the binary and manpage:
| Shell | Path |
|---|---|
@ -107,10 +92,7 @@ files alongside the binary and manpage:
| zsh | `$PREFIX/share/zsh/site-functions/_vaptvupt` |
| fish | `$PREFIX/share/fish/vendor_completions.d/vaptvupt.fish` |
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; the
underlying recipe is straightforward.
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.
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
```
Completions cover every CLI flag the binary actually parses
(`--kdf`, `--comment`, `--comment-file`, `--pq-sdk`, `--dedup`,
etc.) and are validated on every CI run via
`tests/test_completions_manpage.sh`.
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`.
## Debian / Ubuntu
The `packaging/debian/` tree is a Debian source-package layout.
Maintainer flow:
The `packaging/debian/` tree is a Debian source-package layout. Maintainer flow:
```sh
# 1. Produce the upstream tarball with the standard Debian
# orig.tar.gz naming convention:
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:
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
# 3. Build the source package:
@ -150,7 +128,7 @@ dpkg-buildpackage -S -us -uc # source-only
dpkg-buildpackage -b -us -uc # binary
# 4. Lint:
lintian vaptvupt_2.4.4-1_*.deb
lintian vaptvupt_4.1.0-1_*.deb
# 5. Submit via the standard Debian mentors process:
# https://mentors.debian.net/intro-maintainers/
@ -167,7 +145,7 @@ sudo apt install vaptvupt
```sh
# 1. Produce the tarball
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:
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
# 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:
# 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
```
## 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)
```sh
@ -227,12 +230,12 @@ nix build github:cristiancmoises/vaptvupt#vaptvupt
nix run github:cristiancmoises/vaptvupt#vaptvupt -- version
# 2. To consume from another flake:
# inputs.zupt.url = "github:cristiancmoises/vaptvupt?ref=v2.4.4";
# packages.x86_64-linux.default = inputs.zupt.packages.x86_64-linux.zupt;
# inputs.vaptvupt.url = "github:cristiancmoises/vaptvupt?ref=v4.1.0";
# packages.x86_64-linux.default = inputs.vaptvupt.packages.x86_64-linux.vaptvupt;
# 3. To submit to nixpkgs (https://github.com/NixOS/nixpkgs):
# - 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:
# 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:
- [ ] `make dist` produces a reproducible tarball (verified by
`tests/test_dist_reproducible.sh` on every `make test`)
- [ ] `make dist` produces a reproducible tarball (verified by `tests/test_dist_reproducible.sh` on every `make test`)
- [ ] The tarball is uploaded to a stable, immutable URL
- [ ] The recipe's checksum field is updated to match
`sha256sum /tmp/vaptvupt-VERSION.tar.gz`
- [ ] The recipe's checksum field is updated to match `sha256sum /tmp/vaptvupt-VERSION.tar.gz`
- [ ] The recipe builds and tests pass in a clean chroot/container
- [ ] The CHANGELOG mentions distro-relevant changes since the last release
- [ ] The license metadata is correct (AGPL-3.0-or-later for VaptVupt core;
GPL-3.0-or-later for the vendored VaptVupt codec)
- [ ] The license metadata is correct (AGPL-3.0-or-later for VaptVupt core; GPL-3.0-or-later for the vendored VaptVupt codec)
## Security posture for downstream
Every packaging recipe runs `make test` during build (`check()` for AUR,
`override_dh_auto_test` for Debian, `%check` for RPM, `checkPhase` for
Nix, `test` block for Homebrew). The suite includes:
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.
- **F-06**: 2 000 HMAC tamper trials, 0 silent accepts required
- **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.
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:
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.
```
This is correct behavior. The `vaptvupt-gui` deb requires:
- 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)
@ -22,7 +22,7 @@ sudo bash install-zupt-gui.sh
```
This script auto-detects your distribution and installs everything in
the right order. Done.
the right order.
## Manual fix — three commands (if you prefer)
@ -33,8 +33,8 @@ the right order. Done.
sudo apt update
sudo apt install -y python3-pyqt6
# 2. Upgrade vaptvupt CLI to 4.0.0
sudo dpkg -i vaptvupt_4.0.0_amd64.deb
# 2. Upgrade vaptvupt CLI to 4.1.0
sudo dpkg -i vaptvupt_4.1.0_amd64.deb
# 3. Install the GUI
sudo dpkg -i vaptvupt-gui_1.3.0_all.deb
@ -50,7 +50,7 @@ sudo apt --fix-broken install
```bash
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`)
@ -79,8 +79,7 @@ cd vaptvupt-gui.AppDir
./AppRun
```
The AppImage still needs Python 3 + Qt6 binding on the host (those are
universally available on every Linux distribution since 2022). For a
The AppImage still needs Python 3 + Qt6 binding on the host. For a
fully standalone executable with no Python dependency, use a future
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
dialogs.
PyQt6 is in the default repositories of every major Linux distribution
since 2022, so installing it is one apt/dnf/zypper/pacman command away.
We don't bundle Qt6 inside the deb because:
PyQt6 is in the default repositories of major Linux distributions, so
installing it is one apt/dnf/zypper/pacman command away. We don't bundle
Qt6 inside the deb because:
- It's already on most modern systems
- Bundling would make the deb 80 MB+ instead of 35 KB
- 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
post-quantum encryption (HKDF-SHA3 hybrid combiner, key commitment, HPKE
binding, Argon2id). These flags didn't exist in 2.1.7 — they were added
in 2.2.0.
If you have an older vaptvupt installed, the GUI's compress/extract will fail
with "unknown option --pq-sdk".
The GUI calls `vaptvupt --pq` and `vaptvupt keygen` for native
post-quantum encryption (ML-KEM-768 + X25519, in-tree implementation).
Older CLI versions lack these flags, so the GUI's compress/extract will
fail against them.
## After installing — verify
```bash
vaptvupt version # should show: 2.2.3
vaptvupt version # should show: 4.1.0
vaptvupt-gui # should launch the GUI window
```
@ -150,49 +146,40 @@ at https://git.securityops.co/cristiancmoises/vaptvupt/issues with:
## Building from source
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 |
|---|---|
| `gcc` ≥ 7 or `clang` ≥ 10 | C11 compiler |
| `make` | build driver |
| `libargon2-dev` | Argon2id KDF |
| `libssl-dev` | OpenSSL libcrypto (AES, SHA-256) |
| **`libzuptsdk-dev` 2.0.0+** | VaptVupt's cryptographic SDK |
| libm, pthread | math and threading (part of the standard C library/toolchain) |
The `libzuptsdk-dev` package is a separate sister project — it contains
the post-quantum hybrid cryptography that VaptVupt uses on its `--pq-sdk`
path. Both libraries are by the same author (Cristian Cezar Moisés) but
are distributed as separate source/binary packages so each can evolve
on its own release cadence.
The default build uses PBKDF2-SHA256 (600k iterations) for password KDF
and the in-tree native `--pq` mode (ML-KEM-768 + X25519) for post-quantum
encryption. No `libzuptsdk`, no OpenSSL, no libargon2 is required.
### Install build dependencies (Debian/Ubuntu/Mint)
```bash
sudo apt install build-essential libargon2-dev libssl-dev
# Then install libzuptsdk from its package:
sudo apt install ./libzuptsdk2_2.0.0_amd64.deb \
./libzuptsdk-dev_2.0.0_amd64.deb
sudo apt install build-essential
```
### Install build dependencies (Fedora/RHEL/openSUSE)
```bash
sudo dnf install gcc make libargon2-devel openssl-devel
# libzuptsdk from its SRPM:
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
sudo dnf install gcc make # Fedora/RHEL
sudo zypper install gcc make # openSUSE
```
### Build VaptVupt itself
```bash
tar -xzf vaptvupt-2.2.3-source.tar.gz
cd vaptvupt-2.2.3
tar -xzf vaptvupt-4.1.0-source.tar.gz
cd vaptvupt-4.1.0
make # build the `./vaptvupt` binary
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
```
61 tests pass: roundtrip, audit, multi-file, cross-block, dedup property,
path-traversal, argument-order, block-swap regression. Each suite reports
its own pass/fail count.
Covers roundtrip, multi-file, cross-block, dedup property, path-traversal,
argument-order, and block-swap regression. Each suite reports its own
pass/fail count.
### 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
x86_64).
### Static linking against libzuptsdk
### Optional: WITH_SDK=1 build
If you want a fully self-contained `vaptvupt` binary (no `libzuptsdk.so.2`
runtime dependency), you can link against the static library:
The SDK-backed modes — `--pq-sdk`, `--pq-box` (sealed-box), and the
Argon2id KDF — are optional. They are not in the default build and require
building against the separately distributed `libzuptsdk` / `libpqvaptvupt`
libraries:
```bash
make LDLIBS='-l:libzuptsdk.a -lcrypto -largon2'
make WITH_SDK=1
```
This produces a binary that doesn't need `libzuptsdk2` installed at
runtime — useful for containers, embedded systems, or distribution to
machines without package management.
This build additionally needs the SDK development package and its runtime
dependencies (OpenSSL libcrypto, libargon2), which ship with the SDK
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_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_ABS := $(abspath $(ZUPTSDK_DIR))
CFLAGS += -I$(ZUPTSDK_DIR)/include
CFLAGS += -DZUPT_WITH_SDK -I$(ZUPTSDK_DIR)/include
PQVV_DIR ?= vendor/pqvaptvupt
CFLAGS += -I$(PQVV_DIR)/include
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.
LDFLAGS += -Wl,-rpath,'$$ORIGIN/../lib/vaptvupt'
LDLIBS += -lpqvaptvupt
endif
# --- 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 \
@ -313,8 +325,11 @@ install: $(TARGET)
echo "Installed: $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/$(TARGET).fish"; \
fi
# Vendored runtime libraries (NEEDED by the binary): libzuptsdk
# (password KDF + --pq-sdk) and libpqvaptvupt (--pq-box, v4.0.0+).
# Vendored runtime libraries — installed ONLY for a WITH_SDK=1 build. In the
# 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)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
@ -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)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
endif
@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
test-asan: $(SOURCES) $(HEADERS) $(JAZZ_O)
$(Q)$(CC) -Wall -Wextra -std=c11 -Iinclude -Isrc -I$(ZUPTSDK_DIR)/include \
-fsanitize=address,undefined -g -O1 \
$(VV_SIMD_FLAGS) $(SHANI_FLAGS) -I$(PQVV_DIR)/include -L$(ZUPTSDK_DIR) -Wl,-rpath,$(ZUPTSDK_ABS) \
$(SOURCES) $(JAZZ_O) -o zupt_asan -lzuptsdk -L$(PQVV_DIR) -Wl,-rpath,$(PQVV_ABS) $(LDLIBS)
$(Q)$(CC) $(CFLAGS) -fsanitize=address,undefined -g -O1 \
$(VV_SIMD_FLAGS) $(SHANI_FLAGS) $(LDFLAGS) \
$(SOURCES) $(JAZZ_O) -o zupt_asan $(LDLIBS)
@echo "ASAN build: ./zupt_asan"
# Build the format-parser fuzz harness. Runs against ./zupt_asan to catch

567
README.md
View file

@ -3,65 +3,99 @@
# 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](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)
License: AGPL-3.0-or-later (dual-licensed AGPL + commercial).
> **Renamed from "Zupt" in v3.0.0** because of a prior INPI Brasil
> trademark registration on the name "Zupt" for unrelated software.
> The `.zupt` archive extension and `ZUPT` header magic bytes are
> unchanged — v2.x and v3.0.0 archives are bidirectionally compatible.
> The `zupt` command is preserved as a symlink to `vaptvupt` for one
> major version cycle.
> unchanged — v2.x and v3.0.0 archives remain compatible. The `zupt`
> command is preserved as a symlink to `vaptvupt` for one major version
> 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.
- **`--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.
- **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).
- **SHA-NI measured** — 5.8× over scalar SHA-256 (204 → 1184 MB/s); encrypted per-block throughput now 293 MB/s.
- **Source-only tree.** The prebuilt vendored libraries `libzuptsdk.so`
and `libpqvaptvupt.so` have been removed. The default `make` needs only
a C compiler and make (plus libm/pthread) — no external crypto library —
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
[release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.0.0).
> **F-16 (data loss):** archives created by **≤ 3.8.0** at `-l 8`/`-l 9`
> 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
authenticated encryption, post-quantum key encapsulation, and
full-disk backup. Pure C11, zero dependencies, ~13,000 lines. Builds
and runs on x86_64, aarch64, armhf, ppc64le, s390x, and riscv64.
Binaries for the CLI (4.1.0) and GUI (1.3.0) are on the
[release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.1.0).
---
## 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`.
- **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.
- **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.
- **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.
- **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).
- **Multi-threaded** — Compression and decompression both parallelized. `-t 0` auto-detects cores.
- **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.
- **Encrypted backups in one command**`vaptvupt compress -p changeme backup.zupt ~/data/` — AES-256 + HMAC-SHA256, file names hidden.
- **Per-block integrity** — XXH64 checksum + HMAC-SHA256 per block. Wrong password rejected instantly.
- **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`.
- **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).
- **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.
- **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.
- **Zero dependencies** — ML-KEM, X25519, Keccak, SHA-256, AES-256, HMAC, PBKDF2, VaptVupt codec — all pure C11. Builds with `gcc` or `cl` alone.
- **Hardware-adaptive codec** — auto-detects AVX2/NEON at runtime and
selects the codec: VaptVupt (LZ77 + tANS + SIMD decode) on capable
hardware, VaptVupt-LZHP on everything else. Override with `--vv` or
`--lzhp`.
- **Post-quantum encryption**`--pq` uses ML-KEM-768 + X25519 hybrid
KEM (the approach used by Signal and iMessage), protecting against
"harvest now, decrypt later" attacks. In-tree, available in the default
build.
- **AES-NI acceleration** — AES-256-CTR via Jasmin-verified assembly with
a 4-block interleaved pipeline. AVX detection validates OSXSAVE/XCR0 (no
SIGILL). Falls back to C table-based AES on unsupported hardware.
- **SHA-NI acceleration** — HMAC-SHA256 (the Encrypt-then-MAC pass) and
PBKDF2 use the Intel SHA-NI compression path when the CPU supports it
(Intel Goldmont+/Ice Lake+, AMD Zen+), selected at runtime via CPUID.
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
### Fast installation
```
curl -fsSL https://short.securityops.co/vaptvupt | bash
```
### Build & Install
### Build & install
```
git clone https://git.securityops.co/cristiancmoises/vaptvupt.git && \
cd vaptvupt && \
@ -69,20 +103,26 @@ make && \
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
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`.
**Command-line tool (`vaptvupt` 4.0.0):**
**Command-line tool (`vaptvupt` 4.1.0):**
| Format | File | Distros |
|---|---|---|
| Debian/Ubuntu | `vaptvupt_4.0.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 |
| AppDir tarball | `vaptvupt-4.0.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 |
| openSUSE OBS | `vaptvupt-4.0.0-opensuse-obs.tar.gz` | Open Build Service source bundle |
| Debian/Ubuntu | `vaptvupt_4.1.0_amd64.deb` | Debian 11+, Ubuntu 22.04+, Mint 21+ |
| RPM | `vaptvupt-4.1.0-1.x86_64.rpm` | Fedora 38+, RHEL 9+, openSUSE, AlmaLinux, Rocky, other RPM-based distributions |
| AppDir tarball | `vaptvupt-4.1.0-x86_64.AppDir.tar.gz` | Any glibc 2.28+ (extract & run, no FUSE) |
| Source tarball | `vaptvupt-4.1.0.tar.gz` | Build from source on any platform |
| openSUSE OBS | `vaptvupt-4.1.0-opensuse-obs.tar.gz` | Open Build Service source bundle |
**Graphical front-end (`vaptvupt-gui` 1.3.0):**
@ -98,19 +138,19 @@ and verifiable against the published `SHA256SUMS.txt`.
sha256sum -c SHA256SUMS.txt
# 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
# 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
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)
tar xzf vaptvupt-4.0.0-x86_64.AppDir.tar.gz
./vaptvupt-4.0.0-x86_64.AppDir/AppRun --help
tar xzf vaptvupt-4.1.0-x86_64.AppDir.tar.gz
./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
./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)
```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)"
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
```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/
# 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)
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/
# 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/
# List archive contents
vaptvupt list backup.zupt
# Show archive metadata (codec, blocks, encryption — no password needed)
# Show archive metadata (no password needed)
vaptvupt info backup.zupt
# Verify archive integrity (HMAC + per-block checksums)
vaptvupt test backup.zupt
vaptvupt test -p "my-strong-password" backup.zupt
# Extract everything
# Extract
vaptvupt extract -o ~/restored/ backup.zupt
# Extract from encrypted archive
vaptvupt extract -p "my-strong-password" -o ~/restored/ backup.zupt
# Benchmark all 9 levels on a file
@ -165,32 +203,36 @@ vaptvupt bench big-file.tar
#### Post-quantum encryption
```bash
# Recommended: SDK v2 (HKDF combiner + key commitment + HPKE binding + Argon2id).
# New archives should use this.
# Native --pq (ML-KEM-768 + X25519 hybrid KEM, in-tree, default build).
# Recommended for new archives.
vaptvupt keygen -o mykey.key
vaptvupt keygen --pub -o pub.key -k mykey.key
vaptvupt compress --pq pub.key backup.zupt ~/Documents/
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 workflow (v4.0.0; HKDF-SHA256 domain-separated combiner)
# --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
# 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 --pub -o pub.key -k mykey.key
vaptvupt compress --pq pub.key backup.zupt ~/Documents/
vaptvupt extract --pq mykey.key -o ~/restored/ backup.zupt
```
#### Full-disk backup
```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
# Backup with encryption
# With encryption
sudo vaptvupt disk backup -p "passphrase" -l 5 disk.zupt /dev/sda
# 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
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 |
|---|---|---|---|
| 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 |
| 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 |
**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.
Override with `--vv` (force VaptVupt) or `--lzhp` (force VaptVupt-LZHP) when you know what you want.
Decompression is universal. An archive created with VaptVupt on x86_64
extracts on aarch64 (NEON or scalar decode) and vice versa. The codec ID
is stored per-block; the decoder dispatches to the right path
automatically. Override with `--vv` or `--lzhp`.
---
## 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
heap write in the AVX2 decode fast path; adds canonical CBMC-verified
BCJ filters with auto-detection). The codec API is byte-identical
to the 2.48.x line; the 2.48.5 → 2.60.4 upgrades add the optimal parser
(measured: text 1.95%, binary 1.31%, source 4.72% smaller on our
fixtures), large-window extreme mode, faster decode (now roughly on par
with zstd-19, up from 1.52× slower), and six upstream corrupt-input
decoder memory-safety fixes. See `CHANGELOG.md` for the full list.
This release embeds VaptVupt codec 2.60.4 (security release: fixes an OOB
heap write in the AVX2 decode fast path; adds CBMC-verified BCJ filters
with auto-detection). The codec API is byte-identical to the 2.48.x line;
the 2.48.5 → 2.60.4 upgrades add the optimal parser (measured on our
fixtures: text 1.95%, binary 1.31%, source 4.72% smaller), large-window
extreme mode, faster decode (roughly on par with zstd-19, up from 1.52×
slower), and six upstream corrupt-input decoder memory-safety fixes. See
[CHANGELOG.md](CHANGELOG.md).
### 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
```
### Three modes
### Modes
| 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 |
| 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
> across levels, crypto overhead (KDF vs per-block), and a head-to-head
> ratio comparison against zstd — are in **[`BENCHMARKS.md`](BENCHMARKS.md)**,
> with the test machine and method stated for every table.
> **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.
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, AVX2 build. Reproduce with
`vaptvupt bench <file>`.
| Fixture | Tool | Ratio | Dec MB/s |
|---------------|-----------|---------:|---------:|
| text 10 MB | vv-9 | 25.6% | 278 |
| text 10 MB | gzip-9 | 22.6% | 156 |
| 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 | gzip-9 | 46.8% | 123 |
| 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 | gzip-9 | 4.0% | 238 |
| 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 | 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
zstd-3 and zstd-19 on text and binary, beats zstd-3 on source (4.5%
vs 5.6%), and loses to zstd-19 everywhere. If smallest-file is the
only goal, use `xz -9` or `zstd -19`.
- **Decode is now competitive**, not a weakness: 278714 MB/s, in the
same band as zstd-19 (and within ~1.3× of zstd-3). The 2.60.4 codec's
decode-speed work (Sprint 53/58) closed most of the gap that existed
at 2.48.5. The earlier "1.27× zstd-3 decode" headline (inherited from
upstream docs) is **not claimed here** — it did not reproduce in our
own single-vCPU measurement.
- **Encode throughput remains the weakness.** The optimal parser and
depth-24 hash-chain walk that win ratio cost encode speed; balanced
mode is ~6× slower than fast and ~14× slower than zstd-1. For
encode-latency-bound workloads use `vaptvupt compress -l 1`/`-l 2`.
- **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.
- On ratio, zstd-19 wins every fixture. VaptVupt L9 lands between zstd-3
and zstd-19 on text and binary, beats zstd-3 on source (4.5% vs 5.6%),
and loses to zstd-19 everywhere. For smallest-file only, use `xz -9` or
`zstd -19`.
- Decode is competitive: 278714 MB/s, in the same band as zstd-19 and
within ~1.3× of zstd-3.
- Encode throughput is the weakness. The optimal parser and hash-chain
walk that win ratio cost encode speed; balanced mode is ~6× slower than
fast mode. For encode-latency-bound workloads use `-l 1`/`-l 2`.
- On a degenerate single-pattern input, large-window extreme (L9) can be
slightly worse than L5/L7 — a tradeoff of optimizing for real long-range
matches. It does not affect realistic corpora.
- On random / already-compressed data, all codecs hit the
incompressibility wall.
### Security Test Results (v4.0.0 release)
### Security regression tests
Every release re-runs the full security regression matrix. These are
the v4.0.0 numbers:
Every release re-runs the security regression matrix (`make check`,
≈2 minutes on x86_64 and aarch64). It covers:
| Test | Coverage | Result |
|---------------------------------|--------------------------------------------------------------------------|-------------------|
| F-06 HMAC tamper fuzz | 2000 trials, single-bit flip in HMAC tag | **0 silent accepts** / 2000 honest roundtrips OK |
| F-08 archive-integrity trailer | Header/footer tamper detection | **5/5 pass** |
| F-09 byte-level integrity sweep | 1827 positions on a PQ-SDK archive, every byte flipped exhaustively | **0/1827 silent accepts** ✓ |
| F-10 KDF default | Argon2id is the default; PBKDF2 available via `--kdf pbkdf2` | **10/10 pass** |
| F-11 auth-fail wording | Wrong-password vs tampered-archive messages are indistinguishable | **12/12 pass** |
| F-12 encrypted comments | Comment block bound to per-block AAD; tamper rejected at extract | **11/11 pass** |
| F-15 KDF transparency | Argon2id header self-describes its profile; back-compat + fail-closed | **5/5 pass** |
| Constant-time comparisons | dudect Welch t-test (MAC tag + ML-KEM decaps); ~1% of leaky-memcmp + source-routing guard | **2/2 pass** |
| Codec exact-size decode (OOB) | 80 exact-`content_size` cases incl. BCJ payloads, ASan (codec 2.60.4 fix class) | **80/80 pass** |
| pq-box mode | roundtrips L1/L9/BCJ; wrong-key/key-confusion/tamper/cross-mode rejection | **13/13 pass** |
| 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 | Absolute paths and `..` components refused | **5/5 pass** |
| 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** |
- HMAC single-bit tamper detection and honest roundtrips.
- Archive-integrity trailer (header/footer tamper detection).
- Byte-level integrity sweep on a PQ archive (every byte flipped).
- KDF default (PBKDF2-SHA256) and self-describing header transparency,
with back-compat and fail-closed on unknown profiles.
- Indistinguishable wrong-password vs tampered-archive error messages.
- Encrypted comment block bound to per-block AAD.
- Constant-time comparison (dudect Welch t-test on MAC tag and ML-KEM
decaps) plus a source-routing guard.
- Codec exact-`content_size` decode cases (incl. BCJ payloads) under ASan.
- NIST/RFC test vectors: SHA-256, SHA-3, SHAKE-128, ML-KEM-768,
AES-256-CTR (SP 800-38A), HMAC-SHA256, X25519, XXH64.
- Path-traversal refusal, block-swap detection, deduplication correctness,
and CLI argument-order invariance.
Reproduce: `make check` (≈2 minutes, 91 assertions across 10 suites,
all green on x86_64 + aarch64). `make test` runs the full 15-suite
arc including dist reproducibility and packaging-syntax.
`make test` runs the full suite including dist reproducibility and
packaging-syntax checks.
### 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)
- **4-way interleaved ANS** — decodes 4 symbols per bitstream refill cycle, reducing refill overhead by 4×
- **4-stream Huffman literal coding** (`lit_fmt=4`) — Sprint 105 addition that further improves ratio on 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.
- **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.
- **Order-1 context model** — captures byte-pair correlations in structured data (JSON, CSV, logs)
- **Cost-aware lazy parser** (Sprint 120) — the breakthrough that put EXTREME ahead of zstd-3 in aggregate ratio
- **Adaptive window** — trial-compresses at wlog=16 vs wlog=20, picks 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** (Sprint 118) — encoder working buffers scrubbed via `vv_secure_zero` before `free()`
- **~6,500 lines** of pure C11 — auditable, portable, no external dependencies
- **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-stream Huffman literal coding** (`lit_fmt=4`) — improves ratio on
structured data.
- **AVX2/NEON SIMD decode** — inline 32-byte copies with tiered offset
handling. Scalar fallback on unsupported hardware.
- **Rep-match** — checks 3 recent offsets before the hash probe (O(1) vs
O(chain_depth)), hitting ~30% of matches.
- **Order-1 context model** — captures byte-pair correlations in structured
data (JSON, CSV, logs).
- **Cost-aware lazy parser** — puts Extreme mode ahead of zstd-3 in
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
`--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
@ -373,22 +406,28 @@ Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret
→ 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
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
```bash
# Clone a partition (requires read access)
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 --pub -o pub.key -k mykey.key
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)
```
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
**Encryption hierarchy (strongest → fastest):**
Encryption modes:
| Mode | Command | Security Level | Speed Impact |
|------|---------|---------------|-------------|
| 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 |
**Compression levels for disks:**
Compression levels for disks:
| 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 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`.
- **Block devices require root** on Linux. Regular files (disk images, `.img`, `.raw`) do not.
- **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.
- **Verify after backup** with `vaptvupt test archive.zupt` — checks every block's XXH64 checksum without extracting.
- **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.
- **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.
### Comparison with other tools
| Feature | VaptVupt disk | dd + gzip | Clonezilla | partclone |
|---------|-----------|-----------|------------|-----------|
| Compression | VaptVupt/LZHP (adaptive) | gzip (fixed) | Multiple | Multiple |
| Encryption | AES-256 + PQ hybrid | None (pipe to gpg) | None | None |
| Sparse detection | Automatic | None | Filesystem-aware | Filesystem-aware |
| 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 |
- 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`.
- Block devices require root on Linux. Regular files (disk images, `.img`,
`.raw`) do not.
- Sparse-heavy disks compress well — the sparse detector skips zero blocks
at memory-copy speed with no compression overhead.
- Verify after backup with `vaptvupt test archive.zupt` — checks every
block's XXH64 checksum without extracting.
- For long-term disk backups use `--pq`. Generate one keypair, store the
private key offline, distribute the public key.
- Restore is non-destructive on files (creates/overwrites the file);
writing to a block device overwrites the raw device. Double-check the
target path before restoring to a device.
---
## 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 |
|---------|--------|---------|-------|---------|-------|---------|
| Jasmin CT crypto | | C fallback | C fallback | C fallback | C fallback | C fallback |
| AES-NI hardware | (with AVX) | — | — | — | — | — |
| AVX2 SIMD decode | | — | — | — | — | — |
| NEON SIMD decode | — | | — | — | — | — |
| Jasmin CT crypto | yes | C fallback | C fallback | C fallback | C fallback | C fallback |
| AES-NI hardware | yes (with AVX) | — | — | — | — | — |
| AVX2 SIMD decode | yes | — | — | — | — | — |
| NEON SIMD decode | — | yes | — | — | — | — |
| 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):
```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
```
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
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)
Key protection: mlock() prevents swap, buffer canaries detect overflow
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
```
**Audit history:** Three internal audit sprints conducted on the 2.2.x line.
**14 bugs** found and fixed across the sprints — including one **HIGH-severity
Zip Slip path traversal** caught in the formal audit pass. Cumulative test
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.
The `WITH_SDK=1` build adds an HKDF-SHA3 combiner with domain separation,
key commitment, and HPKE binding for the `--pq-sdk`/`--pq-box` modes, plus
the Argon2id KDF.
See [SECURITY.md](SECURITY.md) for threat model. See [AUDIT.md](AUDIT.md) for
audit history. See [FORMAL_AUDIT_PROMPT.md](FORMAL_AUDIT_PROMPT.md) for the
methodology used in audit sprints.
Internal audit passes on the 2.2.x line fixed 14 bugs, including a
HIGH-severity Zip Slip path traversal. There has been no external audit.
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) |
| `-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 |
| `-o <DIR>` | Output directory (extract) |
| `-s` | Store without compression |
@ -562,9 +580,10 @@ vaptvupt help
## Building
```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 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-asan # AddressSanitizer + UBSan build
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 |
|----|------|-----------|------------|----------|
| `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` |
| `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` |
| `0x0009` | VaptVupt-LZH | LZ77 + Huffman | — | — |
| `0x0008` | VaptVupt-LZ | Fast LZ77, 64KB window | — | `-f` |
| `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.7 | Post-quantum hybrid encryption (ML-KEM-768 + X25519) |
| v1.0 | Stable release — format frozen v1.4, security audit |
| v1.1v1.4 | X25519 fix, NIST vectors, CPUID detection, Jasmin source files fixed |
| v1.5 | Jasmin CT assembly linked (MAC verify + ML-KEM select active) |
| v1.5.5 | Build system improvements: man page install rules, verbose mode, multi-arch detection |
| 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.1.0 | VaptVupt 1.4.0: cross-block dictionary carry, context decode prefetch, faster adaptive window (2.6× encode), integration API |
| v2.1.1 | Termux/Android build fix, arch-safety guard, Keccak ROL64 UB fix, zero UBSan violations |
| v2.1.2 | Full-disk backup/restore (`disk` subcommand), sparse detection, all encryption modes, progress bar |
| v2.1.3 | LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests |
| v2.1.4 | CodeQL: 4 security fixes — TOCTOU races eliminated (fstat on fd), X25519 scalar wipe via volatile |
| v2.1.5 | Block-level deduplication (`--dedup`), XXH64 fingerprint index, DEDUP_REF block type, 81 tests |
| 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 |
| 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** |
| v1.1v1.5.5 | X25519 fix, NIST vectors, CPUID detection, Jasmin CT assembly linked, build-system improvements |
| 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` |
| v2.1.x | Cross-block dictionary carry, Termux/Android build fix, full-disk backup/restore, LZHP fix, CodeQL fixes, block-level deduplication |
| 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.3.x | F-08/F-09 closed: archive-integrity trailer + preface-AAD MAC (format v1.5 → v1.6) |
| 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` |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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
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).
- **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.
- **Commercial license** available for relief from AGPL/GPL terms. Contact `sac@securityops.co`.
- **AGPL-3.0-or-later** — most of the codebase (CLI, GUI, Jasmin source).
See [`LICENSE`](LICENSE).
- **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.
Security vulnerabilities: see [SECURITY.md](SECURITY.md).
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.
## 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
- [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)

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
**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.
Response within 48 hours. Fix within 30 days for critical issues.
- Version (`vaptvupt --version`) and platform.
- 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 |
|------|----------|-----------|----------|----------|
| 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 |
| 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 |
| 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) |
| 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 |
| Classical KEM | X25519 | RFC 7748 | 32B scalar | ~128-bit classical |
| 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 |
| 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
@ -54,12 +76,14 @@ For each data block (sequence 0, 1, 2, ...):
### 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
- Padding oracle attacks
- Processing of tampered data
### Hybrid Post-Quantum KEM
### Hybrid Post-Quantum KEM (`--pq`)
```
Encapsulation:
@ -73,7 +97,14 @@ Encapsulation:
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_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)
@ -101,7 +135,7 @@ These functions are compiled from Jasmin source to x86-64 assembly. The Jasmin c
| 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 |
---
@ -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 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 (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 (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. |
| 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 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 (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) | 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 quantum adversary | `--pq` mode: ML-KEM-768 (NIST Level 3) |
| Against tampering of file contents, names, sizes, offsets | HMAC detects any modification |
| 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 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 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 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
| 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 |
| 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 |
| 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. |
| Traffic analysis | Archive size reveals data volume | Outside VaptVupt's scope |
| File permission/ownership | Not stored in archive | Documented in README.md (Architecture & platform support) |
| 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 / 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 |
| Spectre-class side channels in callers | Below the constant-time primitive layer | Host OS / compiler mitigations |
### 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 |
|------|-------------------|-----------------|---------|
| Password (`-p`) | Password-dependent + 256-bit AES | ~128-bit (Grover on AES) but PBKDF2 accelerated | **Vulnerable** — use `--pq` |
| PQ Hybrid (`--pq`) | ~128-bit (X25519) | NIST Level 3 (ML-KEM-768) | **Protected** |
| 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 |
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 |
|----------|---------------|----------|--------------|
| Linux | `getrandom(2)` | `/dev/urandom` | **Hard exit** — no encryption without CSPRNG |
| macOS | `/dev/urandom` | None | **Hard exit** |
| Windows | `RtlGenRandom` | None | **Hard exit** |
| Linux | `getrandom(2)` | `/dev/urandom` | Hard exit — no encryption without CSPRNG |
| macOS | `/dev/urandom` | 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 |
|----------|----------|-----------|--------|--------|
| 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 |
| macOS x86-64/ARM64 | Apple Clang | pthreads | `/dev/urandom` | 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
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
# Build
make # Zero warnings
make
# All functional tests
make test-all # 62/62 pass
# Functional tests
make test-all
# Memory safety
make test-asan # Zero ASAN/UBSAN errors
make test-asan
# NIST/RFC test vectors
make test-vectors && ./test_vectors # 13/13 pass
make test-vectors && ./test_vectors
# Verify Jasmin symbols are active
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
## 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
© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (dual-licensed AGPL + commercial)

View file

@ -1,36 +1,25 @@
THIRD-PARTY NOTICES
===================
**VaptVupt contains no third-party source code.** Every line of source in
this repository is the work of Cristian Cezar Moisés. This document
exists for transparency about runtime dependencies and build-time
tools.
If you redistribute VaptVupt, you must preserve this attribution document
along with the LICENSE file.
This document records VaptVupt's runtime dependencies and build-time
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 |
|---|---|---|---|
| vaptvupt CLI | src/, include/ | AGPL-3.0-or-later | Cristian Cezar Moisés |
| libzuptsdk | sdk/, vendor/zuptsdk/include/ | AGPL-3.0-or-later | Cristian Cezar Moisés |
| VaptVupt LZ codec | src/vv_*.c, src/vaptvupt_api.c, include/vaptvupt*.h | **GPL-3.0-or-later** | Cristian Cezar Moisés |
| Jasmin constant-time crypto | jasmin/*.jazz, jasmin/*.s | AGPL-3.0-or-later | Cristian Cezar Moisés |
| VaptVupt GUI (Python) | gui/ | AGPL-3.0-or-later | Cristian Cezar Moisés |
**Note on VaptVupt LZ codec licensing**: the VaptVupt LZ codec
(src/vv_*.c, src/vaptvupt_api.c, include/vaptvupt*.h) is licensed
GPL-3.0-or-later (not AGPL like the rest of the project) so that, with
sufficient maturity, it can be considered for upstreaming into the Linux
or BSD kernels, which require GPL-compatible licenses. The author retains
the right to dual-license the codec under other terms for commercial use;
contact sac@securityops.co for inquiries.
**Note on VaptVupt licensing**: VaptVupt is licensed GPL-3.0-or-later
(not AGPL like the rest of VaptVupt) so that, with sufficient maturity, it
can be considered for upstreaming into the Linux or BSD kernels, which
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.
The rest of the project (vaptvupt CLI, Jasmin source, GUI) is licensed
AGPL-3.0-or-later. Commercial licenses (relief from the AGPL network-use
clause) are available; contact sac@securityops.co.
-------------------------------------------------------------------------
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)
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
Version expected: 1.0+ (Debian/Ubuntu: libargon2-1)
Upstream: https://github.com/P-H-C/phc-winner-argon2
License: Apache-2.0 OR CC0-1.0 (dual)
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
@ -79,9 +71,11 @@ at runtime and are NOT redistributed as part of VaptVupt.
Compatibility with public standards
-------------------------------------------------------------------------
Where VaptVupt implements public standards, it does so independently
from any reference implementation. No code has been copied from
external projects. Standards followed:
Where VaptVupt implements public standards, it does so independently from
any reference implementation. 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 from any external
project. Standards followed:
- FIPS 197 (AES)
- FIPS 202 (Keccak / SHA-3)
@ -94,11 +88,6 @@ external projects. Standards followed:
- RFC 9106 (Argon2)
- 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
-------------------------------------------------------------------------
@ -114,8 +103,8 @@ with the subject "[third-party]" and details of the issue.
License summary
-------------------------------------------------------------------------
VaptVupt CLI, libzuptsdk, Jasmin source, GUI: AGPL-3.0-or-later
VaptVupt LZ codec: GPL-3.0-or-later
Commercial license (any component): contact sac@securityops.co
VaptVupt CLI, Jasmin source, GUI: AGPL-3.0-or-later
VaptVupt LZ codec: GPL-3.0-or-later
Commercial license (any component): contact sac@securityops.co
Project home: https://git.securityops.co/cristiancmoises/vaptvupt

View file

@ -10,21 +10,37 @@ trusting VaptVupt with anything you can't afford to lose.
## 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
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.
| Use case | VaptVupt is appropriate? |
|---|---|
| Backing up files to an untrusted cloud (S3, Backblaze, Google Drive) | **Yes** |
| Backing up a disk image to external media you might lose | **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) |
| Real-time encrypted communication | **No** (use Signal, age, or TLS) |
| Multi-party access (n-of-m) | **No** (no threshold scheme) |
| 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) |
| Backing up files to an untrusted cloud (S3, Backblaze, Google Drive) | Yes |
| Backing up a disk image to external media you might lose | 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) |
| Real-time encrypted communication | No (use Signal, age, or TLS) |
| Multi-party access (n-of-m) | No (no threshold scheme) |
| 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) |
---
## 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
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
(Argon2id default with m=64 MB, t=3, p=4 makes this very expensive
but not infinite — see "Password strength" below)
- The key file (for `--pq-sdk` / `--pq-box`) was not compromised at generation time
(see "Password strength" below)
- The key file (for `--pq-sdk` / `--pq-box`) was not compromised at
generation time
### 2. Integrity of every byte of an encrypted archive
If any single bit of the on-disk archive bytes is flipped, the
extraction **must fail** with an authentication error. This has been
verified by the v1.6 exhaustive byte sweep:
extraction fails with an authentication error. Coverage layers:
- 0 silent-accept positions out of 1827 (encrypted, no comment)
- 0 silent-accept positions out of 1878 (encrypted, with comment)
Coverage layers:
- **Per-block HMAC-SHA256** with frame-preface AAD (F-09): every data
- Per-block HMAC-SHA256 with frame-preface AAD (F-09): every data
block carries an HMAC over its ciphertext and over the canonical
29-byte preface (block_type, codec_id, block_flags, sizes, plaintext-XXH64)
- **Archive Integrity Trailer (F-08)**: HMAC-SHA256 over the
64-byte header and 24 bytes of footer, appended after the footer
- **Strict structural validation of the encryption-header block (F-09)**:
29-byte preface (block_type, codec_id, block_flags, sizes,
plaintext-XXH64)
- Archive Integrity Trailer (F-08): HMAC-SHA256 over the 64-byte
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
plaintext XXH64 must match
@ -66,10 +78,10 @@ Coverage layers:
Plaintext archives (no `-p`, no `--pq*`) are protected by XXH64
plaintext checksums per block plus structural validation. This is
**not cryptographic integrity** — a determined attacker with
write access can produce a tampered plaintext archive that passes
the checksum (XXH64 is not collision-resistant). It does catch
accidental corruption and naive tampering.
not cryptographic integrity — a determined attacker with write access
can produce a tampered plaintext archive that passes the checksum
(XXH64 is not collision-resistant). It does catch accidental
corruption and naive tampering.
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
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
HKDF combiner. Archives encrypted today cannot be decrypted by a
future quantum adversary holding only the ciphertext, **assuming**:
The native `--pq` mode uses ML-KEM-768 (FIPS 203) hybridized with
X25519 via an HKDF combiner. Archives encrypted today cannot be
decrypted by a future quantum adversary holding only the ciphertext,
assuming:
- ML-KEM-768 retains its claimed security level (NIST Category 3,
192-bit classical / 96-bit quantum strength)
- X25519 hybridization protects against an unforeseen ML-KEM break
- 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
The hot crypto paths (AES-256-CTR, HMAC-SHA256 comparison, X25519
field operations, ML-KEM polynomial arithmetic) are implemented in
Jasmin and proved constant-time at the assembly level on x86_64.
Non-Jasmin platforms (aarch64, fallback x86_64) use careful C
implementations that avoid secret-dependent branches and memory
accesses where feasible — but **without formal proof**.
Non-Jasmin platforms (aarch64, fallback x86_64) use C implementations
that avoid secret-dependent branches and memory accesses where
feasible — but without formal proof.
---
## 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
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:
- 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
or stored in the key file
- 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
Argon2id with m=64 MB, t=3, p=4 makes a single guess cost roughly
~200 ms on commodity hardware. That's **not enough** to protect a
short, common password against a determined attacker with GPU
clusters or cloud compute.
Password mode derives the key with PBKDF2-SHA256 (600k iterations)
in the default build, or Argon2id in a `make WITH_SDK=1` build. A
key derivation function slows offline guessing but does not make a
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 |
|---|---|
| 6-char common word | Hours to days |
| 10-char mixed alphanumeric | Years on a single GPU; days on a cluster |
| 6-word diceware passphrase | Centuries to millennia even with cloud-scale resources |
| 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.
Use a long, high-entropy password — a multi-word diceware passphrase
or a random 16+ character string with a full alphabet. For critical
data, use a key-file mode (native `--pq`, or the optional `--pq-sdk`
with a random key file from `vaptvupt keygen --sdk`) so the key is
CSPRNG output, not derived from human-typed text.
### 4. Metadata leakage from archive structure
Even with encryption, an attacker who can see the archive bytes
can infer:
- **Approximate file count** (from `total_blocks` in the footer)
- **Total archive size** (file size on disk)
- **Whether the archive is encrypted at all** (`ZUPT_FLAG_ENCRYPTED`
- Approximate file count (from `total_blocks` in the footer)
- Total archive size (file size on disk)
- Whether the archive is encrypted at all (`ZUPT_FLAG_ENCRYPTED`
in the global flags is visible)
- **Whether the archive is solid or per-file mode** (visible flag)
- **Whether post-quantum mode is in use** (visible flag)
- **Approximate file size distribution** (block sizes are visible
- Whether the archive is solid or per-file mode (visible flag)
- Whether post-quantum mode is in use (visible flag)
- Approximate file size distribution (block sizes are visible
even when block payloads are encrypted)
- **Archive creation time** (a 64-bit timestamp in the header)
- **A random 16-byte UUID per archive** (no information leak, but
- Archive creation time (a 64-bit timestamp in the header)
- A random 16-byte UUID per archive (no information leak, but
globally identifies the archive across copies)
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
There is **no threshold cryptography, no n-of-m sharing, no
multi-party computation, no proxy re-encryption**. Each archive
There is no threshold cryptography, no n-of-m sharing, no
multi-party computation, no proxy re-encryption. Each archive
has exactly one decryption credential (one password OR one
recipient key). To give two people access to the same archive,
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`
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
@ -219,29 +228,27 @@ archive. VaptVupt has **no hidden-volume or duress-password feature**.
### 9. Trusted setup of post-quantum primitives
The ML-KEM-768 implementation lives in `libzuptsdk` and was not
independently audited at the time of writing. We use NIST KAT
vectors for correctness verification but have not formally proven
constant-time properties for every PQ code path.
The in-tree ML-KEM-768 implementation was not independently audited
at the time of writing. We use NIST KAT vectors for correctness
verification but have not formally proven constant-time properties
for every PQ code path.
For maximum assurance, treat `--pq-sdk` as the post-quantum
**hedge** — it does not replace the X25519 layer; both must be
broken for an attacker to recover plaintext.
For maximum assurance, treat the post-quantum layer as a hedge — it
does not replace the X25519 layer; both must be broken for an
attacker to recover plaintext.
### 10. Format extension attacks
The format is versioned (v1.6). Older readers may accept newer
archives in unexpected ways. We try to maintain forward
compatibility (v2.4.5 readers correctly handle v1.6 archives
including encrypted comments and the Argon2id KDF path), but a
careful attacker who can produce malformed-but-just-valid
archives may find parser-state issues that don't rise to the
level of a CVE. The fuzzing harness (`make fuzz-format`) is the
primary mitigation; report bugs.
compatibility, but a careful attacker who can produce
malformed-but-just-valid archives may find parser-state issues that
don't rise to the level of a CVE. The fuzzing harness
(`make fuzz-format`) is the primary mitigation; report bugs.
### 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)
- 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.
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
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.
---
@ -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 |
| 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 |
| ML-KEM-768 retains NIST Category 3 security | `--pq-sdk` mode reduces to the X25519 layer |
| X25519 retains 128-bit security (no quantum) | `--pq-sdk` mode reduces to the ML-KEM layer; legacy `--pq` mode broken |
| 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` / `--pq-sdk` reduce to the X25519 layer |
| 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 |
| SHA3 / SHAKE retain pre-image and collision resistance | Auxiliary protocol bindings may be forged |
@ -298,8 +305,7 @@ normally.
## Document version
- **v1.0** (sprint 2.4.6): initial threat model. Covers archive
format v1.6.
- Document is part of the source tree (`THREAT_MODEL.md`) and
versioned with the project; this section will be updated as
the format evolves.
This threat model covers archive format v1.6 as shipped in VaptVupt
4.1.0. It is part of the source tree (`THREAT_MODEL.md`) and
versioned with the project; this section will be updated as the
format evolves.

View file

@ -1,7 +1,7 @@
.\" Manpage for vaptvupt (formerly zupt; INPI Brasil trademark rename in v3.0.0)
.\" SPDX-License-Identifier: AGPL-3.0-or-later
.\" 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
vaptvupt \- post-quantum backup compression utility (formerly zupt)
@ -205,7 +205,8 @@ Force a specific codec by id. Accepted values:
.TP
.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
.BR ps (1)
output; prefer
@ -517,7 +518,7 @@ On random / already-compressed data, all codecs hit the incompressibility wall;
.SH EXAMPLES
.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
.nf
@ -530,8 +531,9 @@ Compress with post-quantum hybrid encryption to a published public key:
.RS
.nf
$ vaptvupt keygen --sdk -o ~/.config/vaptvupt-mykey
$ vaptvupt keygen --sdk --pub -o mykey.pub -k ~/.config/vaptvupt-mykey
$ vaptvupt compress --pq-sdk mykey.pub backup.zupt ~/Documents
$ vaptvupt keygen -o ~/.config/vaptvupt-mykey
$ vaptvupt keygen --pub -o mykey.pub -k ~/.config/vaptvupt-mykey
$ vaptvupt compress --pq mykey.pub backup.zupt ~/Documents
.fi
.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.
@ -14,9 +14,6 @@ tar xzf vaptvupt-gui.tar.gz && cd vaptvupt-gui
./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
**Option A — Installer (recommended):**
@ -119,7 +116,7 @@ The GUI calls the vaptvupt CLI binary — all cryptography runs in native C, not
## 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

View file

@ -50,7 +50,7 @@
#define ZUPT_PRODUCT_EXTENSION ".zupt" /* on-disk archive extension (kept stable) */
#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.
* The codec's own VV_VERSION_* is its internal API version, not the release. */
#define ZUPT_CODEC_RELEASE "2.60.4"
@ -177,6 +177,12 @@
#define ZUPT_HMAC_SIZE 32
#define ZUPT_AES_KEY_SIZE 32
#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 {
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);
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 ─── */
int zupt_mlock_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.
pkgname=vaptvupt
pkgver=4.0.0
pkgver=4.1.0
pkgrel=1
provides=('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
* Codec upgraded to canonical VaptVupt 2.60.4 (security release):

View file

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

View file

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

View file

@ -1,54 +1,49 @@
# openSUSE Build Service update for `home:cabelo:innovators/vaptvupt`
This directory contains the three files you need to update your OBS
package from `1.5.5` to `2.4.8`:
This directory contains the three files needed to build vaptvupt `4.1.0`
in OBS:
| File | Status vs. your current files |
|---------------|---------------------------------------------------------------------|
| `_service` | Updated `revision` to `v2.4.8`. 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.changes`| 13 new entries prepended (2.0.0 → 2.4.8). Your existing 1.0.01.5.4 history is preserved verbatim. |
| File | Purpose |
|---------------|-------------------------------------------------------------------------|
| `_service` | `revision` pinned to `v4.1.0`. Format unchanged (still `tar_scm`). |
| `vaptvupt.spec` | `Version: 4.1.0`. `License: AGPL-3.0-or-later`. `%check` calls `make check`. |
| `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
upstream license is **AGPL-3.0-or-later** (dual-licensed
AGPL-3.0-or-later + commercial). This was a bug that should
probably trigger a rebuild even without the version bump.
1. **License**`AGPL-3.0-or-later` (dual-licensed AGPL-3.0-or-later
+ commercial).
2. **`%check` target** — your spec calls `test-all` on non-s390x
architectures. In v2.4.x, `test-all` includes threading tests
that are flaky on emulated build hosts (3 false positives on
x86_64 GitHub-Actions-style sandboxes). The new `make check`
target added in 2.4.8 runs a curated subset:
2. **No BuildRequires beyond the toolchain** — the default build needs
only `gcc gzip make` (plus `libm`/`pthread` from glibc). There are
**no system library BuildRequires**. The repository is source-only:
the previously vendored `libzuptsdk.so` and `libpqvaptvupt.so` have
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)
* F-08 archive-integrity-trailer
* F-09 byte-level integrity preface AAD
* F-10 KDF default
* 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
The optional SDK modes (`--pq-sdk`, `--pq-box`) and the Argon2id KDF
require an upstream `make WITH_SDK=1` build linked against the
separately distributed `libzuptsdk`/`libpqvaptvupt` libraries. They
are not part of this package.
Total ~91 assertions, runs in <2 minutes, no flakes on emulated
hosts. The s390x branch still falls back to just `test-vectors`.
3. **`%check` target** — the s390x branch falls back to `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
`https://git.securityops.co/cristiancmoises/vaptvupt` (the canonical
project URL). The `_service` file still pulls from GitHub
(`https://github.com/cristiancmoises/vaptvupt`) since that's where
your `tar_scm` is already configured and what works in OBS today.
The default password KDF is **PBKDF2-SHA256** (600k iterations).
Argon2id test vectors run only in a `WITH_SDK=1` build and are not
checked here.
4. **`BuildRequires: make`** added — newer openSUSE chroots don't
always pull `make` in transitively. Harmless on older targets.
5. **Docs**`%doc README.md SECURITY.md CHANGELOG.md` now ships
the security boundary docs as well as the README. THREAT_MODEL.md
exists upstream but isn't listed here to keep the package small;
add `%doc THREAT_MODEL.md` if you want it included.
4. **URLs** — the `URL:` field points at the canonical project URL
`https://git.securityops.co/cristiancmoises/vaptvupt`. The `_service`
file still fetches from GitHub
(`https://github.com/cristiancmoises/vaptvupt`), which is what the
existing `tar_scm` configuration uses in OBS.
## 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.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
# 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).
# Produces vaptvupt-4.1.0.tar.gz in the current directory.
# 4. (Optional) Local build to verify before committing
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
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
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
* The `_service` `revision` is pinned to `v2.4.8`. To track a new
release, just edit that one line and re-run `osc service runall`.
* The spec's `Version:` field is hard-coded — when you bump
`_service` `revision`, also bump `Version:` to match. The
`set_version` service in `_service` will auto-sync at OBS-build
time if you want; it's mode="manual" today, which is safer.
* `BuildRequires` is intentionally minimal (just `gcc gzip make`).
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.
* The `_service` `revision` is pinned to `v4.1.0`. To track a new
release, edit that one line and re-run `osc service runall`.
* The spec's `Version:` field is hard-coded — when you bump `_service`
`revision`, also bump `Version:` to match.
* `BuildRequires` is intentionally minimal (`gcc gzip make`). vaptvupt
has no external library dependencies in the default build; do not add
system crypto BuildRequires.
## Reporting issues
* Upstream bugs: https://git.securityops.co/cristiancmoises/vaptvupt
* openSUSE packaging bugs: https://bugs.opensuse.org/
* 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">
<param name="url">https://github.com/cristiancmoises/zupt</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="versionrewrite-pattern">v(.*)</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>

View file

@ -19,7 +19,7 @@
Name: vaptvupt
Version: 4.0.0
Version: 4.1.0
Release: 0
Summary: Post-quantum backup compression with AES-256 + ML-KEM-768 hybrid encryption
License: AGPL-3.0-or-later
@ -38,25 +38,29 @@ Obsoletes: zupt < 3.0.0
%description
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
(VaptVupt codec, ~2-3 GB/s decompression on x86_64 with AVX2 / aarch64
with NEON), AES-256-CTR + HMAC-SHA256 per-block authenticated
encryption, multi-threaded, with optional ML-KEM-768 + X25519
post-quantum hybrid key encapsulation (FIPS 203 + RFC 7748). The
default password KDF is Argon2id; PBKDF2-SHA256 remains available
via --kdf pbkdf2 for backward compatibility.
trademark on the name "Zupt") compresses and encrypts backup archives.
LZ + ANS compression (VaptVupt codec, ~2-3 GB/s decompression on x86_64
with AVX2 / aarch64 with NEON), AES-256-CTR + HMAC-SHA256 per-block
authenticated encryption, multi-threaded, with ML-KEM-768 + X25519
post-quantum hybrid key encapsulation (FIPS 203 + RFC 7748) via --pq.
Pure C11, vendored libzuptsdk, ~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.
This package builds entirely from source with no external library
dependency. The password KDF is PBKDF2-SHA256 (600k iterations). The
optional libzuptsdk-backed modes (Argon2id KDF, --pq-sdk, --pq-box) are
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
%autosetup -p1
chmod +x tests/*.sh
%build
%make_build V=1 \
%make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread"
@ -71,14 +75,14 @@ chmod +x tests/*.sh
# On s390x, fall back to just the vector tests (Jasmin assembly is
# x86_64-only; threading harness has been flaky on big-endian).
%ifarch s390x
%make_build V=1 \
%make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \
test-vectors
./test_vectors
%else
%make_build V=1 \
%make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \
@ -86,7 +90,7 @@ chmod +x tests/*.sh
%endif
%install
%make_install PREFIX=%{_prefix}
%make_install WITH_SDK=0 PREFIX=%{_prefix}
%files
%license LICENSE
@ -95,12 +99,5 @@ chmod +x tests/*.sh
%{_bindir}/zupt
%{_mandir}/man1/vaptvupt.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

View file

@ -20,7 +20,7 @@
# in the base.
Name: vaptvupt
Version: 4.0.0
Version: 4.1.0
Release: 1%{?dist}
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
%license LICENSE
%doc README.md SECURITY.md CHANGELOG.md AUDIT.md
%doc README.md SECURITY.md CHANGELOG.md
%{_bindir}/zupt
%{_libdir}/%{name}/libzuptsdk.so.2.0.0
%{_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.
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
- **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
- **Constant-time crypto** — Jasmin-verified assembly on x86_64
- **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)
@ -95,6 +95,8 @@ make sdk-test # runs C roundtrip suite (15 tests)
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:
- `/usr/local/include/zuptsdk.h`
- `/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`).
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

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.
*
* SAFEZONE_MAX_OFFSET covers the legal offset range (1 << wlog_max).
* SAFEZONE_MAX_RUN covers BOTH max litlen and max matchlen (both are
* bounded by the wire format at 65535: LL encoding ll_base[35]=61440
* + up to 4095 extra bits = 65535; ML encoding likewise). So
* op_safe_end = op_end - 65535 guarantees any single sequence's
* total writes (literals + match) fit without per-iter overflow
* checking.
* SAFEZONE_MAX_RUN bounds EACH of litlen and matchlen (both 65535 by
* the wire format: LL ll_base[35]=61440 + up to 4095 extra = 65535;
* ML ml_base[35]=32768 + up to 32767 extra = 65535). A single sequence
* writes litlen literals THEN a matchlen match copy up to TWO max
* runs and in_safe_zone is computed once (pre-literal) yet gates BOTH
* 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
* 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
* 3 bytes) and is still rejected, preserving the DoS guard. */
enum { SAFEZONE_MAX_OFFSET = 1u << 24 }; /* 3-byte offset wire max */
enum { SAFEZONE_MAX_RUN = 65535 }; /* litlen or matchlen */
uint8_t *op_safe_end = (dst_cap > SAFEZONE_MAX_RUN)
? op_end - SAFEZONE_MAX_RUN : dst;
enum { SAFEZONE_MAX_RUN = 65535 }; /* max litlen OR matchlen */
/* Reserve for a full worst-case sequence (litlen + matchlen). */
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;
size_t seqs_decoded = 0;

View file

@ -32,6 +32,8 @@
* [..] raw key bytes (PQVV_PUBLICKEYBYTES / PQVV_SECRETKEYBYTES)
*/
#include "zupt.h"
#ifdef ZUPT_WITH_SDK
#include "zupt_keccak.h"
#include "pqvaptvupt.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));
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
*/
#include "zupt.h"
#ifdef ZUPT_WITH_SDK
#include "zuptsdk.h"
#include "zuptsdk_easy.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;
int rc = zuptsdk_easy_encrypt(pubkeyfile, session_key, 32, &blob, &blob_sz);
if (rc != 0 || !blob) {
memset(session_key, 0, 32);
zupt_secure_wipe(session_key, 32);
return -1;
}
/* Layout: [1B type][4B blob_sz LE][blob] */
if (1 + 4 + blob_sz > 1500) {
free(blob);
memset(session_key, 0, 32);
zupt_secure_wipe(session_key, 32);
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);
memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16);
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;
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;
free(blob);
memset(session_key, 0, 32);
zupt_secure_wipe(session_key, 32);
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);
memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16);
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;
/* 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);
memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16);
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;
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->canary_tail = ZUPT_CANARY;
memset(key, 0, 32);
memset(salt, 0, 16);
zupt_secure_wipe(key, 32);
zupt_secure_wipe(salt, 16);
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);
memcpy(kdf_buf2 + 32, "ZUPT-SDK-MAC-KEY", 16);
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;
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->canary_tail = ZUPT_CANARY;
memset(key, 0, 32);
zupt_secure_wipe(key, 32);
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) ─── */
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;
/* 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. */
tgt_fd = open(target_path, O_WRONLY);
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) {
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
* enc-header bytes differ. Read-path dispatch on enc_type byte
* 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];
zupt_random_bytes(salt, ZUPT_SALT_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
* 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]) {
out[0] = (uint8_t)b->block_type;
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
* as zupt_serialize_preface_aad both sides must produce identical bytes
* 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,
uint64_t uncompressed_size, uint64_t compressed_size, uint64_t checksum,
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 (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 (!kr || !kr->active) return ZUPT_ERR_AUTH_FAIL;
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(&iter, eb.payload + 49, 4);
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);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter);
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(&iter, eb.payload + 48, 4);
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);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter);
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];
uint64_t 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;
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;
@ -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 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) {
fprintf(stderr, "Error: write failed (disk full?) for %s\n", e->path);
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;
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));
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 cv=0;
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++){
if(lengths[i]==0) continue;
if(lengths[i]==0 || lengths[i]>LZH_MAX_CODELEN) continue;
int bits=lengths[i];
uint16_t c=(uint16_t)nc[bits]++;
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;
ip += cl_len;
} else {
/* Raw code lengths */
if (ip + ll_hdr > slen) return 0;
/* Raw code lengths: one byte per symbol. SECURITY: bound the count
* 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;
for (size_t k = 0; k < ll_hdr; k++)
if (ll_lens[k] > LZH_MAX_CODELEN) return 0;
}
/* 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;
ip += cl_len;
} 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;
for (size_t k = 0; k < d_hdr; k++)
if (d_lens[k] > LZH_MAX_CODELEN) return 0;
}
/* 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;
if (kr && kr->active) {
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; }
/* Output is the encrypted payload (caller frees slot->output) */
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 (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 (!kr || !kr->active) { slot->error = ZUPT_ERR_AUTH_FAIL; return; }
size_t dec_len;
dec_payload = zupt_decrypt_buffer(kr, comp_data, comp_len, slot->block_seq, &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);
}
if (!dec_payload) { slot->error = ZUPT_ERR_AUTH_FAIL; return; }
comp_data = dec_payload;
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,
const zupt_keyring_t *keyring) {
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));
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.