From 43d6306a06f78deee19e448ea4b3d9034090ec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Fri, 10 Jul 2026 12:45:46 -0300 Subject: [PATCH] GUI rework for source-only builds + CLI security fixes + packaging + cross-platform scaffolding Fixes the "GUI functions don't work on GNU Guix" report and a batch of adversarial-audit findings across the CLI, decoder, crypto and packaging. GUI (gui/src/zupt_gui.py) - Root cause of the broken GUI: every encryption path defaulted to the libzuptsdk "SDK v2" modes, which are absent from the source-only build and exit 1. Reworked Keys/Compress/Extract around the native modes with a build-aware PQ-mode selector: Hybrid (--pq, default), Full PQ (--pq-only), and SDK v2 only when the binary reports WITH_SDK support. Capability is detected from `version` ("Build:"/"KDF:" lines) with a `help` fallback. - Extract/Verify gain a PQ private-key input with auto-detect (reads the archive via `info` to pick --pq vs --pq-only). Verify could not verify any PQ archive before (password field only). - run_async now holds a LIST of in-flight (thread, worker) refs; DiskTab's two buttons previously shared one slot, so a second op GC'd the first QThread mid-run. About tab corrected (codec 2.60.4, PBKDF2 default, --pq-only, URL). CLI (src/zupt_main.c) - compress -p swallowed the archive name as the password and truncated the first input file (silent data loss, exit 0). Added a data-loss guard: refuse to overwrite an existing non-.zupt file as the output archive unless -y/--force; plus a self-overwrite guard. - compress -p wrote an UNENCRYPTED archive (exit 0) because options after the first positional were treated as files. Now errors on a misplaced option (with a `--` escape for real dashed filenames). - Removed duplicated dead --pq-box/--pq-sdk branches. version/banner/usage now state the build's real KDF (PBKDF2 on source-only) and repo URL; examples lead with native --pq / --pq-only. Security (audit findings) - vv_decoder.c: the two AVX2 fast-path token decoders read a 2-/3-byte match offset after only checking 1 byte remained -> heap over-read on a crafted archive. Added the `ip + off_bytes > ip_end` guard the tail path already had. - zupt_crypto.c: wipe ML-KEM/X25519 secret-key buffers when hybrid decrypt init fails on key read (matches the pq-only path). - zupt_format.c: bound attacker-controlled encryption_header_off before the (off_t)+7 arithmetic in the info enc_type reader (avoid signed-overflow UB). Packaging (would fail source-only) - debian/rules (staged into debian/zupt, installed vendored .so), aur/PKGBUILD, nix/flake.nix, homebrew (vendored .so + nonexistent AUDIT.md, stale /zupt URLs) now build source-only. opensuse spec %files ships the shell completions make install writes (rpmbuild no longer fails on unpackaged files); _service + debian/control point at the vaptvupt repo; KDF claim corrected. Cross-platform GUI packaging (new) - packaging/portable/: OS-agnostic GUI package (zupt_gui.py + .bat/.command/.sh launchers + README) that runs on Windows/macOS/Linux/BSD with Python+PySide6. - .github/workflows/cross-platform.yml: builds real native binaries on windows-latest + macos-latest runners (CLI, PyInstaller GUI, Inno Setup installer, .dmg) and the portable zip, attaching them to the release on tag. - packaging/windows/vaptvupt-gui.iss: Inno Setup installer script. Validation: make check 16/16 (all distro-safe checks), GUI imports + command contracts verified against the fixed CLI. test_help_consistency updated to assert the truthful (build-aware) default KDF. --- .github/workflows/cross-platform.yml | 180 ++++++++++++++++++ gui/src/zupt_gui.py | 235 ++++++++++++++++++------ packaging/aur/PKGBUILD | 34 ++-- packaging/debian/control | 8 +- packaging/debian/rules | 18 +- packaging/homebrew/vaptvupt.rb | 32 +--- packaging/nix/flake.nix | 30 +-- packaging/opensuse/_service | 2 +- packaging/opensuse/vaptvupt.spec | 5 + packaging/portable/README.txt | 57 ++++++ packaging/portable/vaptvupt-gui.bat | 31 ++++ packaging/portable/vaptvupt-gui.command | 17 ++ packaging/portable/vaptvupt-gui.sh | 21 +++ packaging/windows/vaptvupt-gui.iss | 73 ++++++++ src/vv_decoder.c | 12 ++ src/zupt_crypto.c | 9 +- src/zupt_format.c | 4 + src/zupt_main.c | 135 ++++++++++---- tests/test_help_consistency.sh | 16 +- 19 files changed, 750 insertions(+), 169 deletions(-) create mode 100644 .github/workflows/cross-platform.yml create mode 100644 packaging/portable/README.txt create mode 100644 packaging/portable/vaptvupt-gui.bat create mode 100644 packaging/portable/vaptvupt-gui.command create mode 100644 packaging/portable/vaptvupt-gui.sh create mode 100644 packaging/windows/vaptvupt-gui.iss diff --git a/.github/workflows/cross-platform.yml b/.github/workflows/cross-platform.yml new file mode 100644 index 0000000..d6b02f3 --- /dev/null +++ b/.github/workflows/cross-platform.yml @@ -0,0 +1,180 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# +# Cross-platform GUI + CLI binaries, built on REAL Windows and macOS runners. +# +# Why a dedicated workflow: the GUI is a PySide6/PyQt6 app and the CLI is +# portable C11, but self-contained native installers (Windows .exe/.msi, +# macOS .app/.dmg) can only be produced on the target OS. This workflow builds +# them on GitHub's windows-latest and macos-latest runners and attaches them to +# the GitHub release on a `v*` tag. Run it manually with "Run workflow" +# (workflow_dispatch) to smoke-test the build before tagging. +# +# Artifacts produced: +# Windows: vaptvupt.exe (CLI, mingw), vaptvupt-gui.exe (PyInstaller onefile), +# VaptVupt-Setup-.exe (Inno Setup installer) +# macOS: vaptvupt (CLI, universal where possible), VaptVupt-.dmg +# All: vaptvupt-gui--portable.zip (Python GUI + launchers) + +name: cross-platform + +on: + push: + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: write + +jobs: + # ─────────────────────────── Windows ─────────────────────────── + windows: + runs-on: windows-latest + defaults: + run: { shell: msys2 {0} } + steps: + - uses: actions/checkout@v4 + - name: Set up MSYS2 (mingw gcc + make) + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + make + coreutils + - name: Build CLI (vaptvupt.exe, source-only, C fallback crypto) + run: | + make CC=gcc WITH_SDK=0 -j2 + ./vaptvupt.exe version || ./vaptvupt version + cp "$(ls vaptvupt.exe vaptvupt 2>/dev/null | head -1)" vaptvupt.exe 2>/dev/null || true + - name: Set up Python + shell: pwsh + run: | + # Use the runner's native Python (not MSYS) for PyInstaller so the + # produced .exe targets the standard Windows Python ABI. + python -m pip install --upgrade pip + python -m pip install PySide6 pyinstaller + - name: Get version + id: ver + shell: pwsh + run: | + $ver = (Select-String -Path include/zupt.h -Pattern '^#define ZUPT_VERSION_STRING "([^"]+)"').Matches.Groups[1].Value + "version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + - name: Bundle GUI with PyInstaller (vaptvupt-gui.exe) + shell: pwsh + run: | + # onefile GUI that carries the CLI beside it via --add-binary. + pyinstaller --noconfirm --onefile --windowed ` + --name vaptvupt-gui ` + --icon gui/assets/zupt-icon.png ` + --add-binary "vaptvupt.exe;." ` + --add-data "gui/assets/zupt-icon.png;assets" ` + gui/src/zupt_gui.py + - name: Build Inno Setup installer + shell: pwsh + run: | + choco install innosetup --no-progress -y + & "$env:ChocolateyInstall\bin\ISCC.exe" ` + "/DAppVersion=${{ steps.ver.outputs.version }}" ` + packaging/windows/vaptvupt-gui.iss + - name: Collect artifacts + shell: pwsh + run: | + $v = "${{ steps.ver.outputs.version }}" + New-Item -ItemType Directory -Force out | Out-Null + Copy-Item vaptvupt.exe "out/vaptvupt-$v-windows-x86_64.exe" + Copy-Item dist/vaptvupt-gui.exe "out/vaptvupt-gui-$v-windows-x86_64.exe" + if (Test-Path "packaging/windows/Output") { + Copy-Item packaging/windows/Output/*.exe "out/" -ErrorAction SilentlyContinue + } + - uses: actions/upload-artifact@v4 + with: + name: windows + path: out/* + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: out/* + + # ─────────────────────────── macOS ─────────────────────────── + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Build CLI (vaptvupt, clang) + run: | + make CC=clang WITH_SDK=0 -j3 + ./vaptvupt version + - uses: actions/setup-python@v5 + with: { python-version: '3.12' } + - name: Install GUI build deps + run: | + python -m pip install --upgrade pip + python -m pip install PySide6 pyinstaller + brew install create-dmg || true + - name: Get version + id: ver + run: echo "version=$(awk -F'\"' '/^#define ZUPT_VERSION_STRING/{print $2}' include/zupt.h)" >> "$GITHUB_OUTPUT" + - name: Bundle GUI (.app) with PyInstaller + run: | + pyinstaller --noconfirm --windowed \ + --name "VaptVupt" \ + --add-binary "vaptvupt:." \ + --add-data "gui/assets/zupt-icon.png:assets" \ + gui/src/zupt_gui.py + - name: Build .dmg + run: | + V="${{ steps.ver.outputs.version }}" + create-dmg --volname "VaptVupt $V" --window-size 500 300 \ + --app-drop-link 350 120 --icon "VaptVupt.app" 150 120 \ + "VaptVupt-$V.dmg" "dist/VaptVupt.app" || \ + { mkdir -p dmgroot && cp -R dist/VaptVupt.app dmgroot/ && \ + hdiutil create -volname "VaptVupt $V" -srcfolder dmgroot -ov -format UDZO "VaptVupt-$V.dmg"; } + - name: Collect artifacts + run: | + V="${{ steps.ver.outputs.version }}" + mkdir -p out + cp vaptvupt "out/vaptvupt-$V-macos" + cp "VaptVupt-$V.dmg" out/ + - uses: actions/upload-artifact@v4 + with: + name: macos + path: out/* + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: out/* + + # ─────────────── Portable GUI (works on every OS) ─────────────── + portable: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get version + id: ver + run: echo "version=$(awk -F'\"' '/^#define ZUPT_VERSION_STRING/{print $2}' include/zupt.h)" >> "$GITHUB_OUTPUT" + - name: Assemble portable package + run: | + V="${{ steps.ver.outputs.version }}" + D="vaptvupt-gui-$V-portable" + mkdir -p "$D/assets" + cp gui/src/zupt_gui.py "$D/" + cp gui/assets/zupt-icon.png "$D/assets/" + cp packaging/portable/vaptvupt-gui.bat "$D/" + cp packaging/portable/vaptvupt-gui.command "$D/" + cp packaging/portable/vaptvupt-gui.sh "$D/" + cp packaging/portable/README.txt "$D/" + chmod +x "$D/vaptvupt-gui.command" "$D/vaptvupt-gui.sh" + zip -r "$D.zip" "$D" + - uses: actions/upload-artifact@v4 + with: + name: portable + path: vaptvupt-gui-*-portable.zip + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: vaptvupt-gui-*-portable.zip diff --git a/gui/src/zupt_gui.py b/gui/src/zupt_gui.py index edd99d1..20a83b9 100644 --- a/gui/src/zupt_gui.py +++ b/gui/src/zupt_gui.py @@ -190,6 +190,81 @@ def _get_version(): ZUPT_VER_SHORT, ZUPT_VER_NUMBER, ZUPT_VER_FULL = _get_version() +# ── Detect build capabilities from `version` (and `help` as fallback) ── +# +# The default build is SOURCE-ONLY: the libzuptsdk-backed modes (Argon2id +# KDF, --pq-sdk, --pq-box) are absent and fail with exit 1. Offering them in +# the UI is the #1 reason "functions don't work". We detect what THIS binary +# actually supports and build the encryption UI around it: +# - SDK_AVAILABLE : --pq-sdk / --pq-box / Argon2id compiled in (WITH_SDK=1) +# - PQONLY_AVAILABLE: native --pq-only (full post-quantum, v4.2.0+) +# - DEFAULT_KDF : the password KDF this build actually uses +# The `version` banner carries a machine-readable "Build:" line (v4.2.1+); +# for older binaries we fall back to `help` text and default SDK off (safe: +# the native --pq / --pq-only / password modes work on every build). +def _get_caps(): + sdk = False + pqonly = False + default_kdf = "PBKDF2-SHA256" + blob = ZUPT_VER_FULL or "" + for line in blob.splitlines(): + low = line.lower() + if low.startswith("build:"): + sdk = ("full" in low) and ("libzuptsdk" in low) + elif low.startswith("kdf:"): + default_kdf = "Argon2id" if "argon2id (default)" in low else "PBKDF2-SHA256" + if "--pq-only" in line: + pqonly = True + if not pqonly: + try: + h = subprocess.run([VAPTVUPT, "help"], capture_output=True, text=True, timeout=5) + txt = (h.stdout or "") + (h.stderr or "") + if "--pq-only" in txt: + pqonly = True + except Exception: + pass + return sdk, pqonly, default_kdf + +SDK_AVAILABLE, PQONLY_AVAILABLE, DEFAULT_KDF = _get_caps() + +# Post-quantum recipient modes offered in the UI, keyed to CLI flags. +# token -> (label, keygen-flag-list, compress/extract-flag) +# keygen-flags are extra flags added to `keygen` (private) and `keygen --pub`. +def pq_mode_options(include_auto=False): + """Return [(label, token)] for a PQ-mode dropdown given this build.""" + opts = [] + if include_auto: + opts.append(("Auto-detect from archive", "auto")) + opts.append(("Hybrid — ML-KEM-768 + X25519 (recommended)", "pq")) + if PQONLY_AVAILABLE: + opts.append(("Full PQ — ML-KEM-768 only", "pqonly")) + if SDK_AVAILABLE: + opts.append(("SDK v2 — HKDF + commitment + HPKE", "sdk")) + return opts + +# token -> (extra keygen flags, encrypt/decrypt flag) +_PQ_FLAG = { + "pq": ([], "--pq"), + "pqonly": (["--pq-only"], "--pq-only"), + "sdk": (["--sdk"], "--pq-sdk"), +} + +def _detect_archive_pq(archive): + """Inspect an archive's `info` and return the matching PQ token, or None.""" + try: + r = subprocess.run([VAPTVUPT, "info", archive], capture_output=True, text=True, timeout=15) + txt = (r.stdout or "") + (r.stderr or "") + except Exception: + return None + low = txt.lower() + if "ml-kem-768 only" in low or "no classical" in low: + return "pqonly" + if "sdk v2" in low or "hpke" in low: + return "sdk" + if "ml-kem-768" in low or "hybrid" in low or "x25519" in low: + return "pq" + return None + # ── Find icon file ── def _find_icon(): here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent)) @@ -328,14 +403,22 @@ def run_async(parent, cmd, btn, log, progress=None): if progress: progress.show() t = QThread(); w = Worker(cmd); w.moveToThread(t) w.log.connect(log.append) + # Keep a LIST of live (thread, worker) refs on the parent. Tabs with more + # than one action button (Disk: backup + restore) previously shared a + # single _thread/_worker slot, so starting a second op dropped the only + # Python reference to the first still-running QThread — Python GC'd it + # mid-run and aborted the operation. A list holds every in-flight thread. + if not hasattr(parent, "_jobs"): + parent._jobs = [] + parent._jobs.append((t, w)) def finish(code, out, err): btn.setEnabled(True) if progress: progress.hide() log.append("\nDone." if code == 0 else f"\nFailed (exit {code}).") t.quit() + parent._jobs = [(th, wk) for (th, wk) in parent._jobs if th is not t] w.done.connect(finish) t.started.connect(w.run); t.start() - parent._thread, parent._worker = t, w # ── Tabs ── @@ -345,21 +428,31 @@ class KeysTab(QWidget): inner = QWidget() v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10) - v.addWidget(QLabel("Generate or export ML-KEM-768 + X25519 hybrid keys.")) + v.addWidget(QLabel("Generate or export post-quantum keys (ML-KEM-768).")) + v.addWidget(Sep()) + + # Key type governs both generate and export so the two stay consistent. + v.addWidget(H("Key type")) + self.mode = QComboBox() + self._modes = pq_mode_options() + for label, _tok in self._modes: + self.mode.addItem(label) + self.mode.setToolTip("Hybrid (--pq) is recommended. Full PQ (--pq-only) drops the\n" + "classical X25519 layer for PQ-only compliance postures. Both use\n" + "in-tree crypto and work on every build.") + v.addWidget(self.mode) v.addWidget(Sep()) # Section 1: Generate new keypair v.addWidget(H("Generate new keypair")) - v.addWidget(QLabel("Creates both private and public key files.")) + v.addWidget(QLabel("Writes a private key and its matching public key.")) v.addWidget(H("Private key output")) - self.gen_priv = PathField("e.g. ~/zupt_private.key", "save", "Key (*.key);;All (*)") + self.gen_priv = PathField("e.g. ~/vaptvupt_private.key", "save", "Key (*.key);;All (*)") v.addWidget(self.gen_priv) - - self.gen_sdk = QCheckBox("SDK v2 format (HKDF combiner + commitment + HPKE — recommended)") - self.gen_sdk.setChecked(True) - self.gen_sdk.setToolTip("Generates a libzuptsdk-format keypair. Use --pq-sdk in CLI or 'SDK v2' checkbox in compress to use these keys. Disable for legacy --pq compatibility.") - v.addWidget(self.gen_sdk) + v.addWidget(H("Public key output")) + self.gen_pub = PathField("e.g. ~/vaptvupt_public.key", "save", "Key (*.key);;All (*)") + v.addWidget(self.gen_pub) self.gen_btn = QPushButton("Generate Keypair") self.gen_btn.clicked.connect(self._generate) @@ -370,14 +463,15 @@ class KeysTab(QWidget): # Section 2: Export public key from existing private key v.addWidget(H("Export public key from private key")) - v.addWidget(QLabel("Extract the public key from an existing private key file.")) + v.addWidget(QLabel("Extract the public key from an existing private key file " + "(uses the key type selected above).")) v.addWidget(H("Existing private key")) self.exp_priv = PathField("Select private key", "open", "Key (*.key);;All (*)") v.addWidget(self.exp_priv) v.addWidget(H("Public key output")) - self.exp_pub = PathField("e.g. ~/zupt_public.key", "save", "Key (*.key);;All (*)") + self.exp_pub = PathField("e.g. ~/vaptvupt_public.key", "save", "Key (*.key);;All (*)") v.addWidget(self.exp_pub) self.exp_btn = QPushButton("Export Public Key") @@ -389,27 +483,32 @@ class KeysTab(QWidget): v.addStretch() lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + def _token(self): + return self._modes[self.mode.currentIndex()][1] + + def _default_pub(self, priv): + return (priv.rsplit(".", 1)[0] + "_public.key") if "." in priv else priv + ".pub" + def _generate(self): - p = self.gen_priv.path() or str(Path.home() / "zupt_private.key") + p = self.gen_priv.path() or str(Path.home() / "vaptvupt_private.key") self.gen_priv.edit.setText(p) + pub = self.gen_pub.path() or self._default_pub(p) + self.gen_pub.edit.setText(pub) + tok = self._token() + kflags, _ = _PQ_FLAG[tok] self.gen_log.clear(); self.gen_btn.setEnabled(False) - if self.gen_sdk.isChecked(): - # SDK keygen creates both files in one step. + if tok == "sdk": + # SDK keygen writes the private key and .pub in one step. code, _, err = run_zupt(["keygen", "--sdk", "-o", p]) self.gen_log.append(err.strip()) - if code == 0: - self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {p}.pub") - else: - self.gen_log.append("\nFailed.") + self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {p}.pub" if code == 0 else "\nFailed.") else: - code, _, err = run_zupt(["keygen", "-o", p]) + code, _, err = run_zupt(["keygen"] + kflags + ["-o", p]) self.gen_log.append(err.strip()) if code == 0: - pub = p.rsplit(".", 1)[0] + "_public.key" if "." in p else p + ".pub" - c2, _, e2 = run_zupt(["keygen", "--pub", "-o", pub, "-k", p]) + c2, _, e2 = run_zupt(["keygen", "--pub"] + kflags + ["-o", pub, "-k", p]) self.gen_log.append(e2.strip()) - if c2 == 0: - self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}") + self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}" if c2 == 0 else "\nFailed to export public key.") else: self.gen_log.append("\nFailed.") self.gen_btn.setEnabled(True) @@ -419,15 +518,13 @@ class KeysTab(QWidget): pub = self.exp_pub.path() if not priv: QMessageBox.warning(self, "VaptVupt", "Select the private key file."); return if not pub: - pub = priv.rsplit(".", 1)[0] + "_public.key" if "." in priv else priv + ".pub" - self.exp_pub.edit.setText(pub) + pub = self._default_pub(priv); self.exp_pub.edit.setText(pub) + tok = self._token() + kflags, _ = _PQ_FLAG[tok] self.exp_log.clear(); self.exp_btn.setEnabled(False) - code, _, err = run_zupt(["keygen", "--pub", "-o", pub, "-k", priv]) + code, _, err = run_zupt(["keygen", "--pub"] + kflags + ["-o", pub, "-k", priv]) self.exp_log.append(err.strip()) - if code == 0: - self.exp_log.append(f"\nPublic key: {pub}") - else: - self.exp_log.append("\nFailed.") + self.exp_log.append(f"\nPublic key: {pub}" if code == 0 else "\nFailed.") self.exp_btn.setEnabled(True) @@ -454,10 +551,14 @@ class CompressTab(QWidget): enc = QHBoxLayout(); enc.setSpacing(16) pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField("AES-256"); pw.addWidget(self.pw); enc.addLayout(pw) pq = QVBoxLayout(); pq.addWidget(H("PQ public key")); self.pq = PathField("Optional .key", filters="Key (*.key *.pub);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq) - sdk_box = QVBoxLayout(); sdk_box.addWidget(H("Mode")) - self.sdk = QCheckBox("Use SDK v2 (HKDF + commitment + HPKE)"); self.sdk.setChecked(True) - self.sdk.setToolTip("v2.2+ uses libzuptsdk: HKDF-SHA3 combiner, key commitment, HPKE binding, Argon2id. Disable for legacy --pq compatibility.") - sdk_box.addWidget(self.sdk); sdk_box.addStretch(); enc.addLayout(sdk_box) + mode_box = QVBoxLayout(); mode_box.addWidget(H("PQ mode")) + self.pqmode = QComboBox() + self._pqmodes = pq_mode_options() + for label, _tok in self._pqmodes: + self.pqmode.addItem(label) + self.pqmode.setToolTip("Applies when a PQ public key is set. Must match the key type\n" + "you generated. Hybrid (--pq) is recommended.") + mode_box.addWidget(self.pqmode); mode_box.addStretch(); enc.addLayout(mode_box) v.addLayout(enc) self.btn = QPushButton("Compress"); self.btn.clicked.connect(self._run); v.addWidget(self.btn) self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress) @@ -481,7 +582,8 @@ class CompressTab(QWidget): if self.solid.isChecked(): cmd.append("--solid") if self.pw.text(): cmd += ["-p", self.pw.text()] if self.pq.path(): - flag = "--pq-sdk" if self.sdk.isChecked() else "--pq" + tok = self._pqmodes[self.pqmode.currentIndex()][1] + _, flag = _PQ_FLAG[tok] cmd += [flag, self.pq.path()] cmd.append(dst); cmd.extend(srcs) run_async(self, cmd, self.btn, self.log, self.progress) @@ -500,10 +602,14 @@ class ExtractTab(QWidget): enc = QHBoxLayout(); enc.setSpacing(16) pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField(); pw.addWidget(self.pw); enc.addLayout(pw) pq = QVBoxLayout(); pq.addWidget(H("PQ private key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq) - sdk_box = QVBoxLayout(); sdk_box.addWidget(H("Mode")) - self.sdk = QCheckBox("Auto-detect (SDK or legacy)"); self.sdk.setChecked(True) - self.sdk.setToolTip("Tries --pq-sdk first, falls back to --pq for legacy archives.") - sdk_box.addWidget(self.sdk); sdk_box.addStretch(); enc.addLayout(sdk_box) + mode_box = QVBoxLayout(); mode_box.addWidget(H("PQ mode")) + self.pqmode = QComboBox() + self._pqmodes = pq_mode_options(include_auto=True) + for label, _tok in self._pqmodes: + self.pqmode.addItem(label) + self.pqmode.setToolTip("Auto-detect reads the archive header (vaptvupt info) to pick the\n" + "right mode. Or choose it explicitly to match your private key.") + mode_box.addWidget(self.pqmode); mode_box.addStretch(); enc.addLayout(mode_box) v.addLayout(enc) self.btn = QPushButton("Extract"); self.btn.setObjectName("green"); self.btn.clicked.connect(self._run); v.addWidget(self.btn) self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress) @@ -518,10 +624,13 @@ class ExtractTab(QWidget): if self.out.path(): cmd += ["-o", self.out.path()] if self.pw.text(): cmd += ["-p", self.pw.text()] if self.pq.path(): - # Auto-detect: zupt's extract auto-discovers enc type from header, - # so passing --pq-sdk works for both SDK and legacy keyfiles when - # the archive is SDK-encoded; --pq is needed for legacy archives. - flag = "--pq-sdk" if self.sdk.isChecked() else "--pq" + tok = self._pqmodes[self.pqmode.currentIndex()][1] + if tok == "auto": + # The private-key format must match how the archive was encrypted; + # inspect the header (vaptvupt info) to choose the right flag. + tok = _detect_archive_pq(arc) or "pq" + self.log.append(f"[auto-detect] using {_PQ_FLAG[tok][1]}") + _, flag = _PQ_FLAG[tok] cmd += [flag, self.pq.path()] cmd.append(arc) run_async(self, cmd, self.btn, self.log, self.progress) @@ -536,8 +645,16 @@ class VerifyTab(QWidget): v.addWidget(Sep()) v.addWidget(H("Verify integrity")) self.varc = PathField("Archive to verify", filters="VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.varc) - v.addWidget(H("Password (if encrypted)")) - self.vpw = PwField("Leave empty if not encrypted"); v.addWidget(self.vpw) + enc = QHBoxLayout(); enc.setSpacing(16) + pw = QVBoxLayout(); pw.addWidget(H("Password (if encrypted)")); self.vpw = PwField("Leave empty if not encrypted"); pw.addWidget(self.vpw); enc.addLayout(pw) + pq = QVBoxLayout(); pq.addWidget(H("PQ private key")); self.vpq = PathField("For --pq / --pq-only archives", filters="Key (*.key);;All (*)"); pq.addWidget(self.vpq); enc.addLayout(pq) + mode_box = QVBoxLayout(); mode_box.addWidget(H("PQ mode")) + self.vpqmode = QComboBox() + self._vpqmodes = pq_mode_options(include_auto=True) + for label, _tok in self._vpqmodes: + self.vpqmode.addItem(label) + mode_box.addWidget(self.vpqmode); mode_box.addStretch(); enc.addLayout(mode_box) + v.addLayout(enc) self.vbtn = QPushButton("Verify"); self.vbtn.setObjectName("amber"); self.vbtn.clicked.connect(self._verify); v.addWidget(self.vbtn) self.vlog = Log(120); v.addWidget(self.vlog) v.addWidget(Sep()) @@ -552,7 +669,15 @@ class VerifyTab(QWidget): if not arc: return cmd = ["test"] if self.vpw.text(): cmd += ["-p", self.vpw.text()] - cmd.append(arc); self.vlog.clear() + self.vlog.clear() + if self.vpq.path(): + tok = self._vpqmodes[self.vpqmode.currentIndex()][1] + if tok == "auto": + tok = _detect_archive_pq(arc) or "pq" + self.vlog.append(f"[auto-detect] using {_PQ_FLAG[tok][1]}") + _, flag = _PQ_FLAG[tok] + cmd += [flag, self.vpq.path()] + cmd.append(arc) code, out, err = run_zupt(cmd, timeout=600) self.vlog.append((err + "\n" + out).strip()) self.vlog.append("\nAll checksums passed." if code == 0 else "\nVerification failed.") @@ -621,30 +746,32 @@ class AboutTab(QWidget): ("VAPTVUPT", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), (ZUPT_VER_NUMBER, "color:white;font-size:28px;font-weight:800;font-family:monospace;"), ("", ""), - ("Post-quantum backup compression with ML-KEM-768 + X25519", "color:#6a8898;font-size:13px;"), - ("hybrid encryption, Argon2id KDF, and block deduplication.", "color:#6a8898;font-size:13px;"), + ("Post-quantum backup compression with ML-KEM-768: --pq hybrid", "color:#6a8898;font-size:13px;"), + (f"(+ X25519) or --pq-only (pure). {DEFAULT_KDF} password KDF,", "color:#6a8898;font-size:13px;"), + ("block deduplication, and full-disk backup.", "color:#6a8898;font-size:13px;"), ("Renamed from Zupt in v3.0.0 (INPI Brasil trademark); .zupt", "color:#6a8898;font-size:13px;"), ("archive extension and v1.6 wire format are unchanged.", "color:#6a8898;font-size:13px;"), ("", ""), ("CRYPTOGRAPHIC STACK", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), ("ML-KEM-768 FIPS 203 Post-Quantum KEM", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("X25519 RFC 7748 Elliptic Curve DH (hybrid w/ ML-KEM)", "color:#5a7a88;font-size:12px;font-family:monospace;"), - ("AES-256-CTR FIPS 197 Symmetric Cipher", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("AES-256-CTR FIPS 197 Symmetric Cipher (fresh per-block nonce)", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("HMAC-SHA256 RFC 2104 Authentication (Encrypt-then-MAC)", "color:#5a7a88;font-size:12px;font-family:monospace;"), - ("Argon2id RFC 9106 Password KDF (default since 2.4.1)", "color:#5a7a88;font-size:12px;font-family:monospace;"), - ("PBKDF2 RFC 8018 Password KDF (legacy; --kdf pbkdf2)", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("PBKDF2-SHA256 RFC 8018 Password KDF (default, 600k iterations)", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("Argon2id RFC 9106 Password KDF (WITH_SDK=1 builds only)", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("SHA3-512 FIPS 202 PQ key derivation (--pq / --pq-only)", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("HKDF RFC 5869 Key Derivation Function", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("SHA3/SHAKE FIPS 202 Hash / XOF", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("XXH64 (non-crypto) Per-block checksum (inside AEAD)", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("", ""), ("COMPRESSION CODEC", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), - ("VaptVupt LZ + ANS 2.48.5 LZ77 + tabled ANS entropy", "color:#5a7a88;font-size:12px;font-family:monospace;"), - ("AVX2 / NEON SIMD acceleration; 1.27x zstd-3 decode aggregate", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("VaptVupt LZ + ANS 2.60.4 LZ77 + tabled ANS entropy", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("AVX2 / NEON SIMD acceleration; CBMC-verified BCJ filters", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("", ""), ("CREDITS", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), ("VaptVupt application Cristian Cezar Moisés", "color:#5a7a88;font-size:12px;font-family:monospace;"), (" License: AGPL-3.0-or-later (commercial license available)", "color:#5a7a88;font-size:12px;font-family:monospace;"), - (" git.securityops.co/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"), + (" git.securityops.co/cristiancmoises/vaptvupt", "color:#3a5868;font-size:11px;font-family:monospace;"), ("", ""), ("VaptVupt LZ + ANS codec Cristian Cezar Moisés", "color:#5a7a88;font-size:12px;font-family:monospace;"), (" License: GPL-3.0-or-later (commercial license available)", "color:#5a7a88;font-size:12px;font-family:monospace;"), diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index d0e2066..d6ebeb4 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -18,7 +18,7 @@ replaces=('zupt') conflicts=('zupt') pkgdesc='Pure-C11 post-quantum backup compression utility (AES-256-CTR + HMAC-SHA256 + ML-KEM-768 + X25519)' arch=('x86_64' 'aarch64') -url='https://git.securityops.co/cristiancmoises/zupt' +url='https://git.securityops.co/cristiancmoises/vaptvupt' license=('AGPL-3.0-or-later') depends=('glibc') makedepends=('gcc') @@ -26,39 +26,31 @@ checkdepends=('python') # Replace SHA256 placeholder with output of: # sha256sum /tmp/zupt-2.4.4.tar.gz -source=("${pkgname}-${pkgver}.tar.gz::https://git.securityops.co/cristiancmoises/zupt/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") +source=("${pkgname}-${pkgver}.tar.gz::https://git.securityops.co/cristiancmoises/vaptvupt/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") sha256sums=('SKIP') build() { cd "${pkgname}-${pkgver}" - # Strict-warning build that the project's own §6 verification matrix uses. + # Source-only build (WITH_SDK=0) with the project's strict warning set. CFLAGS="${CFLAGS:--O2 -std=c11} -Wall -Wextra -Wpedantic" \ - make -j"$(nproc)" + make WITH_SDK=0 -j"$(nproc)" } check() { cd "${pkgname}-${pkgver}" - # Project regression suite — F-06 HMAC, F-08 top-MAC, F-09 byte sweep, etc. - make test + # Distro-safe regression subset (F-06..F-12, dedup-nonce, NIST/RFC vectors). + make WITH_SDK=0 check } package() { cd "${pkgname}-${pkgver}" - make DESTDIR="${pkgdir}" PREFIX=/usr install + # Source-only build (no vendored libraries); `make install` places the + # binary, the zupt symlink, the man pages and the shell completions. + make DESTDIR="${pkgdir}" PREFIX=/usr WITH_SDK=0 install # Docs that aren't part of `make install` - install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" - install -Dm644 SECURITY.md "${pkgdir}/usr/share/doc/${pkgname}/SECURITY.md" - install -Dm644 CHANGELOG.md "${pkgdir}/usr/share/doc/${pkgname}/CHANGELOG.md" - install -Dm644 AUDIT.md "${pkgdir}/usr/share/doc/${pkgname}/AUDIT.md" - install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" - - # Vendored libzuptsdk shipped alongside the binary because the binary - # is linked with -Wl,-rpath,$ORIGIN/vendor/zuptsdk. For system install - # we move it to /usr/lib/zupt/ and the binary's rpath remains relative. - install -d "${pkgdir}/usr/lib/${pkgname}" - install -Dm755 vendor/zuptsdk/libzuptsdk.so.2.0.0 \ - "${pkgdir}/usr/lib/${pkgname}/libzuptsdk.so.2.0.0" - ln -sf libzuptsdk.so.2.0.0 "${pkgdir}/usr/lib/${pkgname}/libzuptsdk.so.2" - ln -sf libzuptsdk.so.2.0.0 "${pkgdir}/usr/lib/${pkgname}/libzuptsdk.so" + install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" + install -Dm644 SECURITY.md "${pkgdir}/usr/share/doc/${pkgname}/SECURITY.md" + install -Dm644 CHANGELOG.md "${pkgdir}/usr/share/doc/${pkgname}/CHANGELOG.md" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } diff --git a/packaging/debian/control b/packaging/debian/control index 45e1419..caa35b8 100644 --- a/packaging/debian/control +++ b/packaging/debian/control @@ -8,9 +8,9 @@ Build-Depends: libc6-dev, python3 (>= 3.8) Standards-Version: 4.6.2 -Homepage: https://git.securityops.co/cristiancmoises/zupt -Vcs-Browser: https://git.securityops.co/cristiancmoises/zupt -Vcs-Git: https://git.securityops.co/cristiancmoises/zupt.git +Homepage: https://git.securityops.co/cristiancmoises/vaptvupt +Vcs-Browser: https://git.securityops.co/cristiancmoises/vaptvupt +Vcs-Git: https://git.securityops.co/cristiancmoises/vaptvupt.git Rules-Requires-Root: no Package: vaptvupt @@ -24,7 +24,7 @@ Description: Post-quantum backup compression utility (formerly Zupt) a pure-C11 backup compression utility featuring: * Post-quantum hybrid encryption (ML-KEM-768 + X25519, FIPS 203) * AES-256-CTR + HMAC-SHA256 authenticated encryption (Encrypt-then-MAC) - * Argon2id password-based key derivation (default since 2.4.1) + * PBKDF2-SHA256 password key derivation (Argon2id in WITH_SDK=1 builds) * Multi-threaded compression with the VaptVupt LZ + ANS codec 2.48.5 * Full-disk backup and restore with sparse-region detection * End-to-end byte-level tamper detection on encrypted archives diff --git a/packaging/debian/rules b/packaging/debian/rules index 9663278..6681fb1 100755 --- a/packaging/debian/rules +++ b/packaging/debian/rules @@ -14,20 +14,18 @@ export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed dh $@ override_dh_auto_build: - $(MAKE) -j$$(nproc) + # Source-only build: no vendored libraries, native crypto only. + $(MAKE) WITH_SDK=0 -j$$(nproc) override_dh_auto_test: - # Project's own regression suite covers F-06..F-12. - $(MAKE) test + # Distro-safe regression subset (F-06..F-12, dedup-nonce, NIST/RFC vectors). + $(MAKE) WITH_SDK=0 check override_dh_auto_install: - $(MAKE) DESTDIR=$(CURDIR)/debian/zupt PREFIX=/usr install - # Vendored libzuptsdk goes alongside the binary at a relative rpath. - install -d $(CURDIR)/debian/zupt/usr/lib/zupt - install -m 0755 vendor/zuptsdk/libzuptsdk.so.2.0.0 \ - $(CURDIR)/debian/zupt/usr/lib/zupt/libzuptsdk.so.2.0.0 - ln -sf libzuptsdk.so.2.0.0 $(CURDIR)/debian/zupt/usr/lib/zupt/libzuptsdk.so.2 - ln -sf libzuptsdk.so.2.0.0 $(CURDIR)/debian/zupt/usr/lib/zupt/libzuptsdk.so + # Binary package is `vaptvupt` -> stage into debian/vaptvupt (dh derives the + # staging dir from the Package: name in debian/control). Source-only: nothing + # to install beyond `make install` (no .so). + $(MAKE) DESTDIR=$(CURDIR)/debian/vaptvupt PREFIX=/usr WITH_SDK=0 install override_dh_auto_clean: $(MAKE) clean diff --git a/packaging/homebrew/vaptvupt.rb b/packaging/homebrew/vaptvupt.rb index b5cd21a..e1edacf 100644 --- a/packaging/homebrew/vaptvupt.rb +++ b/packaging/homebrew/vaptvupt.rb @@ -22,8 +22,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.2.1/vaptvupt-4.2.1.tar.gz" + homepage "https://git.securityops.co/cristiancmoises/vaptvupt" + url "https://git.securityops.co/cristiancmoises/vaptvupt/releases/download/v4.2.1/vaptvupt-4.2.1.tar.gz" version "4.2.1" sha256 "REPLACE_WITH_SHA256_OF_RELEASE_TARBALL" license "AGPL-3.0-or-later" @@ -31,30 +31,16 @@ class Vaptvupt < Formula depends_on "python@3.12" => :test # only for test-suite tamper harness def install - # macOS build: no Jasmin, C-fallback crypto paths are used. - # The Makefile auto-detects Jasmin availability and falls back cleanly. + # Source-only build (WITH_SDK=0): native crypto only, no vendored libraries. + # macOS uses the C-fallback crypto paths (no Jasmin); the Makefile detects + # this and falls back cleanly. ENV["CFLAGS"] = "#{ENV.cflags} -O2 -std=c11 -Wall -Wextra" - system "make", "-j#{ENV.make_jobs}" - system "make", "DESTDIR=#{prefix}", "PREFIX=", "install" + system "make", "WITH_SDK=0", "-j#{ENV.make_jobs}" + system "make", "DESTDIR=#{prefix}", "PREFIX=", "WITH_SDK=0", "install" - # Vendored libzuptsdk goes into lib/zupt/ with @loader_path rpath. - # Note: Linux ships .so.2.0.0; macOS .dylib equivalent must be built - # separately by the vendored makefile. For the initial Homebrew - # submission this assumes the upstream tarball includes a .dylib build; - # if not, build it here. - lib_zupt = lib/"zupt" - lib_zupt.mkpath - if File.exist?("vendor/zuptsdk/libzuptsdk.dylib") - cp "vendor/zuptsdk/libzuptsdk.dylib", lib_zupt - elsif File.exist?("vendor/zuptsdk/libzuptsdk.so.2.0.0") - # Fallback: link Linux-style .so on macOS (works for direct loads but - # not for dlopen-on-Darwin scenarios). Upstream is tracking this. - cp "vendor/zuptsdk/libzuptsdk.so.2.0.0", lib_zupt - end - - # Docs - doc.install "README.md", "SECURITY.md", "CHANGELOG.md", "AUDIT.md" + # Docs (no vendored .so/.dylib in the source-only build). + doc.install "README.md", "SECURITY.md", "CHANGELOG.md" end test do diff --git a/packaging/nix/flake.nix b/packaging/nix/flake.nix index efb3bc8..0e8d4cc 100644 --- a/packaging/nix/flake.nix +++ b/packaging/nix/flake.nix @@ -52,48 +52,38 @@ # hardening flags. Don't override -O2 from stdenv. NIX_CFLAGS_COMPILE = "-Wall -Wextra -Wpedantic -std=c11"; - # `make` builds the binary using vendored libzuptsdk via rpath. + # Source-only build (WITH_SDK=0): native crypto, no vendored libraries. buildPhase = '' runHook preBuild - make -j$NIX_BUILD_CORES + make WITH_SDK=0 -j$NIX_BUILD_CORES runHook postBuild ''; - # Run the full upstream regression suite. Disable per-package by - # setting doCheck = false; on by default. + # Distro-safe regression subset. Disable with doCheck = false;. doCheck = true; checkPhase = '' runHook preCheck - make test + make WITH_SDK=0 check runHook postCheck ''; installPhase = '' runHook preInstall - make DESTDIR=$out PREFIX= install - - # Move libzuptsdk into $out/lib/zupt/. The binary's rpath is - # $ORIGIN/../lib/zupt after autopatchelf rewrites it during - # the fixup phase. - mkdir -p $out/lib/zupt - install -m 0755 vendor/zuptsdk/libzuptsdk.so.2.0.0 \ - $out/lib/zupt/libzuptsdk.so.2.0.0 - ln -sf libzuptsdk.so.2.0.0 $out/lib/zupt/libzuptsdk.so.2 - ln -sf libzuptsdk.so.2.0.0 $out/lib/zupt/libzuptsdk.so + make DESTDIR=$out PREFIX= WITH_SDK=0 install # Docs - mkdir -p $out/share/doc/zupt - cp README.md SECURITY.md CHANGELOG.md AUDIT.md $out/share/doc/zupt/ + mkdir -p $out/share/doc/vaptvupt + cp README.md SECURITY.md CHANGELOG.md $out/share/doc/vaptvupt/ runHook postInstall ''; meta = with pkgs.lib; { - description = "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256 + Argon2id)"; - homepage = "https://git.securityops.co/cristiancmoises/zupt"; + description = "Post-quantum backup compression utility (ML-KEM-768 + X25519 + AES-256-CTR + HMAC-SHA256)"; + homepage = "https://git.securityops.co/cristiancmoises/vaptvupt"; license = with licenses; [ agpl3Plus gpl3Plus ]; maintainers = [ ]; platforms = [ "x86_64-linux" "aarch64-linux" ]; - mainProgram = "zupt"; + mainProgram = "vaptvupt"; }; }; in { diff --git a/packaging/opensuse/_service b/packaging/opensuse/_service index 4453127..1e294a4 100644 --- a/packaging/opensuse/_service +++ b/packaging/opensuse/_service @@ -1,6 +1,6 @@ - https://github.com/cristiancmoises/zupt + https://github.com/cristiancmoises/vaptvupt git v4.2.1 @PARENT_TAG@ diff --git a/packaging/opensuse/vaptvupt.spec b/packaging/opensuse/vaptvupt.spec index 5cdb1a5..b6e3565 100644 --- a/packaging/opensuse/vaptvupt.spec +++ b/packaging/opensuse/vaptvupt.spec @@ -97,6 +97,11 @@ chmod +x tests/*.sh %doc README.md SECURITY.md CHANGELOG.md %{_bindir}/vaptvupt %{_bindir}/zupt +%{_datadir}/bash-completion/completions/vaptvupt +%{_datadir}/bash-completion/completions/zupt +%{_datadir}/zsh/site-functions/_vaptvupt +%{_datadir}/zsh/site-functions/_zupt +%{_datadir}/fish/vendor_completions.d/vaptvupt.fish %{_mandir}/man1/vaptvupt.1%{?ext_man} %{_mandir}/man1/zupt.1%{?ext_man} diff --git a/packaging/portable/README.txt b/packaging/portable/README.txt new file mode 100644 index 0000000..c144889 --- /dev/null +++ b/packaging/portable/README.txt @@ -0,0 +1,57 @@ +VaptVupt GUI — portable cross-platform package +============================================== + +The VaptVupt GUI is a single Python file (zupt_gui.py) built on Qt for Python +(PySide6, or PyQt6 as a fallback). It runs on Windows, macOS, Linux and the +BSDs — anywhere Python 3 and a Qt binding are installed. This portable package +contains the GUI plus a launcher for each platform; it drives the `vaptvupt` +command-line tool under the hood. + +Contents +-------- + zupt_gui.py The GUI (PySide6 / PyQt6). + vaptvupt-gui.bat Windows launcher. + vaptvupt-gui.command macOS launcher (double-clickable in Finder). + vaptvupt-gui.sh Linux / BSD launcher. + assets/zupt-icon.png Application icon. + +Requirements +------------ + 1. Python 3.8 or newer. + Windows: https://python.org (tick "Add python.exe to PATH") + macOS: python.org, or `brew install python` + Linux: your distro's python3 package + FreeBSD: pkg install python311 + OpenBSD: pkg_add python%3 + 2. A Qt binding: + pip (any OS): python3 -m pip install PySide6 + Debian/Ubuntu: sudo apt install python3-pyqt6 + Fedora/RHEL: sudo dnf install python3-pyqt6 + FreeBSD: pkg install py311-pyside6 + OpenBSD: pkg_add py3-pyside6 + 3. The vaptvupt CLI, either: + * placed next to the launcher (vaptvupt.exe on Windows, vaptvupt + elsewhere) — the launcher auto-detects it via VAPTVUPT_BIN, or + * installed on PATH (deb/rpm/AppImage/Homebrew/pkg — see the project + release page). + +Running +------- + Windows: double-click vaptvupt-gui.bat + macOS: double-click vaptvupt-gui.command + (first run: right-click > Open to bypass Gatekeeper for an + unsigned script, or `xattr -dr com.apple.quarantine .`) + Linux/BSD: ./vaptvupt-gui.sh + +Troubleshooting +--------------- + * "requires PySide6 or PyQt6" -> install a Qt binding (requirement 2). + * "vaptvupt not found" -> put the CLI next to the launcher or on PATH. + * Set VAPTVUPT_DEBUG=1 to print the binary-discovery log to stderr. + +Fully self-contained native installers (Windows .exe/.msi, macOS .dmg) that +bundle Python + Qt + the CLI are produced by the project's CI on real Windows +and macOS runners — see the release page. This portable package is the +dependency-light option that works identically on every platform. + +License: AGPL-3.0-or-later. Project: https://git.securityops.co/cristiancmoises/vaptvupt diff --git a/packaging/portable/vaptvupt-gui.bat b/packaging/portable/vaptvupt-gui.bat new file mode 100644 index 0000000..2bd6ec6 --- /dev/null +++ b/packaging/portable/vaptvupt-gui.bat @@ -0,0 +1,31 @@ +@echo off +rem SPDX-License-Identifier: AGPL-3.0-or-later +rem VaptVupt GUI launcher for Windows (portable package). +rem +rem Requirements on the target machine: +rem * Python 3.8+ (https://python.org — tick "Add python.exe to PATH") +rem * PySide6 or PyQt6: py -m pip install PySide6 +rem * The vaptvupt CLI: vaptvupt.exe next to this file, or on PATH. +rem +rem If vaptvupt.exe sits beside this launcher we pin it via VAPTVUPT_BIN so the +rem GUI drives the bundled CLI rather than any other copy on PATH. +setlocal +set "HERE=%~dp0" +if exist "%HERE%vaptvupt.exe" set "VAPTVUPT_BIN=%HERE%vaptvupt.exe" + +rem Prefer the py launcher, fall back to python on PATH. +where py >nul 2>nul +if %ERRORLEVEL%==0 ( + py -3 "%HERE%zupt_gui.py" %* +) else ( + python "%HERE%zupt_gui.py" %* +) +set "RC=%ERRORLEVEL%" +if not "%RC%"=="0" ( + echo. + echo vaptvupt-gui exited with code %RC%. + echo If you saw an import error, install the Qt binding: py -m pip install PySide6 + echo If the CLI was not found, put vaptvupt.exe next to this launcher or on PATH. + pause +) +endlocal diff --git a/packaging/portable/vaptvupt-gui.command b/packaging/portable/vaptvupt-gui.command new file mode 100644 index 0000000..da4b28a --- /dev/null +++ b/packaging/portable/vaptvupt-gui.command @@ -0,0 +1,17 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# VaptVupt GUI launcher for macOS (portable package). +# Double-clickable in Finder (.command). Requirements on the target Mac: +# * Python 3.8+ (python.org, Homebrew `brew install python`, or Xcode CLT) +# * PySide6 or PyQt6: python3 -m pip install PySide6 +# * The vaptvupt CLI: `vaptvupt` next to this file, or on PATH +# (Homebrew: `brew install cristiancmoises/tap/vaptvupt`). +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[ -x "$HERE/vaptvupt" ] && export VAPTVUPT_BIN="$HERE/vaptvupt" + +PY="$(command -v python3 || command -v python || true)" +if [ -z "$PY" ]; then + osascript -e 'display alert "VaptVupt GUI" message "Python 3 not found. Install it from python.org or `brew install python`, then run: python3 -m pip install PySide6"' 2>/dev/null + echo "Python 3 not found." >&2; exit 1 +fi +exec "$PY" "$HERE/zupt_gui.py" "$@" diff --git a/packaging/portable/vaptvupt-gui.sh b/packaging/portable/vaptvupt-gui.sh new file mode 100644 index 0000000..8a5fa8e --- /dev/null +++ b/packaging/portable/vaptvupt-gui.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# VaptVupt GUI launcher for Linux and the BSDs (portable package). +# Requirements on the target system: +# * Python 3.8+ +# * PySide6 or PyQt6 +# Debian/Ubuntu: sudo apt install python3-pyqt6 +# Fedora/RHEL: sudo dnf install python3-pyqt6 +# FreeBSD: pkg install py311-pyside6 (or py311-qt6-pyqt) +# OpenBSD: pkg_add py3-pyside6 +# any OS via pip: python3 -m pip install PySide6 +# * The vaptvupt CLI: `vaptvupt` next to this file, or on PATH. +HERE="$(cd "$(dirname "$0")" && pwd)" +[ -x "$HERE/vaptvupt" ] && export VAPTVUPT_BIN="$HERE/vaptvupt" + +PY="$(command -v python3 || command -v python || true)" +if [ -z "$PY" ]; then + echo "vaptvupt-gui: Python 3 not found on PATH." >&2 + exit 1 +fi +exec "$PY" "$HERE/zupt_gui.py" "$@" diff --git a/packaging/windows/vaptvupt-gui.iss b/packaging/windows/vaptvupt-gui.iss new file mode 100644 index 0000000..40b1ada --- /dev/null +++ b/packaging/windows/vaptvupt-gui.iss @@ -0,0 +1,73 @@ +; SPDX-License-Identifier: AGPL-3.0-or-later +; Inno Setup script for the VaptVupt GUI Windows installer. +; +; Compiled by the cross-platform CI (.github/workflows/cross-platform.yml) with: +; ISCC.exe /DAppVersion= packaging/windows/vaptvupt-gui.iss +; after PyInstaller has produced dist\vaptvupt-gui.exe (a onefile bundle that +; already contains Python, PySide6 and vaptvupt.exe). Requires Inno Setup 6+. +; +; To build locally on Windows: install Inno Setup, then run the same ISCC line +; from the repo root (with dist\vaptvupt-gui.exe present). + +#ifndef AppVersion + #define AppVersion "0.0.0" +#endif + +[Setup] +AppName=VaptVupt +AppVersion={#AppVersion} +AppPublisher=Cristian Cezar Moises +AppPublisherURL=https://git.securityops.co/cristiancmoises/vaptvupt +DefaultDirName={autopf}\VaptVupt +DefaultGroupName=VaptVupt +UninstallDisplayIcon={app}\vaptvupt-gui.exe +OutputDir=packaging\windows\Output +OutputBaseFilename=VaptVupt-Setup-{#AppVersion} +Compression=lzma2 +SolidCompression=yes +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +WizardStyle=modern +LicenseFile=LICENSE + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Files] +; PyInstaller onefile bundle (Python + PySide6 + the GUI + vaptvupt.exe). +Source: "dist\vaptvupt-gui.exe"; DestDir: "{app}"; Flags: ignoreversion +; Ship the raw CLI too so it can be added to PATH and used from a terminal. +Source: "vaptvupt.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist +Source: "README.md"; DestDir: "{app}"; Flags: ignoreversion isreadme +Source: "CHANGELOG.md"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\VaptVupt"; Filename: "{app}\vaptvupt-gui.exe" +Name: "{group}\Uninstall VaptVupt"; Filename: "{uninstallexe}" +Name: "{autodesktop}\VaptVupt"; Filename: "{app}\vaptvupt-gui.exe"; Tasks: desktopicon + +[Tasks] +Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:" +Name: "addtopath"; Description: "Add the vaptvupt CLI to PATH (current user)"; GroupDescription: "Command line:" + +[Registry] +; Optionally add the install dir to the user PATH (for the vaptvupt.exe CLI). +Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; \ + ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath('{app}') + +[Run] +Filename: "{app}\vaptvupt-gui.exe"; Description: "Launch VaptVupt"; \ + Flags: nowait postinstall skipifsilent + +[Code] +function NeedsAddPath(Param: string): Boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then + begin + Result := True; + exit; + end; + Result := Pos(';' + ExpandConstant(Param) + ';', ';' + OrigPath + ';') = 0; +end; diff --git a/src/vv_decoder.c b/src/vv_decoder.c index 49fea91..2f923b7 100644 --- a/src/vv_decoder.c +++ b/src/vv_decoder.c @@ -202,6 +202,12 @@ decode_block_tokens_impl( if (VV_UNLIKELY(ip >= ip_end)) break; + /* Bound the 2-/3-byte offset read against the compressed-block end. + * Without this a crafted block whose last literal advances ip to + * ip_end-1 (or ip_end-2 for 3-byte offsets) makes vv_read16 / the + * 3-byte load read past the heap buffer. The general/tail decode path + * already carries this guard; the AVX2 fast paths were missing it. */ + if (VV_UNLIKELY(ip + off_bytes > ip_end)) return VV_ERR_CORRUPT; uint32_t offset; if (off_bytes == 2) { offset = vv_read16(ip); @@ -285,6 +291,12 @@ decode_block_tokens_impl( if (VV_UNLIKELY(ip >= ip_end)) break; + /* Bound the 2-/3-byte offset read against the compressed-block end. + * Without this a crafted block whose last literal advances ip to + * ip_end-1 (or ip_end-2 for 3-byte offsets) makes vv_read16 / the + * 3-byte load read past the heap buffer. The general/tail decode path + * already carries this guard; the AVX2 fast paths were missing it. */ + if (VV_UNLIKELY(ip + off_bytes > ip_end)) return VV_ERR_CORRUPT; uint32_t offset; if (off_bytes == 2) { offset = vv_read16(ip); diff --git a/src/zupt_crypto.c b/src/zupt_crypto.c index 44a1c3e..79497c0 100644 --- a/src/zupt_crypto.c +++ b/src/zupt_crypto.c @@ -817,7 +817,14 @@ int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile, const uint8_t *nonce = enc_hdr + 1 + 1088 + 32; uint8_t ml_pk[1184], x_pk[32], ml_sk[2400], x_sk[32]; - if (read_privkey(privkeyfile, ml_pk, x_pk, ml_sk, x_sk) != 0) return -1; + if (read_privkey(privkeyfile, ml_pk, x_pk, ml_sk, x_sk) != 0) { + /* Wipe any partially-read secret-key material on error, matching the + * pq-only decrypt path (a bad/truncated key file must not leave secret + * bytes on the stack). */ + zupt_secure_wipe(ml_sk, sizeof(ml_sk)); + zupt_secure_wipe(x_sk, sizeof(x_sk)); + return -1; + } /* ML-KEM-768 decapsulation */ uint8_t ml_ss[32]; diff --git a/src/zupt_format.c b/src/zupt_format.c index 5139d42..cc49027 100644 --- a/src/zupt_format.c +++ b/src/zupt_format.c @@ -2911,7 +2911,11 @@ zupt_error_t zupt_archive_info(const char *path) { * write_enc_header: 7-byte prefix (magic0,magic1,block_type,codec u16, * flags u16) + varint(len) + varint(len) + u64 xxh64 + enc_hdr[0]=enc_type. */ uint8_t enc_type = 0; + /* encryption_header_off is attacker-controlled; bound it inside the file + * before the (off_t)+7 arithmetic so the signed addition cannot overflow + * (UB) and the seek stays in-range. All subsequent reads are EOF-checked. */ if ((hdr.global_flags & ZUPT_FLAG_ENCRYPTED) && hdr.encryption_header_off != 0 && + hdr.encryption_header_off < file_size && (file_size - hdr.encryption_header_off) > 7 && fseeko(f, (off_t)hdr.encryption_header_off + 7, SEEK_SET) == 0) { uint64_t l1 = 0, l2 = 0; if (zupt_read_varint(f, &l1) > 0 && zupt_read_varint(f, &l2) > 0 && diff --git a/src/zupt_main.c b/src/zupt_main.c index a106865..ed7f1b0 100644 --- a/src/zupt_main.c +++ b/src/zupt_main.c @@ -12,6 +12,12 @@ #include #include #include +#include /* stat()/S_ISREG for the compress output-overwrite guard */ + +/* MSVC's defines _S_IFREG/S_IFREG but not the S_ISREG macro. */ +#ifndef S_ISREG +# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#endif #ifdef _WIN32 #include @@ -23,7 +29,12 @@ static void banner(void) { fprintf(stderr, "%s %s - %s\n" "Format v%d.%d | Codec: VaptVupt + Zupt-LZ | Checksum: XXH64\n" - "Encryption: AES-256-CTR + HMAC-SHA256 | KDF: Argon2id (default) / PBKDF2 (--kdf pbkdf2)\n\n", + "Encryption: AES-256-CTR + HMAC-SHA256 | KDF: " +#ifdef ZUPT_WITH_SDK + "Argon2id (default) / PBKDF2 (--kdf pbkdf2)\n\n", +#else + "PBKDF2-SHA256 (Argon2id needs a WITH_SDK=1 build)\n\n", +#endif ZUPT_PRODUCT_NAME, ZUPT_VERSION_STRING, ZUPT_PRODUCT_TAGLINE, ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR); } @@ -67,19 +78,25 @@ static void usage(void) { " -f, --fast Use fast LZ codec (less compression)\n" " --vv, --vaptvupt Use VaptVupt codec (LZ + ANS entropy, default)\n" " --lzhp Use Zupt-LZHP codec (LZ77+Huffman, no SIMD needed)\n" - " -p, --password Encrypt with AES-256 (prompted if empty)\n" - " --kdf KDF for password mode. Default: argon2id (v2.4.1+).\n" + " -p, --password Encrypt with AES-256 (bare -p prompts). Options must\n" + " precede ; a value ending in .zupt is\n" + " taken as the password, so put -p before the archive.\n" +#ifdef ZUPT_WITH_SDK + " --kdf KDF for password mode. Default: argon2id.\n" " Use 'pbkdf2' for v2.4.0-and-older reader compatibility.\n" +#else + " --kdf KDF for password mode. Default (and only, this build):\n" + " PBKDF2-SHA256 600k. Argon2id needs a WITH_SDK=1 build.\n" +#endif " -c, --comment Embed a free-form archive comment (v2.4.3+).\n" " --comment-file Read comment from file (max 4096 bytes).\n" " --pq Post-quantum HYBRID encryption (ML-KEM-768 + X25519) [recommended]\n" " --pq-only FULL post-quantum encryption (ML-KEM-768 only, no classical layer)\n" " --pq-sdk Post-quantum encryption via libzuptsdk (WITH_SDK=1 builds only)\n" - " --pq-box Post-quantum sealed box via libpqvaptvupt (HKDF combiner)\n" - " (HKDF combiner + key commitment + HPKE binding\n" - " + Argon2id; recommended for new archives)\n" + " --pq-box Post-quantum sealed box via libpqvaptvupt (WITH_SDK=1 builds only)\n" " --dedup, -D Block-level deduplication\n" " --solid Solid mode (single stream)\n" + " -y, --force Overwrite an existing non-.zupt file as the output archive\n" " -v, --verbose Verbose per-file output\n" " -t, --threads Thread count (0=auto, 1=single, 2-64=explicit)\n" "\n"); @@ -112,35 +129,44 @@ static void usage(void) { /* ── Section 4: examples ── */ fprintf(stderr, "Examples:\n" - " # Legacy PQ workflow\n" - " vaptvupt keygen -o mykey.key # Generate keypair\n" + " # Post-quantum HYBRID workflow (--pq, recommended)\n" + " vaptvupt keygen -o mykey.key # Generate hybrid private key\n" " vaptvupt keygen --pub -o pub.key -k mykey.key # Export public key\n" " vaptvupt compress --pq pub.key backup.zupt ~/Documents/ # Encrypt\n" " vaptvupt extract --pq mykey.key backup.zupt -o ~/restored/ # Decrypt\n" "\n" - " # SDK v2 PQ workflow (recommended for new archives)\n" - " vaptvupt keygen --sdk -o mykey.priv # Writes mykey.priv + .pub\n" - " vaptvupt compress --pq-sdk mykey.priv.pub backup.zupt files/ # Encrypt (HKDF+commit+HPKE)\n" - " vaptvupt extract --pq-sdk mykey.priv backup.zupt # Decrypt\n" + " # Full (pure) post-quantum workflow (--pq-only, ML-KEM-768 only)\n" + " vaptvupt keygen --pq-only -o pqkey # Generate pq-only private key\n" + " vaptvupt keygen --pub --pq-only -o pqkey.pub -k pqkey # Export public key\n" + " vaptvupt compress --pq-only pqkey.pub backup.zupt files/ # Encrypt (no classical layer)\n" + " vaptvupt extract --pq-only pqkey backup.zupt -o out/ # Decrypt\n" "\n" - " # Conventional / password\n" + " # Conventional / password (PBKDF2-SHA256)\n" " vaptvupt compress backup.zupt ~/Documents/ # No encryption\n" " vaptvupt compress -l 9 -p mysecret secure.zupt data/ # Password + max compression\n" " vaptvupt list secure.zupt -p mysecret # List with password\n" " vaptvupt extract -o restored/ -p mysecret secure.zupt # Extract with password\n" " vaptvupt bench ~/Documents/ # Benchmark\n" + "\n" + " # SDK v2 / sealed-box modes require an upstream 'make WITH_SDK=1' build:\n" + " # keygen --sdk / --box, compress/extract --pq-sdk / --pq-box\n" "\n"); /* ── Section 5: footer ── */ fprintf(stderr, "Default codec: VaptVupt LZ + ANS " ZUPT_CODEC_RELEASE " (AVX2/NEON SIMD)\n" "Encryption: AES-256-CTR + HMAC-SHA256 (Encrypt-then-MAC)\n" - "KDF: Argon2id (default, v2.4.1+); PBKDF2-SHA256 600k iter via --kdf pbkdf2\n" +#ifdef ZUPT_WITH_SDK + "KDF: Argon2id (default); PBKDF2-SHA256 600k iter via --kdf pbkdf2\n" +#else + "KDF: PBKDF2-SHA256 600k iter (default; Argon2id needs WITH_SDK=1)\n" +#endif + "Post-quantum: --pq (hybrid ML-KEM-768 + X25519, recommended); --pq-only (ML-KEM-768 only)\n" "Format: v1.6 (since v2.3.1); archives byte-compatible with v2.3.1+\n" "\n" "License: AGPL-3.0-or-later (VaptVupt) + GPL-3.0-or-later (VaptVupt codec)\n" " Dual-licensed: commercial license available: sac@securityops.co\n" - "Project: https://git.securityops.co/cristiancmoises/zupt\n" + "Project: https://git.securityops.co/cristiancmoises/vaptvupt\n" ); } @@ -191,11 +217,18 @@ int main(int argc, char **argv) { "Format: v%d.%d | Archive extension: .zupt (unchanged)\n" "Codec: VaptVupt " ZUPT_CODEC_RELEASE " (0x%04X) — LZ + ANS, optimal parser + large-window extreme\n" "Encryption: AES-256-CTR + HMAC-SHA256\n" +#ifdef ZUPT_WITH_SDK "KDF: Argon2id (default) / PBKDF2-SHA256 %d iter (--kdf pbkdf2)\n" - "Post-quantum: ML-KEM-768 + X25519 hybrid (FIPS 203 + RFC 7748)\n" + "Post-quantum: --pq hybrid (ML-KEM-768 + X25519), --pq-only (ML-KEM-768), --pq-sdk/--pq-box (libzuptsdk)\n" + "Build: full (libzuptsdk: Argon2id, --pq-sdk, --pq-box available)\n" +#else + "KDF: PBKDF2-SHA256 %d iter (default; Argon2id needs WITH_SDK=1)\n" + "Post-quantum: --pq hybrid (ML-KEM-768 + X25519), --pq-only (ML-KEM-768 only) — FIPS 203 + RFC 7748\n" + "Build: source-only (native crypto; --pq-sdk/--pq-box/Argon2id need WITH_SDK=1)\n" +#endif "License: AGPL-3.0-or-later (VaptVupt) + GPL-3.0-or-later (codec)\n" " Dual-licensed: commercial license available\n" - "Project: https://git.securityops.co/cristiancmoises/zupt\n" + "Project: https://git.securityops.co/cristiancmoises/vaptvupt\n" "Commercial: sac@securityops.co\n", ZUPT_VERSION_STRING, ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR, ZUPT_CODEC_VAPTVUPT, ZUPT_KDF_ITERATIONS); @@ -219,6 +252,7 @@ int main(int argc, char **argv) { if (streq(cmd,"compress")||streq(cmd,"c")) { zupt_options_t opts; zupt_default_options(&opts); int ai = 2; + int force = 0; /* -y/--force: allow overwriting a non-.zupt output */ while (ai9)opts.level=9; @@ -260,18 +294,6 @@ int main(int argc, char **argv) { } else if (streq(argv[ai],"--pq-sdk")&&ai+1ai ? argv[ai] : "")); + return 1; + } + } + + /* Data-loss guard. `compress -p out.zupt a.txt b.txt` makes -p swallow + * "out.zupt" as the PASSWORD, shifts positionals so the output archive + * becomes "a.txt", and truncates a.txt (a user data file) with archive + * bytes — silently, exit 0. Refuse to overwrite an existing regular file + * that is not a .zupt archive unless -y/--force is given. Archives the + * tool writes end in .zupt, so this never blocks normal use. */ + { + struct stat ost; + size_t olen = strlen(output); + int is_zupt = (olen >= 5 && strcmp(output + olen - 5, ".zupt") == 0); + if (!force && !is_zupt && stat(output, &ost) == 0 && S_ISREG(ost.st_mode)) { + fprintf(stderr, + "Error: refusing to overwrite existing file '%s' as the output archive\n" + " (it does not end in .zupt). If you meant to set a password, use\n" + " '-p' or put '-p PASSWORD' BEFORE the archive name.\n" + " Pass -y/--force to overwrite '%s' anyway.\n", + output, output); + return 1; + } + } + + /* Skip a leading `--` separator before the file list. */ + if (ai < argc && streq(argv[ai], "--")) ai++; + + /* Collect files (expand directories recursively). Guard against the + * output archive also being one of the inputs (self-overwrite). */ zupt_filelist_t fl; zupt_filelist_init(&fl); - for (int i=ai; i