release: restore ZUPT and harden source-only 5.2.2

This commit is contained in:
Cristian Cezar Moisés 2026-08-31 14:14:36 -03:00
commit ff99770bd0
205 changed files with 19627 additions and 13215 deletions

View file

@ -3,14 +3,13 @@
Copyright (C) 2026 Cristian Cezar Moisés <zupt@riseup.net>
VaptVupt GUI (formerly Zupt GUI; parent application renamed in v3.0.0
due to a prior INPI Brasil trademark registration of "Zupt") is free
ZUPT GUI is free
software: you can redistribute it and/or modify it under the terms of
the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your
option) any later version.
VaptVupt GUI is distributed in the hope that it will be useful, but
ZUPT GUI is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Affero General Public License for more details.
@ -23,21 +22,26 @@
─────────────────────────────────────────────────────────────────────
PRIOR LICENSE NOTE
HISTORICAL LICENSE NOTE
Earlier copies of this file may have stated "MIT License" — that was
a packaging mistake inherited from a template. The GUI source code's
SPDX-License-Identifier header has always been AGPL-3.0-or-later;
the file-level license here is corrected to match. There is no
historical MIT-licensed release of VaptVupt GUI; do not assume MIT
grant from any prior tarball that contained this file.
Published repository history includes earlier copies of this file
under the MIT License. In particular, commit d4660e6539c8b6eeba81751c018217d978fdd618
and the v2.2.2 source tag contain an MIT-form gui/LICENSE-GUI. Those
permissions remain applicable to the exact material distributed under
them; this file does not revoke or reinterpret a historical grant.
The current GUI source carries AGPL-3.0-or-later SPDX notices. Apply
the notices shipped with the exact source revision being used. See the
5.2.2 licensing erratum in CHANGELOG.md for the repository record.
─────────────────────────────────────────────────────────────────────
COMMERCIAL LICENSING
The VaptVupt GUI may be commercially relicensed by the author. If
you require relief from copyleft terms (proprietary derivatives,
closed-source bundling, etc.), contact:
The applicable copyright holder may offer controlled first-party
rights under a separately executed commercial agreement. This notice
is not a commercial license grant, does not revoke a public license,
and cannot relicense rights that the licensor does not control. For
inquiries, contact:
sac@securityops.co

View file

@ -1,123 +1,149 @@
# VaptVupt GUI
# ZUPT GUI
Desktop application for [vaptvupt](https://git.securityops.co/cristiancmoises/vaptvupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption.
The ZUPT GUI is a Python/Qt front end for the ZUPT 5.2.2 command-line
program. It starts the CLI as a subprocess; compression, archive parsing, and
cryptography remain in the C program.
Works on GNU/Linux, BSD, macOS, and Windows.
The canonical project repository is
`https://github.com/cristiancmoises/zupt`.
## Install
## Requirements
### Linux (recommended)
- a working `zupt` CLI from the same release, available on `PATH` or through
the `ZUPT_BIN` environment variable;
- Python 3.9 or newer;
- PySide6 or PyQt6;
- a graphical session for normal use.
```bash
tar xzf vaptvupt-gui.tar.gz && cd vaptvupt-gui
./vaptvupt-gui # auto-creates venv, installs PySide6
./install.sh --user # adds right-click menu integration
Install and test the source-only CLI first:
```sh
make clean
make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf 1)" \
WITH_SDK=0 WITH_PQBOX=0
make WITH_SDK=0 WITH_PQBOX=0 check
./zupt --version
```
### Windows
The GUI detects the capabilities reported by that binary. Native `--pq` and
`--pq-only` are available in the default build. SDK and PQBOX controls are
usable only when the CLI was built explicitly against the corresponding system
development libraries; no precompiled optional library is shipped in Git.
**Option A — Installer (recommended):**
## Run from the source tree
Download `VaptVuptGUI-1.3.0-Setup.exe` and run it. Installs to Program Files, adds Start Menu shortcut, desktop shortcut, right-click context menus, and .zupt file association. Includes uninstaller.
Using a virtual environment keeps Python packages outside the repository:
**Option B — Build from source:**
```cmd
cd packaging\windows
build-windows.bat
```sh
python3 -m venv ~/.local/share/zupt-gui-venv
~/.local/share/zupt-gui-venv/bin/pip install PySide6
ZUPT_BIN="$PWD/zupt" \
~/.local/share/zupt-gui-venv/bin/python gui/src/zupt_gui.py
```
Requires Python 3.9+, NSIS 3.x, and a compiled `vaptvupt.exe`.
Installing PySide6 can access the Python package index. Do that as an explicit
setup step; upstream CLI builds, package builds, and checks do not download
dependencies.
**Option C — Run directly:**
For noninteractive checks:
```cmd
pip install PySide6
python src\zupt_gui.py
```sh
python3 gui/src/zupt_gui.py --version
python3 gui/src/zupt_gui.py --selftest
```
### macOS / BSD
The first command does not prove that a full desktop session works. Test the
actual windows and archive operations on every platform for which a GUI package
is published.
```bash
pip3 install PySide6
python3 src/zupt_gui.py
## Functions
| Area | Function |
|---|---|
| Keys | Generate keys supported by the selected CLI build |
| Compress | Select input, compression settings, and an available encryption mode |
| Extract | Detect archive encryption, request the needed credential, and extract |
| Verify | Run archive integrity validation without extraction |
| Info | Display metadata reported by the CLI |
| Disk | Front end for the CLI disk backup and restore commands |
Disk operations can require additional operating-system privileges. Run only
the specific CLI operation that needs them; do not run the whole desktop session
as root.
## Desktop integration
`gui/install.sh --user` installs the integration supported by that script for
the current user. Review the script and its destination paths before running
it. File-manager menus and file associations differ across desktops and
operating systems and must be tested on the target system.
## Packaged GUI builds
Release pages provide only these GUI artifacts after their separate package and
installed off-screen GUI/CLI integration gates pass:
- `zupt-gui_5.2.2_all.deb`;
- `zupt-gui-5.2.2-1.noarch.rpm`;
- `zupt-gui-5.2.2-1.src.rpm`;
- `zupt-gui-5.2.2-portable.zip`.
The DEB/RPM packages install the Python/Qt source and depend on the matching
`zupt` CLI package. The portable ZIP contains source, launchers, icons, licenses,
and provenance only; it bundles no Python, Qt, CLI, or compiled runtime. Its
gate scans the assembled and extracted trees, enforces an exact safe-member
allowlist, and runs the extracted launcher off-screen against the tested CLI.
An absent artifact did not pass its gate and must not be inferred from another
format's result.
GUI AppImage, AppDir and Flatpak bundles, and Windows/macOS GUI installers are
not promoted by the upstream 5.2.2 release gates.
`packaging/build-gui-appimage.sh` is a downstream-only helper and fails unless
its operator supplies the exact verified runtime plus a complete
license/source-relink notice through `APPIMAGE_RUNTIME_COMPLIANCE_FILE`; that
material is included in the resulting AppDir.
The downstream Windows GUI helper similarly requires
`ZUPT_WINDOWS_RUNTIME_NOTICES_DIR` with a `MANIFEST.txt` that identifies
the exact Python, PyInstaller, Qt and PySide/PyQt runtime inputs and their
notices. It fails unless the directory also has non-empty
`PYTHON-NOTICE.txt`, `PYINSTALLER-NOTICE.txt`, `QT-NOTICE.txt`, and either
`PYSIDE6-NOTICE.txt` or `PYQT6-NOTICE.txt`. The installer includes that
directory together with every ZUPT license and notice. This requirement does
not make the untested GUI installer a 5.2.2 release asset. The promoted Windows
ZIP and macOS DMG are CLI-only.
Packaging recipes and scripts under `gui/packaging/` and `packaging/` are build
inputs, not evidence that a package has been accepted by a distribution. They
must build the CLI from the immutable source tag with
`WITH_SDK=0 WITH_PQBOX=0` unless source-built system dependencies are declared.
Generated packages, application bundles, and executables must remain outside
Git and outside upstream source archives.
The former standalone `gui/setup.py` sdist/wheel route is intentionally absent:
its outputs did not carry the complete project license payload. Use
`gui/install.sh` or the reviewed distribution helpers so the AGPL text and
artwork provenance are installed with the GUI.
## Troubleshooting
Verify the exact interpreter and CLI used by the GUI:
```sh
python3 -c 'import PySide6.QtWidgets'
zupt --version
ZUPT_BIN=/absolute/path/to/zupt \
python3 gui/src/zupt_gui.py --selftest
```
### AppImage (universal Linux)
```bash
chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage
./VaptVupt-GUI-1.3.0-x86_64.AppImage
```
### Flatpak
```bash
flatpak-builder --install build packaging/flatpak/dev.zupt.gui.yml
flatpak run dev.zupt.gui
```
## Features
| Tab | Function |
|-----|----------|
| Keys | Generate ML-KEM-768 + X25519 hybrid keypairs |
| Compress | All codecs, levels 1-9, dedup, solid, password, PQ keys |
| Extract | Decrypt and extract .zupt archives |
| Verify | Check block checksums, view archive metadata |
| Disk | Full-disk backup and restore |
| About | Version, cryptographic stack, credits |
All tabs support drag-and-drop. Drop a .zupt file anywhere on the window to extract it. Drop any other file to compress it.
## System Integration
### Linux (Nemo / Cinnamon)
After `./install.sh --user`:
- Right-click any file: **Compress with VaptVupt**
- Right-click .zupt file: **Extract with VaptVupt**
- Double-click .zupt: opens in VaptVupt GUI
### Windows (after installer)
- Right-click any file: **Compress with VaptVupt**
- Right-click any folder: **Compress with VaptVupt**
- Double-click .zupt: opens in VaptVupt GUI
- Right-click .zupt: **Verify Integrity**
## Architecture
```
VaptVupt GUI (PySide6, Python)
|
|-- subprocess.Popen() with streaming stderr
|
v
vaptvupt CLI (Pure C11 binary)
ML-KEM-768 + X25519 + AES-256-CTR
VaptVupt / LZHP / Store codecs
Block deduplication, full-disk backup
```
The GUI calls the vaptvupt CLI binary — all cryptography runs in native C, not Python.
## Packaging
| Platform | Format | Tool |
|----------|--------|------|
| Any | pip | `pip install .` |
| Debian/Ubuntu/Mint | .deb | `packaging/deb/control` |
| Fedora/RHEL | .rpm | `rpmbuild -ba packaging/rpm/vaptvupt.spec` |
| Universal Linux | .AppImage | `packaging/appimage/build-appimage.sh` |
| Sandboxed Linux | .flatpak | `packaging/flatpak/dev.zupt.gui.yml` |
| Windows | .exe installer | `packaging/windows/build-windows.bat` |
| Windows | NSIS .exe | `packaging/windows/zupt-installer.nsi` |
## Credits
- **vaptvupt** v5.2.1 — Cristian Cezar Moisés ([github](https://git.securityops.co/cristiancmoises/vaptvupt))
If no window appears, run the GUI from a terminal and check the display/Wayland
or X11 error. When reporting a problem, include OS and desktop versions, Python
and Qt binding versions, `zupt --version`, and the non-sensitive error
message. Never attach passwords, private keys, tokens, or confidential archives.
## License
AGPL-3.0-or-later
The current GUI source is AGPL-3.0-or-later. Published historical revisions
include MIT notices whose grants remain applicable to the exact material
distributed under them. See `gui/LICENSE-GUI`, the 5.2.2 licensing erratum in
`CHANGELOG.md`, and the repository-level license notices.

23
gui/assets/README.md Normal file
View file

@ -0,0 +1,23 @@
# GUI image assets
These files are runtime data used by the graphical interface and its packaging;
they are not executable code or compiler output.
| File | Purpose |
| --- | --- |
| `zupt-icon.png` | Main 48 px application icon used by the GUI and packaging. |
| `zupt-128.png` | 128 px application icon. |
| `zupt.png` | 256 px application/AppDir icon. |
| `zupt.ico` | Windows application icon container. |
Git provenance: all four files were first added by Cristian Cezar Moisés in
ZUPT repository commit `d4660e6539c8b6eeba81751c018217d978fdd618`; the repository
records no earlier or external origin. Their current Git blobs are byte-for-byte
the same blobs present in that commit. That revision distributed them with MIT
license notices, whose permissions remain available for those exact files.
The current GUI tree also carries AGPL-3.0-or-later notices; apply the license
option appropriate to the exact material and revision being redistributed and
preserve `gui/LICENSE-GUI` and the repository notices. Their historical
filenames are retained because build and desktop-integration files refer to
them.

View file

@ -1,90 +1,130 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
# Install Zupt GUI + desktop integration
# Run: sudo ./install.sh (or ./install.sh --user for per-user install)
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
USER_INSTALL=0
[ "$1" = "--user" ] && USER_INSTALL=1
# Install the integrated ZUPT GUI from the checked-out source tree.
# This script never downloads Python modules or operating-system packages.
if [ "$USER_INSTALL" -eq 1 ]; then
BIN="$HOME/.local/bin"
APPS="$HOME/.local/share/applications"
NEMO="$HOME/.local/share/nemo/actions"
MIME="$HOME/.local/share/mime"
NAUTILUS="$HOME/.local/share/nautilus/scripts"
else
BIN="/usr/local/bin"
APPS="/usr/share/applications"
NEMO="/usr/share/nemo/actions"
MIME="/usr/share/mime"
NAUTILUS=""
set -Eeuo pipefail
die() {
printf 'zupt-gui install: %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: gui/install.sh [OPTIONS]
--user install below $HOME/.local
--prefix DIR installation prefix (default: /usr/local)
--destdir DIR staging root for package builds
--legacy-alias install opt-in vaptvupt-gui compatibility symlink
-h, --help show this help
Python 3.9+ and either PySide6 or PyQt6 must already be installed. The
zupt CLI must also be installed or selected with ZUPT_BIN at runtime.
VAPTVUPT_BIN remains a renamed-era compatibility fallback.
EOF
}
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
repo_root=$(cd -- "$script_dir/.." && pwd -P)
prefix=/usr/local
destdir=${DESTDIR:-}
legacy_alias=0
while (($#)); do
case $1 in
--user)
[[ -n ${HOME:-} ]] || die 'HOME is unset; cannot use --user'
prefix=$HOME/.local
;;
--prefix)
(($# >= 2)) || die '--prefix requires a directory'
prefix=$2
shift
;;
--destdir)
(($# >= 2)) || die '--destdir requires a directory'
destdir=$2
shift
;;
--legacy-alias) legacy_alias=1 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: $1" ;;
esac
shift
done
[[ $prefix == /* ]] || die '--prefix must be an absolute path'
[[ -z $destdir || $destdir == /* ]] || die '--destdir must be an absolute path'
bindir=${BINDIR:-$prefix/bin}
libexecdir=${LIBEXECDIR:-$prefix/lib/zupt-gui}
datadir=${DATADIR:-$prefix/share}
for source_file in \
"$script_dir/src/zupt_gui.py" \
"$script_dir/assets/zupt-icon.png" \
"$script_dir/packaging/zupt-gui.desktop" \
"$repo_root/doc/zupt-gui.1" \
"$repo_root/LICENSE" \
"$repo_root/LICENSE-AGPL-3.0" \
"$script_dir/LICENSE-GUI" \
"$script_dir/assets/README.md"; do
[[ -f $source_file ]] || die "required source file is missing: $source_file"
done
stage_bindir=$destdir$bindir
stage_libexecdir=$destdir$libexecdir
stage_datadir=$destdir$datadir
install -d -- "$stage_bindir" "$stage_libexecdir" \
"$stage_datadir/applications" \
"$stage_datadir/icons/hicolor/256x256/apps" \
"$stage_datadir/man/man1" \
"$stage_datadir/licenses/zupt-gui"
install -m 0644 -- "$script_dir/src/zupt_gui.py" "$stage_libexecdir/zupt_gui.py"
install -m 0644 -- "$script_dir/packaging/zupt-gui.desktop" \
"$stage_datadir/applications/zupt-gui.desktop"
install -m 0644 -- "$script_dir/assets/zupt-icon.png" \
"$stage_datadir/icons/hicolor/256x256/apps/zupt-gui.png"
install -m 0644 -- "$repo_root/doc/zupt-gui.1" \
"$stage_datadir/man/man1/zupt-gui.1"
install -m 0644 -- "$repo_root/LICENSE" \
"$stage_datadir/licenses/zupt-gui/LICENSE"
install -m 0644 -- "$repo_root/LICENSE-AGPL-3.0" \
"$stage_datadir/licenses/zupt-gui/LICENSE-AGPL-3.0"
install -m 0644 -- "$script_dir/LICENSE-GUI" \
"$stage_datadir/licenses/zupt-gui/LICENSE-GUI"
install -m 0644 -- "$script_dir/assets/README.md" \
"$stage_datadir/licenses/zupt-gui/ASSET-PROVENANCE.md"
# Quote the installed module path for a POSIX shell without embedding DESTDIR.
quoted_libexec=${libexecdir//\'/\'\\\'\'}
launcher_tmp=$(mktemp "${TMPDIR:-/tmp}/zupt-gui-launcher.XXXXXXXX")
trap 'rm -f -- "$launcher_tmp"' EXIT HUP INT TERM
cat >"$launcher_tmp" <<EOF
#!/bin/sh
exec python3 '$quoted_libexec/zupt_gui.py' "\$@"
EOF
install -m 0755 -- "$launcher_tmp" "$stage_bindir/zupt-gui"
if ((legacy_alias)); then
ln -s -- zupt-gui "$stage_bindir/vaptvupt-gui"
fi
mkdir -p "$BIN" "$APPS"
# ── Install launcher ──
cat > "$BIN/zupt-gui" << LAUNCHER
#!/bin/bash
DIR="$DIR"
VENV="\$DIR/.venv"
PY="\$VENV/bin/python3"
[ ! -x "\$PY" ] && python3 -m venv "\$VENV" && "\$VENV/bin/pip" install PySide6 -q
exec "\$PY" "\$DIR/src/zupt_gui.py" "\$@"
LAUNCHER
chmod +x "$BIN/zupt-gui"
echo "Installed: $BIN/zupt-gui"
# ── Desktop entry ──
cp "$DIR/packaging/zupt-gui.desktop" "$APPS/"
echo "Installed: $APPS/zupt-gui.desktop"
# ── Nemo actions (Linux Mint / Cinnamon) ──
if [ -d "$(dirname "$NEMO")" ] || [ "$USER_INSTALL" -eq 1 ]; then
mkdir -p "$NEMO"
cp "$DIR/packaging/desktop-integration/nemo/"*.nemo_action "$NEMO/" 2>/dev/null && \
echo "Installed: Nemo right-click actions" || true
fi
# ── Nautilus scripts (GNOME) ──
if [ -n "$NAUTILUS" ]; then
mkdir -p "$NAUTILUS"
cat > "$NAUTILUS/Compress with Zupt" << 'NSCRIPT'
#!/bin/bash
zupt-gui --compress $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
NSCRIPT
chmod +x "$NAUTILUS/Compress with Zupt"
echo "Installed: Nautilus script"
fi
# ── MIME type for .zupt files ──
MIME_XML="$MIME/packages/zupt.xml"
if [ ! -f "$MIME_XML" ]; then
mkdir -p "$(dirname "$MIME_XML")"
cat > "$MIME_XML" << 'MIMEXML'
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-zupt">
<comment>Zupt Archive</comment>
<glob pattern="*.zupt"/>
<icon name="package-x-generic"/>
</mime-type>
</mime-info>
MIMEXML
if command -v update-mime-database >/dev/null; then
update-mime-database "$MIME" 2>/dev/null
if [[ -z $destdir ]]; then
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$datadir/applications" >/dev/null 2>&1 || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q "$datadir/icons/hicolor" >/dev/null 2>&1 || true
fi
echo "Registered: .zupt MIME type"
fi
# ── Associate .zupt files with zupt-gui ──
if command -v xdg-mime >/dev/null; then
xdg-mime default zupt-gui.desktop application/x-zupt 2>/dev/null
echo "Associated: .zupt files open with Zupt GUI"
printf 'Installed zupt-gui below %s%s\n' "$destdir" "$prefix"
if ((!legacy_alias)); then
printf 'Legacy vaptvupt-gui alias was not installed (use --legacy-alias to opt in).\n'
fi
echo ""
echo "Done. Right-click any file in your file manager to see Zupt options."
echo "Double-click any .zupt file to open it in Zupt GUI."

View file

@ -1,51 +1,7 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
# Build Zupt GUI AppImage
# Requires: appimagetool, python3, pip
set -e
APP="zupt-gui"
VERSION="1.0.0"
APPDIR="${APP}.AppDir"
rm -rf "$APPDIR" "${APP}-${VERSION}-x86_64.AppImage"
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/zupt-gui" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps"
# Install Python + deps into AppDir
python3 -m venv "$APPDIR/usr/python"
"$APPDIR/usr/python/bin/pip" install PySide6 --quiet
# Copy app
cp ../../src/zupt_gui.py "$APPDIR/usr/share/zupt-gui/"
cp ../../assets/zupt.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" 2>/dev/null || true
# Create launcher
cat > "$APPDIR/AppRun" << 'APPRUN'
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
export PATH="$HERE/usr/bin:$HERE/usr/python/bin:$PATH"
exec python3 "$HERE/usr/share/zupt-gui/zupt_gui.py" "$@"
APPRUN
chmod +x "$APPDIR/AppRun"
# Desktop file
cat > "$APPDIR/${APP}.desktop" << DESKTOP
[Desktop Entry]
Type=Application
Name=Zupt GUI
Comment=Post-Quantum Backup Utility
Exec=zupt-gui
Icon=zupt-gui
Categories=Utility;Archiving;Security;
Terminal=false
DESKTOP
# Build AppImage
if command -v appimagetool >/dev/null; then
ARCH=x86_64 appimagetool "$APPDIR" "${APP}-${VERSION}-x86_64.AppImage"
echo "Built: ${APP}-${VERSION}-x86_64.AppImage"
else
echo "appimagetool not found. Install from https://github.com/AppImage/AppImageKit"
echo "AppDir ready at: $APPDIR/"
fi
# Compatibility entry point for the canonical source-only GUI builder.
set -Eeuo pipefail
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd -P)
exec "$repo_root/packaging/build-gui-appimage.sh" "$@"

View file

@ -1,94 +1,7 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
# Build zupt-gui .deb package
set -e
cd "$(dirname "$0")/.."
VERSION="${VERSION:-2.2.1}"
ARCH="all"
PKG="zupt-gui_${VERSION}_${ARCH}"
ROOT="/tmp/$PKG"
rm -rf "$ROOT"
mkdir -p "$ROOT/DEBIAN" \
"$ROOT/usr/bin" \
"$ROOT/usr/lib/python3/dist-packages" \
"$ROOT/usr/share/applications" \
"$ROOT/usr/share/icons/hicolor/256x256/apps" \
"$ROOT/usr/share/doc/zupt-gui"
# Python module
install -m 644 src/zupt_gui.py "$ROOT/usr/lib/python3/dist-packages/zupt_gui.py"
# Launcher
cat > "$ROOT/usr/bin/zupt-gui" <<'LAUNCH'
#!/usr/bin/env python3
import sys
sys.path.insert(0, "/usr/lib/python3/dist-packages")
from zupt_gui import main
sys.exit(main())
LAUNCH
chmod +x "$ROOT/usr/bin/zupt-gui"
# Desktop file
install -m 644 packaging/zupt-gui.desktop "$ROOT/usr/share/applications/" 2>/dev/null || cat > "$ROOT/usr/share/applications/zupt-gui.desktop" <<'DESK'
[Desktop Entry]
Name=Zupt GUI
GenericName=Post-Quantum Backup Utility
Comment=Compress and encrypt files with hybrid PQ crypto
Exec=zupt-gui
Terminal=false
Type=Application
Categories=Utility;Archiving;Security;
Icon=zupt-gui
DESK
# Icon (use a real one if assets exist)
if [ -f assets/zupt-256.png ]; then
install -m 644 assets/zupt-256.png "$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
elif [ -d ../assets ] && [ -f ../assets/zupt-256.png ]; then
install -m 644 ../assets/zupt-256.png "$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
else
# 1×1 placeholder
printf '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xfc\xcf\xc0\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82' > "$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
fi
# Docs
install -m 644 README.md "$ROOT/usr/share/doc/zupt-gui/"
gzip -9n -c ../CHANGELOG.md > "$ROOT/usr/share/doc/zupt-gui/changelog.gz" 2>/dev/null || true
cat > "$ROOT/usr/share/doc/zupt-gui/copyright" <<'COPYRIGHT'
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: zupt-gui
Upstream-Contact: Cristian Cezar Moisés <zupt@riseup.net>
Source: https://git.securityops.co/cristiancmoises/zupt
Files: *
Copyright: 2025-2026 Cristian Cezar Moisés
License: AGPL-3.0+
COPYRIGHT
INSTALLED_SIZE=$(du -sk "$ROOT" | cut -f1)
cat > "$ROOT/DEBIAN/control" <<EOF
Package: zupt-gui
Version: $VERSION
Section: utils
Priority: optional
Architecture: $ARCH
Depends: python3 (>= 3.9), python3-pyside6, zupt (>= 2.2.0)
Maintainer: Cristian Cezar Moisés <zupt@riseup.net>
Installed-Size: $INSTALLED_SIZE
Homepage: https://git.securityops.co/cristiancmoises/zupt
Description: Zupt GUI — Post-Quantum Backup Utility
Cross-platform graphical interface for the zupt backup compression
utility with post-quantum hybrid encryption (ML-KEM-768 + X25519).
.
v2.2+ uses libzuptsdk under the hood for HKDF-SHA3 hybrid combiner,
32-byte key commitment, HPKE binding (RFC 9180), and anti-fault
double-decapsulation. Supports legacy archives via auto-detection.
EOF
dpkg-deb --build --root-owner-group "$ROOT" "/tmp/$PKG.deb"
echo "Built: /tmp/$PKG.deb"
dpkg-deb --info "/tmp/$PKG.deb" | head -12
# Compatibility entry point for the canonical source-only GUI builder.
set -Eeuo pipefail
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)
exec "$repo_root/packaging/build-gui-deb.sh" "$@"

View file

@ -1,13 +1,12 @@
Package: zupt-gui
Version: 1.0.0
Version: 5.2.2
Section: utils
Priority: optional
Architecture: all
Depends: python3 (>= 3.9), python3-pyside6, zupt (>= 2.1.6)
Maintainer: Cristian Cezar Moises <cristian@zupt.dev>
Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.2)
Maintainer: Cristian Cezar Moisés <sac@securityops.co>
Homepage: https://github.com/cristiancmoises/zupt
Description: Zupt GUI — Post-Quantum Backup Utility
Cross-platform graphical interface for the zupt backup compression
utility with post-quantum hybrid encryption (ML-KEM-768 + X25519),
hardware-adaptive codecs, block-level deduplication, and full-disk
backup/restore support.
Description: Qt graphical interface for the ZUPT backup utility
The GUI creates, inspects, verifies, and extracts .zupt archives through the
separately packaged ZUPT command. Native post-quantum modes are available
in the baseline build; SDK and PQ-box controls follow CLI capability detection.

View file

@ -1,7 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[Nemo Action]
Name=Compress with Zupt
Comment=Create encrypted .zupt archive
Name=Compress with ZUPT
Comment=Create a .zupt archive with ZUPT GUI
Exec=zupt-gui --compress %F
Icon-Name=package-x-generic
Icon-Name=zupt-gui
Selection=Any
Extensions=any;

View file

@ -1,7 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[Nemo Action]
Name=Extract with Zupt
Comment=Decrypt and extract .zupt archive
Name=Extract with ZUPT
Comment=Extract a .zupt archive with ZUPT GUI
Exec=zupt-gui --extract %F
Icon-Name=package-x-generic
Icon-Name=zupt-gui
Selection=S
Extensions=zupt;

View file

@ -1,39 +1,42 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2026 Cristian Cezar Moisés
# Offline build manifest for the integrated source tree. The Qt/Python base and
# SDK are resolved by Flatpak before the build; no command below uses pip or
# downloads project dependencies.
app-id: dev.zupt.gui
runtime: org.freedesktop.Platform
runtime-version: '24.08'
sdk: org.freedesktop.Sdk
runtime: org.kde.Platform
runtime-version: '6.8'
sdk: org.kde.Sdk
base: io.qt.PySide.BaseApp
base-version: '6.8'
command: zupt-gui
finish-args:
- --share=ipc
- --socket=x11
- --socket=fallback-x11
- --socket=wayland
- --filesystem=home
- --device=all # For disk backup (block devices)
modules:
- name: python3-pyside6
buildsystem: simple
build-commands:
- pip3 install --prefix=/app PySide6
- name: zupt
buildsystem: simple
build-commands:
- make
- install -Dm755 zupt /app/bin/zupt
sources:
- type: git
url: https://git.securityops.co/cristiancmoises/zupt
tag: v2.1.6
- name: zupt-gui
buildsystem: simple
build-commands:
- install -Dm755 src/zupt_gui.py /app/bin/zupt-gui
- install -Dm644 packaging/zupt-gui.desktop /app/share/applications/dev.zupt.gui.desktop
- make -j${FLATPAK_BUILDER_N_JOBS} WITH_SDK=0 WITH_PQBOX=0
- make WITH_SDK=0 WITH_PQBOX=0 check
- make PREFIX=/app WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 install
- install -Dm644 gui/src/zupt_gui.py /app/bin/zupt_gui.py
- install -Dm755 packaging/portable/zupt-gui.sh /app/bin/zupt-gui
- install -Dm644 gui/packaging/zupt-gui.desktop /app/share/applications/dev.zupt.gui.desktop
- sed -i 's/^Icon=.*/Icon=dev.zupt.gui/' /app/share/applications/dev.zupt.gui.desktop
- install -Dm644 gui/assets/zupt-icon.png /app/share/icons/hicolor/256x256/apps/dev.zupt.gui.png
- install -Dm644 doc/zupt-gui.1 /app/share/man/man1/zupt-gui.1
- install -d /app/share/licenses/zupt
- install -m 0644 LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE THIRD-PARTY-NOTICES.md /app/share/licenses/zupt/
- install -d /app/share/licenses/zupt-gui
- install -m 0644 LICENSE-AGPL-3.0 /app/share/licenses/zupt-gui/LICENSE-AGPL-3.0
- install -m 0644 gui/LICENSE-GUI /app/share/licenses/zupt-gui/LICENSE-GUI
- install -m 0644 gui/assets/README.md /app/share/licenses/zupt-gui/ASSET-PROVENANCE.md
sources:
- type: dir
path: .
path: ../../..

View file

@ -1,92 +1,102 @@
@echo off
REM ══════════════════════════════════════════════════
REM Zupt GUI — Windows Build Script
REM Creates: ZuptGUI-2.1.6-Setup.exe
REM
REM Prerequisites:
REM 1. Python 3.9+ (python.org)
REM 2. NSIS 3.x (nsis.sourceforge.io)
REM 3. zupt.exe (compiled zupt CLI binary for Windows)
REM
REM Usage:
REM cd packaging\windows
REM build-windows.bat
REM ══════════════════════════════════════════════════
setlocal
rem SPDX-License-Identifier: AGPL-3.0-or-later
rem Build the ZUPT GUI installer without downloading dependencies.
rem
rem Prerequisites must already be installed: Python 3.9+, PySide6, PyInstaller,
rem Inno Setup 6, and a source-built/tested zupt.exe. The CLI path can be
rem selected with ZUPT_CLI_EXE. Final output goes to ZUPT_DIST_DIR,
rem which defaults to a directory below %%TEMP%% (outside the Git checkout).
rem ZUPT_WINDOWS_RUNTIME_NOTICES_DIR is mandatory and must contain the
rem license/notices for the exact Python, PyInstaller, Qt and PySide/PyQt
rem runtime files embedded by this local build.
echo.
echo Zupt GUI — Windows Build
echo ════════════════════════
echo.
setlocal EnableExtensions
for %%I in ("%~dp0\..\..\..") do set "REPO_ROOT=%%~fI"
set "VERSION=%~1"
if not defined VERSION set "VERSION=5.2.2"
if not defined ZUPT_DIST_DIR set "ZUPT_DIST_DIR=%TEMP%\zupt-release"
if not defined ZUPT_CLI_EXE set "ZUPT_CLI_EXE=%REPO_ROOT%\zupt.exe"
set "WORK=%TEMP%\zupt-gui-build-%RANDOM%-%RANDOM%"
set "RC=1"
REM ── Step 1: Install Python deps ──
echo [1/4] Installing dependencies...
pip install PySide6 pyinstaller --quiet --upgrade
if errorlevel 1 (
echo ERROR: pip install failed. Is Python in PATH?
pause
exit /b 1
where pyinstaller >nul 2>nul || (
echo ERROR: PyInstaller is required and is not downloaded by this script.>&2
goto :cleanup
)
where ISCC.exe >nul 2>nul || (
echo ERROR: Inno Setup 6 ISCC.exe is required.>&2
goto :cleanup
)
if not exist "%ZUPT_CLI_EXE%" (
echo ERROR: source-built CLI not found: %ZUPT_CLI_EXE%>&2
goto :cleanup
)
if not defined ZUPT_WINDOWS_RUNTIME_NOTICES_DIR (
echo ERROR: set ZUPT_WINDOWS_RUNTIME_NOTICES_DIR for the exact bundled runtime.>&2
goto :cleanup
)
if not exist "%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%\MANIFEST.txt" (
echo ERROR: runtime notice directory must contain MANIFEST.txt.>&2
goto :cleanup
)
for %%I in ("%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%\MANIFEST.txt") do if %%~zI LEQ 0 (
echo ERROR: runtime notice MANIFEST.txt must not be empty.>&2
goto :cleanup
)
for %%N in (PYTHON-NOTICE.txt PYINSTALLER-NOTICE.txt QT-NOTICE.txt) do (
if not exist "%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%\%%N" (
echo ERROR: runtime notice directory is missing %%N.>&2
goto :cleanup
)
for %%I in ("%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%\%%N") do if %%~zI LEQ 0 (
echo ERROR: runtime notice %%N must not be empty.>&2
goto :cleanup
)
)
set "QT_BINDING_NOTICE_FOUND="
for %%N in (PYSIDE6-NOTICE.txt PYQT6-NOTICE.txt) do if exist "%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%\%%N" (
for %%I in ("%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%\%%N") do if %%~zI GTR 0 set "QT_BINDING_NOTICE_FOUND=1"
)
if not defined QT_BINDING_NOTICE_FOUND (
echo ERROR: runtime notices need non-empty PYSIDE6-NOTICE.txt or PYQT6-NOTICE.txt.>&2
goto :cleanup
)
REM ── Step 2: Build .exe with PyInstaller ──
echo [2/4] Building ZuptGUI.exe...
if exist dist rmdir /s /q dist
if exist build rmdir /s /q build
mkdir "%WORK%" || goto :cleanup
if not exist "%ZUPT_DIST_DIR%" mkdir "%ZUPT_DIST_DIR%" || goto :cleanup
pyinstaller --onefile --windowed ^
--name "ZuptGUI" ^
--icon "..\..\assets\zupt.ico" ^
--add-data "..\..\assets\zupt.ico;assets" ^
--add-data "..\..\assets\zupt.png;assets" ^
"..\..\src\zupt_gui.py"
if not exist "dist\ZuptGUI.exe" (
echo ERROR: PyInstaller build failed.
pause
exit /b 1
"%ZUPT_CLI_EXE%" version >"%WORK%\cli-version.txt" 2>&1 || goto :cleanup
findstr /b /c:"zupt %VERSION%" "%WORK%\cli-version.txt" >nul || (
echo ERROR: CLI version does not match %VERSION%.>&2
goto :cleanup
)
echo Built: dist\ZuptGUI.exe
"%ZUPT_CLI_EXE%" help >nul 2>&1 || goto :cleanup
REM ── Step 3: Check for zupt.exe ──
echo [3/4] Checking for zupt.exe...
if not exist "zupt.exe" (
echo.
echo WARNING: zupt.exe not found in this directory.
echo The installer needs zupt.exe to bundle the CLI tool.
echo Options:
echo a) Copy zupt.exe here and re-run this script
echo b) Build zupt from source with MSYS2/MinGW:
echo pacman -S mingw-w64-x86_64-gcc make
echo cd zupt-2.1.6 ^&^& make
echo cp zupt.exe packaging/windows/
echo.
)
pyinstaller --noconfirm --clean --onefile --windowed ^
--name zupt-gui ^
--icon "%REPO_ROOT%\gui\assets\zupt.ico" ^
--add-data "%REPO_ROOT%\gui\assets\zupt.ico;assets" ^
--add-data "%REPO_ROOT%\gui\assets\zupt-icon.png;assets" ^
--distpath "%WORK%\dist" ^
--workpath "%WORK%\build" ^
--specpath "%WORK%" ^
"%REPO_ROOT%\gui\src\zupt_gui.py" || goto :cleanup
REM ── Step 4: Build NSIS installer ──
echo [4/4] Building installer...
where makensis >nul 2>&1
if errorlevel 1 (
echo.
echo NSIS not found. Install from: https://nsis.sourceforge.io
echo Then run: makensis zupt-installer.nsi
echo.
echo Standalone exe ready at: dist\ZuptGUI.exe
pause
exit /b 0
)
set "GUI_EXE=%WORK%\dist\zupt-gui.exe"
if not exist "%GUI_EXE%" goto :cleanup
set "ZUPT_BIN=%ZUPT_CLI_EXE%"
"%GUI_EXE%" --version >"%WORK%\gui-version.txt" 2>&1 || goto :cleanup
findstr /b /c:"zupt-gui %VERSION%" "%WORK%\gui-version.txt" >nul || goto :cleanup
makensis zupt-installer.nsi
if errorlevel 1 (
echo ERROR: NSIS build failed.
pause
exit /b 1
)
ISCC.exe "/DAppVersion=%VERSION%" "/DGuiExecutable=%GUI_EXE%" ^
"/DCliExecutable=%ZUPT_CLI_EXE%" "/DBuildOutputDir=%ZUPT_DIST_DIR%" ^
"/DRuntimeNoticesDir=%ZUPT_WINDOWS_RUNTIME_NOTICES_DIR%" ^
"%REPO_ROOT%\packaging\windows\zupt-gui.iss" || goto :cleanup
echo.
echo ════════════════════════════════════════════
echo Build complete!
echo Standalone: dist\ZuptGUI.exe
echo Installer: ZuptGUI-2.1.6-Setup.exe
echo ════════════════════════════════════════════
echo.
pause
if not exist "%ZUPT_DIST_DIR%\ZUPT-Setup-%VERSION%.exe" goto :cleanup
echo PASS: built %ZUPT_DIST_DIR%\ZUPT-Setup-%VERSION%.exe
set "RC=0"
:cleanup
if exist "%WORK%" rmdir /s /q "%WORK%"
endlocal & exit /b %RC%

View file

@ -1,132 +0,0 @@
; Zupt GUI NSIS Installer Script
; Builds: ZuptGUI-Setup.exe
;
; Prerequisites on the build machine:
; 1. NSIS 3.x installed (https://nsis.sourceforge.io)
; 2. Run build-windows.bat first to create dist/ZuptGUI.exe
; 3. Place zupt.exe in this directory
; 4. Then: makensis zupt-installer.nsi
!include "MUI2.nsh"
!include "FileFunc.nsh"
; Config
!define APPNAME "Zupt"
!define APPVERSION "2.1.6"
!define GUIVERSION "1.0.0"
!define PUBLISHER "Cristian Cezar Moises"
!define HELPURL "https://github.com/cristiancmoises/zupt"
!define EXE "ZuptGUI.exe"
!define CLI "zupt.exe"
Name "${APPNAME} ${APPVERSION}"
OutFile "ZuptGUI-${APPVERSION}-Setup.exe"
InstallDir "$PROGRAMFILES\${APPNAME}"
InstallDirRegKey HKLM "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel admin
; UI
!define MUI_ICON "..\..\assets\zupt.ico"
!define MUI_UNICON "..\..\assets\zupt.ico"
!define MUI_ABORTWARNING
!define MUI_WELCOMEPAGE_TITLE "Install ${APPNAME} ${APPVERSION}"
!define MUI_WELCOMEPAGE_TEXT "Post-quantum backup compression with ML-KEM-768 + X25519 hybrid encryption.$\r$\n$\r$\nThis will install the Zupt GUI and CLI tools."
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "..\..\LICENSE"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; Install
Section "Install"
SetOutPath $INSTDIR
; Copy files
File "dist\${EXE}"
File "${CLI}"
File "..\..\assets\zupt.ico"
File "..\..\LICENSE"
File "..\..\README.md"
; Write uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
; Start Menu
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\${EXE}" "" "$INSTDIR\zupt.ico"
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
; Desktop shortcut
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\${EXE}" "" "$INSTDIR\zupt.ico"
; Add to PATH (so zupt.exe is available system-wide)
EnVar::AddValue "PATH" "$INSTDIR"
; Register .zupt file association
WriteRegStr HKCR ".zupt" "" "ZuptArchive"
WriteRegStr HKCR "ZuptArchive" "" "Zupt Archive"
WriteRegStr HKCR "ZuptArchive\DefaultIcon" "" "$INSTDIR\zupt.ico"
WriteRegStr HKCR "ZuptArchive\shell\open\command" "" '"$INSTDIR\${EXE}" --extract "%1"'
WriteRegStr HKCR "ZuptArchive\shell\verify\command" "" '"$INSTDIR\${CLI}" test "%1"'
WriteRegStr HKCR "ZuptArchive\shell\verify" "" "Verify Integrity"
; Right-click "Compress with Zupt" on any file
WriteRegStr HKCR "*\shell\ZuptCompress" "" "Compress with Zupt"
WriteRegStr HKCR "*\shell\ZuptCompress\Icon" "" "$INSTDIR\zupt.ico"
WriteRegStr HKCR "*\shell\ZuptCompress\command" "" '"$INSTDIR\${EXE}" --compress "%1"'
; Right-click on directories
WriteRegStr HKCR "Directory\shell\ZuptCompress" "" "Compress with Zupt"
WriteRegStr HKCR "Directory\shell\ZuptCompress\Icon" "" "$INSTDIR\zupt.ico"
WriteRegStr HKCR "Directory\shell\ZuptCompress\command" "" '"$INSTDIR\${EXE}" --compress "%1"'
; Add/Remove Programs entry
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayName" "${APPNAME} — Post-Quantum Backup"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "UninstallString" "$INSTDIR\Uninstall.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayIcon" "$INSTDIR\zupt.ico"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "Publisher" "${PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayVersion" "${APPVERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "URLInfoAbout" "${HELPURL}"
; Calculate installed size
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "EstimatedSize" "$0"
SectionEnd
; Uninstall
Section "Uninstall"
; Remove files
Delete "$INSTDIR\${EXE}"
Delete "$INSTDIR\${CLI}"
Delete "$INSTDIR\zupt.ico"
Delete "$INSTDIR\LICENSE"
Delete "$INSTDIR\README.md"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"
; Remove shortcuts
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk"
Delete "$SMPROGRAMS\${APPNAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
Delete "$DESKTOP\${APPNAME}.lnk"
; Remove from PATH
EnVar::DeleteValue "PATH" "$INSTDIR"
; Remove file associations
DeleteRegKey HKCR ".zupt"
DeleteRegKey HKCR "ZuptArchive"
DeleteRegKey HKCR "*\shell\ZuptCompress"
DeleteRegKey HKCR "Directory\shell\ZuptCompress"
; Remove Add/Remove Programs entry
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
DeleteRegKey HKLM "Software\${APPNAME}"
SectionEnd

View file

@ -1,12 +1,13 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[Desktop Entry]
Type=Application
Name=Zupt GUI
GenericName=Post-Quantum Backup
Comment=Compress, encrypt, and backup with quantum-resistant cryptography
Exec=zupt-gui
Name=ZUPT GUI
GenericName=Backup and Compression Utility
Comment=Create, inspect, verify, and extract ZUPT archives
Exec=zupt-gui %f
Icon=zupt-gui
Categories=Utility;Archiving;Security;
Keywords=backup;compress;encrypt;quantum;zupt;
Categories=Utility;Archiving;Compression;
Keywords=backup;archive;compression;encryption;post-quantum;zupt;
Terminal=false
StartupNotify=true
MimeType=application/x-zupt;

View file

@ -1,34 +0,0 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
from setuptools import setup, find_packages
setup(
name="zupt-gui",
version="1.1.1",
description="Zupt GUI — Cross-Platform Post-Quantum Backup Utility",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Cristian Cezar Moisés",
url="https://git.securityops.co/cristiancmoises/zupt",
license="AGPL-3.0-or-later",
packages=find_packages(where="src"),
package_dir={"": "src"},
py_modules=["zupt_gui"],
python_requires=">=3.9",
install_requires=["PySide6>=6.5"],
entry_points={
"console_scripts": ["zupt-gui=zupt_gui:main"],
"gui_scripts": ["zupt-gui=zupt_gui:main"],
},
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: Qt",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Topic :: Security :: Cryptography",
"Topic :: System :: Archiving :: Compression",
],
)

View file

@ -1,10 +1,10 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
"""VaptVupt GUI — Cross-Platform Post-Quantum Backup.
"""ZUPT GUI — Cross-platform post-quantum backup.
Renamed from "Zupt" in v3.0.0 due to INPI Brasil trademark.
The .zupt file extension is preserved.
The original ZUPT product name was restored in 5.2.2. The .zupt archive
extension, format, codec identifiers, and compatibility remain unchanged.
Tries PySide6 first (preferred), falls back to PyQt6 if PySide6 is
not installed. PyQt6 is the default available package on Debian/Ubuntu
@ -40,32 +40,32 @@ except ImportError:
except ImportError:
if sys.stderr is not None: # None under PyInstaller --windowed
sys.stderr.write(
"ERROR: vaptvupt-gui requires PySide6 or PyQt6. Install one of:\n"
"ERROR: zupt-gui requires PySide6 or PyQt6. Install one of:\n"
" Debian/Ubuntu: sudo apt install python3-pyqt6\n"
" Fedora/RHEL: sudo dnf install python3-pyqt6\n"
" pip (any OS): pip install PySide6\n"
)
sys.exit(1)
# ── Find vaptvupt binary ──
# ── Find the ZUPT binary ──
#
# v3.0.0 rename: the binary is now `vaptvupt`; older installations
# (1.x/2.x) ship `zupt`. We try the new name first, fall back to the
# old name, and on every candidate verify it's actually executable
# ZUPT 5.2.2 restores `zupt` as the primary command. Renamed-era
# installations may still provide `vaptvupt`, so discovery accepts it as a
# compatibility fallback. Every candidate is verified as actually executable
# (not just present). After picking a candidate, we run a quick
# `version` liveness check — this catches the case where the binary
# exists but can't load its shared library (the original bug report:
# "GUI doesn't find zupt; copying to /usr/local/bin fixes it").
#
# Diagnostic output goes to stderr so users can `vaptvupt-gui 2>log`
# Diagnostic output goes to stderr so users can `zupt-gui 2>log`
# to see exactly which path was tried and why each failed.
_DISCOVERY_LOG = []
def _discovery_log(msg):
_DISCOVERY_LOG.append(msg)
# Echo to stderr if VAPTVUPT_DEBUG or ZUPT_DEBUG is set
if ((os.environ.get("VAPTVUPT_DEBUG") or os.environ.get("ZUPT_DEBUG"))
# Echo to stderr if ZUPT_DEBUG or its renamed-era alias is set.
if ((os.environ.get("ZUPT_DEBUG") or os.environ.get("VAPTVUPT_DEBUG"))
and sys.stderr is not None): # None under PyInstaller --windowed
sys.stderr.write(f" [discovery] {msg}\n")
@ -91,9 +91,9 @@ def _is_runnable(path):
except OSError as e:
return False, f"OSError: {e}"
def _find_vaptvupt():
def _find_zupt():
# 1. Explicit env override
for env in ("VAPTVUPT_BIN", "ZUPT_BIN"):
for env in ("ZUPT_BIN", "VAPTVUPT_BIN"):
p = os.environ.get(env)
if p:
ok, reason = _is_runnable(p)
@ -102,18 +102,18 @@ def _find_vaptvupt():
return p
# 2. Local project tree (running from a source checkout)
# Try BOTH names (vaptvupt is v3.0.0+, zupt is legacy).
# Prefer the canonical name, then the renamed-era compatibility name.
here = Path(getattr(sys, "_MEIPASS", Path(__file__).parent))
for parent in (here.parent.parent, here.parent, here):
for name in ("vaptvupt", "zupt", "vaptvupt.exe", "zupt.exe"):
for name in ("zupt", "vaptvupt", "zupt.exe", "vaptvupt.exe"):
c = parent / name
ok, reason = _is_runnable(c)
_discovery_log(f"local {c}: {reason}")
if ok:
return str(c.resolve())
# 3. System PATH — try new name first, then legacy
for name in ("vaptvupt", "zupt"):
# 3. System PATH — try the canonical name first, then compatibility.
for name in ("zupt", "vaptvupt"):
found = shutil.which(name)
if found:
ok, reason = _is_runnable(found)
@ -127,17 +127,17 @@ def _find_vaptvupt():
# from a desktop session with a minimal PATH that omits /usr/bin"
# scenario reported against v2.4.8.
common = [
# New name (v3.0.0+)
"/usr/local/bin/vaptvupt", "/usr/bin/vaptvupt",
"/opt/vaptvupt/bin/vaptvupt", "/opt/homebrew/bin/vaptvupt",
# Legacy name (1.x/2.x)
# Canonical name
"/usr/local/bin/zupt", "/usr/bin/zupt",
"/opt/zupt/bin/zupt", "/opt/homebrew/bin/zupt",
# Renamed-era compatibility name
"/usr/local/bin/vaptvupt", "/usr/bin/vaptvupt",
"/opt/vaptvupt/bin/vaptvupt", "/opt/homebrew/bin/vaptvupt",
# Termux (Android) install path
"/data/data/com.termux/files/usr/bin/vaptvupt",
"/data/data/com.termux/files/usr/bin/zupt",
"/data/data/com.termux/files/usr/bin/vaptvupt",
# Flatpak sandbox runtime path
"/app/bin/vaptvupt", "/app/bin/zupt",
"/app/bin/zupt", "/app/bin/vaptvupt",
]
for path in common:
ok, reason = _is_runnable(path)
@ -145,15 +145,13 @@ def _find_vaptvupt():
if ok:
return path
# 5. Last resort — return "vaptvupt" and let exec fail loudly later.
# 5. Last resort — return "zupt" and let exec fail loudly later.
# A caller-visible error is better than silently returning a path
# that doesn't work.
_discovery_log("FAILED: no runnable vaptvupt/zupt binary found")
return "vaptvupt"
_discovery_log("FAILED: no runnable zupt/vaptvupt binary found")
return "zupt"
# Backward-compat: code elsewhere in this file still uses `ZUPT`.
VAPTVUPT = _find_vaptvupt()
ZUPT = VAPTVUPT # legacy alias used throughout the rest of zupt_gui.py
ZUPT_CLI = _find_zupt()
# ── Query version ONCE at import (cached) ──
#
@ -174,11 +172,11 @@ ZUPT = VAPTVUPT # legacy alias used throughout the rest of zupt_gui.py
_VERSION_RE = re.compile(r'^(?:vaptvupt|zupt)\s+(\d+\.\d+\.\d+(?:[._A-Za-z0-9-]*)?)')
def _get_version():
short = "vaptvupt (not found)"
short = "zupt (not found)"
number = "?"
full = ""
try:
r = subprocess.run([VAPTVUPT, "version"], capture_output=True, stdin=subprocess.DEVNULL, text=True, timeout=5)
r = subprocess.run([ZUPT_CLI, "version"], capture_output=True, stdin=subprocess.DEVNULL, text=True, timeout=5)
if r.returncode == 0:
full = r.stdout.strip()
lines = full.split("\n")
@ -194,24 +192,30 @@ 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 libvuptsdk-backed modes (Argon2id
# KDF, --pq-sdk, --pq-box) are absent and fail with exit 1. Offering them in
# The default build is SOURCE-ONLY: the system-lib-backed modes (Argon2id,
# --pq-sdk and --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)
# - SDK_AVAILABLE : --pq-sdk / Argon2id compiled in (WITH_SDK=1)
# - PQBOX_AVAILABLE: --pq-box compiled in (WITH_PQBOX=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+);
# The `version` banner carries a machine-readable "Build integrations:" line;
# 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
pqbox = False
pqonly = False
default_kdf = "PBKDF2-SHA256"
blob = ZUPT_VER_FULL or ""
for line in blob.splitlines():
low = line.lower()
if low.startswith("build:"):
if low.startswith("build integrations:"):
sdk = "libvuptsdk=enabled" in low
pqbox = "libpqvaptvupt=enabled" in low
elif low.startswith("build:"):
# Compatibility with the pre-5.2.2 combined build banner.
sdk = ("full" in low) and ("vuptsdk" in low)
elif low.startswith("kdf:"):
default_kdf = "Argon2id" if "argon2id (default)" in low else "PBKDF2-SHA256"
@ -219,15 +223,15 @@ def _get_caps():
pqonly = True
if not pqonly:
try:
h = subprocess.run([VAPTVUPT, "help"], capture_output=True, stdin=subprocess.DEVNULL, text=True, timeout=5)
h = subprocess.run([ZUPT_CLI, "help"], capture_output=True, stdin=subprocess.DEVNULL, 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
return sdk, pqbox, pqonly, default_kdf
SDK_AVAILABLE, PQONLY_AVAILABLE, DEFAULT_KDF = _get_caps()
SDK_AVAILABLE, PQBOX_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)
@ -242,6 +246,8 @@ def pq_mode_options(include_auto=False):
opts.append(("Full PQ — ML-KEM-768 only", "pqonly"))
if SDK_AVAILABLE:
opts.append(("SDK v2 — HKDF + commitment + HPKE", "sdk"))
if PQBOX_AVAILABLE:
opts.append(("PQ sealed box — system libpqvaptvupt", "box"))
return opts
# token -> (extra keygen flags, encrypt/decrypt flag)
@ -249,12 +255,13 @@ _PQ_FLAG = {
"pq": ([], "--pq"),
"pqonly": (["--pq-only"], "--pq-only"),
"sdk": (["--sdk"], "--pq-sdk"),
"box": (["--box"], "--pq-box"),
}
def _archive_info_text(archive):
"""Return the `info` output for an archive (no password/key needed), or ""."""
try:
r = subprocess.run([VAPTVUPT, "info", archive], capture_output=True,
r = subprocess.run([ZUPT_CLI, "info", archive], capture_output=True,
stdin=subprocess.DEVNULL, text=True, timeout=15)
return (r.stdout or "") + (r.stderr or "")
except Exception:
@ -263,6 +270,8 @@ def _archive_info_text(archive):
def _detect_archive_pq(archive):
"""Inspect an archive's `info` and return the matching PQ token, or None."""
low = _archive_info_text(archive).lower()
if "pq box" in low or "pq-box" in low or "sealed box" in low or "sealed-box" in low:
return "box"
if "ml-kem-768 only" in low or "no classical" in low:
return "pqonly"
if "sdk v2" in low or "hpke" in low:
@ -274,7 +283,7 @@ def _detect_archive_pq(archive):
def _detect_archive_enc(archive):
"""Detect how an archive is protected, reading only its header (`info`, no
credential). Returns (kind, human_label):
kind: "none" | "password" | "pq" | "pqonly" | "sdk" | "unknown"
kind: "none" | "password" | "pq" | "pqonly" | "sdk" | "box" | "unknown"
Used to guide the user (which credential to supply) and to pick the right
decrypt flag automatically instead of relying on a mode dropdown."""
txt = _archive_info_text(archive)
@ -289,6 +298,8 @@ def _detect_archive_enc(archive):
break
if encrypted is False:
return "none", "not encrypted"
if "pq box" in low or "pq-box" in low or "sealed box" in low or "sealed-box" in low:
return "box", "PQ sealed box (system libpqvaptvupt)"
if "ml-kem-768 only" in low or "no classical" in low:
return "pqonly", "full post-quantum (ML-KEM-768)"
if "sdk v2" in low or "hpke" in low:
@ -362,10 +373,10 @@ QFrame#sep { background: #1a2a30; max-height: 1px; }
def run_zupt(args, timeout=30):
try:
r = subprocess.run([ZUPT]+list(args), capture_output=True, text=True,
r = subprocess.run([ZUPT_CLI]+list(args), capture_output=True, text=True,
stdin=subprocess.DEVNULL, timeout=timeout)
return r.returncode, r.stdout, r.stderr
except FileNotFoundError: return -1, "", f"vaptvupt not found: {VAPTVUPT}\n\nDiscovery log:\n" + "\n".join(_DISCOVERY_LOG[-10:])
except FileNotFoundError: return -1, "", f"zupt not found: {ZUPT_CLI}\n\nDiscovery log:\n" + "\n".join(_DISCOVERY_LOG[-10:])
except subprocess.TimeoutExpired: return -1, "", "Timed out"
class Worker(QObject):
@ -382,7 +393,7 @@ class Worker(QObject):
def __init__(self, args):
super().__init__(); self.args = args; self.proc = None; self._cancelled = False
def run(self):
self.log.emit(f"$ {Path(VAPTVUPT).name} {' '.join(self.args)}")
self.log.emit(f"$ {Path(ZUPT_CLI).name} {' '.join(self.args)}")
try:
# stdin=DEVNULL: the CLI prompts on a terminal for some inputs
# (e.g. bare -p); a child that reads stdin inherited from the GUI's
@ -391,7 +402,7 @@ class Worker(QObject):
# (the CLI's human output is on stderr; stdout is empty) — no
# second pipe that could fill while we drain the first.
self.proc = proc = subprocess.Popen(
[ZUPT]+self.args, stdout=subprocess.PIPE,
[ZUPT_CLI]+self.args, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
if self._cancelled: # cancel() ran before Popen finished (see below)
proc.kill()
@ -424,7 +435,7 @@ class Worker(QObject):
if buf.strip() and not self._PCT_RE.search(buf):
lines.append(buf); self.log.emit(buf)
self.done.emit(proc.returncode, "", "\n".join(lines))
except FileNotFoundError: self.done.emit(-1, "", f"vaptvupt not found: {VAPTVUPT}")
except FileNotFoundError: self.done.emit(-1, "", f"zupt not found: {ZUPT_CLI}")
except subprocess.TimeoutExpired: proc.kill(); self.done.emit(-1, "", "Timed out")
except Exception as exc:
# Any escape from this slot would strand the job forever (done never
@ -611,10 +622,10 @@ class KeysTab(QWidget):
v.addWidget(QLabel("Writes a private key and its matching public key."))
v.addWidget(H("Private key output"))
self.gen_priv = PathField("e.g. ~/vaptvupt_private.key", "save", "Key (*.key);;All (*)")
self.gen_priv = PathField("e.g. ~/zupt_private.key", "save", "Key (*.key);;All (*)")
v.addWidget(self.gen_priv)
v.addWidget(H("Public key output"))
self.gen_pub = PathField("e.g. ~/vaptvupt_public.key", "save", "Key (*.key);;All (*)")
self.gen_pub = PathField("e.g. ~/zupt_public.key", "save", "Key (*.key);;All (*)")
v.addWidget(self.gen_pub)
self.gen_btn = QPushButton("Generate Keypair")
@ -634,7 +645,7 @@ class KeysTab(QWidget):
v.addWidget(self.exp_priv)
v.addWidget(H("Public key output"))
self.exp_pub = PathField("e.g. ~/vaptvupt_public.key", "save", "Key (*.key);;All (*)")
self.exp_pub = PathField("e.g. ~/zupt_public.key", "save", "Key (*.key);;All (*)")
v.addWidget(self.exp_pub)
self.exp_btn = QPushButton("Export Public Key")
@ -653,7 +664,7 @@ class KeysTab(QWidget):
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() / "vaptvupt_private.key")
p = self.gen_priv.path() or str(Path.home() / "zupt_private.key")
self.gen_priv.edit.setText(p)
pub = self.gen_pub.path() or self._default_pub(p)
self.gen_pub.edit.setText(pub)
@ -679,7 +690,7 @@ class KeysTab(QWidget):
def _export(self):
priv = self.exp_priv.path()
pub = self.exp_pub.path()
if not priv: QMessageBox.warning(self, "VaptVupt", "Select the private key file."); return
if not priv: QMessageBox.warning(self, "ZUPT", "Select the private key file."); return
if not pub:
pub = self._default_pub(priv); self.exp_pub.edit.setText(pub)
tok = self._token()
@ -701,7 +712,7 @@ class CompressTab(QWidget):
v.addWidget(H("Source files / directory"))
self.src = PathField("Drop files here or browse", "multi"); v.addWidget(self.src)
v.addWidget(H("Output archive"))
self.dst = PathField("e.g. backup.zupt", "save", "VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.dst)
self.dst = PathField("e.g. backup.zupt", "save", "ZUPT archive (*.zupt);;All (*)"); v.addWidget(self.dst)
row = QHBoxLayout(); row.setSpacing(16)
for label, widget in [("Codec", self._mk_codec()), ("Level", self._mk_level())]:
c = QVBoxLayout(); c.addWidget(H(label)); c.addWidget(widget); row.addLayout(c)
@ -735,7 +746,7 @@ class CompressTab(QWidget):
def _run(self):
srcs = self.src.paths()
if not srcs or not srcs[0]: QMessageBox.warning(self, "VaptVupt", "Select files."); return
if not srcs or not srcs[0]: QMessageBox.warning(self, "ZUPT", "Select files."); return
dst = self.dst.path() or srcs[0] + ".zupt"; self.dst.edit.setText(dst)
cmd = ["compress", "-l", str(self.level.value())]
cm = {"AUTO": None, "VaptVupt": "--vv", "LZHP": "--lzhp", "Store": "-s"}
@ -758,7 +769,7 @@ class ExtractTab(QWidget):
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
v.addWidget(QLabel("Extract and decrypt a .zupt archive."))
v.addWidget(Sep())
v.addWidget(H("Archive")); self.arc = PathField("Drop .zupt here", filters="VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.arc)
v.addWidget(H("Archive")); self.arc = PathField("Drop .zupt here", filters="ZUPT archive (*.zupt);;All (*)"); v.addWidget(self.arc)
v.addWidget(H("Output directory")); self.out = PathField("Same as archive", "dir"); v.addWidget(self.out)
enc = QHBoxLayout(); enc.setSpacing(16)
pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField(); pw.addWidget(self.pw); enc.addLayout(pw)
@ -768,7 +779,7 @@ class ExtractTab(QWidget):
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"
self.pqmode.setToolTip("Auto-detect reads the archive header (zupt 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)
@ -780,7 +791,7 @@ class ExtractTab(QWidget):
def _run(self):
arc = self.arc.path()
if not arc: QMessageBox.warning(self, "VaptVupt", "Select an archive."); return
if not arc: QMessageBox.warning(self, "ZUPT", "Select an archive."); return
if not os.path.isfile(arc):
self.log.clear(); self.log.append(f"No such file: {arc}"); return
# Read the header (no credential) so we can guide the user instead of
@ -791,7 +802,7 @@ class ExtractTab(QWidget):
self.log.append("This archive is password-encrypted.\n"
"Enter the password above, then click Extract again.")
return
if kind in ("pq", "pqonly", "sdk") and not self.pq.path():
if kind in ("pq", "pqonly", "sdk", "box") and not self.pq.path():
self.log.clear()
self.log.append(f"This archive uses {label} encryption.\n"
"Select the matching private key above, then click Extract again.")
@ -803,7 +814,7 @@ class ExtractTab(QWidget):
if self.pq.path():
# Prefer the header-detected mode; fall back to the dropdown for an
# unreadable header. Auto-detect can't pick the wrong flag this way.
tok = kind if kind in ("pq", "pqonly", "sdk") else self._pqmodes[self.pqmode.currentIndex()][1]
tok = kind if kind in ("pq", "pqonly", "sdk", "box") else self._pqmodes[self.pqmode.currentIndex()][1]
if tok == "auto":
tok = _detect_archive_pq(arc) or "pq"
_, flag = _PQ_FLAG[tok]
@ -822,7 +833,7 @@ class VerifyTab(QWidget):
v.addWidget(QLabel("Verify checksums or inspect archive metadata."))
v.addWidget(Sep())
v.addWidget(H("Verify integrity"))
self.varc = PathField("Archive to verify", filters="VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.varc)
self.varc = PathField("Archive to verify", filters="ZUPT archive (*.zupt);;All (*)"); v.addWidget(self.varc)
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 (if post-quantum)")); self.vpq = PathField("Auto-detected; needed for --pq / --pq-only archives", filters="Key (*.key);;All (*)"); pq.addWidget(self.vpq); enc.addLayout(pq)
@ -836,7 +847,7 @@ class VerifyTab(QWidget):
self.vlog = Log(120); v.addWidget(self.vlog)
v.addWidget(Sep())
v.addWidget(H("Archive info (no password needed)"))
self.iarc = PathField("Archive to inspect", filters="VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.iarc)
self.iarc = PathField("Archive to inspect", filters="ZUPT archive (*.zupt);;All (*)"); v.addWidget(self.iarc)
self.ibtn = QPushButton("Show Info"); self.ibtn.clicked.connect(self._info); v.addWidget(self.ibtn)
self.ilog = Log(140); v.addWidget(self.ilog); v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
@ -844,7 +855,7 @@ class VerifyTab(QWidget):
def _verify(self):
arc = self.varc.path()
if not arc:
QMessageBox.warning(self, "VaptVupt", "Select an archive to verify."); return
QMessageBox.warning(self, "ZUPT", "Select an archive to verify."); return
if not os.path.isfile(arc):
self.vlog.clear(); self.vlog.append(f"No such file: {arc}"); return
self.vlog.clear()
@ -860,7 +871,7 @@ class VerifyTab(QWidget):
"Enter the password above, then click Verify again.")
return
cmd += ["-p", self.vpw.text()]
elif kind in ("pq", "pqonly", "sdk"):
elif kind in ("pq", "pqonly", "sdk", "box"):
if not self.vpq.path():
self.vlog.append(f"This archive uses {label} encryption.\n"
"Select the matching private key above, then click Verify again.")
@ -899,7 +910,7 @@ class DiskTab(QWidget):
v.addWidget(H("Backup — source device or image"))
self.bsrc = PathField("/dev/sdX or disk.img"); v.addWidget(self.bsrc)
v.addWidget(H("Backup — output archive"))
self.bout = PathField("backup.zupt", "save", "VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.bout)
self.bout = PathField("backup.zupt", "save", "ZUPT archive (*.zupt);;All (*)"); v.addWidget(self.bout)
bopt = QHBoxLayout(); bopt.setSpacing(16)
oc = QVBoxLayout(); oc.addWidget(H("Options")); self.bdedup = QCheckBox("Block deduplication"); oc.addWidget(self.bdedup); bopt.addLayout(oc)
pc = QVBoxLayout(); pc.addWidget(H("Password")); self.bpw = PwField("Optional — AES-256"); pc.addWidget(self.bpw); bopt.addLayout(pc)
@ -908,7 +919,7 @@ class DiskTab(QWidget):
self.blog = Log(100); v.addWidget(self.blog)
v.addWidget(Sep())
v.addWidget(H("Restore — archive"))
self.rarc = PathField("backup.zupt", filters="VaptVupt archive (*.zupt);;All (*)"); v.addWidget(self.rarc)
self.rarc = PathField("backup.zupt", filters="ZUPT archive (*.zupt);;All (*)"); v.addWidget(self.rarc)
v.addWidget(H("Restore — target device or file"))
self.rtgt = PathField("/dev/sdX or output.img", "save"); v.addWidget(self.rtgt)
v.addWidget(H("Restore — password"))
@ -919,7 +930,7 @@ class DiskTab(QWidget):
def _backup(self):
s, o = self.bsrc.path(), self.bout.path()
if not s or not o: QMessageBox.warning(self, "VaptVupt", "Set source and output."); return
if not s or not o: QMessageBox.warning(self, "ZUPT", "Set source and output."); return
cmd = ["disk", "backup"]
if self.bdedup.isChecked(): cmd.append("--dedup")
if self.bpw.text(): cmd += ["-p", self.bpw.text()]
@ -927,7 +938,7 @@ class DiskTab(QWidget):
def _restore(self):
a, t = self.rarc.path(), self.rtgt.path()
if not a or not t: QMessageBox.warning(self, "VaptVupt", "Set archive and target."); return
if not a or not t: QMessageBox.warning(self, "ZUPT", "Set archive and target."); return
SB = QMessageBox.StandardButton
if QMessageBox.warning(self, "Confirm", f"OVERWRITE {t}?", SB.Yes|SB.Cancel) != SB.Yes: return
cmd = ["disk", "restore"]
@ -941,14 +952,14 @@ class AboutTab(QWidget):
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(4)
for text, style in [
("VAPTVUPT", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
("ZUPT", "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: --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;"),
("Original ZUPT name restored in 5.2.2; the .zupt extension", "color:#6a8898;font-size:13px;"),
("and v1.6 version byte remain; 5.2.2 adds flag-gated records.", "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;"),
@ -963,22 +974,21 @@ class AboutTab(QWidget):
("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.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;"),
("VaptVupt LZ + ANS 2.65.3 LZ77 + tabled ANS entropy", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("AVX2 / NEON SIMD acceleration; portable scalar fallbacks", "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/vaptvupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
("ZUPT application Cristian Cezar Moisés", "color:#5a7a88;font-size:12px;font-family:monospace;"),
(" License: AGPL-3.0-or-later (commercial terms may be available)", "color:#5a7a88;font-size:12px;font-family:monospace;"),
(" github.com/cristiancmoises/zupt", "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;"),
(" git.securityops.co/cristiancmoises/vaptvupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
(" License: GPL-3.0-or-later (commercial terms may be available)", "color:#5a7a88;font-size:12px;font-family:monospace;"),
(" github.com/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
("", ""),
("WEBSITE & CONTACT", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
("https://zupt.securityops.co", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("sac@securityops.co (commercial licensing)", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("zupt@riseup.net (general / bugs)", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("https://github.com/cristiancmoises/zupt", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("sac@securityops.co (commercial-terms inquiries)", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("", ""),
(ZUPT_VER_SHORT, "color:#3a5868;font-size:11px;font-family:monospace;"),
]:
@ -995,7 +1005,7 @@ class AboutTab(QWidget):
class ZuptWindow(QMainWindow):
def __init__(self, compress_files=None, extract_file=None):
super().__init__()
self.setWindowTitle(f"VaptVupt {ZUPT_VER_NUMBER}")
self.setWindowTitle(f"ZUPT {ZUPT_VER_NUMBER}")
self.setMinimumSize(720, 500)
self.resize(880, 640)
self.setAcceptDrops(True)
@ -1010,7 +1020,7 @@ class ZuptWindow(QMainWindow):
# Header
hdr = QFrame(); hdr.setStyleSheet("background:#050a0e;border-bottom:1px solid #1a2a30;")
hl = QHBoxLayout(hdr); hl.setContentsMargins(20,10,20,10)
title = QLabel("VAPTVUPT"); title.setStyleSheet("color:white;font-size:15px;font-weight:800;letter-spacing:3px;")
title = QLabel("ZUPT"); title.setStyleSheet("color:white;font-size:15px;font-weight:800;letter-spacing:3px;")
hl.addWidget(title)
sub = QLabel("Post-Quantum Backup"); sub.setStyleSheet("color:#3a5868;font-size:10px;font-weight:600;letter-spacing:1px;margin-left:8px;")
hl.addWidget(sub); hl.addStretch()
@ -1033,7 +1043,7 @@ class ZuptWindow(QMainWindow):
layout.addWidget(self.tabs)
sb = QStatusBar()
sb.showMessage(f"VaptVupt {ZUPT_VER_NUMBER} | {VAPTVUPT}")
sb.showMessage(f"ZUPT {ZUPT_VER_NUMBER} | {ZUPT_CLI}")
self.setStatusBar(sb)
def dragEnterEvent(self, e):
@ -1058,7 +1068,7 @@ class ZuptWindow(QMainWindow):
# leaves the target half-written), so never do it silently.
SB = QMessageBox.StandardButton
if QMessageBox.warning(
self, "VaptVupt",
self, "ZUPT",
"An operation is still running.\nQuit and abort it?",
SB.Yes | SB.Cancel) != SB.Yes:
e.ignore(); return
@ -1081,19 +1091,19 @@ class ZuptWindow(QMainWindow):
def main():
args = sys.argv[1:]
# Lightweight non-GUI flags first, so `vaptvupt-gui --version|--help|--selftest`
# Lightweight non-GUI flags first, so `zupt-gui --version|--help|--selftest`
# work with no display and aren't mistaken for files to compress. `--selftest`
# is a headless-friendly smoke test: it builds the whole UI and spins the event
# loop once, then exits 0 — the reliable way to confirm the GUI stack launches
# on a machine where the window itself is hard to see (tiling WM, remote, CI).
if args and args[0] in ("--version", "-V", "version"):
print(f"vaptvupt-gui {ZUPT_VER_NUMBER} ({QT_BINDING}) | CLI: {VAPTVUPT}")
print(f"zupt-gui {ZUPT_VER_NUMBER} ({QT_BINDING}) | CLI: {ZUPT_CLI}")
return 0
if args and args[0] in ("--help", "-h", "help"):
print("usage: vaptvupt-gui [ARCHIVE.zupt | --extract ARCHIVE.zupt |\n"
print("usage: zupt-gui [ARCHIVE.zupt | --extract ARCHIVE.zupt |\n"
" --compress FILE [FILE ...]]\n"
" vaptvupt-gui --selftest # verify the GUI launches (no window kept)\n"
" vaptvupt-gui --version")
" zupt-gui --selftest # verify the GUI launches (no window kept)\n"
" zupt-gui --version")
return 0
compress_files = extract_file = None
@ -1105,7 +1115,7 @@ def main():
else: compress_files = args
app = QApplication(sys.argv)
app.setApplicationName("VaptVupt")
app.setApplicationName("ZUPT")
if ICON_PATH: app.setWindowIcon(QIcon(ICON_PATH))
app.setStyle("Fusion")
app.setStyleSheet(STYLE)
@ -1126,7 +1136,7 @@ def main():
QTimer.singleShot(400, app.quit)
rc = app.exec()
print(f"selftest OK — {QT_BINDING}: window + {win.tabs.count()} tabs built, "
f"event loop ran (rc={rc}); CLI={VAPTVUPT}")
f"event loop ran (rc={rc}); CLI={ZUPT_CLI}")
return rc
# Center + raise + focus ONLY on X11 (xcb), where a stacking WM may place
@ -1162,7 +1172,7 @@ def main():
# is unaffected. The sentinel env var prevents any relaunch loop (e.g.
# "-platform wayland" in argv outranks the env override and would come up
# wayland again). Nothing auto-starts jobs before the deadline, so the exec
# cannot interrupt real work. Opt out with VAPTVUPT_NO_XCB_FALLBACK=1.
# cannot interrupt real work. Opt out with ZUPT_NO_XCB_FALLBACK=1.
if app.platformName().startswith("wayland"):
class _ExposeLatch(QObject):
exposed_once = False
@ -1177,10 +1187,14 @@ def main():
def _wayland_map_check():
if latch.exposed_once or (handle is not None and handle.isExposed()):
return
no_fallback = (os.environ.get("ZUPT_NO_XCB_FALLBACK")
or os.environ.get("VAPTVUPT_NO_XCB_FALLBACK"))
fallback_done = (os.environ.get("ZUPT_XCB_FALLBACK_DONE")
or os.environ.get("VAPTVUPT_XCB_FALLBACK_DONE"))
can_fallback = (os.environ.get("DISPLAY")
and sys.executable
and os.environ.get("VAPTVUPT_NO_XCB_FALLBACK") != "1"
and os.environ.get("VAPTVUPT_XCB_FALLBACK_DONE") != "1")
and no_fallback != "1"
and fallback_done != "1")
if sys.stderr is not None:
try:
sys.stderr.write(
@ -1195,7 +1209,7 @@ def main():
pass
if can_fallback:
env = dict(os.environ, QT_QPA_PLATFORM="xcb",
VAPTVUPT_XCB_FALLBACK_DONE="1")
ZUPT_XCB_FALLBACK_DONE="1")
argv = (list(sys.argv) if getattr(sys, "frozen", False)
else [sys.executable] + sys.argv)
try:
@ -1217,7 +1231,7 @@ def main():
# a courtesy notice must never take the GUI down.
if sys.stderr is not None:
try:
sys.stderr.write(f"VaptVupt {ZUPT_VER_NUMBER} GUI started — "
sys.stderr.write(f"ZUPT {ZUPT_VER_NUMBER} GUI started — "
f"window open (close it to exit).\n")
sys.stderr.flush()
except OSError:

View file

@ -1,46 +1,31 @@
#!/bin/bash
set -e
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
DIR="$(cd "$(dirname "$0")" && pwd)"
VENV="$DIR/.venv"
PY="$VENV/bin/python3"
PIP="$VENV/bin/pip"
GUI="$DIR/src/zupt_gui.py"
# Source-tree launcher for ZUPT GUI.
# It performs no package installation and never downloads dependencies.
set -Eeuo pipefail
# ─── System deps (Qt xcb needs these on Debian/Mint/Ubuntu) ───
NEED_APT=0
for pkg in libxcb-cursor0 libxcb-xinerama0 libxkbcommon-x11-0 libegl1 python3-full; do
dpkg -s "$pkg" >/dev/null 2>&1 || NEED_APT=1
done
if [ "$NEED_APT" -eq 1 ]; then
echo "Installing system dependencies..."
sudo apt-get update -qq
sudo apt-get install -y python3-full python3-venv \
libxcb-cursor0 libxcb-xinerama0 libxkbcommon-x11-0 libegl1 2>/dev/null
fi
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
gui=$script_dir/src/zupt_gui.py
[[ -f $gui ]] || {
printf 'zupt-gui: GUI source is missing: %s\n' "$gui" >&2
exit 1
}
# ─── Venv ───
if [ ! -x "$PY" ]; then
rm -rf "$VENV"
python3 -m venv "$VENV"
fi
if ! "$PY" -c "import PySide6" 2>/dev/null; then
echo "Installing PySide6..."
"$PIP" install --upgrade pip -q 2>/dev/null
"$PIP" install PySide6 -q
fi
# ─── Find zupt — local build FIRST, then system ───
if [ -z "$ZUPT_BIN" ]; then
# Check project tree first (gui/ is inside zupt-2.1.6/)
for p in "$DIR/../zupt" "$DIR/../../zupt" "$DIR/zupt"; do
[ -x "$p" ] && export ZUPT_BIN="$(readlink -f "$p")" && break
done
# Then system PATH
if [ -z "$ZUPT_BIN" ]; then
p="$(command -v zupt 2>/dev/null)"
[ -x "$p" ] && export ZUPT_BIN="$p"
if [[ -z ${ZUPT_BIN:-} ]]; then
if [[ -n ${VAPTVUPT_BIN:-} ]]; then
export ZUPT_BIN=$VAPTVUPT_BIN
elif [[ -x $script_dir/../zupt ]]; then
export ZUPT_BIN=$script_dir/../zupt
elif command -v zupt >/dev/null 2>&1; then
ZUPT_BIN=$(command -v zupt)
export ZUPT_BIN
elif [[ -x $script_dir/../vaptvupt ]]; then
export ZUPT_BIN=$script_dir/../vaptvupt
elif command -v vaptvupt >/dev/null 2>&1; then
ZUPT_BIN=$(command -v vaptvupt)
export ZUPT_BIN
fi
fi
exec "$PY" "$GUI" "$@"
exec python3 "$gui" "$@"