From e5f5d32aabb789f9c73d1c3dcde07928df31b2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Fri, 1 May 2026 09:58:47 -0300 Subject: [PATCH] v2.2.2 --- .github/workflows/ci.yml | 104 ++ AUDIT.md | 496 +++++-- CHANGELOG.md | 641 +++++++-- INSTALL.md | 242 ++++ LICENSE | 798 +----------- LICENSE-VAPTVUPT | 737 ----------- Makefile | 155 ++- README.md | 112 +- ROADMAP.md | 4 +- SECURITY.md | 172 ++- THIRD-PARTY-NOTICES.md | 121 ++ doc/zupt-gui.1 | 109 ++ doc/zupt.1 | 379 ++++-- gui/LICENSE-GUI | 47 +- gui/README.md | 11 +- gui/install.sh | 2 + gui/packaging/appimage/build-appimage.sh | 2 + gui/packaging/build-gui-deb.sh | 94 ++ gui/packaging/flatpak/dev.zupt.gui.yml | 4 +- gui/packaging/rpm/zupt-gui.spec | 43 - gui/setup.py | 6 +- gui/src/zupt_gui.py | 129 +- gui/zupt-gui | 0 include/vaptvupt.h | 3 +- include/vaptvupt_api.h | 3 +- include/vv_ans.h | 6 +- include/vv_huffman.h | 6 +- include/vv_platform.h | 2 - include/zupt.h | 19 +- include/zupt_acsl.h | 5 +- include/zupt_cpuid.h | 5 +- include/zupt_jasmin.h | 5 +- install.sh | 4 +- jasmin/zupt_aes_ctr.jazz | 3 +- jasmin/zupt_aes_ctr.s | 3 + jasmin/zupt_aes_ctr4.jazz | 3 +- jasmin/zupt_aes_ctr4.s | 3 + jasmin/zupt_mac_verify.jazz | 3 +- jasmin/zupt_mac_verify.s | 3 + jasmin/zupt_mlkem_select.jazz | 3 +- jasmin/zupt_mlkem_select.s | 3 + jasmin/zupt_x25519_fe.jazz | 3 +- jasmin/zupt_x25519_fe.s | 3 + packaging/build-appimage.sh | 53 + packaging/build-deb.sh | 133 ++ packaging/build-gui-appimage.sh | 121 ++ packaging/build-gui-deb.sh | 176 +++ packaging/build-gui-rpm.sh | 132 ++ packaging/build-rpm-manual.py | 339 +++++ packaging/build-rpm.sh | 68 + packaging/install-zupt-gui.sh | 139 ++ packaging/zupt-installer-header.sh | 312 +++++ sdk/LICENSE | 56 + sdk/Makefile.sdk | 141 ++ sdk/README.md | 183 +++ .../__pycache__/zuptsdk.cpython-312.pyc | Bin 0 -> 27397 bytes sdk/bindings/python/zuptsdk.py | 468 +++++++ sdk/doc/example.c | 80 ++ sdk/include/zuptsdk.h | 605 +++++++++ sdk/src/zuptsdk.c | 1149 +++++++++++++++++ sdk/tests/test_python.py | 122 ++ sdk/tests/test_sdk_roundtrip.c | 412 ++++++ sdk/zuptsdk.map | 98 ++ src/vaptvupt_api.c | 10 +- src/vv_ans.c | 308 +---- src/vv_decoder.c | 6 +- src/vv_encoder.c | 122 +- src/vv_huffman.c | 6 +- src/vv_simd.c | 6 +- src/vv_xxh64.c | 6 +- src/zupt_aes256.c | 7 +- src/zupt_cpuid.c | 5 +- src/zupt_crypto.c | 80 +- src/zupt_crypto_sdk.c | 226 ++++ src/zupt_dedup.c | 5 +- src/zupt_disk.c | 5 +- src/zupt_filetype.c | 5 +- src/zupt_format.c | 376 +++++- src/zupt_lz.c | 26 +- src/zupt_lzh.c | 6 +- src/zupt_main.c | 129 +- src/zupt_parallel.c | 6 +- src/zupt_parallel.h | 6 +- src/zupt_predict.c | 6 +- src/zupt_sha256.c | 7 +- src/zupt_thread.h | 6 +- src/zupt_xxh.c | 8 +- tests/fuzz_decompress.c | 5 +- tests/fuzz_format | Bin 0 -> 16976 bytes tests/fuzz_format.c | 157 +++ tests/fuzz_vv_decompress.c | 5 +- tests/regression.sh | 2 + tests/run_quick.sh | 2 + tests/test_arg_order.sh | 78 ++ tests/test_audit.sh | 137 ++ tests/test_block_swap.sh | 180 +++ tests/test_dedup_props.sh | 130 ++ tests/test_path_traversal.sh | 156 +++ tests/test_pq.sh | 2 + tests/test_sdk.sh | 72 ++ tests/test_threaded.sh | 2 + tests/test_vectors.c | 5 +- vendor/zuptsdk/include/vaptvupt.h | 465 +++++++ vendor/zuptsdk/include/vaptvupt_api.h | 43 + vendor/zuptsdk/include/vv_ans.h | 136 ++ vendor/zuptsdk/include/vv_huffman.h | 127 ++ vendor/zuptsdk/include/vv_platform.h | 117 ++ vendor/zuptsdk/include/zsdk_aes256_gcm_siv.h | 39 + vendor/zuptsdk/include/zsdk_aes256_siv.h | 39 + vendor/zuptsdk/include/zsdk_argon2id.h | 39 + vendor/zuptsdk/include/zsdk_blake2b.h | 46 + vendor/zuptsdk/include/zsdk_hkdf.h | 41 + .../zuptsdk/include/zsdk_xchacha20_poly1305.h | 57 + vendor/zuptsdk/include/zupt.h | 401 ++++++ vendor/zuptsdk/include/zupt_acsl.h | 43 + vendor/zuptsdk/include/zupt_cpuid.h | 33 + vendor/zuptsdk/include/zupt_jasmin.h | 65 + vendor/zuptsdk/include/zupt_keccak.h | 49 + vendor/zuptsdk/include/zupt_mlkem.h | 65 + vendor/zuptsdk/include/zupt_x25519.h | 22 + vendor/zuptsdk/include/zuptsdk.h | 605 +++++++++ vendor/zuptsdk/include/zuptsdk.hpp | 230 ++++ vendor/zuptsdk/include/zuptsdk_easy.h | 89 ++ vendor/zuptsdk/include/zuptsdk_metrics.h | 57 + 124 files changed, 11914 insertions(+), 2483 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 INSTALL.md delete mode 100644 LICENSE-VAPTVUPT create mode 100644 THIRD-PARTY-NOTICES.md create mode 100644 doc/zupt-gui.1 mode change 100644 => 100755 gui/install.sh mode change 100644 => 100755 gui/packaging/appimage/build-appimage.sh create mode 100755 gui/packaging/build-gui-deb.sh delete mode 100644 gui/packaging/rpm/zupt-gui.spec mode change 100644 => 100755 gui/zupt-gui create mode 100755 packaging/build-appimage.sh create mode 100755 packaging/build-deb.sh create mode 100755 packaging/build-gui-appimage.sh create mode 100755 packaging/build-gui-deb.sh create mode 100755 packaging/build-gui-rpm.sh create mode 100644 packaging/build-rpm-manual.py create mode 100755 packaging/build-rpm.sh create mode 100755 packaging/install-zupt-gui.sh create mode 100644 packaging/zupt-installer-header.sh create mode 100644 sdk/LICENSE create mode 100644 sdk/Makefile.sdk create mode 100644 sdk/README.md create mode 100644 sdk/bindings/python/__pycache__/zuptsdk.cpython-312.pyc create mode 100644 sdk/bindings/python/zuptsdk.py create mode 100644 sdk/doc/example.c create mode 100644 sdk/include/zuptsdk.h create mode 100644 sdk/src/zuptsdk.c create mode 100644 sdk/tests/test_python.py create mode 100644 sdk/tests/test_sdk_roundtrip.c create mode 100644 sdk/zuptsdk.map create mode 100644 src/zupt_crypto_sdk.c create mode 100755 tests/fuzz_format create mode 100644 tests/fuzz_format.c create mode 100755 tests/test_arg_order.sh create mode 100755 tests/test_audit.sh create mode 100755 tests/test_block_swap.sh create mode 100755 tests/test_dedup_props.sh create mode 100755 tests/test_path_traversal.sh create mode 100755 tests/test_sdk.sh create mode 100644 vendor/zuptsdk/include/vaptvupt.h create mode 100644 vendor/zuptsdk/include/vaptvupt_api.h create mode 100644 vendor/zuptsdk/include/vv_ans.h create mode 100644 vendor/zuptsdk/include/vv_huffman.h create mode 100644 vendor/zuptsdk/include/vv_platform.h create mode 100644 vendor/zuptsdk/include/zsdk_aes256_gcm_siv.h create mode 100644 vendor/zuptsdk/include/zsdk_aes256_siv.h create mode 100644 vendor/zuptsdk/include/zsdk_argon2id.h create mode 100644 vendor/zuptsdk/include/zsdk_blake2b.h create mode 100644 vendor/zuptsdk/include/zsdk_hkdf.h create mode 100644 vendor/zuptsdk/include/zsdk_xchacha20_poly1305.h create mode 100644 vendor/zuptsdk/include/zupt.h create mode 100644 vendor/zuptsdk/include/zupt_acsl.h create mode 100644 vendor/zuptsdk/include/zupt_cpuid.h create mode 100644 vendor/zuptsdk/include/zupt_jasmin.h create mode 100644 vendor/zuptsdk/include/zupt_keccak.h create mode 100644 vendor/zuptsdk/include/zupt_mlkem.h create mode 100644 vendor/zuptsdk/include/zupt_x25519.h create mode 100644 vendor/zuptsdk/include/zuptsdk.h create mode 100644 vendor/zuptsdk/include/zuptsdk.hpp create mode 100644 vendor/zuptsdk/include/zuptsdk_easy.h create mode 100644 vendor/zuptsdk/include/zuptsdk_metrics.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c925b61 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + build-and-test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + libargon2-dev libargon2-1 \ + libssl-dev libssl3 \ + python3 + + - name: Build zupt + run: make + + - name: Run quick tests + run: make test + + - name: Run audit suite + run: bash tests/test_audit.sh + + - name: Run dedup property tests + run: bash tests/test_dedup_props.sh + + asan-build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libargon2-dev libssl-dev + + - name: Build zupt (release) + run: make + + - name: Build zupt (ASAN/UBSAN) + run: make test-asan + + - name: Run all suites under ASAN/UBSAN + env: + ASAN_OPTIONS: detect_leaks=0:abort_on_error=1 + run: make test-asan-run + + fuzz-format: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libargon2-dev libssl-dev + + - name: Build zupt + ASAN binary + run: | + make + make test-asan + + - name: Build fuzz harness + run: make fuzz-format + + - name: Run 1000 fuzz iterations under ASAN/UBSAN + env: + ASAN_OPTIONS: detect_leaks=0:abort_on_error=1 + run: make fuzz-format-run + + package-deb: + runs-on: ubuntu-24.04 + needs: [build-and-test] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libargon2-dev libssl-dev dpkg-dev + + - name: Build zupt + run: make + + - name: Build .deb + run: bash packaging/build-deb.sh + + - name: Build GUI .deb + run: bash packaging/build-gui-deb.sh + + - name: Verify deb installs + run: | + sudo dpkg -i /tmp/zupt_*.deb + zupt version + which zupt + ls /usr/include/zuptsdk*.h diff --git a/AUDIT.md b/AUDIT.md index 56f13fe..43bd9d8 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,101 +1,85 @@ -# Security Audit — Zupt v2.1.7 +# Security Audit — Zupt v2.0.0 -**Date:** April 26, 2026 +**Date:** March 29, 2026 **Author:** Cristian Cezar Moisés **Audit type:** Self-audit with formal verification (Jasmin CT proofs, ACSL contracts) and NIST/RFC test vectors **Status:** No independent third-party audit performed -**License (audit covers):** AGPL-3.0-or-later (Zupt core), GPL-3.0-or-later (VaptVupt codec) — commercial: sac@securityops.co --- -## 1. Cryptographic Test Vector Verification — re-validated on v2.1.7 +## 1. Cryptographic Test Vector Verification -All primitives re-tested against published reference vectors after the v2.1.7 relicense. Identical results to v2.0.0 — as expected, since the relicense did not modify any cryptographic code paths. +All primitives tested against published reference vectors: -| Primitive | Standard | Vectors | v2.1.7 Status | -|-----------|----------|---------|---------------| +| Primitive | Standard | Vectors | Status | +|-----------|----------|---------|--------| | SHA-256 | FIPS 180-4 | 3 (empty, "abc", 448-bit) | **PASS** | | HMAC-SHA256 | RFC 4231 | 2 (TC2: "Jefe", TC3: 20×0xAA) | **PASS** | | SHA3-256 | FIPS 202 | 2 (empty, "abc") | **PASS** | | SHAKE-128 | FIPS 202 | 1 (empty, 128-bit output) | **PASS** | -| X25519 | RFC 7748 §6.1 | 2 (TV1, TV2) | **PASS** | +| X25519 | RFC 7748 §5.2 | 2 (both test vectors) | **PASS** | | ML-KEM-768 | FIPS 203 | 2 (5-trial roundtrip + implicit rejection) | **PASS** | | XXH64 | xxHash spec | 1 (empty string, seed=0) | **PASS** | | **Total** | | **13** | **13/13 PASS** | -Reproduction: `make test-vectors && ./test_vectors` - ## 2. Jasmin Constant-Time Verification -| Function | Purpose | v2.1.7 Status | -|----------|---------|---------------| +| Function | Purpose | Status | +|----------|---------|--------| | `zupt_mac_verify_ct` | HMAC comparison | **✅ Linked, CT-proven** | | `zupt_ct_select_32` | ML-KEM FO select | **✅ Linked, CT-proven** | +| `zupt_fe_cswap` | X25519 conditional swap | **✅ Linked, CT-proven** | +| `zupt_aes256_blk` | AES-256 single-block (AES-NI) | **✅ Linked, CT by hardware** | | `zupt_aes256_ctr4` | AES-256 4-block pipeline | **✅ Linked, CT by hardware** | -| `zupt_fe_cswap` | X25519 conditional swap | **⚠ Not yet linked** (limb representation mismatch: 5×u51 vs 4×u64) | -| `zupt_aes256_blk` | AES-256 single-block (AES-NI) | **⚠ Not yet linked** (Jasmin stack offset bug: `[rsp+1]` instead of `[rsp+16]`) | - -The two unwired functions remain on the v1.6+ roadmap; their fallback implementations (C masked XOR for `fe_cswap`, C T-table AES for `aes256_blk`) are functionally correct and continue to be the production code path. ## 3. ACSL Formal Annotations 19 security-critical functions annotated with `requires/ensures/assigns` contracts. Target: `frama-c -wp -wp-rte -wp-model Typed+Cast` -## 4. Security Hardening — current status +## 4. Security Hardening -| Feature | v2.1.7 Status | -|---------|---------------| -| `mlock()` key protection | **✅ Active** | +| Feature | Status | +|---------|--------| +| mlock() key protection | **✅ Active** | | Buffer canaries (keyring) | **✅ Active** | | Always-decrypt timing mitigation | **✅ Active** | | AFL++ fuzz harnesses | **✅ Available** (`make fuzz-build`) | -| TOCTOU mitigation (`fstat` on fd, not `stat` on path) | **✅ Active** (since v2.1.4) | -| X25519 scalar wipe via `volatile` | **✅ Active** (since v2.1.4) | -| Password strength warnings | **✅ Active** (since v2.1.6) | -| Block-level deduplication with content verification | **✅ Active** (since v2.1.5) | -## 5. VaptVupt Codec Tests — re-validated on v2.1.7 +## 5. VaptVupt Codec Tests -| Test | v2.1.7 Status | -|------|---------------| -| Roundtrip text mode 0 / 1 / 2 (each 64 KB) | **PASS** (3) | -| Roundtrip binary mode 1 (128 KB) | **PASS** | +| Test | Status | +|------|--------| +| Roundtrip all 3 modes (UF/BAL/EXT) | **PASS** | +| Roundtrip + AES-256 encryption | **PASS** | +| Roundtrip + PQ hybrid encryption | **PASS** | +| Roundtrip + multi-threaded | **PASS** | +| Roundtrip + solid mode | **PASS** | | Incompressible fallback to store | **PASS** | -| Empty / small input (3 bytes) | **PASS** (2) | -| `vv_xxh64` ↔ `zupt_xxh64` alias | **PASS** | -| Roundtrip large 2 MB (mode 1) | **PASS** | -| RLE-like data roundtrip | **PASS** | -| `window_log=20` roundtrip | **PASS** | +| Empty/small input | **PASS** | +| Multi-block (2 MB) | **PASS** | | **Total** | **11/11 PASS** | -## 6. Test Suite Summary — v2.1.7 - -| Suite | Tests | v2.1.7 Result | What It Covers | -|-------|-------|---------------|----------------| -| NIST/RFC vectors | 13 | **13/13 PASS** | See §1 | -| VaptVupt unit | 11 | **11/11 PASS** | See §5 | -| Regression | 22 | **22/22 PASS** | All codecs, modes, encryption, edge cases, corruption detection | -| Multi-threaded | 14 | **13/14 PASS** ⚠ | N=1/2/4/8, large files, MT+encryption | -| Post-quantum | 10 | **10/10 PASS** | Keygen, PQ encrypt/decrypt, wrong key, password compat, PQ+MT, 2 MB | -| **Total** | **70** | **69/70 PASS** | | - -**The single failing case is `Solid+N=8 (3 mismatches)` — see §11 (newly-recorded known limitation).** +| Suite | Tests | Result | What It Covers | +|-------|-------|--------|----------------| +| Regression | 16 | **16/16 PASS** | All codecs, modes, encryption, edge cases, corruption detection | +| Multi-threaded | 14 | **14/14 PASS** | N=1/2/4/8 threads, large files, 1000 files, MT+encryption | +| Post-quantum | 10 | **10/10 PASS** | Keygen, PQ encrypt/decrypt, wrong key, password compat, PQ+MT, 2MB | +| Quick smoke | 9 | **9/9 PASS** | Normal, solid, encrypted, wrong pw, MT, fast, store, PQ, integrity | +| NIST vectors | 13 | **13/13 PASS** | See table above | +| **Total** | **62** | **62/62 PASS** | | Reproduction: `make test-all` --- -## 7. Memory Safety — re-validated on v2.1.7 +## 3. Memory Safety -| Tool | Command | v2.1.7 Result | -|------|---------|---------------| -| AddressSanitizer (plain) | `./zupt_asan compress` / `extract` | **Zero errors** | -| AddressSanitizer (encrypted, PBKDF2) | `./zupt_asan compress -p` | **Zero errors** | -| AddressSanitizer (PQ hybrid) | `./zupt_asan compress --pq` / `extract --pq` | **Zero errors** | -| AddressSanitizer (multi-threaded N=4) | `./zupt_asan compress -t 4` | **Zero errors** | -| AddressSanitizer (deduplication) | `./zupt_asan compress --dedup` | **Zero errors** | +| Tool | Command | Result | +|------|---------|--------| +| AddressSanitizer | `make test-asan` | **Zero errors** | | UndefinedBehaviorSanitizer | Built with `-fsanitize=address,undefined` | **Zero errors** | +| All code paths tested | Normal + solid + encrypted + PQ + MT | **Clean** | Reproduction: ```bash @@ -103,53 +87,94 @@ make test-asan ./zupt_asan compress /tmp/t.zupt /path/to/data/ ./zupt_asan extract -o /tmp/out/ /tmp/t.zupt ./zupt_asan keygen -o /tmp/k.key -./zupt_asan keygen --pub -o /tmp/pub.key -k /tmp/k.key ./zupt_asan compress --pq /tmp/pub.key /tmp/pq.zupt /path/to/data/ ./zupt_asan extract --pq /tmp/k.key -o /tmp/pqout/ /tmp/pq.zupt -./zupt_asan compress -t 4 /tmp/mt.zupt /path/to/data/ -./zupt_asan compress --dedup /tmp/d.zupt /path/to/data/ ``` --- -## 8. Compiler Warning Audit +## 4. Compiler Warning Audit -| Compiler | Flags | v2.1.7 Warnings | -|----------|-------|-----------------| -| GCC 13.x | `-Wall -Wextra -O2 -std=c11` | **Zero** | -| GCC 13.x (pedantic) | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** | +| Compiler | Flags | Warnings | +|----------|-------|----------| +| GCC 13.x | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** | +| Clang 18.x | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** | --- -## 9. Constant-Time Analysis — unchanged from v2.0.0 +## 5. Constant-Time Analysis | Function | Location | CT Method | Jasmin Verified? | Risk Level | |----------|----------|-----------|-----------------|------------| -| HMAC comparison | `zupt_crypto.c` | 4×u64 XOR accumulation | **Yes** — `zupt_mac_verify_ct` linked | **None** (Jasmin proven) | -| ML-KEM FO select | `zupt_mlkem.c` | 4×u64 masked select | **Yes** — `zupt_ct_select_32` linked | **None** (Jasmin proven) | -| AES-256 CTR (4-block) | `zupt_aes256.c` (AES-NI dispatch) | Hardware AES-NI + AVX | **Yes** — `zupt_aes256_ctr4` linked | **None** (CT by hardware) | +| HMAC comparison | `zupt_crypto.c:252` | 4×u64 XOR accumulation | **Yes** — `zupt_mac_verify_ct` linked | **None** (Jasmin proven) | +| ML-KEM FO select | `zupt_mlkem.c:593` | 4×u64 masked select | **Yes** — `zupt_ct_select_32` linked | **None** (Jasmin proven) | | ML-KEM NTT butterfly | `zupt_mlkem.c` | Montgomery reduction (branchless) | No | Low | | ML-KEM CBD sampling | `zupt_mlkem.c` | Bitwise operations only | No | Low | -| X25519 fe_cswap | `zupt_x25519.c` | Masked XOR swap | No (limb mismatch) | Low (C is branchless) | -| X25519 Montgomery ladder | `zupt_x25519.c` | Fixed 255 iterations | No | Low | -| AES-256 encrypt (C fallback) | `zupt_aes256.c` | Table-based S-box | No | **HIGH on shared HW without AES-NI** | +| X25519 fe_cswap | `zupt_x25519.c:95` | Masked XOR swap | No (limb mismatch) | Low (C is branchless) | +| X25519 Montgomery ladder | `zupt_x25519.c:243` | Fixed 255 iterations | No | Low | +| AES-256 encrypt | `zupt_aes256.c:59` | **Table-based S-box** | **No** | **HIGH on shared HW** | | SHA-256 | `zupt_sha256.c` | Table-based constants | No | Low (not secret-indexed) | | Keccak-f[1600] | `zupt_keccak.c` | Bitwise XOR/ROT only | No | None | | Key wipe | `zupt_crypto.c` | `explicit_bzero` / volatile | No | Low | ### Jasmin Assembly Verification -Functions confirmed active in binary via `nm`: +Two functions confirmed active in binary via `nm`: + ``` -T zupt_mac_verify_ct ← Jasmin assembly, CT proven -T zupt_ct_select_32 ← Jasmin assembly, CT proven -T zupt_aes256_ctr4 ← Jasmin assembly, CT by hardware +0000000000014ae0 T zupt_mac_verify_ct ← Jasmin assembly, CT proven +0000000000014b20 T zupt_ct_select_32 ← Jasmin assembly, CT proven ``` + Assembly generated by `jasminc 2026.03.0`. Constant-time enforced by Jasmin type system: secret-typed variables cannot flow into branch conditions or memory indices. +### Not Wired (with reason) + +| Function | Issue | Fallback | +|----------|-------|----------| +| `zupt_fe_cswap` | Jasmin: 4×u64 limbs, C: 5×u51 — incompatible | C masked XOR (branchless) | +| `zupt_aes256_blk` | Stack offset bug: `rk.[1]` → `[rsp+1]` not `[rsp+16]` | C table-based AES | + --- -## 10. Bugs Found and Fixed (v0.5.1 → v2.1.7) +## 6. Key Material Lifecycle + +| Phase | Method | Verified | +|-------|--------|----------| +| Generation | OS CSPRNG: `getrandom(2)` / `/dev/urandom` / `RtlGenRandom` | Hard fail if unavailable | +| Storage | Stack-local arrays (no heap allocation for keys) | ASAN verified | +| Usage | Passed by const pointer to AES-CTR / HMAC | No copies to heap | +| Wipe | `zupt_secure_wipe()`: `explicit_bzero` (glibc 2.25+), `SecureZeroMemory` (Win), volatile fallback | Compiler cannot optimize out | +| Scope exit | Stack frame destroyed | Keys were on stack | + +All intermediate buffers in PBKDF2, hybrid KEM, ML-KEM encaps/decaps, and X25519 wiped before return. + +--- + +## 7. Nonce Security + +**Scheme:** `per_block_nonce = base_nonce XOR pad_le(block_seq, 8)` + +- `base_nonce`: 128-bit random from CSPRNG, generated once per archive. +- `block_seq`: monotonically increasing 0, 1, 2, ... per archive. +- **Uniqueness within archive:** Guaranteed (distinct seq → distinct nonce). +- **Uniqueness across archives:** 2^-128 collision probability per pair (birthday bound on random base). + +--- + +## 8. Encrypt-then-MAC Ordering + +| Step | Action | Verified | +|------|--------|----------| +| 1 | Compute HMAC over `nonce ‖ ciphertext` | HMAC input is nonce+ct, not plaintext | +| 2 | Verify HMAC before any decryption | Code path: MAC check → early return if fail → decrypt only on success | +| 3 | Decrypt only authenticated data | No plaintext produced from unauthenticated ciphertext | + +**Prevents:** Chosen-ciphertext attacks, padding oracles, ciphertext tampering. + +--- + +## 9. Bugs Found and Fixed (v0.5.1 → v1.5.0) | Bug | Severity | Version Fixed | Impact | |-----|----------|---------------|--------| @@ -162,62 +187,291 @@ Assembly generated by `jasminc 2026.03.0`. Constant-time enforced by Jasmin type | ML-KEM `inv_ntt` wrong table | High | v1.0.0 | NTT roundtrip failure | | PQ nonce mismatch | High | v1.0.0 | Encrypt/decrypt used different nonces | | X25519 `AA + a24*E` formula | High | v1.1.0 | Wrong curve, not interoperable | -| `copy_match_scalar` overlap corruption | High | v2.0.0 | Silent corruption on offset 4–7 (8-byte bulk overlap) | -| `vva_encode_sequences` heap overflow | High | v2.0.0 | nseq×5+1 sizing insufficient on large literal runs | -| `SIGILL` on Linux Mint (AES dispatch) | High | v2.0.0 | Jasmin AES uses VEX → needs OSXSAVE+XCR0, not just AES-NI | -| TOCTOU race in `get_device_size` | High | v2.1.4 | Path swap between `stat` and `open`; CodeQL alert | -| LZHP prediction encoding desync | High | v2.1.3 | Data corruption on structured data | +| Dead `match_cost()` | Low | v1.1.0 | Clang warning | +| `const polyvec` qualifier | Low | v1.1.0 | Pedantic warnings | +| `__int128` pedantic | Low | v1.1.0 | Pedantic warning | --- -## 11. Known Limitations +## 10. Known Limitations | Limitation | Impact | Mitigation | Status | |------------|--------|------------|--------| -| **Solid mode + multi-threaded archives over ~3 MB** ⚠ NEW | Decompression error "Solid block N decompression failed" on extract. Reproduces on v2.1.6 and v2.1.7 (not introduced by relicense). Compression succeeds; extraction fails on a specific block. The CLI clamps `--solid` to N=1 with a stderr note; this bug appears in the inner solid-stream framing when the on-disk archive happens to span ≥4 blocks of ~512 KB each. | Use plain `--solid` (without `-t >1`) for solid archives; non-solid MT works correctly. Investigation in progress in `zupt_format.c:1428–1452` (block_seq accounting around encryption-header consumption). | **Open** — pre-existing, not relicense-induced | -| Table-based AES (C fallback) | Cache-timing on shared hardware | Jasmin AES-NI 4-block pipeline (`zupt_aes256_ctr4`) IS linked and is the default on x86_64+AVX. Single-block Jasmin (`zupt_aes256_blk`) has stack offset bug. | **Partial** — 4-block path covered | +| Table-based AES (C fallback) | Cache-timing on shared hardware | Jasmin AES-NI path exists but has offset bug | **Open** — fix `.jazz` source | | Table-based SHA-256 | Theoretical cache-timing | Not used on secret-indexed data | **Accepted** | | PBKDF2 not quantum-safe | Quantum password brute-force | Use `--pq` mode | **Documented** | -| No fuzzing performed | Undiscovered bugs | AFL++ harnesses available via `make fuzz-build`; not yet executed in production | **Planned** | -| No independent audit | Self-assessed only | Open source under AGPL-3.0 + Jasmin formal proofs + commercial support available at sac@securityops.co | **Planned** | -| X25519 Jasmin not linked | C fallback for `fe_cswap` | C is branchless but compiler-dependent. Limb mismatch (Jasmin: 4×u64, C: 5×u51) is the blocker. | **Open** | - ---- - -## 12. v2.1.7 Relicense + VaptVupt Upgrade — Audit Implications - -The v2.1.7 release does two distinct things, both of which were validated separately: - -**(A) Relicense from MIT to AGPL-3.0-or-later (Zupt core only).** VaptVupt was already GPL-3.0-or-later in upstream and remains so. The relicense touches only license metadata (SPDX headers, copyright lines, `LICENSE` files, `--help` strings, README badges, packaging metadata). **No cryptographic, compression, threading, encryption, or control-flow code was modified by the relicense itself.** - -**(B) Upgrade VaptVupt 2.40.0 → 2.46.1.** The 12 VaptVupt files (`src/vv_*.c`, `src/vaptvupt_api.c`, `include/vaptvupt*.h`, `include/vv_*.h`) were replaced from upstream master at `github.com/cristiancmoises/vaptvupt`. Public API and `vv_options_t` are byte-identical; no caller changes required. Most relevantly, **v2.46.1 includes a memory-safety fix** for a 16,384-byte leak on three decoder error paths in `vva_decode_sequences_impl` — closing a denial-of-service vector where many malformed compressed frames could exhaust memory. - -Audit results on the upgraded + relicensed tree: -- **All NIST/RFC test vectors continue to pass on v2.1.7.** -- **All ASAN/UBSAN sweeps continue to be clean on v2.1.7.** -- **The single MT failing case (`Solid+N=8`) is verified pre-existing on v2.1.6** by stashing v2.1.7 changes, rebuilding the v2.1.6 tree, and reproducing the same failure on the same fixture — it is independent of both the relicense and the codec upgrade. - -The relicense formally establishes a commercial-licensing channel at **sac@securityops.co** for users whose use case is incompatible with AGPL's source-disclosure obligations. - ---- - -## 13. Reproduction - -```bash -# Full validation -make clean && make -make test-all # NIST vectors + VV unit + regression + threaded + PQ -make test-asan # AddressSanitizer build -# (run ASAN sweep manually per §7) - -# Spot-check the relicense -./zupt --version # should print: zupt 2.1.7 -./zupt 2>&1 | grep -i lic # should print: License: AGPL-3.0-or-later (commercial: sac@securityops.co) -grep -c "SPDX-License-Identifier" src/*.c src/*.h include/*.h tests/*.c -# (should be > 30, all sources covered) -``` +| No `mlock()` | Keys swappable to disk | Short key lifetime + `zupt_secure_wipe` | **Planned** | +| No fuzzing performed | Undiscovered bugs | AFL++ setup in FUZZING.md | **Planned** | +| No independent audit | Self-assessed only | Open source + Jasmin proofs | **Planned** | +| X25519 Jasmin not linked | C fallback for fe_cswap | C is branchless but compiler-dependent | **Open** — limb mismatch | --- © 2026 Cristian Cezar Moisés — AGPL-3.0-or-later -Commercial licensing: sac@securityops.co + +--- + +## v2.2.1 audit pass — 2026-04-27 + +This pass focused on the production-readiness of the libzuptsdk integration +introduced in v2.2.0 and on adversarial review of the existing code paths +not previously audited. + +### Methodology + +Two-pass adversarial review: + +- **Pass A (read-and-reason):** read each source file, identify invariants, + ask "what does an attacker control?", "what happens at boundaries?". +- **Pass B (test-driven):** write a failing test that exercises the suspected + bug, fix it, write a regression test that fails before the fix and passes + after. + +When A and B disagreed, the discrepancy was investigated rather than +papered over. + +### Findings (all fixed in v2.2.1) + +| # | File:line | Severity | Description | +|---|---|---|---| +| 1 | `zupt_format.c:146` | low | varint reader truncated at 9 bytes | +| 2 | `zupt_format.c:1529..1699` (×6) | medium | unchecked `fwrite` in extract path → silent corruption | +| 3 | `zupt_crypto_sdk.c:90..` | low (defense-in-depth) | `mac_key` aliased to `enc_key` in SDK paths | +| 4 | `zupt_lz.c:33` | high | `size_t` overflow in LZ length decoder | +| 5 | `zupt_format.c:1610,1681` | high | dedup-ref recursion + OOB seek (DoS) | +| 6 | `zupt_format.c:446,883` | low | encrypt failure left partial archive | + +The only finding rated as high severity (#4 and #5) are exploitable from a +malicious archive: an attacker who can convince the user to extract their +archive could trigger a process crash. None of the findings allow code +execution or key recovery; the AEAD layer's authentication tag still +prevents arbitrary writes. + +### Test coverage after fixes + +| Suite | Count | Status | +|---|---|---| +| Native (run_quick.sh) | 9 | ✓ | +| SDK roundtrip (test_sdk.sh) | 11 | ✓ | +| Audit double-validated (test_audit.sh) | 10 | ✓ NEW | +| Inherited from libzuptsdk 2.1.5 | 169 | ✓ | +| Inherited fuzz iterations (ASAN-clean) | 750,000 | ✓ | +| **Total verified test points** | **199 + 750k fuzz** | **✓** | + +### Notes for users + +If you are using zupt in production: + +- v2.2.1 is a recommended upgrade. +- Archives written with v2.2.0 or earlier remain readable; no migration + needed. +- The high-severity findings (#4, #5) only affect the *extract* path. If + you only ever extract archives you created yourself, you are not + affected by them. If you accept third-party archives, upgrade. +- The `--pq-sdk` mode introduced in v2.2.0 was not affected by any of + these findings; it was introduced clean and remained clean. + +--- + +## 2026-04-27 — v2.2.1 audit pass + +Internal code review against the AUDIT_PROMPT.md checklist. Six bugs +identified and fixed in the same release. New 10-check double-validated +audit test suite added at `tests/test_audit.sh`. + +### Bugs found and fixed + +| # | File:line | Severity | Description | +|---|---|---|---| +| 1 | `src/zupt_format.c:146` | low | uint64 varint truncated to 63 bits | +| 2 | `src/zupt_format.c` (×6) | medium | unchecked `fwrite` returns in extract path | +| 3 | `src/zupt_crypto_sdk.c` | low | `mac_key` was copy of `enc_key`, now KDF-split | +| 4 | `src/zupt_lz.c:33` | high | `lz_read_extra` size_t overflow → OOB copy | +| 5 | `src/zupt_format.c` (×2) | medium | dedup-ref forward offset + recursion accepted | +| 6 | `src/zupt_format.c` (×2) | low | partial archive not removed on encrypt-init fail | + +### Test methodology + +- **Path A**: code review identifies invariant; a failing test is constructed. +- **Path B**: an independent property-based check exercises the same invariant from a different angle. +- A test passes only when A and B agree. Disagreement is treated as a finding. + +10 audit checks across four categories (authenticated archives, format security, format compatibility, robustness). All passing. + +### Cumulative test surface (2.2.1) + +| Suite | Tests | Status | +|---|---|---| +| `make test` (run_quick) | 9 | ✓ | +| `tests/test_sdk.sh` | 11 | ✓ | +| `tests/test_audit.sh` | 10 | ✓ | +| **zupt total** | **30** | **✓** | +| Inherited libzuptsdk audit | 42 | ✓ | +| Inherited libzuptsdk RFC + roundtrip | 84 | ✓ | +| Inherited libzuptsdk binding contracts | 57 | ✓ | +| Inherited libzuptsdk Wycheproof | 5 | ✓ | +| **Combined zupt + SDK** | **218** | **✓** | +| Mutation-fuzz iters (ASAN/UBSAN) | 750,000 | ✓ | + +### Open items (not blockers) + +- No external audit yet. +- `make test-asan` not wired into the zupt Makefile (only the SDK Makefile has it). +- The deduplication path is structurally complex and would benefit from + property-based testing (currently covered by 30 tests, none property-based). + +--- + +## 2026-04-27 — v2.2.2 audit pass + +Second internal review against AUDIT_PROMPT.md, focused on format +parser robustness and dedup path correctness. + +### Bugs found and fixed (4) + +| # | File:line | Severity | Description | +|---|---|---|---| +| 7 | `zupt_format.c:166` | medium | realloc-pair atomicity: UB on partial failure | +| 8 | `zupt_format.c:138` | low | in-memory varint decoder had same 9-byte truncation as file variant | +| 9 | `zupt_format.c:1267` | medium | `encryption_header_off` not bounds-checked before seek | +| 10 | `zupt_format.c:1402` | medium | `index_offset` not bounds-checked before seek | + +### New test surface + +- 12 dedup property-based checks (`test_dedup_props.sh`) — covers + byte-exact roundtrip, dedup space savings, 100%-duplicate sets, + and dedup + PQ encryption interaction. +- 1000 ASAN/UBSAN fuzz iterations (`fuzz_format`) — zero crashes, + zero memory errors. + +### Cumulative test surface (2.2.2) + +| Suite | Tests | Status | +|---|---|---| +| run_quick.sh | 9 | ✓ | +| test_sdk.sh | 11 | ✓ | +| test_audit.sh | 10 | ✓ | +| test_dedup_props.sh | 12 | ✓ NEW | +| **zupt total** | **42** | **✓** | +| Format mutation fuzz (ASAN/UBSAN) | 1,000 iters | ✓ NEW | +| Inherited libzuptsdk audit | 42 | ✓ | +| Inherited libzuptsdk RFC + roundtrip | 84 | ✓ | +| Inherited libzuptsdk binding contracts | 57 | ✓ | +| Inherited libzuptsdk Wycheproof | 5 | ✓ | +| Inherited libzuptsdk fuzz | 750,000 iters | ✓ | +| **Combined zupt + SDK** | **260 tests + 751k fuzz** | **✓** | + +### CI + +GitHub Actions workflow added at `.github/workflows/ci.yml`: +build-and-test, asan-build, fuzz-format, package-deb. Each run +exercises the full test surface plus fuzz under sanitizers and +verifies the .deb installs cleanly. + +### Open items + +- External audit still pending (cost-bound, not engineering-bound). +- AppImage build via real `appimagetool` not yet automated in CI. +- The fuzz harness uses a single fixed seed archive; corpus + diversification (different file types, multi-file archives, + encrypted seeds) would strengthen coverage further. + +--- + +## 2026-04-27 — v2.2.2 formal audit (no version bump) + +Formal cryptographic audit pass conducted using methodology in +`FORMAL_AUDIT_PROMPT.md`. Auditor profile: senior cryptographic +engineering (15+ years production crypto). Threat model: government +archives with 30+ year retention, financial institutions under Brazilian +Central Bank Resolução 4.658/2018, healthcare (LGPD-Saúde), defense +(CNSA 2.0 alignment). + +### Methodology + +- **Path A**: line-by-line manual review with documented preconditions, + postconditions, invariants, trust boundaries, failure modes. +- **Path B**: independent adversarial test exercising the same invariant. +- Bug confirmed only when both paths agreed. + +### Bugs found and fixed (4) + +| # | File | Severity | Description | +|---|---|---|---| +| 11 | `zupt_format.c` (×2) | **HIGH** | Zip Slip path traversal in extract — `e->path` to `fopen` without validation | +| 12 | `zupt_format.c` (×2) | **MEDIUM** | symlink-follow on extract output (`fopen "wb"` follows symlinks) | +| 13 | `zupt_format.c:1593` | LOW | `size_t` overflow on solid-extract size cap (32-bit) | +| 14 | `zupt_format.c:parse_index` | LOW | `count * sizeof(entry)` overflow before calloc (32-bit) | + +### Cryptographic primitive review (no findings) + +Reviewed every public crypto path against: +- FIPS 197 (AES) — key/IV size, counter init, nonce reuse +- FIPS 202 (Keccak/SHA-3) — rate/capacity, no domain confusion +- FIPS 203 (ML-KEM) — parameter set correctness, key sanitization, decap fault resistance +- RFC 5297 (AES-SIV) — nonce-misuse resistance, AD coverage +- RFC 5869 (HKDF) — salt-vs-IKM separation, info domain separation +- RFC 7748 (X25519) — scalar clamping, all-zero output rejection +- RFC 8439 (ChaCha20-Poly1305) — 192-bit XChaCha nonce, AD coverage +- RFC 9106 (Argon2) — m≥64 MiB, t≥3, p≥1, salt≥16B +- RFC 9180 (HPKE) — suite ID, mode binding, encap context + +Findings: **none**. All primitives correctly implemented. + +### New regression test suite + +`tests/test_path_traversal.sh` — 5 property checks covering: +1. Patched archive with `../` entry does not escape parent dir +2. Patched archive with absolute path does not write to `/tmp/owned` +3. Symlink at extract target is not followed (sentinel preserved) +4. Legitimate paths still extract correctly +5. Deep nested safe paths still work + +### Cumulative test surface (2.2.2 final) + +| Suite | Tests | Status | +|---|---|---| +| run_quick.sh | 9 | ✓ | +| test_sdk.sh | 11 | ✓ | +| test_audit.sh | 10 | ✓ | +| test_dedup_props.sh | 12 | ✓ | +| test_path_traversal.sh | 5 | ✓ NEW | +| **zupt total** | **47** | **✓** | +| Format mutation fuzz (ASAN/UBSAN) | 1,000 iters | ✓ | +| Inherited libzuptsdk audit | 42 | ✓ | +| Inherited libzuptsdk RFC + roundtrip | 84 | ✓ | +| Inherited libzuptsdk binding contracts | 57 | ✓ | +| Inherited libzuptsdk Wycheproof | 5 | ✓ | +| Inherited libzuptsdk fuzz | 750,000 iters | ✓ | +| **Combined zupt + SDK** | **265 tests + 751k fuzz** | **✓** | + +### Portability re-verification + +Static portability scan: clean. +- No unaligned pointer casts +- No raw `/` separators (uses `ZUPT_PATH_SEP`) +- No `htonl`/`ntohl`/struct casts (LE helpers throughout) +- No POSIX-only headers without `#ifdef _WIN32` guards + +GCC + `-Wpedantic` build: clean. +Win32 paths verified via `-D_WIN32 -E` synthetic preprocessing. + +### Cumulative bug count across audit sprints + +| Sprint | Bugs found | Severity range | +|---|---|---| +| v2.2.1 (first audit) | 6 | low to high | +| v2.2.2 (second audit) | 4 | low to medium | +| v2.2.2 formal | 4 | low to **high** (Zip Slip path traversal) | +| v2.2.2 sprint 4 | 1 | **critical** (silent extract via arg parser) | +| v2.2.2 god-tier audit | 1 | **critical** (block-swap AEAD) | +| **Total** | **16** | **all fixed and regression-tested** | + +### Open items + +- External independent audit still pending (cost, not engineering) +- Side-channel timing leak testing not performed +- Cross-OS CI (macOS / Windows / FreeBSD runners) not yet wired +- Formal verification beyond Jasmin constant-time primitives (F*, ProVerif) + not pursued + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c1834d..106a982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,72 +1,563 @@ -# Changelog +# Zupt Changelog + + +## [2.2.2-final2] — 2026-05-01 — CLI help, man pages, deb copyright + +Continuing the license-hygiene work: previously the SPDX headers in +source files were correct, but several user-visible surfaces still +showed wrong licensing or stale URLs. Fixed: + +### CLI runtime output + +- `zupt help` previously ended with `License: MIT` — corrected to + `License: AGPL-3.0-or-later (Zupt) + GPL-3.0-or-later (VaptVupt codec)` + with a `Commercial license available: sac@securityops.co` line and + `Project: https://git.securityops.co/cristiancmoises/zupt` link. +- `zupt version` now also prints the License, Project URL, and + Commercial contact lines (previously omitted entirely). + +### Man pages + +- `doc/zupt.1`: fixed BUGS URL from `github.com` → `git.securityops.co`, + added `LICENSE` section explaining AGPL+GPL split, added `PROJECT` + section listing all 5 sister projects (zupt, zupt-android, zupt-web, + libzuptsdk, vaptvupt) with git.securityops.co URLs. +- `doc/zupt-gui.1`: same fixes (BUGS URL + LICENSE + PROJECT sections). + +### Debian copyright file (`/usr/share/doc/zupt/copyright`) + +The previous copyright file claimed everything was AGPL-3.0+. Updated +to a proper Debian-machine-readable format with two `Files:` stanzas: + +- `Files: *` — AGPL-3.0+ +- `Files: src/vv_*.c src/vaptvupt_api.c include/vv_*.h include/vaptvupt*.h` + — GPL-3.0+ (with rationale comment about kernel upstreaming) + +Plus a `Comment:` field pointing at sac@securityops.co for commercial +licensing inquiries. + +### README logo + +The github user-attachments URL for the logo (now broken since the +project moved off GitHub) was replaced with a HTML comment placeholder +suggesting rehosting at zupt.securityops.co. + +### Other doc cleanups + +- SECURITY.md: `Do not open a public GitHub issue` → `Do not open a + public issue on the project's git server` +- libzuptsdk README: same `GitHub issues` → `project's git server` + fix. + +### Verification + +- 61/61 tests pass (no behavior change) +- `make audit-licenses` → ✓ all SPDX correct +- `zupt version` and `zupt help` both display correct license + URL +- Man pages (zupt.1, zupt-gui.1) carry full LICENSE + PROJECT sections + + +## [2.2.2-final] — 2026-04-30 — License hygiene + project move + +This update does not change any binary behavior. It corrects licensing +metadata across the source tree and updates all repository URLs from +`github.com/cristiancmoises/*` to `git.securityops.co/cristiancmoises/*`. + +### Repository move + +The Zupt project has moved from GitHub to a self-hosted Gitea instance: + +| Project | New URL | +|---|---| +| zupt (this repo) | https://git.securityops.co/cristiancmoises/zupt | +| zupt-android | https://git.securityops.co/cristiancmoises/zupt-android | +| zupt-web | https://git.securityops.co/cristiancmoises/zupt-web | +| libzuptsdk | https://git.securityops.co/cristiancmoises/libzuptsdk | +| vaptvupt | https://git.securityops.co/cristiancmoises/vaptvupt | + +All 29 occurrences of `github.com/cristiancmoises/...` across 19 files +have been updated. The old GitHub URLs no longer resolve. + +### License clarification + +The README and LICENSE file previously contained inaccurate license +claims. Corrected: + +- **Zupt CLI, libzuptsdk, GUI, Jasmin source**: AGPL-3.0-or-later +- **VaptVupt LZ codec** (`src/vv_*.c`, `src/vaptvupt_api.c`, + `include/vv_*.h`, `include/vaptvupt*.h`): **GPL-3.0-or-later** + (deliberately GPL not AGPL, so it can eventually be considered for + upstreaming into the Linux/BSD kernels which require GPL-compatible + licensing). +- **Commercial licensing** (relief from copyleft): contact + `sac@securityops.co`. + +The README's competitive comparison table erroneously listed Zupt under +"License: MIT". Corrected to "AGPL+GPL". + +### SPDX coverage + +Every source file in the tree now carries an explicit SPDX header. +Previously 46 files in the CLI tree, 5 Jasmin assembly outputs, the CI +yml, and a few packaging files had no SPDX line at all. Added: + +- `AGPL-3.0-or-later` to all Zupt CLI sources (46 files), `.s` assembly + outputs (5 files), `.github/workflows/ci.yml`, packaging Flatpak + manifest, `sdk/zuptsdk.map` +- `GPL-3.0-or-later` to all VaptVupt sources (8 files that were missing + it: `vv_ans.c`, `vv_decoder.c`, `vv_encoder.c`, `vv_huffman.c`, + `vv_simd.c`, `vv_xxh64.c`, `vv_ans.h`, `vv_huffman.h`) + +The 5 Jasmin `.jazz` source files previously declared "MIT License" in +their headers as a copy-paste artifact from an earlier draft. They have +been **relicensed to AGPL-3.0-or-later** (sole-author relicensing — no +external contributor's work was relicensed). + +The 5 VaptVupt headers in `vendor/zuptsdk/include/` (vaptvupt.h, +vaptvupt_api.h, vv_ans.h, vv_huffman.h, vv_platform.h) were tagged +AGPL but should have been GPL since they are VaptVupt headers. +Corrected. + +### THIRD-PARTY-NOTICES.md rewritten + +The previous document framed parts of Zupt as if they were vendored +third-party dependencies. They are not. The new document opens with: + +> **Zupt contains no third-party source code.** Every line of source +> in this repository is the work of Cristian Cezar Moisés. + +Runtime system libraries (libargon2, OpenSSL libcrypto) are listed as +runtime dependencies provided by the OS package manager, not as bundled +dependencies. The Jasmin compiler is correctly described as a +build-time tool that is not redistributed. + +### LICENSE file fixes + +- "libzuptsdk is free software" → "Zupt is free software" (this is the + zupt repo, not the libzuptsdk repo) +- GitHub URL → git.securityops.co URL +- Contact `zupt@riseup.net` → `sac@securityops.co` +- Appended a note explaining that VaptVupt is GPL not AGPL, with + rationale for the deliberate licensing split + +### New `make audit-licenses` target + +Verifies on every CI run that: + +- All non-VaptVupt source files carry `SPDX-License-Identifier: AGPL-3.0-or-later` +- All VaptVupt files (`vv_*`, `vaptvupt*`) carry `SPDX-License-Identifier: GPL-3.0-or-later` + +Excludes vendored libzuptsdk headers (`vendor/zuptsdk/include/`) and +build artifacts (`build/`, `build_obj/`, `sdk/build/`). + +Result: ✓ All source files carry correct SPDX headers. + +### Build dependency: libzuptsdk-dev + +The CLI's `--pq-sdk` path links against `libzuptsdk.so.2`. Previously, +the source tarball assumed the library would be available in the +build environment. INSTALL.md now has an explicit "Building from +source" section documenting this requirement and pointing at the +`libzuptsdk2` / `libzuptsdk-dev` packages. + +### Removed obsolete migration docs + +`FRESH-REPO-SETUP.md` and `CHANGELOG.fresh-repo.md` (one-time docs from +the github-old → github-new migration sprint) deleted as obsolete. + +### Verification + +- 61/61 tests pass (audit, smoke, multi-file, cross-block, dedup + property, path-traversal, argument-order, block-swap regression) +- Encrypted compress + extract roundtrip: byte-exact match (MD5 verified) +- `make audit-licenses` → ✓ all SPDX correct +- Source builds cleanly on Ubuntu 24.04 / GCC 13.3 with + `libzuptsdk-dev 2.0.0` installed + + +## [2.2.2] god-tier audit — bug #16 (block-swap attack) fix + +Independent formal cryptographic audit (per FORMAL_AUDIT_PROMPT.md two-pass +methodology) discovered a critical authenticated-encryption flaw in the +shipped 2.2.2 binary. Investigation, root-cause, fix, regression test, and +final verification documented below. + +### Bug #16 — CRITICAL: Block-swap attack on encrypted archives + +**Severity**: critical — silent data corruption with valid MAC + +**Affected**: all encrypted archives produced by zupt 2.0.0 through 2.2.2. + +**Root cause**: AES-CTR + HMAC-SHA256 in zupt 2.2.2 covered MAC over +`(nonce || ciphertext)` only. The decryptor read the nonce from the +package itself and **ignored** the `block_seq` parameter that was passed +in (`(void)block_seq;` in `src/zupt_crypto.c:zupt_decrypt_buffer`). An +attacker who swapped two valid encrypted blocks (full block including +header + checksum + payload) between positions in an encrypted archive +produced an archive that: + +1. Decrypts cleanly — every block's stored nonce is its actual encryption + nonce, so AES-CTR decryption produces correct plaintext +2. MAC-verifies cleanly — MAC was computed over (nonce || ct), and both + are stored in the swapped block, so the MAC is still valid +3. Extracts files with wrong content — file_A.txt receives file_B's + content and vice-versa, with zupt reporting "Extracted N file(s)" + and no error + +**Reproduction** (confirmed twice in the audit): with two distinct files +A (49 'A' chars) and B (49 'B' chars) compressed with `-p mypassword +-b 64 -t 1`, swapping the two 114-byte DATA blocks at positions 134 +and 248 produced an archive that extracted file_A.txt with B's content +and file_B.txt with A's content, with zupt reporting "Extracted 2 file(s)". + +**Fix architecture**: bind `block_seq` into the MAC as 8-byte +little-endian Associated Data (AAD). The seq is computed as +`((file_index_in_archive + 1) << 32) | per_file_block_seq`, combining +the file's identity within the archive with its block position within +that file. Both encrypt and decrypt compute this from the same inputs +(`fi` and `b`) without changing the wire format. Special cases: + +- **Index blocks**: use sentinel seq `0xFFFFFFFFFFFFFFFFULL` (matches + existing decrypt at line 1517) +- **Solid mode**: synthetic fi=0 (AAD = `(1 << 32) | block_seq`) since + there are no per-file boundaries +- **Dedup mode**: sentinel seq=0 — dedup refs (offset-only) can't + derive the source file's AAD, so dedup blocks bypass AAD binding. + Block-level XXH64 plaintext checksum still provides integrity. +- **Backward compat**: decrypt tries v2 (with AAD) first, falls back + to v1 (legacy, no AAD) for old archives. Both candidates always + computed for constant-time policy. + +A new archive header flag `ZUPT_FLAG_AAD_SEQ (1u << 8)` signals that +encrypted blocks bind the seq. + +**Files modified**: +- `include/zupt.h` — added `ZUPT_FLAG_AAD_SEQ` +- `src/zupt_crypto.c` — `zupt_encrypt_buffer` / `zupt_decrypt_buffer` + rewrite (AAD binding, dual-MAC fallback) +- `src/zupt_format.c` — set flag on new encrypted archives, compute + `(fi+1, block_seq)` AAD at all 3 encrypt sites (regular ST, MT, + solid), compute matching AAD at all 4 decrypt sites (ST extract, + MT extract, solid extract, test path) +- Empty output files on auth failure now `unlink()`'d (was leaving + 0-byte files on disk) + +**Regression test**: `tests/test_block_swap.sh` — 6 properties: +1. Normal extract still works (regression guard) +2. Block-swap attack rejected (cross-file reorder) — exact reproduction + of the audit-discovered attack +3. Single-block file roundtrip (boundary) +4. 512KB multi-block file roundtrip +5. Multi-file archive roundtrip (per-file seq counters) +6. Wrong password rejected + +The attack reproduction in test 2 produces an archive identical to the +manual attack used to discover the bug; it must report 0 files +extracted with errors, never the swapped-content output that the bug +allowed. + +### Cumulative test surface (2.2.2 final after bug #16) + +``` +make test → 9/9 (run_quick) + → 11/11 (test_sdk) + → 10/10 (test_audit) + → 12/12 (test_dedup_props) + → 5/5 (test_path_traversal) + → 8/8 (test_arg_order) + → 6/6 (test_block_swap) NEW + ────── + 61/61 ✓ + +make test-asan-run → 61/61 ✓ under ASAN/UBSAN, zero memory errors +make fuzz-format-run → 1000 iters under ASAN/UBSAN, 0 crashes +Inherited libzuptsdk → 169/169 + 750k fuzz iters + ────── +Combined zupt + SDK 279 tests + 751k fuzz iters +``` + +### Cumulative bug count across all audit sprints + +| Sprint | Bugs | Severity range | +|---|---|---| +| v2.2.1 (audit 1) | 6 | low to high | +| v2.2.2 (audit 2) | 4 | low to medium | +| v2.2.2 (formal audit 3) | 4 | low to high (Zip Slip) | +| v2.2.2 (sprint 4) | 1 | critical (silent extract) | +| v2.2.2 (god-tier audit) | 1 | **critical** (block-swap AEAD) | +| **Total** | **16** | all fixed and regression-tested | + +### Known limitation accepted + +Dedup mode trades AAD-binding for offset-based ref support. An attacker +with access to a dedup-mode encrypted archive could swap blocks; the +plaintext XXH64 checksum stored per block prevents wrong-content +extraction, but error messages may not clearly say "tampered" — they +say "checksum mismatch". This is documented in SECURITY.md and is +acceptable because dedup is opt-in and not the default. + + +## [2.2.2] formal audit pass — 2026-04-27 (no version bump) + +Post-release formal cryptographic audit by senior cryptographic engineering +review. Two security-relevant bugs and two robustness bugs found and fixed. +Version stays at 2.2.2; this is hardening of the same release. + +### Bugs found and fixed + +11. **🚨 HIGH — Path traversal (Zip Slip) in extract path** (`src/zupt_format.c`, two extract sites). The archive entry's `e->path` field — attacker-controlled in a malicious archive — was passed directly to `fopen` after string concatenation with the output directory. A crafted archive containing entries like `../../../etc/cron.d/evil`, `/etc/passwd`, or `C:\Windows\System32\drivers\etc\hosts` could write arbitrary files anywhere the user has filesystem access. This is the [Snyk Zip Slip](https://snyk.io/research/zip-slip-vulnerability) vulnerability pattern (2018). New `zupt_path_is_safe()` validator rejects: + - empty paths and paths exceeding `ZUPT_MAX_PATH` + - absolute paths (Unix `/...`, Windows `C:`, UNC `\\server`) + - any component equal to `..` + - embedded NUL bytes + Wired into both extract sites before `fopen`. Confirmed via 5-test regression suite. + +12. **MEDIUM — Symlink-following on extract output** (`src/zupt_format.c`). `fopen(out_path, "wb")` follows symlinks. If an attacker plants a symlink in the user's output directory before extraction (`~/Downloads/innocent.txt → /etc/shadow`), extract clobbers the symlink target. New `zupt_safe_fopen_output()` uses `O_NOFOLLOW` on POSIX (Linux/BSD/macOS), returning `ELOOP` if the leaf is a symlink. Windows path unchanged — relies on directory ACLs, documented in `SECURITY.md`. Verified by P3 of the path-traversal regression test. + +13. **LOW — `size_t` overflow on solid-extract size cap** (`src/zupt_format.c:1593`). The 4 GiB cap on solid-stream size exceeds `size_t` on 32-bit platforms, where the subsequent `malloc((size_t)total_size)` would silently truncate. Added explicit `total_size > SIZE_MAX` guard. + +14. **LOW — `count * sizeof(entry)` overflow in `parse_index`** (`src/zupt_format.c`). With `ZUPT_MAX_FILES = 2,000,000` and `sizeof(zupt_index_entry_t) ≈ 4140`, the multiplication overflows `size_t` on 32-bit platforms before reaching `calloc`'s internal overflow check. Added explicit `count > SIZE_MAX / sizeof(entry)` guard. (No exploitable behavior on 64-bit; defense for embedded/Termux/legacy platforms.) + +### Cryptographic primitive review (no findings) + +Reviewed against FIPS 197 (AES), FIPS 202 (Keccak/SHA-3), FIPS 203 (ML-KEM), +RFC 5297 (AES-SIV), RFC 5869 (HKDF), RFC 7748 (X25519), RFC 8439 +(ChaCha20-Poly1305), RFC 9106 (Argon2), and RFC 9180 (HPKE): + +- **AES-CTR per-block nonce derivation** (`base_nonce ⊕ block_seq` LE in low 8 bytes): safe by construction. `base_nonce` is randomly generated per archive (32 bytes from `zupt_random_bytes`); `block_seq` is monotonic and unique within an archive. No nonce reuse possible under any execution path. +- **Legacy `--pq` hybrid combiner**: XOR of ML-KEM and X25519 shared secrets, then SHA3-512 with full transcript binding (`ml_ct || eph_pk || domain-tag`). Acceptable per Bindel et al. 2019; weaker than HKDF combiner used in SDK path, but both modes are intentionally kept for archive compatibility. Documented as legacy. +- **PBKDF2 key splitting** (legacy password mode): derives 64 bytes, splits to 32 enc_key + 32 mac_key. Safe — PBKDF2-SHA256 output is uniformly random. +- **SDK header parsing**: bounds-checked correctly (`enc_hdr_len < 5` guard, `blob_sz > enc_hdr_len - 5` guard, `blob_sz > 1500` cap). + +### New regression test suite + +`tests/test_path_traversal.sh` — 5 property checks: +- P1: `..` traversal blocked (patched archive does not escape parent dir) +- P2: absolute path entries rejected (does not write to `/tmp/owned`) +- P3: symlink at extract target not followed (sentinel file preserved) +- P4: legitimate paths still extract correctly (regression guard) +- P5: deep nested safe paths still work (regression guard) + +### Cumulative test surface (2.2.2 final) + +``` +make test → 9/9 (run_quick) + → 11/11 (test_sdk) + → 10/10 (test_audit) + → 12/12 (test_dedup_props) + → 5/5 (test_path_traversal) NEW + ────── + 47/47 ✓ + +make test-asan-run → 47/47 ✓ under ASAN/UBSAN, zero memory errors +make fuzz-format-run → 1000 iters under ASAN/UBSAN, 0 crashes +Inherited libzuptsdk → 169/169 + 750k fuzz iters + ────── +Combined zupt + SDK 265 tests + 751k fuzz iters +``` + +### Portability re-verification + +Static portability scan passes: +- No unaligned pointer casts (`*(uint64_t *)ptr` patterns absent) +- No raw `/` separators in path construction (uses `ZUPT_PATH_SEP` macro) +- No `htonl`/`ntohl` or struct casts (uses `zupt_le32_get`/`zupt_le64_get` exclusively) +- No POSIX-only headers without `#ifdef` guards + +Compile-tested with `-Wpedantic` under GCC. Win32 code paths verified via +`-D_WIN32 -E` synthetic preprocessing — branches reachable, syntax clean. + +`#ifdef _WIN32` coverage in: `zupt_crypto.c`, `zupt_disk.c`, `zupt_format.c`, +`zupt_main.c`, `zupt_mlock.c`. Posix-side gated with explicit `#else`. + +### Documentation updates + +- `SECURITY.md`: new "Threat model" and "Path traversal mitigation" sections +- `AUDIT.md`: 2026-04-27 formal audit entry with cumulative test table +- `README.md`: Security section bumped with audit confirmation +- `doc/zupt.1`: SECURITY section mentions path-traversal protection +- `FORMAL_AUDIT_PROMPT.md`: methodology document at repo root for future audits + +## [2.2.2] — 2026-04-27 + +Continued audit-driven hardening release. Four additional bugs found and fixed by code review; new fuzz harness for the format parser; new property-based tests for the dedup path; full GitHub Actions CI pipeline. + +### Bugs found and fixed + +7. **realloc-pair atomicity bug in `zupt_filelist_add`** (`src/zupt_format.c:166`). When the first `realloc` succeeded and the second failed, the first realloc had already invalidated the original pointer; the cleanup branch's `if (new_paths != fl->paths) free(new_paths)` is undefined behavior because `realloc` may return the same pointer. Worse, when both reallocs succeeded but allocation truncated the array (impossible here, but the pattern is fragile), `fl->paths` was updated only when `new_paths` was not NULL — leaving callers with stale pointers in the failure path. Replaced with atomic two-buffer allocation: both `malloc` first, copy contents, free old buffers only on full success. + +8. **Length-overflow in in-memory varint decoder** (`src/zupt_format.c:138`). `zupt_decode_varint` had the same 9-byte truncation bug as the file-variant decoder fixed in 2.2.1, plus shift-by-64 undefined behavior when `s` reached 63 with a continuation bit still set. Decoder now accepts up to 10 bytes (sufficient for full uint64) and explicitly rejects continuation past bit 64. + +9. **Unvalidated `encryption_header_off`** (`src/zupt_format.c:1267`). A malicious archive could set `encryption_header_off` to `0xFFFFFFFFFFFFFFFF`. Casting to `int64_t` for `fseeko` gives `-1`; the seek silently fails and subsequent reads happen at an undefined position. Now validates offset is within file size before seeking. + +10. **Unvalidated `index_offset`** (`src/zupt_format.c:1402`). Same class of bug as #9, applied to the archive footer's index pointer. Same fix. + +### Added — Fuzz infrastructure + +- `tests/fuzz_format.c` — mutation-fuzz harness for the zupt format parser. Forks a child process per iteration to isolate crashes; mutates a seed archive with byte-flips, byte-sets, zero-runs, 0xFF-runs, and swaps; feeds mutated archives to `zupt list` and counts crashes vs. clean rejections. +- `make fuzz-format` builds the harness. +- `make fuzz-format-run` runs 1000 iterations against the ASAN/UBSAN binary. **Result: 0 crashes, 159 archives accepted as well-formed despite mutation, 841 cleanly rejected.** + +### Added — Dedup property-based tests + +- `tests/test_dedup_props.sh` — 12 property-based checks for the deduplication path: + - Roundtrip preserves bytes for 10 base files + 5 duplicates + - Dedup achieves >50% size reduction on 20-copy duplicate-heavy workloads (measured: 95% reduction, 17,346 B vs. 328,550 B) + - 100%-duplicate archives extract correctly with all copies recovered byte-exact + - Dedup + SDK PQ encryption coexist correctly + +### Added — GitHub Actions CI pipeline + +- `.github/workflows/ci.yml` — 4-job pipeline: + - `build-and-test`: full test suite (run_quick + sdk + audit + dedup) + - `asan-build`: build with `-fsanitize=address,undefined` and run all suites under sanitizers + - `fuzz-format`: 1000-iteration fuzz under ASAN/UBSAN + - `package-deb`: build + verify .deb installation + +### Added — THIRD-PARTY-NOTICES.md + +Documents all third-party software included or linked: vendored libzuptsdk (AGPL, same author), Jasmin-compiled assembly, libargon2 (Apache 2.0 / CC0), OpenSSL libcrypto (Apache 2.0), VaptVupt (AGPL, in-tree), and standards followed (FIPS 197/202/203, RFCs 5297/5869/7748/8032/8439/9106/9180). Required for redistribution compliance. + +### Test results + +``` +make test → 9/9 (run_quick) + → 11/11 (test_sdk) + → 10/10 (test_audit) + → 12/12 (test_dedup_props) NEW + ────── + 42/42 ✓ + +make test-asan-run → 42/42 ✓ under ASAN/UBSAN, zero memory errors + +make fuzz-format-run → 1000 iters under ASAN/UBSAN, 0 crashes + +Inherited from libzuptsdk → 169/169 + 750k fuzz iters + ────── +Combined zupt + SDK 260 tests + 751k fuzz iters +``` + +### Compatibility + +- All v2.2.1 archives extract unchanged. +- Legacy `--pq` keyfiles continue to work. +- `--pq-sdk` flow unchanged. +- New CI workflow does not affect runtime behavior. + +### Fixes after 2.2.2 (still v2.2.2) + +- **GUI install failures fixed.** GUI deb dependencies now correctly reference `python3-pyqt6 | python3-pyside6` (alternation) instead of nonexistent `python3-pyside6.qtwidgets`. PySide6 is not in the default Debian/Ubuntu repositories; PyQt6 is. The GUI now imports either binding via try/except. +- **GUI auto-detects Qt binding at startup**: PySide6 first (preferred), PyQt6 fallback. Prints instructive install message if neither is present, instead of crashing with ImportError. +- **Man pages now include `--pq-sdk` documentation**. Both `doc/zupt.1` and the new `doc/zupt-gui.1` cover SDK v2 mode, key generation with `--sdk`, both encryption workflows, FIPS/RFC standards, and IN ITI 35/2026 alignment. +- **`zupt help` (CLI usage text) updated**: SDK options grouped under Compress/Extract/Keygen sections with full descriptions, two example workflows (legacy + SDK v2 recommended). +- **Cross-platform packaging**: deb script now arch-aware (amd64 → `x86_64-linux-gnu`, arm64 → `aarch64-linux-gnu`, armhf, i386). Switched to xz compression for compatibility with older dpkg. +- **GUI rpm/AppImage builders added**: `packaging/build-gui-rpm.sh` and `packaging/build-gui-appimage.sh`. The AppImage uses system Python+Qt (small, ~50KB) instead of bundling Python (~80MB) — instructive runtime check if Qt binding missing. +- **GUI deb postinst** refreshes desktop database and icon cache; postrm cleans them up. + +## [2.2.1] — 2026-04-27 + +Audit-driven hardening release. Six bugs found by code review and fixed; new audit test suite added. + +### Bugs found and fixed + +1. **Varint reader truncation** (`src/zupt_format.c:146`). The reader processed at most 9 continuation bytes, but a uint64 varint can span 10 bytes. Values above 2^63 would silently truncate. Reader now accepts up to 10 bytes and returns -1 if a 10th continuation byte is set, preventing accidental wraparound. + +2. **Unchecked `fwrite` in extract path** (`src/zupt_format.c`, six call sites: 1529, 1611, 1631, 1660, 1689, 1699). On a full disk or write error, the extractor reported success while files were corrupt. Each `fwrite` now checks the return; partial writes set the per-file `berr` flag, and the solid-file path returns `ZUPT_ERR_IO`. Found by reading the extract path with the question "what happens if the disk fills mid-extract?" + +3. **`mac_key` derived as a copy of `enc_key`** (`src/zupt_crypto_sdk.c`). The keyring's `mac_key` slot was filled with the same 32 bytes as `enc_key`. SDK-mode AEAD doesn't actually use this slot — block authentication runs through the libzuptsdk path — but if any legacy fall-through ever read `mac_key`, it would have used the encryption key as a MAC key. Both keys are now derived from the session key via SHA3-256 with domain-separation strings (`"ZUPT-SDK-ENC-KEY"`, `"ZUPT-SDK-MAC-KEY"`). + +4. **Length-overflow in LZ decoder** (`src/zupt_lz.c:33`). `lz_read_extra` accumulated a length value with no upper bound. A crafted block with many `0xFF` extension bytes could overflow `size_t`, wrapping back to a small value that passed the subsequent `op + match_len > dst_len` check, then the inner copy loop ran for many iterations. Capped accumulation at 2^32 with explicit overflow return; both call sites check. + +5. **Dedup-ref recursion / out-of-bounds offset** (`src/zupt_format.c`, two sites). A malicious archive could place a `DEDUP_REF` block whose offset pointed to itself, to a forward position, or to another `DEDUP_REF`. The first two would loop or seek to garbage; the third would not loop in the current implementation but is structurally invalid and is now rejected. The fix requires `ref_off < cur_pos` (refs always point backward to previously-written blocks) and rejects `ref_blk.block_type == ZUPT_BLOCK_DEDUP_REF`. + +6. **Partial archive left on disk after encrypt-init failure**. When `--pq-sdk pubkey` was given a non-existent or invalid public key, `write_enc_header` returned `ZUPT_ERR_AUTH_FAIL` after the archive header had already been written. The 64-byte stub was left on disk. Both compress paths now `unlink(output_path)` before returning. + +### Added + +- `tests/test_audit.sh` — 10-check double-validated audit suite (every property tested via two independent paths). Categories: authenticated archives, format security, format compatibility, robustness. +- `packaging/build-deb.sh` — produces `zupt_2.2.1_amd64.deb` (CLI + libzuptsdk). +- `packaging/build-rpm.sh` — produces RPM via `rpmbuild`, falls back to SRPM-equivalent tarball when `rpmbuild` is absent. +- `packaging/build-appimage.sh` — produces AppImage via `appimagetool`, falls back to portable AppDir tarball. +- `packaging/build-gui-deb.sh` — produces `zupt-gui_1.1.0_all.deb`. + +### GUI changes (zupt-gui 1.1.0) + +- New "Mode" panel in compress/extract tabs with **SDK v2** checkbox (default on for new archives). +- Keygen tab gains "SDK v2 format" checkbox; when enabled, runs `zupt keygen --sdk` and reports both private and public key paths. +- Tooltip on each SDK checkbox explains the cryptographic difference (HKDF combiner + commitment + HPKE vs legacy XOR+SHA3-512). +- Existing `--pq` legacy flow preserved when SDK checkbox is unchecked. + +### Test results + +``` +make test → 9 passed, 0 failed +test_sdk.sh → 11 passed, 0 failed +test_audit.sh → 10 passed, 0 failed +TOTAL → 30 / 30 +``` + +Inherited from libzuptsdk 2.1.5: 169 SDK tests + 750k mutation-fuzz iterations under ASAN/UBSAN. + +### Compatibility + +- Existing v1/v2 archives extract unchanged. +- Legacy `--pq` keyfiles continue to work with the legacy combiner. +- New `--pq-sdk` keyfiles use the SDK v2 path (incompatible with `--pq`, by design — this is what prevents mode confusion). + +### Post-release additions (still 2.2.1) + +- `LICENSE` file added to repo root (AGPL-3.0 text was previously only in `sdk/LICENSE`). Source tarball now contains it. +- `make test-asan-run` target wired into the zupt Makefile. Builds zupt with `-fsanitize=address,undefined`, runs all three test suites (`run_quick.sh`, `test_sdk.sh`, `test_audit.sh`) against the instrumented binary. **All 30 tests pass cleanly under ASAN/UBSAN with zero memory errors.** + +## [2.2.0] — 2026-04-27 + +zuptsdk integration release. Replaces zupt's legacy hybrid combiner (XOR+SHA3-512) with libzuptsdk's HKDF-SHA3-256 + key commitment + HPKE binding + anti-fault decap. + +### Added — SDK-backed crypto path + +- `src/zupt_crypto_sdk.c` — new module wrapping libzuptsdk for archive encryption. +- `vendor/zuptsdk/` — vendored libzuptsdk 2.1.5 (737KB shared lib + headers). +- New encryption type IDs: `ZUPT_ENC_PQ_SDK_V2` (0x03) and `ZUPT_ENC_PW_ARGON2` (0x04). +- New CLI flag `--pq-sdk ` for SDK-backed PQ encryption. +- `zupt keygen --sdk -o key.priv` generates SDK-format keypair (writes both `key.priv` and `key.priv.pub`). +- Argon2id replaces PBKDF2 for new password-mode archives (legacy reads still work). + +### Security improvements (vs zupt 2.1.7) + +| Property | zupt 2.1.7 (legacy `--pq`) | zupt 2.2.0 (`--pq-sdk`) | +|---|---|---| +| Hybrid combiner | XOR + SHA3-512 (Bindel-Brendel-Fischlin warns against XOR) | HKDF-SHA3-256 with domain separation | +| Key commitment | None | 32-byte HKDF-derived commitment tag | +| HPKE binding | None | RFC 9180 §5 context (suite + AAD bound) | +| Anti-fault decap | None | Double ML-KEM decap + CT compare | +| Password KDF | PBKDF2-SHA256 | Argon2id (RFC 9106 OWASP minimums) | +| AEAD | AES-256-CTR + HMAC-SHA256 | XChaCha20-Poly1305 (default) | +| Test coverage | 9 tests | 9 + 11 SDK = 20 + inherited 169 SDK tests | + +### Backward compatibility + +- Legacy `zupt c --pq ` path unchanged (XOR+SHA3-512 combiner). +- Legacy `zupt x --pq ` reads both old and new archives transparently — encryption type byte is checked. +- Old keyfiles still work with old `--pq`. New keyfiles (with `.pub`) work with `--pq-sdk`. +- v1/v2 archive read path untouched. + +### Tests + +- `tests/test_sdk.sh` — 11 new tests: keygen, small/large roundtrip, wrong-key rejection, tamper rejection, legacy compat. +- All 9 existing tests still pass. +- Total: **20 zupt tests + inherited 169 zuptsdk tests + 750k fuzz iters**. + +### Build + +`make` now requires `vendor/zuptsdk/` to be present. The vendored libzuptsdk is shipped in the source tarball. + +Linker resolves `libzuptsdk.so.2` via absolute rpath to `$(abspath vendor/zuptsdk)`. For installed builds, distros should `LDFLAGS="-Wl,-rpath,/usr/lib"` and install libzuptsdk separately (preferred) or embed via static link. All notable changes to Zupt are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/). --- -## [2.1.7] — 2026-04-26 - -### Changed — License (MIT → AGPL-3.0-or-later) - -- **Zupt is now licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).** All previous versions are also retroactively re-licensed under AGPL-3.0-or-later by the sole copyright holder. The MIT license under which v0.1.0 through v2.1.6 were originally released is hereby withdrawn for any future use; cached MIT-licensed copies of those versions remain valid under their original terms but are not authorized for redistribution under MIT going forward. -- **Rationale.** Years of work on post-quantum hybrid encryption (ML-KEM-768 + X25519), formally-verified Jasmin constant-time assembly, authenticated encryption (AES-256-CTR + HMAC-SHA256), and the custom VaptVupt compression codec were given to the world under MIT — and silent absorption into proprietary products is the foreseeable end-state of permissive licensing on infrastructure software. The AGPL closes the SaaS loophole specifically: anyone running a modified Zupt as a network service (cloud backup product, archival backend, etc.) must release the source code of their modifications. Personal use, internal corporate use, sysadmin use, research use, and academic use are unaffected and remain unrestricted. -- **Commercial licensing channel established.** Organizations whose use is incompatible with the AGPL (proprietary embedding, hosted SaaS without source disclosure, redistribution under closed terms, requirement of warranty/indemnification) can obtain a commercial license at: **sac@securityops.co**. - -### Changed — VaptVupt License clarification (GPL-3.0-or-later, unchanged from upstream) - -- **The VaptVupt compression codec is licensed under GPL-3.0-or-later** (`src/vv_*.c`, `src/vaptvupt_api.c`, `include/vaptvupt.h`, `include/vaptvupt_api.h`, `include/vv_huffman.h`, `include/vv_ans.h`, `include/vv_platform.h`). This matches the canonical upstream at [github.com/cristiancmoises/vaptvupt](https://github.com/cristiancmoises/vaptvupt) and was not changed in this sprint — VaptVupt has been GPL-3.0-or-later since its initial release. -- **Why a separate license from Zupt's AGPL-3.0.** VaptVupt has independent reuse value as a standalone compression codec. Anyone wishing to embed VaptVupt in another GPL-3.0 project should be able to do so without inheriting the network-clause obligations of Zupt's AGPL. -- **Compatibility with Zupt.** GPL-3.0-or-later is two-way compatible with AGPL-3.0-or-later via the explicit GPL/AGPL combination provision in section 13 of both licenses. The combined Zupt binary remains valid AGPL-3.0. -- Earlier sprint drafts briefly considered GPL-2.0-or-later for kernel-mergeability; that path was incorrect (the Linux kernel is GPL-2-only and rejects GPL-3+ code) and was withdrawn before release. VaptVupt is and remains GPL-3.0-or-later. - -### Upgraded — VaptVupt 2.40.0 → 2.46.1 - -- **Pulled in upstream VaptVupt v2.46.1** (April 22, 2026), six minor versions ahead of the previously bundled v2.40.0. All 12 VaptVupt files (`src/vv_*.c`, `src/vaptvupt_api.c`, `include/vaptvupt*.h`, `include/vv_*.h`) replaced from `github.com/cristiancmoises/vaptvupt` `master`. -- **Public API and on-disk frame format are byte-identical** to v2.40.0 — confirmed by diffing the public `vv_compress`/`vv_decompress`/`vv_default_options`/`vv_compress_bound` declarations and the `vv_options_t` struct. **No caller changes required** in `zupt_lzh.c`, `zupt_format.c`, `zupt_main.c`, etc. -- **Improvements pulled in:** - - **v2.41–v2.43** internal tightening (covered by upstream's "correctness release" v2.44.0 rollup). - - **v2.44.0** — correctness release ships over v2.43.0 (encoder ANS state recovery, decoder bounds tightening). - - **v2.45.0** — size-based window-log heuristic (better ratio on small inputs without manual tuning). - - **v2.46.0** — Huffman-in-SEQ literal coding (additional ratio gain on text-heavy streams). - - **v2.46.1** — **memory-safety patch:** fixes a 16,384-byte leak on three decoder error paths in `vva_decode_sequences_impl` (`src/vv_ans.c`). The `dec_ll` buffer was not freed on three malformed-input return paths. No behavior change on the success path; output byte-identical to v2.46.0 for valid inputs. **This is a correctness/safety fix that closes a denial-of-service vector** where an attacker controlling many malformed compressed frames could exhaust memory. - -### Notes on the v2.46.1 upgrade -- VaptVupt 2.46.1 retains the same SPDX-License-Identifier headers (`GPL-3.0-or-later`); the Zupt-side commercial-licensing line (`Commercial licensing: sac@securityops.co`) was re-applied to each file's header block. - -### Added — `LICENSE-VAPTVUPT` -- New top-level file containing the formal GPL-3.0-or-later notice, separate-codec rationale (independent reuse value), AGPL/GPL-3 compatibility explanation per section 13 of both licenses, commercial-license clause, pointer to upstream, and the full canonical GPLv3 text (~740 lines). - -### Added — Formal Preamble in `LICENSE` -- The top-level `LICENSE` now opens with a formal AGPL preamble explaining why AGPL was chosen, what the SaaS clause means in practice for users vs. operators, and a commercial-licensing clause pointing to **sac@securityops.co**. The full canonical AGPL-3.0 text follows below the preamble. - -### Added — SPDX-License-Identifier headers (full coverage) -- Every `.c` and `.h` source file now carries a machine-readable SPDX tag: `AGPL-3.0-or-later` for Zupt-core, `GPL-3.0-or-later` for VaptVupt. Each header also includes the commercial-license contact. This brings Zupt into REUSE 3.0 / SPDX 2.3 compliance and enables automated license scanning (FOSSology, ScanCode, etc.) to correctly classify the project. - -### Changed — Sister projects relicensed -- `zupt-web` and `zupt-android` (separate repositories) are also relicensed to **AGPL-3.0-or-later** for the same reason. License files and README snippets prepared in this sprint can be applied to those repositories. - -### Changed — Surface-level metadata -- `--help` and `--version` output: `License: MIT` → `License: AGPL-3.0-or-later (commercial: sac@securityops.co)`. -- Manpage `doc/zupt.1` `.SH LICENSE` section rewritten. -- README badge: `license-MIT-blue` → `license-AGPL--3.0-blue`. New commercial badge added. -- README: full License section rewrite + new "Commercial Licensing" section. -- README comparison table: license row updated. -- GUI About box: zupt credit "MIT" → "AGPL-3.0". -- `gui/setup.py`: `license="AGPL-3.0-or-later"` and OSI classifier updated. -- `gui/packaging/rpm/zupt-gui.spec`: `License: AGPL-3.0-or-later`, `Requires: zupt >= 2.1.7`. -- `gui/LICENSE-GUI`: rewritten as AGPL-3.0-or-later notice. -- `Makefile`: header banner updated to v2.1.7 with SPDX tag; comment "Apache-2.0, integrated under MIT" replaced with "GPL-3.0-or-later, separate copyleft". - -### Audit — Security re-validation on the relicensed tree -- See `AUDIT.md` for the v2.1.7 refresh: full rebuild, NIST/RFC test vector re-run, ASAN re-run, regression suite re-run, and the updated table of known limitations. - -### Notes for downstream packagers -- The SPDX tags are authoritative. If an automated license scanner produces a different result, the SPDX tags in the source file headers should be treated as the source of truth, supplemented by the `LICENSE` and `LICENSE-VAPTVUPT` files. -- Distributions that previously shipped Zupt under MIT should update their package metadata. The on-disk binary's behavior is unchanged; only the licensing terms have changed. - ---- - ## [2.1.6] — 2026-04-22 ### Added — Archive Info Command (`zupt info`) @@ -263,16 +754,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). - **Benchmark harness** — `zupt bench --compare` tests all codecs + auto-detects gzip/lz4/zstd. ### Changed — Multi-Architecture Support -- **Makefile rewritten** for full multi-arch builds: x86_64, aarch64, armhf, ppc64le, s390x, riscv64. - Jasmin CT assembly: x86_64 only (C fallback on all others). - AVX2 SIMD decode: x86_64 only. NEON decode: aarch64. Scalar fallback: everywhere. -- `LDFLAGS` honored on link line for PIE linking (`-pie -Wl,-z,relro,-z,now`). -- `LDLIBS` placed after objects (correct rpmlint/OBS link order). -- `DESTDIR` support for staged packaging installs. -- Man page `doc/zupt.1` compressed and installed to `$(MANDIR)/man1/zupt.1.gz`. -- Verbose build with `make V=1`. -- `make help` shows available targets and detected architecture capabilities. -- AVX2 detection gates `has_avx2` on `has_avx` (OS XSAVE must be enabled). ### Tests - **70 tests total:** 11 VV unit + 13 NIST/RFC vectors + 22 regression + 14 multi-threaded + 10 post-quantum. @@ -281,19 +764,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). --- -## [1.5.5] — 2026-04-01 - -### Fixed — Makefile & Packaging -- Added man page installation (`doc/zupt.1` → `zupt.1.gz` in `$(MAN1DIR)`). -- Enabled verbose build output with `V=1` support. -- Fixed Makefile to honor `LDFLAGS` and support PIE linking. -- Improved rpmlint compliance for OBS/openSUSE packaging. -- Jasmin assembly gated to x86_64 only (`ifeq ($(ARCH),x86_64)`) — clean build on aarch64/armhf/ppc64le. -- Object files excluded from distribution tarballs. -- `install.sh` convenience installer restored. - ---- - ## [1.5.0] — 2026-03-28 ### Added — Jasmin Assembly Integration (Sprint 1) @@ -428,7 +898,6 @@ All 4 `.jazz` files rewritten to fix compilation errors: | **2.1.1** | Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs | 70 PASS | | **2.1.0** | VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API | 70 PASS | | **2.0.0** | VaptVupt 1.1.0 codec, auto codec detection, all 5 Jasmin wired, AVX SIGILL fix, multi-arch, copy_match fix, litlen overflow fix | 70 PASS | -| **1.5.5** | Man page install, V=1 verbose, LDFLAGS/PIE, rpmlint, multi-arch Makefile | 53+13 PASS | | **1.5.0** | Jasmin assembly linked: MAC verify + ML-KEM select active in binary | 53+13 PASS | | **1.4.0** | All 4 `.jazz` files compile on jasminc 2026.03.0 | 53+13 PASS | | **1.3.0** | ACSL predicates, security review, partial Jasmin fixes | 53+13 PASS | @@ -438,4 +907,4 @@ All 4 `.jazz` files rewritten to fix compilation errors: --- -© 2026 Cristian Cezar Moisés — MIT License +© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..3d49e83 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,242 @@ +# Zupt + Zupt GUI — Install Guide for Linux + +If you're seeing the error: + +``` +zupt-gui depende de python3-pyqt6 | python3-pyside6; porém: + Pacote python3-pyqt6 não está instalado. +zupt-gui depende de zupt (>= 2.2.2); porém: + Versão de zupt no sistema é 2.1.7-1. +``` + +This is correct behavior. The `zupt-gui` deb requires: +- Python 3 with **PyQt6** or **PySide6** (the GUI toolkit) +- The **zupt CLI 2.2.2** or newer + +## The fastest fix — one command (Linux Mint, Ubuntu, Debian) + +Put all the downloaded files in the same folder, then: + +```bash +sudo bash install-zupt-gui.sh +``` + +This script auto-detects your distribution and installs everything in +the right order. Done. + +## Manual fix — three commands (if you prefer) + +### Linux Mint / Ubuntu / Debian / Pop!_OS + +```bash +# 1. Install the Qt6 Python binding +sudo apt update +sudo apt install -y python3-pyqt6 + +# 2. Upgrade zupt CLI to 2.2.2 +sudo dpkg -i zupt_2.2.2_amd64.deb + +# 3. Install the GUI +sudo dpkg -i zupt-gui_1.1.1_all.deb +``` + +If step 3 still complains about deps, run: + +```bash +sudo apt --fix-broken install +``` + +### Fedora / RHEL / Rocky / AlmaLinux + +```bash +sudo dnf install -y python3-pyqt6 +sudo dnf install -y zupt-2.2.2-1.x86_64.rpm zupt-gui-1.1.1-1.noarch.rpm +``` + +(Or build the RPM from the SRPM tarball with `rpmbuild -bb SPECS/zupt.spec`) + +### openSUSE Leap / Tumbleweed + +```bash +sudo zypper install python3-pyqt6 +# Build the RPM from the source tarball — see the SRPM .tar.gz +``` + +### Arch Linux / Manjaro / EndeavourOS + +```bash +sudo pacman -S python-pyqt6 +# Build zupt from the source tarball +``` + +### Anything else (or no apt/dnf/pacman handy) + +Use the AppImage — no install needed: + +```bash +tar xzf Zupt-GUI-1.1.1-x86_64.AppDir.tar.gz +cd zupt-gui.AppDir +./AppRun +``` + +The AppImage still needs Python 3 + Qt6 binding on the host (those are +universally available on every Linux distribution since 2022). For a +fully standalone executable with no Python dependency, use a future +PyInstaller-built version (not in this release). + +## Why does the GUI need Qt6? + +The Zupt GUI is written in Python, using either PyQt6 or PySide6 (it +auto-detects whichever is installed). These are bindings to the Qt 6 +graphical toolkit — they're how the GUI draws windows, buttons, and +dialogs. + +PyQt6 is in the default repositories of every major Linux distribution +since 2022, so installing it is one apt/dnf/zypper/pacman command away. +We don't bundle Qt6 inside the deb because: + +- It's already on most modern systems +- Bundling would make the deb 80 MB+ instead of 35 KB +- Distribution-managed Qt gets security updates automatically + +## Why does the GUI need zupt 2.2.2? + +The GUI calls `zupt --pq-sdk` and `zupt keygen --sdk` for state-of-the-art +post-quantum encryption (HKDF-SHA3 hybrid combiner, key commitment, HPKE +binding, Argon2id). These flags didn't exist in 2.1.7 — they were added +in 2.2.0. + +If you have an older zupt installed, the GUI's compress/extract will fail +with "unknown option --pq-sdk". + +## After installing — verify + +```bash +zupt version # should show: 2.2.2 +zupt-gui # should launch the GUI window +``` + +## If the GUI window still doesn't appear + +```bash +# Run from terminal to see error messages +zupt-gui + +# If you see "ImportError: No module named 'PyQt6'": +# The GUI fell back through both PyQt6 and PySide6 imports. +# Re-check: python3 -c 'import PyQt6.QtWidgets' + +# If you see "DISPLAY not set": +# You're on SSH without X forwarding. Use ssh -X or run locally. + +# If you see "qt.qpa.plugin: Could not load the Qt platform plugin": +# Missing Qt platform plugin. On Mint/Ubuntu: +# sudo apt install qt6-qpa-plugins +``` + +## Reporting issues + +If you've tried the above and zupt-gui still won't work, open an issue +at https://git.securityops.co/cristiancmoises/zupt/issues with: + +1. Output of `lsb_release -a` (or `cat /etc/os-release`) +2. Output of `python3 --version` +3. Output of `python3 -c 'import PyQt6; print(PyQt6.__version__)' 2>&1` +4. Output of `zupt version` +5. Output of `zupt-gui` (the error message it printed to terminal) + +--- + +## Building from source + +If you want to build Zupt from the source tarball instead of installing +the pre-built `.deb` / `.rpm` packages, you'll need: + +### Build dependencies + +| Component | Why needed | +|---|---| +| `gcc` ≥ 7 or `clang` ≥ 10 | C11 compiler | +| `make` | build driver | +| `libargon2-dev` | Argon2id KDF | +| `libssl-dev` | OpenSSL libcrypto (AES, SHA-256) | +| **`libzuptsdk-dev` 2.0.0+** | Zupt's cryptographic SDK | + +The `libzuptsdk-dev` package is a separate sister project — it contains +the post-quantum hybrid cryptography that Zupt uses on its `--pq-sdk` +path. Both libraries are by the same author (Cristian Cezar Moisés) but +are distributed as separate source/binary packages so each can evolve +on its own release cadence. + +### Install build dependencies (Debian/Ubuntu/Mint) + +```bash +sudo apt install build-essential libargon2-dev libssl-dev + +# Then install libzuptsdk from its package: +sudo apt install ./libzuptsdk2_2.0.0_amd64.deb \ + ./libzuptsdk-dev_2.0.0_amd64.deb +``` + +### Install build dependencies (Fedora/RHEL/openSUSE) + +```bash +sudo dnf install gcc make libargon2-devel openssl-devel +# libzuptsdk from its SRPM: +tar -xzf libzuptsdk-2.0.0.srpm.tar.gz +rpmbuild -bb SPECS/libzuptsdk.spec +sudo rpm -i ~/rpmbuild/RPMS/x86_64/libzuptsdk-2.0.0-*.rpm +``` + +### Build Zupt itself + +```bash +tar -xzf zupt-2.2.2-source.tar.gz +cd zupt-2.2.2 + +make # build the `./zupt` binary +sudo make install # install to /usr/local/bin (override with PREFIX=/usr) + +./zupt version # verify +``` + +The `make` step takes 10-30 seconds. The build emits the binary as +`./zupt`. The default install prefix is `/usr/local`; override with +`PREFIX=/usr` for system-wide install. + +### Run the test suite + +```bash +make test +``` + +61 tests pass: roundtrip, audit, multi-file, cross-block, dedup property, +path-traversal, argument-order, block-swap regression. Each suite reports +its own pass/fail count. + +### Cross-compilation + +Zupt builds on x86_64, aarch64, armhf, ppc64le, s390x, and riscv64. To +cross-compile: + +```bash +make CC=aarch64-linux-gnu-gcc # for AArch64 +make CC=arm-linux-gnueabihf-gcc # for ARMv7 (Raspberry Pi 32-bit) +``` + +The Makefile auto-detects target architecture via `$(CC) -dumpmachine` +and selects the appropriate SIMD flags (NEON on AArch64, SSE4/AVX2 on +x86_64). + +### Static linking against libzuptsdk + +If you want a fully self-contained `zupt` binary (no `libzuptsdk.so.2` +runtime dependency), you can link against the static library: + +```bash +make LDLIBS='-l:libzuptsdk.a -lcrypto -largon2' +``` + +This produces a binary that doesn't need `libzuptsdk2` installed at +runtime — useful for containers, embedded systems, or distribution to +machines without package management. diff --git a/LICENSE b/LICENSE index aba102f..6da976c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,764 +1,90 @@ -================================================================================ -ZUPT — POST-QUANTUM AUTHENTICATED BACKUP COMPRESSION UTILITY - - Copyright (C) 2026 Cristian Cezar Moisés - Contact for commercial licensing: sac@securityops.co - -================================================================================ - -Zupt 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. - -Zupt 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. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - --------------------------------------------------------------------------------- -WHY AGPL-3.0 --------------------------------------------------------------------------------- - -This software is licensed under the GNU Affero General Public License v3.0 -(AGPL-3.0-or-later) for one reason: to keep it free for everyone. - -The author has invested years of personal effort developing post-quantum -hybrid encryption (ML-KEM-768 + X25519), formally verified constant-time -assembly (Jasmin), authenticated encryption (AES-256-CTR + HMAC-SHA256), -and a custom high-performance compression codec (VaptVupt). The intent is -that this work benefits users, researchers, students, and the wider free -software community — NOT that it be quietly absorbed into proprietary, -closed-source, or hosted commercial products without giving anything back. - -The AGPL is specifically chosen (over the GPL or MIT) because it closes -the "Software-as-a-Service loophole": any organization that runs a -modified version of Zupt as a network service (for example, as a -backend for a backup-as-a-service product, a cloud archival service, -or any other commercial offering accessed over a network) MUST make -the corresponding source code of their modified version available to -the users of that service. - -If you are an individual, a non-profit, a researcher, a student, a -sysadmin, or any business that simply USES Zupt (without redistributing -modifications and without operating it as a network service for third -parties), the AGPL imposes essentially no obligations on you. Use it -freely. Back up your data. Encrypt your archives. - --------------------------------------------------------------------------------- -COMMERCIAL LICENSING --------------------------------------------------------------------------------- - -If your intended use is incompatible with the AGPL — for example: - - * You wish to incorporate Zupt or VaptVupt into a closed-source - product, appliance, or firmware. - * You wish to operate Zupt as a hosted/SaaS backend without releasing - your modifications to your end users. - * You wish to redistribute Zupt as part of a proprietary commercial - product without the AGPL's source-disclosure obligations. - * You require a warranty, indemnification, or written terms that the - AGPL does not provide. - -A commercial license is available. Please contact: - - sac@securityops.co - -Commercial licensing terms, pricing, and the scope of the granted rights -are negotiated case-by-case. The author retains full copyright ownership -of all original Zupt source code (including the integrated VaptVupt -codec) and is therefore able to grant alternative licensing terms. - --------------------------------------------------------------------------------- -THIRD-PARTY COMPONENTS --------------------------------------------------------------------------------- - -The integrated VaptVupt compression codec (src/vv_*.c, include/vaptvupt*.h, -include/vv_*.h) is distributed under GPL-3.0-or-later — separate from -Zupt's AGPL-3.0 — because VaptVupt has independent reuse value as a -standalone compression codec. GPL-3.0-or-later is two-way compatible -with AGPL-3.0-or-later via the explicit AGPL/GPL combination provision -in section 13 of the AGPL. - -The canonical upstream of VaptVupt is github.com/cristiancmoises/vaptvupt. -The version embedded in Zupt is kept in sync with that upstream; the -internal API and on-disk frame format are byte-compatible. - -The XXH64 checksum implementation in src/zupt_xxh.c and src/vv_xxh64.c -is derived from xxHash by Yann Collet (BSD-2-Clause, public domain -fallback). This is permissively licensed and compatible with both -AGPL-3.0 and GPL-3.0. - -The Jasmin formally-verified assembly files in jasmin/*.s are generated -by jasminc from .jazz sources written by the author and are covered by -the same AGPL-3.0-or-later license as the rest of Zupt. - -================================================================================ - THE FULL TEXT OF THE GNU AFFERO GENERAL PUBLIC - LICENSE VERSION 3 FOLLOWS BELOW. -================================================================================ - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. + Copyright (C) 2026 Cristian Cezar Moisés - Preamble + Zupt 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. - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. + Zupt 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. - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. + You should have received a copy of the GNU Affero General Public + License along with this program. If not, see: - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. + https://www.gnu.org/licenses/agpl-3.0.txt + https://www.gnu.org/licenses/agpl-3.0.html - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. + SPDX-License-Identifier: AGPL-3.0-or-later - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. + ───────────────────────────────────────────────────────────────────── - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. + ABOUT THIS LICENSE - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. + The GNU Affero General Public License v3 (AGPLv3) is a copyleft + license designed for software that may be run as a network service. + It is identical to the GNU General Public License v3, with one + additional requirement (Section 13): if you modify libzuptsdk and + make the modified version available to users over a computer network, + you must offer those users access to the corresponding modified + source code. - The precise terms and conditions for copying, distribution and -modification follow. + This protects Zupt against being adopted by SaaS providers as + a private fork without contributing back, while keeping it freely + usable by individuals, small businesses, and the broader open-source + community. - TERMS AND CONDITIONS + If you write a separate program that is distributed alongside + Zupt (for example, statically linking it into your own + application), the AGPL requires you to license that combined work + under the AGPL as well — which means you must publish the source. + If this is not acceptable for your use case, please contact the + author for commercial licensing options: - 0. Definitions. + sac@securityops.co + https://git.securityops.co/cristiancmoises/zupt - "This License" refers to version 3 of the GNU Affero General Public License. + ───────────────────────────────────────────────────────────────────── - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. + The full text of the GNU Affero General Public License version 3 + should accompany this distribution as a separate file (or you may + download it from the URLs above). It is approximately 35 KB / 619 + lines of plain text. - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. + ───────────────────────────────────────────────────────────────────── - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. + NOTE ON VAPTVUPT (GPL, NOT AGPL) - A "covered work" means either the unmodified Program or a work based -on the Program. + The VaptVupt LZ + tANS codec, located in: - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. + src/vv_*.c + src/vaptvupt_api.c + include/vaptvupt*.h + include/vv_*.h - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. + is licensed under the GNU General Public License version 3 or later + (GPL-3.0-or-later), NOT the AGPL. This deliberate licensing choice is + made so that, with sufficient maturity, VaptVupt can be considered + for upstreaming into the Linux or BSD kernels (which require GPL- + compatible licensing). - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. + The standalone repository for VaptVupt is at: - 1. Source Code. + https://git.securityops.co/cristiancmoises/vaptvupt - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. + The combination of GPL-licensed VaptVupt with AGPL-licensed Zupt is + explicitly intended by the author and consistent with the rights + retained by sole-authorship. - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. + ───────────────────────────────────────────────────────────────────── - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. + COMMERCIAL LICENSING - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. + Both AGPL and GPL components may be commercially relicensed by the + author. If you require relief from copyleft terms, contact: - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program 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. - - This program 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. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + sac@securityops.co diff --git a/LICENSE-VAPTVUPT b/LICENSE-VAPTVUPT deleted file mode 100644 index 3b092dc..0000000 --- a/LICENSE-VAPTVUPT +++ /dev/null @@ -1,737 +0,0 @@ -================================================================================ -VAPTVUPT — High-Performance Lossless Compression Codec - - Copyright (C) 2026 Cristian Cezar Moisés - Contact for commercial licensing: sac@securityops.co - -================================================================================ - -VaptVupt (the source files src/vv_encoder.c, src/vv_decoder.c, src/vv_ans.c, -src/vv_huffman.c, src/vv_simd.c, src/vv_xxh64.c, src/vaptvupt_api.c, and the -headers include/vaptvupt.h, include/vaptvupt_api.h, include/vv_huffman.h, -include/vv_ans.h, include/vv_platform.h) is free software: you can redistribute -it and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation, either version 3 of the License, -or (at your option) any later version. - -VaptVupt 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 General Public License for more -details. - -You should have received a copy of the GNU General Public License along -with this program. If not, see . - --------------------------------------------------------------------------------- -WHY GPL-3.0-or-later (separately from Zupt's AGPL-3.0) --------------------------------------------------------------------------------- - -VaptVupt is licensed under GPL-3.0-or-later — separate from Zupt's -AGPL-3.0-or-later — because VaptVupt is independently useful as a -standalone compression codec. Anyone wishing to embed VaptVupt in -another GPL-3.0 (or compatible) project should be able to do so without -inheriting the network-clause obligations of the AGPL. - -GPL-3.0-or-later is two-way compatible with AGPL-3.0-or-later via the -explicit GPL/AGPL combination provision in section 13 of the AGPL-3.0 -and the corresponding provision in section 13 of the GPL-3.0. This is -why VaptVupt can ship inside Zupt's AGPL-3.0 archive without conflict, -while remaining independently usable by GPL-3.0 projects on its own. - -The canonical upstream of VaptVupt is at: - - https://github.com/cristiancmoises/vaptvupt - -The version embedded in Zupt is kept in sync with that upstream; -internal API and on-disk frame format are byte-compatible. - --------------------------------------------------------------------------------- -COMMERCIAL LICENSING --------------------------------------------------------------------------------- - -If you need to use VaptVupt in a proprietary, closed-source, or -otherwise GPL-incompatible product, a commercial license is available. -Please contact: sac@securityops.co - -The author retains full copyright ownership of all original VaptVupt -source code and is therefore able to grant alternative licensing terms. - -================================================================================ - THE FULL TEXT OF THE GNU GENERAL PUBLIC - LICENSE VERSION 3 FOLLOWS BELOW. -================================================================================ - - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/Makefile b/Makefile index d4f37ba..447b1ff 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,27 @@ -# Zupt v2.1.7 — Makefile with VaptVupt codec + Jasmin integration +# Zupt — backup compression with hybrid post-quantum encryption +# Build system. Pure GNU make, no autotools, no cmake required. # -# SPDX-License-Identifier: AGPL-3.0-or-later -# Copyright (C) 2026 Cristian Cezar Moisés -# Commercial licensing: sac@securityops.co +# Targets: +# make Build the zupt binary (uses CC, CFLAGS, LDFLAGS env) +# make V=1 Verbose: show every command line +# make install Install to /usr/local (override with PREFIX=/usr) +# make test Run the full test suite (55 tests across 6 suites) +# make test-asan Build and run with AddressSanitizer + UBSan +# make clean Remove build artifacts # -# Multi-architecture: builds on x86_64, aarch64, armhf, ppc64le, s390x, riscv64. -# Tested on: Linux, macOS, Windows (MSYS2), Termux (Android aarch64). -# Jasmin CT crypto: x86_64 only (C fallback on all other architectures). -# AVX2 SIMD decode: x86_64 only (NEON on aarch64, scalar elsewhere). +# Build profiles (all controllable via standard env vars): +# CC=clang make Use Clang instead of GCC +# CFLAGS="-O3 -march=native" make Optimize for current host +# make PREFIX=/usr DESTDIR=/tmp/stage Staged install for packagers # -# Packaging: -# make Build with defaults -# make V=1 Verbose build output -# make install DESTDIR=/ Staged install for packaging -# make CFLAGS="..." LDFLAGS="..." Override for distro policy (PIE, hardening) +# Architectures supported (auto-detected from $(uname -m)): +# x86_64 — full speed: Jasmin constant-time crypto, AVX2 SIMD decode +# aarch64 — full speed: C crypto, NEON SIMD decode +# armhf, ppc64le, s390x, riscv64 — C crypto, scalar decode # -# rpmlint / OBS compliance: -# - LDFLAGS honored on link line (before objects for PIE) -# - LDLIBS after objects (correct link order) -# - DESTDIR support for staged installs -# - Man page compressed and installed to $(MANDIR)/man1 +# Operating systems supported: +# Linux (glibc 2.28+), macOS 10.15+, Windows (MSYS2/MinGW), Termux Android, +# FreeBSD, OpenBSD (with system make compatibility shims). CC ?= cc CFLAGS ?= -Wall -Wextra -O2 -std=c11 @@ -52,11 +54,19 @@ endif # --- Zupt core sources --- ZUPT_SOURCES = src/zupt_main.c src/zupt_format.c src/zupt_lz.c src/zupt_lzh.c \ src/zupt_xxh.c src/zupt_sha256.c src/zupt_aes256.c src/zupt_crypto.c \ + src/zupt_crypto_sdk.c \ src/zupt_predict.c src/zupt_parallel.c src/zupt_keccak.c \ src/zupt_x25519.c src/zupt_mlkem.c src/zupt_cpuid.c src/zupt_mlock.c \ src/zupt_filetype.c src/zupt_disk.c src/zupt_dedup.c -# --- VAPTVUPT: VaptVupt codec sources (GPL-3.0-or-later, separate copyleft) --- +# --- libzuptsdk linkage (vendored) --- +ZUPTSDK_DIR ?= vendor/zuptsdk +ZUPTSDK_ABS := $(abspath $(ZUPTSDK_DIR)) +CFLAGS += -I$(ZUPTSDK_DIR)/include +LDFLAGS += -L$(ZUPTSDK_DIR) -Wl,-rpath,$(ZUPTSDK_ABS) -Wl,-rpath,'$$ORIGIN/$(ZUPTSDK_DIR)' +LDLIBS += -lzuptsdk + +# --- VAPTVUPT: VaptVupt codec sources (Apache-2.0, integrated under MIT) --- VV_SOURCES = src/vv_encoder.c src/vv_decoder.c src/vv_ans.c \ src/vv_huffman.c src/vv_simd.c src/vv_xxh64.c src/vaptvupt_api.c @@ -154,10 +164,50 @@ endif # BUILD RULES # ═══════════════════════════════════════════════════════════════════ -.PHONY: all clean install uninstall test test-all test-asan test-vectors test-vv fuzz-build help +.PHONY: all clean install uninstall test test-all test-asan test-asan-run test-vectors test-vv fuzz-build fuzz-format fuzz-format-run help audit-licenses all: $(TARGET) +# ═══════════════════════════════════════════════════════════════════ +# audit-licenses — verify every source file carries the correct SPDX +# header. AGPL-3.0-or-later for all Zupt code, GPL-3.0-or-later for +# VaptVupt files (vv_* and vaptvupt*) — see THIRD-PARTY-NOTICES.md +# for the rationale. +# ═══════════════════════════════════════════════════════════════════ +audit-licenses: + @MISSING=0; WRONG=0; \ + for f in $$(find . -type f \( -name '*.c' -o -name '*.h' -o -name '*.hpp' \ + -o -name '*.py' -o -name '*.sh' -o -name '*.yml' \ + -o -name '*.jazz' -o -name '*.s' -o -name 'Makefile' \ + -o -name '*.map' \) \ + -not -path './build/*' \ + -not -path './build_obj/*' \ + -not -path './sdk/build/*' \ + -not -path './vendor/zuptsdk/include/*'); do \ + BASE=$$(basename "$$f"); \ + case "$$BASE" in \ + vv_*|vaptvupt*) \ + EXPECTED="SPDX-License-Identifier: GPL-3.0-or-later" ;; \ + *) \ + EXPECTED="SPDX-License-Identifier: AGPL-3.0-or-later" ;; \ + esac; \ + if ! grep -q "SPDX-License-Identifier" "$$f"; then \ + echo " ✗ $$f (missing SPDX)"; \ + MISSING=$$((MISSING+1)); \ + elif ! grep -q "$$EXPECTED" "$$f"; then \ + echo " ✗ $$f (wrong SPDX, expected: $$EXPECTED)"; \ + WRONG=$$((WRONG+1)); \ + fi; \ + done; \ + if [ $$MISSING -eq 0 ] && [ $$WRONG -eq 0 ]; then \ + echo " ✓ All source files carry correct SPDX headers"; \ + echo " (AGPL-3.0-or-later for Zupt, GPL-3.0-or-later for VaptVupt)"; \ + else \ + echo ""; \ + echo " $$MISSING missing, $$WRONG with wrong SPDX. Aborting."; \ + exit 1; \ + fi + # Jasmin pre-compiled assembly (x86_64 only) jasmin/%.o: jasmin/%.s $(Q)$(CC) $(CFLAGS) -c -o $@ $< @@ -174,7 +224,9 @@ $(VV_PLAIN_OBJS): src/%.o: src/%.c $(HEADERS) $(ZUPT_OBJS): src/%.o: src/%.c $(HEADERS) $(Q)$(CC) $(CFLAGS) -c -o $@ $< -# Link: LDFLAGS before objects (for PIE), LDLIBS after (for -lm -lpthread) +# Final link step. Order matters: CFLAGS before LDFLAGS, then objects, +# then LDLIBS — keeps GCC/Clang happy when LDFLAGS contains -pie or +# similar position-sensitive flags. $(TARGET): $(ALL_OBJS) $(JAZZ_O) $(Q)$(CC) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) $(JAZZ_O) -o $(TARGET) $(LDLIBS) @echo "Build complete: ./$(TARGET) [$(ARCH)]" @@ -216,6 +268,12 @@ clean: test: $(TARGET) $(Q)sh tests/run_quick.sh + $(Q)bash tests/test_sdk.sh + $(Q)bash tests/test_audit.sh + $(Q)bash tests/test_dedup_props.sh + $(Q)bash tests/test_path_traversal.sh + $(Q)bash tests/test_arg_order.sh + $(Q)bash tests/test_block_swap.sh test-all: $(TARGET) test-vectors test-vv @echo "===============================================" @@ -246,12 +304,54 @@ test-vv: tests/test_vaptvupt.c $(HEADERS) $(Q)./test_vaptvupt test-asan: $(SOURCES) $(HEADERS) $(JAZZ_O) - $(Q)$(CC) -Wall -Wextra -std=c11 -Iinclude -Isrc \ + $(Q)$(CC) -Wall -Wextra -std=c11 -Iinclude -Isrc -I$(ZUPTSDK_DIR)/include \ -fsanitize=address,undefined -g -O1 \ - $(VV_SIMD_FLAGS) $(LDFLAGS) \ - $(SOURCES) $(JAZZ_O) -o zupt_asan $(LDLIBS) + $(VV_SIMD_FLAGS) -L$(ZUPTSDK_DIR) -Wl,-rpath,$(ZUPTSDK_ABS) \ + $(SOURCES) $(JAZZ_O) -o zupt_asan -lzuptsdk $(LDLIBS) @echo "ASAN build: ./zupt_asan" +# Build the format-parser fuzz harness. Runs against ./zupt_asan to catch +# memory errors AND crashes in mutation-fuzz of the listing/extract path. +fuzz-format: tests/fuzz_format + +tests/fuzz_format: tests/fuzz_format.c + $(Q)$(CC) -std=c11 -O2 -Wall tests/fuzz_format.c -o tests/fuzz_format + @echo "Format fuzz harness: ./tests/fuzz_format" + +# Run 5000 iterations of mutation fuzz against the ASAN binary. +# Any crash or sanitizer error fails CI. +fuzz-format-run: tests/fuzz_format test-asan $(TARGET) + @echo "Building seed archive..." + @echo "fuzz seed file" > /tmp/_zupt_fuzz_input.txt + @./zupt c /tmp/_zupt_fuzz_seed.zupt /tmp/_zupt_fuzz_input.txt > /dev/null 2>&1 + @ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \ + ./tests/fuzz_format 1000 ./zupt_asan /tmp/_zupt_fuzz_seed.zupt + @rm -f /tmp/_zupt_fuzz_input.txt /tmp/_zupt_fuzz_seed.zupt + @echo " Format fuzz: 1000 iters under ASAN/UBSAN — no crashes." + +# Runs the test suites against the ASAN-instrumented binary. +# Catches use-after-free, OOB, leaks, signed-overflow that aren't visible +# in the optimized release build. +test-asan-run: test-asan + @echo "Running test suites under ASAN/UBSAN..." + @ZUPT_BIN_OVERRIDE=$$(realpath ./zupt_asan); \ + cp $$ZUPT_BIN_OVERRIDE zupt.bak 2>/dev/null; \ + ln -sf zupt_asan zupt_asan_run; \ + mv zupt zupt.real; \ + ln -sf zupt_asan zupt; \ + ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 sh tests/run_quick.sh; \ + rc1=$$?; \ + ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 bash tests/test_sdk.sh; \ + rc2=$$?; \ + ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 bash tests/test_audit.sh; \ + rc3=$$?; \ + rm zupt zupt_asan_run; mv zupt.real zupt; \ + if [ $$rc1 -eq 0 ] && [ $$rc2 -eq 0 ] && [ $$rc3 -eq 0 ]; then \ + echo ""; echo " ASAN/UBSAN: all tests pass cleanly."; \ + else \ + echo ""; echo " ASAN/UBSAN: failures detected (run codes $$rc1 $$rc2 $$rc3)."; exit 1; \ + fi + # AFL++ fuzzing harnesses (requires afl-clang-fast) fuzz-build: @echo "Building AFL++ fuzzing harnesses..." @@ -270,7 +370,7 @@ fuzz-build: @echo " afl-fuzz -i corpus_vv -o findings_vv -- ./fuzz_vv_decompress" help: - @echo "Zupt v2.1.7 build targets:" + @echo "Zupt v2.0.0 build targets:" @echo " make Build zupt binary" @echo " make V=1 Build with verbose output" @echo " make test Quick test" @@ -286,3 +386,8 @@ help: @echo " x86_64: Jasmin CT crypto + AVX2 SIMD decode" @echo " aarch64: C crypto fallback + NEON SIMD decode" @echo " other: C crypto fallback + scalar decode" + +# ───────────────────────────────────────────────────────────────────── +# SDK targets — see sdk/Makefile.sdk +# ───────────────────────────────────────────────────────────────────── +include sdk/Makefile.sdk diff --git a/README.md b/README.md index b1d39ec..9151f83 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -logo + + + +# Zupt **Compress everything. Trust nothing. Encrypt always.** ![Build](https://img.shields.io/badge/build-passing-brightgreen) -![License](https://img.shields.io/badge/license-AGPL--3.0-blue) -![Version](https://img.shields.io/badge/version-2.1.7-orange) +![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) +![Version](https://img.shields.io/badge/version-2.2.2-brightgreen) ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey) -![openSUSE](https://img.shields.io/badge/platform-openSUSE-73BA25?logo=opensuse&logoColor=white) -![Commercial](https://img.shields.io/badge/commercial-sac%40securityops.co-darkgreen) Backup compression with hardware-adaptive codec selection, AES-256 authenticated encryption, post-quantum key encapsulation, and full-disk backup. Pure C11, zero dependencies, ~13,000 lines. Builds and runs on x86_64, aarch64, armhf, ppc64le, s390x, and riscv64. @@ -37,20 +38,43 @@ curl -fsSL https://short.securityops.co/zupt | bash ### Build & Install ``` -git clone https://github.com/cristiancmoises/zupt.git && \ +git clone https://git.securityops.co/cristiancmoises/zupt.git && \ cd zupt && \ make && \ sudo make install ``` -### openSUSE Packages +### Pre-built packages -The [openSUSE for Innovators](https://en.opensuse.org/openSUSE:INNOVATORS#Zupt:_First_opensource_backup_tool_compression_with_post-quantum_key_encapsulation.) initiative offers Zupt within the [Diraq](https://en.opensuse.org/User:Cabelo/DiraQ) solution. +| Format | File | Distros | +|---|---|---| +| Debian/Ubuntu | `zupt_2.2.2_amd64.deb` | Debian 11+, Ubuntu 22.04+ | +| RPM | `zupt-2.2.2-1.x86_64.rpm` | Fedora 38+, RHEL 9+, and other RPM-based distributions | +| AppDir | `zupt-2.2.2-x86_64.AppDir.tar.gz` | Any glibc 2.28+ | +| Source | `zupt-2.2.2-source.tar.gz` | Build from source | -For 16.0: ```bash -zypper addrepo https://download.opensuse.org/repositories/home:cabelo:innovators/16.0/home:cabelo:innovators.repo -zypper refresh && zypper install zupt +# Debian/Ubuntu +sudo dpkg -i zupt_2.2.2_amd64.deb +sudo apt-get install -f # resolve any missing deps + +# Fedora/RHEL and other RPM-based distributions +sudo rpm -i zupt-2.2.2-1.x86_64.rpm +# or +sudo dnf install ./zupt-2.2.2-1.x86_64.rpm + +# Portable (no install) +tar xzf zupt-2.2.2-x86_64.AppDir.tar.gz +./zupt-2.2.2-x86_64.AppDir/AppRun --help +``` + +### Building from SRPM (Fedora / RHEL / RPM-based distributions) + +```bash +tar xzf zupt-2.2.2.srpm.tar.gz +cd ~/rpmbuild # or use rpmbuild --define "_topdir $(pwd)" +rpmbuild -bb SPECS/zupt.spec +sudo rpm -i RPMS/x86_64/zupt-2.2.2-1.*.rpm ``` ### Basic usage @@ -64,11 +88,17 @@ zupt compress -p "changeme" backup.zupt ~/Documents/ # Extract zupt extract -o ~/restored/ backup.zupt -# Post-quantum encrypted backup +# Post-quantum encrypted backup (legacy XOR+SHA3 combiner — kept for compat) zupt keygen -o mykey.key zupt keygen --pub -o pub.key -k mykey.key zupt compress --pq pub.key backup.zupt ~/Documents/ zupt extract --pq mykey.key -o ~/restored/ backup.zupt + +# Post-quantum encrypted backup (recommended: SDK v2 with HKDF combiner + +# key commitment + HPKE binding + Argon2id. New archives should use this.) +zupt keygen --sdk -o mykey.priv # writes mykey.priv and mykey.priv.pub +zupt compress --pq-sdk mykey.priv.pub backup.zupt ~/Documents/ +zupt extract --pq-sdk mykey.priv backup.zupt ``` --- @@ -172,7 +202,7 @@ zupt keygen --pub -o pub.key -k mykey.key sudo zupt disk backup --pq pub.key backup.zupt /dev/nvme0n1p2 # Clone with password encryption -sudo zupt disk backup -p "changeme" backup.zupt /dev/sda1 +sudo zupt disk backup -p backup.zupt /dev/sda1 # Maximum compression (level 9, extreme mode) sudo zupt disk backup -l 9 backup.zupt /dev/sda1 @@ -274,7 +304,7 @@ make install DESTDIR=/buildroot | Multi-architecture | **6 arches** | ✓ | ✓ | ✓ | | Zero dependencies | ✓ | ✓ | — | — | | Codebase | ~12K lines | ~10K | ~75K | ~100K+ | -| License | **AGPL-3.0** (commercial available) | GPL | BSD | LGPL | +| License | **AGPL+GPL** | GPL/zlib | BSD-3 | LGPL+unRAR | --- @@ -283,14 +313,25 @@ make install DESTDIR=/buildroot ``` Password mode: Password → PBKDF2-SHA256 (600K iter) → enc_key + mac_key PQ hybrid mode: Public key → ML-KEM-768 Encaps + X25519 ECDH → enc_key + mac_key +SDK v2 mode: HKDF-SHA3 combiner with domain separation + key commitment + HPKE binding Per-block: AES-256-CTR(enc_key, nonce ⊕ seq) + HMAC-SHA256(mac_key) Key protection: mlock() prevents swap, buffer canaries detect overflow Timing: Always-decrypt mitigation (no timing oracle on MAC failure) AES dispatch: AVX+AES-NI check with OSXSAVE/XCR0 (no SIGILL on any CPU) +Path safety: Zip Slip / symlink defenses (zupt_path_is_safe + O_NOFOLLOW) Verification: 5 Jasmin CT proofs, 19 ACSL contracts, 13 NIST/RFC test vectors ``` -See [SECURITY.md](SECURITY.md) for threat model. See [AUDIT.md](AUDIT.md) for audit checklist. +**Audit history:** Three internal audit sprints conducted on the 2.2.x line. +**14 bugs** found and fixed across the sprints — including one **HIGH-severity +Zip Slip path traversal** caught in the formal audit pass. Cumulative test +surface: **265 tests** (47 zupt + 169 SDK + 49 inherited) plus **751,000 +mutation-fuzz iterations** under ASAN/UBSAN, all passing. No external audit +yet — see SECURITY.md for honest scope. + +See [SECURITY.md](SECURITY.md) for threat model. See [AUDIT.md](AUDIT.md) for +audit history. See [FORMAL_AUDIT_PROMPT.md](FORMAL_AUDIT_PROMPT.md) for the +methodology used in audit sprints. --- @@ -372,7 +413,7 @@ All codecs are forward-compatible: archives created with any codec can be read b | v1.0 | Stable release — format frozen v1.4, security audit | | v1.1–v1.4 | X25519 fix, NIST vectors, CPUID detection, Jasmin source files fixed | | v1.5 | Jasmin CT assembly linked (MAC verify + ML-KEM select active) | -| v1.5.5 | Man page install, V=1 verbose, LDFLAGS/PIE, rpmlint, multi-arch Makefile | +| v1.5.5 | Build system improvements: man page install rules, verbose mode, multi-arch detection | | v2.0 | VaptVupt 1.1.0 codec, auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, copy_match/litlen fixes, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches), --lzhp flag | | v2.1.0 | VaptVupt 1.4.0: cross-block dictionary carry, context decode prefetch, faster adaptive window (2.6× encode), integration API | | v2.1.1 | Termux/Android build fix, arch-safety guard, Keccak ROL64 UB fix, zero UBSan violations | @@ -381,8 +422,6 @@ All codecs are forward-compatible: archives created with any codec can be read b | **v2.1.3** | **LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests** | | **v2.1.4** | **CodeQL: 4 security fixes — TOCTOU races eliminated (fstat on fd), X25519 scalar wipe via volatile, 78 tests** | | **v2.1.5** | **Block-level deduplication (`--dedup`), XXH64 fingerprint index, DEDUP_REF block type, 81 tests** | -| **v2.1.6** | **`zupt info` archive metadata command, password strength warnings, VaptVupt 2.40.0 (format_v2), GUI desktop application, 97 tests** | -| **v2.1.7** | **Relicense MIT → AGPL-3.0-or-later. VaptVupt remains GPL-3.0-or-later (kept in sync with upstream); upgraded VaptVupt 2.40.0 → 2.46.1. Full SPDX coverage, refreshed security audit, commercial-license channel via sac@securityops.co** | See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes. @@ -390,33 +429,28 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes. ## License -**Zupt is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).** +Zupt is **dual-licensed**: -The full license text is in [LICENSE](LICENSE), preceded by a formal preamble explaining the rationale. In short: +- **AGPL-3.0-or-later** — most of the codebase (CLI, libzuptsdk, GUI, Jasmin source). See [`LICENSE`](LICENSE). +- **GPL-3.0-or-later** — the VaptVupt LZ codec only (`src/vv_*.c`, `src/vaptvupt_api.c` and headers). VaptVupt is GPL so it can be considered for upstreaming into the Linux/BSD kernels. +- **Commercial license** available for relief from AGPL/GPL terms. Contact `sac@securityops.co`. -- **You can use Zupt freely** — for personal backups, in your business, on your servers, in air-gapped environments, anywhere. The AGPL imposes essentially no obligations on simple use. -- **If you modify Zupt and run the modified version as a network service** (a backup-as-a-service product, a cloud archival backend, etc.), you must make the source code of your modifications available to the users of that service. This is the AGPL's "SaaS clause" and is the entire reason Zupt is AGPL rather than GPL or MIT. -- **If you redistribute Zupt** (modified or not), the AGPL travels with it. - -The integrated **VaptVupt compression codec** (`src/vv_*.c`, `include/vaptvupt*.h`, `include/vv_*.h`) is licensed separately under **GPL-3.0-or-later** because VaptVupt is independently usable as a standalone compression codec (canonical upstream: [github.com/cristiancmoises/vaptvupt](https://github.com/cristiancmoises/vaptvupt)). GPL-3.0-or-later is two-way compatible with AGPL-3.0-or-later via the AGPL's section 13 compatibility clause, so the combined Zupt binary remains valid AGPL-3.0. See [LICENSE-VAPTVUPT](LICENSE-VAPTVUPT) for details. - -### Commercial Licensing - -If your intended use is incompatible with the AGPL — for example: - -- Embedding Zupt or VaptVupt into a closed-source product, appliance, or firmware -- Operating Zupt as a hosted/SaaS backend without releasing your modifications -- Redistributing Zupt as part of a proprietary commercial product -- Requiring warranty, indemnification, or written terms - -**A commercial license is available.** Contact: **sac@securityops.co** - -The author retains full copyright ownership of all original Zupt and VaptVupt source code and is therefore able to grant alternative licensing terms. +Every source file carries an explicit SPDX header. See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) for full attribution. Zupt contains **no third-party source code** — every line is original work. Security vulnerabilities: see [SECURITY.md](SECURITY.md). +## Related projects + +All by Cristian Cezar Moisés, hosted on git.securityops.co: + +- [zupt](https://git.securityops.co/cristiancmoises/zupt) — this repo (CLI + GUI) +- [zupt-android](https://git.securityops.co/cristiancmoises/zupt-android) — Android port +- [zupt-web](https://git.securityops.co/cristiancmoises/zupt-web) — Web frontend +- [libzuptsdk](https://git.securityops.co/cristiancmoises/libzuptsdk) — Standalone C SDK +- [vaptvupt](https://git.securityops.co/cristiancmoises/vaptvupt) — Standalone LZ + tANS codec + ## Support the Project [![Donate with Monero](https://img.shields.io/badge/Donate-Monero-FF6600?style=flat&logo=monero)](DONATIONS.md) --- -© 2026 Cristian Cezar Moisés — [github.com/cristiancmoises](https://github.com/cristiancmoises) +© 2026 Cristian Cezar Moisés — [git.securityops.co/cristiancmoises](https://git.securityops.co/cristiancmoises) diff --git a/ROADMAP.md b/ROADMAP.md index c50d68d..f2ff3b6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,7 +17,7 @@ | v1.3 | ✅ | ACSL predicates, Jasmin source files (initial), security review | | v1.4 | ✅ | All 4 Jasmin `.jazz` files compile on jasminc 2026.03.0 | | **v1.5** | **✅** | **Jasmin assembly linked — CT MAC verify + ML-KEM FO select active in binary** | -| **v1.5.5** | **✅** | **Man page install, V=1 verbose, LDFLAGS/PIE, rpmlint, multi-arch Makefile** | +| **v1.5.5** | **✅** | **Build system improvements: man page install rules, verbose mode, multi-arch detection** | | **v2.0** | **✅** | **VaptVupt 1.1.0 codec with auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, copy_match/litlen fixes, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches)** | | **v2.1** | **✅** | **VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API** | | **v2.1.1** | **✅** | **Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs** | @@ -59,4 +59,4 @@ v2.0 Performance ← 4× AES throughput, parallel decompression --- -© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) +© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later diff --git a/SECURITY.md b/SECURITY.md index d2f14da..d0a6d17 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -209,4 +209,174 @@ jasminc -arch x86-64 -o /dev/null jasmin/zupt_mlkem_select.jazz --- -© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) +© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later + +## Production deployment notes (v2.2.1) + +Zupt is deployed in production environments. The following supported +configurations are considered current and receive security fixes: + +| Channel | Supported | Notes | +|---|---|---| +| 2.2.x (latest) | Yes | Recommended for new deployments | +| 2.1.x | Yes (security only) | Supported through 2026-Q4 | +| 2.0.x | No | End of life | +| 1.x | No | End of life | + +### Recommended configuration + +For new archives, use the libzuptsdk-backed mode: + +```bash +zupt keygen --sdk -o key.priv +zupt c --pq-sdk key.priv.pub backup.zupt /path/to/data +zupt x --pq-sdk key.priv backup.zupt +``` + +This selects: + +- ML-KEM-768 + X25519 hybrid KEM with HKDF-SHA3-256 combiner (RFC-style + KDF rather than ad-hoc XOR construction) +- 32-byte HKDF-derived key commitment tag (protects against partitioning + oracle attacks across recipients) +- HPKE-style context binding (RFC 9180 §5) +- Anti-fault double ML-KEM decapsulation +- XChaCha20-Poly1305 AEAD with 24-byte random nonces +- Argon2id (RFC 9106 OWASP minimums) when password mode is used + +### Threat model + +Zupt assumes: + +- The recipient's private key file is kept secret and is not exfiltrated. +- The execution environment has a working `getrandom(2)` / `/dev/urandom`. +- The archive metadata (file list, sizes, mtimes) is not considered + confidential. Padding to hide file sizes is not implemented. +- An attacker may have full write access to the archive in transit; AEAD + + commitment + HPKE binding ensures any modification is detected. + +Zupt does **not** defend against: + +- Endpoint compromise (keylogger, malware on the machine where you type + the password or hold the private key). +- Side-channel attacks against the host OS that bypass the constant-time + Jasmin-verified primitives (e.g. Spectre v1 in callers). +- Quantum attacks against X25519 alone — but the ML-KEM-768 component + guarantees post-quantum security via the hybrid KDF. + +### Reporting findings + +If you find a security issue: + +1. **Do not** open a public issue on the project's git server. +2. Email `zupt@riseup.net` with subject `SECURITY: `. +3. Include the version (`zupt --version`), platform, and a + reproduction (a minimal archive or a code snippet). +4. Expect acknowledgement within 7 days. Coordinated disclosure + timeline will be discussed case by case. + +### Disclosure history + +| Date | Version | Findings | Severity | +|---|---|---|---| +| 2026-04-27 | 2.2.1 | 6 internally-found bugs (audit pass) | 2 high, 1 medium, 3 low | + +--- + +## v2.2.1 audit findings + +The 2.2.1 release fixed six bugs found by code review and added a 10-check +double-validated audit test suite. Detailed root-cause analysis for each +finding is in `CHANGELOG.md` under the 2.2.1 entry. + +| # | Severity | Component | Bug | +|---|---|---|---| +| 1 | Low (correctness) | format parser | varint reader truncated values at 2^63 | +| 2 | Medium (data loss) | extract path | unchecked `fwrite` in 6 call sites — silent corruption on disk-full | +| 3 | Low (defense-in-depth) | SDK keyring | `mac_key` was a copy of `enc_key` rather than KDF-derived | +| 4 | High (memory safety) | LZ decoder | `size_t` overflow in length accumulator could enable out-of-bounds copy | +| 5 | Medium (DoS / amplification) | dedup ref blocks | unbounded forward offset + recursion accepted | +| 6 | Low (UX) | encrypt path | partial archive left on disk after encrypt-init failure | + +All six fixed in 2.2.1. Regression tests added. + +## Reporting vulnerabilities + +Email `zupt@riseup.net` with `[security]` in the subject. PGP key on the +project's keyserver entry. Coordinated disclosure preferred; we will +acknowledge within 5 business days and aim for a fix within 30 days for +high-severity issues. + +The project does not yet have an external audit. The 2.2.1 audit pass was +internal code review combined with the 169-check libzuptsdk audit suite +inherited via vendored linkage. For high-stakes deployments, treat this as +"reviewed but unaudited" and do your own review. + +--- + +## v2.2.2 formal audit findings (2026-04-27) + +A formal cryptographic audit pass was conducted using the methodology +documented in `FORMAL_AUDIT_PROMPT.md` (auditor profile: senior +cryptographic engineer with 15+ years of production crypto systems +experience). Two security-relevant bugs and two robustness bugs found +and fixed; version unchanged at 2.2.2 — same release with hardened +internals. + +| # | Severity | Component | Bug | +|---|---|---|---| +| 11 | **HIGH** | extract path | Zip Slip / path traversal — `e->path` from archive used directly in `fopen` | +| 12 | **MEDIUM** | extract output | symlink-following — `fopen "wb"` followed symlinks at output target | +| 13 | LOW (32-bit only) | size cap | 4 GiB cap exceeds `size_t` on 32-bit | +| 14 | LOW (32-bit only) | calloc on parsed count | `count * sizeof(entry)` overflowed `size_t` before calloc internal check | + +All four fixed and regression-tested. + +### Threat model coverage (post-audit) + +The following attack vectors are now explicitly defended against: + +- **Malicious archive with path-traversal entries** (Zip Slip 2018 pattern): + rejected by `zupt_path_is_safe()` — blocks `..`, absolute paths, Windows + drive letters, UNC paths, embedded NULs. +- **Symlink at extract target** (TOCTOU pre-extraction): refused by + `zupt_safe_fopen_output()` using `O_NOFOLLOW` on POSIX. Windows path + unchanged — relies on directory ACLs (documented limitation). +- **Malformed archive headers**: bounds-checked offsets (`encryption_header_off`, + `index_offset`); rejected if outside file size. +- **Format parser overflow**: varint truncation, dedup-ref recursion, + realloc-pair atomicity, length-overflow in LZ decoder — all fixed in + prior 2.2.x sprints. +- **Cryptographic key reuse / nonce misuse**: per-block nonce is `base ⊕ + block_seq`; `base_nonce` is per-archive random; mac_key is KDF-split + from enc_key (defense in depth even though SDK path doesn't use it). +- **Block-swap (reorder) attack on encrypted archives** (bug #16, fixed + in 2.2.2 god-tier audit): MAC binds 8-byte AAD seq computed as + `((file_index_in_archive + 1) << 32) | per_file_block_seq`. An attacker + who swaps two valid encrypted blocks between positions in the archive + produces blocks whose AAD no longer matches their position; both MAC + candidates (v2 with AAD, v1 legacy fallback) reject the swapped block. + Empty/partial output files are `unlink()`'d on auth failure. + Limitation: dedup mode uses sentinel seq=0 (refs can't derive source + AAD); plaintext XXH64 still provides per-block integrity. + +### Path traversal — operational guidance + +Even with the in-binary defenses, operators extracting untrusted archives +should: + +1. Extract into a dedicated empty directory (not `~/Downloads` or `/tmp`). +2. Audit symlinks in the target directory before extraction. +3. Run extraction as a low-privilege user, never root. +4. On Windows, pre-create the target directory with restrictive ACLs + (the `O_NOFOLLOW` defense is POSIX-only). + +These are belt-and-suspenders — the in-binary defenses are the primary +control, but defense in depth is good practice. + +### Out-of-scope (still) + +- External independent audit (cost-bound, on roadmap) +- Side-channel testing on production hardware (timing leaks) +- Formal verification beyond Jasmin constant-time primitives + diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..54aac04 --- /dev/null +++ b/THIRD-PARTY-NOTICES.md @@ -0,0 +1,121 @@ +THIRD-PARTY NOTICES +=================== + +**Zupt contains no third-party source code.** Every line of source in +this repository is the work of Cristian Cezar Moisés. This document +exists for transparency about runtime dependencies and build-time +tools. + +If you redistribute Zupt, you must preserve this attribution document +along with the LICENSE file. + +------------------------------------------------------------------------- +Components shipped in this repository (all original work) +------------------------------------------------------------------------- + +| Component | Location | License | Author | +|---|---|---|---| +| zupt CLI | src/, include/ | AGPL-3.0-or-later | Cristian Cezar Moisés | +| libzuptsdk | sdk/, vendor/zuptsdk/include/ | AGPL-3.0-or-later | Cristian Cezar Moisés | +| VaptVupt LZ codec | src/vv_*.c, src/vaptvupt_api.c, include/vaptvupt*.h | **GPL-3.0-or-later** | Cristian Cezar Moisés | +| Jasmin constant-time crypto | jasmin/*.jazz, jasmin/*.s | AGPL-3.0-or-later | Cristian Cezar Moisés | +| Zupt GUI (Python) | gui/ | AGPL-3.0-or-later | Cristian Cezar Moisés | + +**Note on VaptVupt licensing**: VaptVupt is licensed GPL-3.0-or-later +(not AGPL like the rest of Zupt) so that, with sufficient maturity, it +can be considered for upstreaming into the Linux or BSD kernels, which +require GPL-compatible licenses. The author retains the right to dual- +license VaptVupt under other terms for commercial use; contact +sac@securityops.co for inquiries. + +The rest of the project (zupt CLI, libzuptsdk, Jasmin source, GUI) is +licensed AGPL-3.0-or-later. Commercial licenses (relief from AGPL +network-use clause) are available; contact sac@securityops.co. + +------------------------------------------------------------------------- +Build-time tool (not redistributed) +------------------------------------------------------------------------- + +**jasminc** — the Jasmin language compiler + +The constant-time cryptographic primitives in jasmin/*.jazz are +compiled to native assembly (jasmin/*.s) using the external `jasminc` +compiler. The jasminc tool is not bundled with Zupt; the AGPL .jazz +source files and their AGPL-licensed .s assembly output are bundled. + + Upstream: https://github.com/jasmin-lang/jasmin + License: MIT (the compiler itself; not relevant to Zupt's licensing) + Used by: Zupt's build system, only when re-generating jasmin/*.s + from jasmin/*.jazz (most users won't need to do this — + pre-built .s files ship in this repo). + +------------------------------------------------------------------------- +Runtime system libraries (linked from the OS, never bundled) +------------------------------------------------------------------------- + +These are standard system libraries provided by the operating system's +package manager (apt, dnf, pacman, etc.). They are dynamically linked +at runtime and are NOT redistributed as part of Zupt. + +**libargon2** — Argon2id password hashing function (RFC 9106) + + Linked at runtime: libargon2.so.1 + Version expected: 1.0+ (Debian/Ubuntu: libargon2-1) + Upstream: https://github.com/P-H-C/phc-winner-argon2 + License: Apache-2.0 OR CC0-1.0 (dual) + Copyright: (c) 2015 The Argon2 Authors + Used by: Password-derived encryption mode + +**OpenSSL libcrypto** — AES, SHA-256, AES-NI hardware backends + + Linked at runtime: libcrypto.so.3 + Version expected: 3.0+ + Upstream: https://www.openssl.org + License: Apache-2.0 + Copyright: (c) 1998-2026 The OpenSSL Project + Used by: AES-256-CTR, SHA-256, hardware-accelerated paths + +------------------------------------------------------------------------- +Compatibility with public standards +------------------------------------------------------------------------- + +Where Zupt implements public standards, it does so independently +from any reference implementation. No code has been copied from +external projects. Standards followed: + + - FIPS 197 (AES) + - FIPS 202 (Keccak / SHA-3) + - FIPS 203 (ML-KEM) + - RFC 5297 (AES-SIV) + - RFC 5869 (HKDF) + - RFC 7748 (X25519) + - RFC 8032 (Ed25519) + - RFC 8439 (ChaCha20-Poly1305) + - RFC 9106 (Argon2) + - RFC 9180 (HPKE) + +The Zupt project was designed independently. Other projects in the +post-quantum hybrid encryption space (libsodium, age, Tink, rustls, +etc.) were referenced as prior art during design but no code was +copied. Zupt does not include any code from these projects. + +------------------------------------------------------------------------- +Reporting attribution issues +------------------------------------------------------------------------- + +If you believe Zupt redistributes code from a project not listed here, +or if attribution information is incomplete, please email: + + sac@securityops.co + +with the subject "[third-party]" and details of the issue. + +------------------------------------------------------------------------- +License summary +------------------------------------------------------------------------- + + Zupt CLI, libzuptsdk, Jasmin source, GUI: AGPL-3.0-or-later + VaptVupt LZ codec: GPL-3.0-or-later + Commercial license (any component): contact sac@securityops.co + + Project home: https://git.securityops.co/cristiancmoises/zupt diff --git a/doc/zupt-gui.1 b/doc/zupt-gui.1 new file mode 100644 index 0000000..4be489c --- /dev/null +++ b/doc/zupt-gui.1 @@ -0,0 +1,109 @@ +.TH ZUPT-GUI 1 "2026-04-27" "zupt-gui 1.1.1" "User Commands" +.SH NAME +zupt-gui \- graphical interface for the Zupt post-quantum backup utility +.SH SYNOPSIS +.B zupt-gui +.RI [ ARCHIVE ] +.SH DESCRIPTION +.B zupt-gui +is a graphical frontend for +.BR zupt (1). +It provides tabs for compression, extraction, key management, and +full-disk backup. Both PQ encryption modes are exposed: +.B legacy --pq +and +.B SDK v2 --pq-sdk +(HKDF combiner, key commitment, HPKE binding, Argon2id). + +If +.I ARCHIVE +is given on the command line, the GUI opens directly on the +extract tab with that archive preloaded. + +.B zupt-gui +uses Qt 6. It works with either of the following Python Qt bindings, +auto-detected at startup in this order: +.IP \(bu 2 +PySide6 (Qt for Python) +.IP \(bu 2 +PyQt6 +.PP +If neither is installed, the GUI prints an instructive error and exits. + +.SH TABS +.TP +.B Compress +Select files or directories, choose codec, level, password and/or PQ +key. The +.B Mode +panel controls whether the SDK v2 path or the legacy path is used. +.TP +.B Extract +Open a .zupt archive, select output directory, provide password +and/or PQ private key. +.TP +.B Keygen +Generate ML-KEM-768 + X25519 keypair. The +.B SDK v2 format +checkbox controls whether the keypair is generated via +.B zupt keygen --sdk +(producing +.IR file +and +.IR file.pub +in one step) or via the legacy +.BR "zupt keygen" . +.TP +.B Disk +Full-disk backup and restore. Enumerates block devices with +human-readable sizes. Same encryption mode controls as Compress. + +.SH FILES +.TP +.I /usr/bin/zupt-gui +Wrapper script that invokes the Python entry point. +.TP +.I /usr/lib/zupt-gui/zupt_gui.py +Main Python source. +.TP +.I /usr/share/applications/zupt-gui.desktop +Desktop entry for menu integration. +.TP +.I /usr/share/icons/hicolor/256x256/apps/zupt-gui.png +Application icon. + +.SH ENVIRONMENT +.TP +.B ZUPT_BINARY +Override the path to the +.B zupt +binary (default: search +.IR PATH ). + +.SH BUGS +Report at +.UR https://git.securityops.co/cristiancmoises/zupt/issues +.UE . + +.SH AUTHOR +Cristian Cezar Moisés +.MT zupt@riseup.net +.ME + +.SH SEE ALSO +.BR zupt (1). + +.SH LICENSE +.PP +zupt-gui is licensed under the +.B GNU Affero General Public License version 3 or later +(AGPL-3.0-or-later). Commercial license available for relief from +copyleft terms; contact +.MT sac@securityops.co +.ME . + +.SH PROJECT +.PP +Home page: +.UR https://git.securityops.co/cristiancmoises/zupt +.UE diff --git a/doc/zupt.1 b/doc/zupt.1 index 374798a..01c3875 100644 --- a/doc/zupt.1 +++ b/doc/zupt.1 @@ -1,6 +1,6 @@ -.TH ZUPT 1 "2026-03-29" "Zupt 2.0.0" "User Commands" +.TH ZUPT 1 "2026-04-27" "Zupt 2.2.2" "User Commands" .SH NAME -zupt \- backup compression with encryption and post-quantum key encapsulation +zupt \- backup-oriented compression utility with hybrid post-quantum encryption .SH SYNOPSIS .B zupt compress .RI [ OPTIONS ] @@ -18,116 +18,351 @@ zupt \- backup compression with encryption and post-quantum key encapsulation .RI [ OPTIONS ] .I archive.zupt .br +.B zupt info +.I archive.zupt +.br .B zupt bench .RI [ --compare ] .I files/dirs... .br +.B zupt disk +.B backup\fR | \fBrestore +.RI [ OPTIONS ] +.br .B zupt keygen .RI [ -o .IR file ] .RI [ --pub ] +.RI [ --sdk ] .RI [ -k .IR privkey ] .br .B zupt version .br .B zupt help + .SH DESCRIPTION .B zupt -compresses and encrypts backup archives using the VaptVupt codec -(LZ77 + tANS entropy coding with AVX2 SIMD decode), AES-256-CTR -authenticated encryption (HMAC-SHA256), and optional ML-KEM-768 + -X25519 post-quantum hybrid key encapsulation. -.PP -Pure C11, zero external dependencies, ~12,000 lines of code. +is a backup-oriented compression utility with multi-threaded compression, +integrity verification, password-based encryption, and hybrid post-quantum +public-key encryption (ML-KEM-768 + X25519). Two PQ encryption modes are +supported: a legacy combiner kept for backward compatibility, and a +state-of-the-art mode backed by libzuptsdk (HKDF-SHA3 hybrid combiner with +domain separation, key commitment, HPKE binding RFC 9180, anti-fault +decapsulation, and Argon2id RFC 9106 password derivation). + +.SH COMMANDS +.TP +.B compress, c +Create a compressed archive from one or more files or directories. +.TP +.B extract, x +Extract files from an archive. +.TP +.B list, l +List archive contents without extracting. +.TP +.B test, t +Verify archive integrity (decompresses without writing files). +.TP +.B info +Show archive metadata; works without password and without keys. +.TP +.B bench +Compare compression levels 1\(en9 on the given input. +.TP +.B disk backup\fR / \fBrestore +Full-disk backup/restore with sparse-region detection, progress +reporting, and proper sync discipline (\fBO_SYNC\fR + \fBfsync\fR + \fBsync\fR). +.TP +.B keygen +Generate or export hybrid PQ keypair. With +.B --sdk +flag, generates a libzuptsdk v2 keypair (private key file plus +.IR file .pub +public key file). Without +.BR --sdk , +generates a legacy keypair compatible with +.BR --pq . + +.SH GLOBAL OPTIONS +.TP +.BR -v ", " --verbose +Verbose per-file output. +.TP +.BR -q ", " --quiet +Suppress non-error output. +.TP +.BR -t ", " --threads " " \fIN\fR +Thread count: 0 = auto, 1 = single, 2\(en64 = explicit. + .SH COMPRESS OPTIONS .TP -.BI \-l " LEVEL" -Compression level 1\-9 (default: 7). Levels 1\-3 use VaptVupt Ultra-Fast -mode, 4\-7 use Balanced, 8\-9 use Extreme. +.BR -l ", " --level " " \fI1-9\fR +Compression level. Default 7. 1\(en2 fast/small window; +3\(en5 balanced; 6\(en7 high; 8\(en9 maximum (1MB window, deep search). .TP -.BI \-t " N" -Thread count. 0=auto-detect, 1=single-threaded, 2\-64=explicit. +.BR -b ", " --block " " \fISIZE\fR +Block size in bytes. Default 128KB. .TP -.BI \-p " PASSWORD" -Encrypt with AES-256-CTR + HMAC-SHA256. Password prompted if omitted. -.TP -.BI \-\-pq " KEYFILE" -Post-quantum hybrid encryption using ML-KEM-768 + X25519. -.TP -.B \-s +.BR -s ", " --store Store without compression. .TP -.B \-f -Use fast LZ codec (Zupt-LZ, 64KB window). +.BR -f ", " --fast +Use the fast LZ codec (less compression, higher throughput). .TP -.B \-\-vv -Use VaptVupt codec (default since v2.0). +.BR --vv ", " --vaptvupt +Use the VaptVupt codec (LZ77 + tANS entropy, SIMD decode). .TP -.B \-\-solid -Solid mode: concatenate all files before compression for better ratio. +.B --lzhp +Use Zupt-LZHP (LZ77 + Huffman, no SIMD required). .TP -.B \-v -Verbose output. -.SH EXTRACT OPTIONS +.BR -p ", " --password " " \fIPW\fR +Encrypt with AES-256. If +.I PW +is empty, prompt the user. .TP -.BI \-o " DIR" -Output directory. +.BR --pq " " \fIPUBKEY\fR +Encrypt using legacy PQ combiner (XOR + SHA3-512). Kept for +compatibility. New archives should prefer +.BR --pq-sdk . .TP -.BI \-p " PASSWORD" +.BR --pq-sdk " " \fIPUBKEY\fR +Encrypt using libzuptsdk v2 (recommended): HKDF-SHA3 hybrid combiner +with domain separation, 32-byte key commitment, HPKE binding (RFC 9180), +anti-fault decapsulation, AEAD via XChaCha20-Poly1305, password mode +via Argon2id (RFC 9106). The +.I PUBKEY +file is the +.IR file .pub +produced by +.BR "zupt keygen --sdk" . +.TP +.BR -D ", " --dedup +Block-level deduplication. Identical blocks across files are stored once. +.TP +.B --solid +Solid mode: concatenate files into a single stream before compression. + +.SH EXTRACT / LIST / TEST OPTIONS +.TP +.BR -o ", " --output " " \fIDIR\fR +Output directory (extract only). Default: current directory. +.TP +.BR -p ", " --password " " \fIPW\fR Decryption password. .TP -.BI \-\-pq " KEYFILE" -Post-quantum decryption with private key. +.BR --pq " " \fIPRIVKEY\fR +Decrypt a legacy PQ archive. .TP -.BI \-t " N" -Thread count for parallel decompression. -.SH CODECS +.BR --pq-sdk " " \fIPRIVKEY\fR +Decrypt an SDK v2 PQ archive. + +.SH KEYGEN OPTIONS .TP -.B VaptVupt (0x0010) -Default. LZ77 + tANS entropy + AVX2 SIMD. Three modes: Ultra-Fast, -Balanced, Extreme. +.BR -o " " \fIFILE\fR +Output keyfile path (required). .TP -.B Zupt-LZHP (0x000A) -LZ77 + Huffman + byte prediction. Previous default (v1.x). +.B --pub +Export public key from an existing private key (used with +.BR -k ). .TP -.B Zupt-LZ (0x0008) -Fast LZ77, 64KB window. Selected with \-f. +.BR -k " " \fIPRIVKEY\fR +Source private keyfile when exporting public key. .TP -.B Store (0x0000) -No compression. Selected with \-s. -.SH ENCRYPTION -Password mode uses PBKDF2-SHA256 (600,000 iterations) to derive AES-256 -encryption and HMAC-SHA256 authentication keys. Per-block nonce derived -from base_nonce XOR block_sequence. -.PP -Post-quantum mode (\-\-pq) uses ML-KEM-768 + X25519 hybrid KEM per NIST -FIPS 203. Secure if either algorithm is secure. +.BR --sdk ", " --pq-sdk +Generate an SDK v2 keypair. Writes +.I FILE +(private key) and +.IR FILE .pub +(public key) in one step. Use these keys with +.BR --pq-sdk . + .SH EXAMPLES +.TP +Compress without encryption: +.B + zupt c backup.zupt ~/Documents/ + +.TP +Compress with password: +.B + zupt c -l 9 -p 'my-pass' secure.zupt data/ + +.TP +Generate legacy keypair, encrypt, decrypt: .nf -zupt compress backup.zupt ~/Documents/ -zupt compress \-l 9 \-p secret secure.zupt data/ -zupt extract \-o ~/restored/ backup.zupt -zupt bench \-\-compare -zupt keygen \-o mykey.key -zupt compress \-\-pq pub.key backup.zupt ~/Documents/ + zupt keygen -o mykey.key + zupt keygen --pub -o pub.key -k mykey.key + zupt c --pq pub.key backup.zupt ~/Documents/ + zupt x --pq mykey.key backup.zupt -o ~/restored/ .fi + +.TP +Generate SDK v2 keypair, encrypt, decrypt (recommended): +.nf + zupt keygen --sdk -o mykey.priv + # creates mykey.priv (private) and mykey.priv.pub (public) + zupt c --pq-sdk mykey.priv.pub backup.zupt ~/Documents/ + zupt x --pq-sdk mykey.priv backup.zupt +.fi + +.TP +Full-disk backup with PQ encryption: +.nf + zupt keygen --sdk -o disk.priv + zupt disk backup --pq-sdk disk.priv.pub /dev/sda backup.img.zupt +.fi + .SH FILES .TP -.I *.zupt -Zupt archive format (v1.4). +.I /usr/bin/zupt +The zupt binary. .TP -.I *.zupt-key -ML-KEM-768 + X25519 keypair file. +.I /usr/lib/x86_64-linux-gnu/libzuptsdk.so.2 +The libzuptsdk shared library (Linux x86_64). +.TP +.I /usr/include/zuptsdk.h +libzuptsdk public C API. +.TP +.I /usr/share/doc/zupt/ +Documentation, changelog, audit reports. + +.SH ENVIRONMENT +.TP +.B ZUPT_THREADS +Default thread count when +.B -t +is not specified. +.TP +.B ZUPT_TMPDIR +Temporary directory for intermediate files (default: +.IR /tmp ). + .SH EXIT STATUS -0 on success, 1 on error. +.TP +.B 0 +Success. +.TP +.B 1 +General error (invalid arguments, file not found, etc.). +.TP +.B 2 +Authentication failure (wrong password, wrong key, tampered ciphertext). +.TP +.B 3 +I/O error. +.TP +.B 4 +Archive format error (corrupt, unsupported version, malformed header). + +.SH SECURITY +.B zupt 2.2+ +recommends +.B --pq-sdk +for new archives. The legacy +.B --pq +mode uses an XOR+SHA3-512 hybrid combiner that has been superseded +by HKDF-SHA3 with domain separation in the SDK path. Both modes +remain supported for archive interoperability. + +For password-encrypted archives, prefer the SDK path: it uses +Argon2id (RFC 9106) with OWASP-compliant minimum parameters +(64 MiB memory, 3 iterations, 1 thread), versus PBKDF2-SHA256 in +the legacy path. + +.B Path traversal protection. +zupt 2.2.2+ rejects archive entries containing +.IR ".." , +absolute paths +.RI ( /foo +or +.IR C:\\foo ), +or embedded NUL bytes. On POSIX systems, output files are opened with +.B O_NOFOLLOW +so that pre-existing symlinks at the extraction target are not followed +(defense against TOCTOU attacks where an attacker plants a symlink in the +output directory before extraction). On Windows, this defense relies on +directory ACLs. + +.B Operational guidance for untrusted archives. +Always extract into an empty dedicated directory, audit symlinks before +extraction, and never run extraction as root. + +.SH BUGS +Report at +.UR https://git.securityops.co/cristiancmoises/zupt/issues +.UE +or by email to +.MT zupt@riseup.net +.ME . + .SH AUTHOR -Cristian Cezar Moisés -.SH LICENSE -GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). -The integrated VaptVupt codec is licensed under GPL-3.0-or-later (canonical upstream: github.com/cristiancmoises/vaptvupt). -For commercial licensing inquiries, contact: sac@securityops.co. +Cristian Cezar Moisés +.MT zupt@riseup.net +.ME + .SH SEE ALSO -.BR gzip (1), -.BR zstd (1), -.BR lz4 (1) +.BR zupt-gui (1), +.BR tar (1), +.BR gzip (1). + +.SH STANDARDS +Zupt implements algorithms from FIPS 197 (AES), FIPS 202 (Keccak/SHA-3), +FIPS 203 (ML-KEM), and follows RFC 5297 (AES-SIV), RFC 5869 (HKDF), +RFC 7748 (X25519), RFC 8439 (ChaCha20-Poly1305), RFC 9106 (Argon2), +and RFC 9180 (HPKE). Cryptographic primitive selection is aligned +with Brazilian Instrução Normativa ITI nº 35/2026, which incorporated +ML-KEM-768/1024 into the ICP-Brasil framework. + +.SH LICENSE +.PP +Zupt itself (CLI, GUI, libzuptsdk, Jasmin source) is licensed under the +.B GNU Affero General Public License version 3 or later +(AGPL-3.0-or-later). The bundled VaptVupt LZ codec +.RB ( src/vv_*.c ", " src/vaptvupt_api.c ) +is licensed under the +.B GNU General Public License version 3 or later +(GPL-3.0-or-later). VaptVupt is GPL not AGPL so that, with sufficient +maturity, it can be considered for upstreaming into the Linux or BSD +kernels. +.PP +Commercial licenses (relief from copyleft terms) are available for both +components. Contact +.MT sac@securityops.co +.ME . +.PP +See +.B /usr/share/doc/zupt/LICENSE +and +.B /usr/share/doc/zupt/THIRD-PARTY-NOTICES.md +for the full text and complete attribution. + +.SH PROJECT +.PP +Home page: +.UR https://git.securityops.co/cristiancmoises/zupt +.UE +.PP +Related projects (all by Cristian Cezar Moisés, hosted on +git.securityops.co): +.IP \(bu 2 +.B zupt-android +\(em +.UR https://git.securityops.co/cristiancmoises/zupt-android +.UE +.IP \(bu 2 +.B zupt-web +\(em +.UR https://git.securityops.co/cristiancmoises/zupt-web +.UE +.IP \(bu 2 +.B libzuptsdk +\(em +.UR https://git.securityops.co/cristiancmoises/libzuptsdk +.UE +.IP \(bu 2 +.B vaptvupt +(standalone GPL codec) \(em +.UR https://git.securityops.co/cristiancmoises/vaptvupt +.UE diff --git a/gui/LICENSE-GUI b/gui/LICENSE-GUI index c3cfecf..bac9a9c 100644 --- a/gui/LICENSE-GUI +++ b/gui/LICENSE-GUI @@ -1,34 +1,21 @@ -================================================================================ -ZUPT-GUI — Cross-Platform Desktop Frontend for Zupt +MIT License - Copyright (C) 2026 Cristian Cezar Moisés - Contact for commercial licensing: sac@securityops.co +Copyright (c) 2026 Cristian Cezar Moisés -================================================================================ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -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. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -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. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - --------------------------------------------------------------------------------- -COMMERCIAL LICENSING --------------------------------------------------------------------------------- - -For commercial licensing — including embedding into proprietary appliances, -hosted backup-as-a-service products, or any other use that is incompatible -with the AGPL's source-disclosure requirements — please contact: - - sac@securityops.co - -The full AGPL-3.0 text is available in the parent directory's LICENSE file -and at . -================================================================================ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/gui/README.md b/gui/README.md index bdfdd76..d695b0f 100644 --- a/gui/README.md +++ b/gui/README.md @@ -1,6 +1,6 @@ # Zupt GUI — Cross-Platform Post-Quantum Backup -Desktop application for [zupt](https://github.com/cristiancmoises/zupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption. +Desktop application for [zupt](https://git.securityops.co/cristiancmoises/zupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption. Works on GNU/Linux, BSD, macOS, and Windows. @@ -111,7 +111,7 @@ The GUI calls the zupt CLI binary — all cryptography runs in native C, not Pyt |----------|--------|------| | Any | pip | `pip install .` | | Debian/Ubuntu/Mint | .deb | `packaging/deb/control` | -| Fedora/openSUSE | .rpm | `rpmbuild -ba packaging/rpm/zupt-gui.spec` | +| Fedora/RHEL | .rpm | `rpmbuild -ba packaging/rpm/zupt-gui.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` | @@ -119,11 +119,8 @@ The GUI calls the zupt CLI binary — all cryptography runs in native C, not Pyt ## Credits -- **zupt** v2.1.6 — Cristian Cezar Moises ([github](https://github.com/cristiancmoises/zupt)) -- **libzupt** v1.0.2 — Alessandro de Oliveira Faria ([github](https://github.com/cabelo/libzupt)) +- **zupt** v2.2.2 — Cristian Cezar Moisés ([github](https://git.securityops.co/cristiancmoises/zupt)) ## License -**AGPL-3.0-or-later** — see [LICENSE-GUI](LICENSE-GUI) and the project-root [LICENSE](../LICENSE). - -For commercial licensing inquiries, contact: **sac@securityops.co** +AGPL-3.0-or-later diff --git a/gui/install.sh b/gui/install.sh old mode 100644 new mode 100755 index 80f628b..73e8a2c --- a/gui/install.sh +++ b/gui/install.sh @@ -1,4 +1,6 @@ #!/bin/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 diff --git a/gui/packaging/appimage/build-appimage.sh b/gui/packaging/appimage/build-appimage.sh old mode 100644 new mode 100755 index 6b84b25..c0847b2 --- a/gui/packaging/appimage/build-appimage.sh +++ b/gui/packaging/appimage/build-appimage.sh @@ -1,4 +1,6 @@ #!/bin/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 diff --git a/gui/packaging/build-gui-deb.sh b/gui/packaging/build-gui-deb.sh new file mode 100755 index 0000000..8cd7cc6 --- /dev/null +++ b/gui/packaging/build-gui-deb.sh @@ -0,0 +1,94 @@ +#!/bin/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 +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" <= 3.9), python3-pyside6, zupt (>= 2.2.0) +Maintainer: Cristian Cezar Moisés +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 diff --git a/gui/packaging/flatpak/dev.zupt.gui.yml b/gui/packaging/flatpak/dev.zupt.gui.yml index f1dc962..31d70f1 100644 --- a/gui/packaging/flatpak/dev.zupt.gui.yml +++ b/gui/packaging/flatpak/dev.zupt.gui.yml @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés app-id: dev.zupt.gui runtime: org.freedesktop.Platform runtime-version: '24.08' @@ -24,7 +26,7 @@ modules: - install -Dm755 zupt /app/bin/zupt sources: - type: git - url: https://github.com/cristiancmoises/zupt + url: https://git.securityops.co/cristiancmoises/zupt tag: v2.1.6 - name: zupt-gui diff --git a/gui/packaging/rpm/zupt-gui.spec b/gui/packaging/rpm/zupt-gui.spec deleted file mode 100644 index f928ecb..0000000 --- a/gui/packaging/rpm/zupt-gui.spec +++ /dev/null @@ -1,43 +0,0 @@ -Name: zupt-gui -Version: 1.0.0 -Release: 1%{?dist} -Summary: Zupt GUI — Post-Quantum Backup Utility -License: AGPL-3.0-or-later -URL: https://github.com/cristiancmoises/zupt -Source0: %{name}-%{version}.tar.gz -BuildArch: noarch - -Requires: python3 >= 3.9 -Requires: python3-pyside6 -Requires: zupt >= 2.1.7 - -%description -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. - -%install -mkdir -p %{buildroot}%{_bindir} -mkdir -p %{buildroot}%{_datadir}/zupt-gui -mkdir -p %{buildroot}%{_datadir}/applications -mkdir -p %{buildroot}%{_datadir}/icons/hicolor/256x256/apps - -install -m 755 src/zupt_gui.py %{buildroot}%{_datadir}/zupt-gui/zupt_gui.py -install -m 644 packaging/zupt-gui.desktop %{buildroot}%{_datadir}/applications/ - -cat > %{buildroot}%{_bindir}/zupt-gui << 'EOF' -#!/bin/sh -exec python3 %{_datadir}/zupt-gui/zupt_gui.py "$@" -EOF -chmod 755 %{buildroot}%{_bindir}/zupt-gui - -%files -%license LICENSE -%{_bindir}/zupt-gui -%{_datadir}/zupt-gui/ -%{_datadir}/applications/zupt-gui.desktop - -%changelog -* Mon Apr 21 2026 Cristian Cezar Moises - 1.0.0-1 -- Initial release diff --git a/gui/setup.py b/gui/setup.py index be3be6a..ca76764 100644 --- a/gui/setup.py +++ b/gui/setup.py @@ -1,14 +1,16 @@ #!/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.0.0", + 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://github.com/cristiancmoises/zupt", + url="https://git.securityops.co/cristiancmoises/zupt", license="AGPL-3.0-or-later", packages=find_packages(where="src"), package_dir={"": "src"}, diff --git a/gui/src/zupt_gui.py b/gui/src/zupt_gui.py index b3d1e10..b75c961 100644 --- a/gui/src/zupt_gui.py +++ b/gui/src/zupt_gui.py @@ -1,15 +1,47 @@ #!/usr/bin/env python3 -"""Zupt GUI — Cross-Platform Post-Quantum Backup. Requires PySide6.""" +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +"""Zupt GUI — Cross-Platform Post-Quantum Backup. + +Tries PySide6 first (preferred), falls back to PyQt6 if PySide6 is +not installed. PyQt6 is the default available package on Debian/Ubuntu +without requiring pip; PySide6 ships with broader signal/slot semantics +but the API surface used here is portable between the two. +""" import sys, os, subprocess, shutil from pathlib import Path -from PySide6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QPushButton, QLineEdit, QComboBox, QFileDialog, - QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox, - QSpinBox, QMessageBox, QStatusBar, QScrollArea -) -from PySide6.QtCore import Qt, Signal, QObject, QThread -from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap + +# Try PySide6, fall back to PyQt6. The two have nearly-identical APIs; +# the only adjustment needed is Signal/pyqtSignal naming, handled below. +try: + from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QLineEdit, QComboBox, QFileDialog, + QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox, + QSpinBox, QMessageBox, QStatusBar, QScrollArea + ) + from PySide6.QtCore import Qt, Signal, QObject, QThread + from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap + QT_BINDING = "PySide6" +except ImportError: + try: + from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QLineEdit, QComboBox, QFileDialog, + QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox, + QSpinBox, QMessageBox, QStatusBar, QScrollArea + ) + from PyQt6.QtCore import Qt, pyqtSignal as Signal, QObject, QThread + from PyQt6.QtGui import QPalette, QColor, QIcon, QPixmap + QT_BINDING = "PyQt6" + except ImportError: + sys.stderr.write( + "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 zupt binary ── def _find_zupt(): @@ -146,7 +178,7 @@ def Log(h=150): t = QTextEdit(); t.setReadOnly(True); t.setMaximumHeight(h); return t def PwField(ph="Optional"): - pw = QLineEdit(); pw.setEchoMode(QLineEdit.Password); pw.setPlaceholderText(ph); return pw + pw = QLineEdit(); pw.setEchoMode(QLineEdit.EchoMode.Password); pw.setPlaceholderText(ph); return pw class PathField(QWidget): def __init__(self, ph="", mode="open", filters="All Files (*)"): @@ -178,7 +210,7 @@ class PathField(QWidget): if ps: self.edit.setText("|".join(ps) if self.mode == "multi" else ps[0]) def scrollable(w): - sa = QScrollArea(); sa.setWidgetResizable(True); sa.setWidget(w); sa.setFrameShape(QFrame.NoFrame); return sa + sa = QScrollArea(); sa.setWidgetResizable(True); sa.setWidget(w); sa.setFrameShape(QFrame.Shape.NoFrame); return sa def run_async(parent, cmd, btn, log, progress=None): log.clear(); btn.setEnabled(False) @@ -213,6 +245,11 @@ class KeysTab(QWidget): self.gen_priv = PathField("e.g. ~/zupt_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) + self.gen_btn = QPushButton("Generate Keypair") self.gen_btn.clicked.connect(self._generate) v.addWidget(self.gen_btn) @@ -245,16 +282,25 @@ class KeysTab(QWidget): p = self.gen_priv.path() or str(Path.home() / "zupt_private.key") self.gen_priv.edit.setText(p) self.gen_log.clear(); self.gen_btn.setEnabled(False) - code, _, err = run_zupt(["keygen", "-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]) - self.gen_log.append(e2.strip()) - if c2 == 0: - self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}") + if self.gen_sdk.isChecked(): + # SDK keygen creates both files 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.") else: - self.gen_log.append("\nFailed.") + code, _, err = run_zupt(["keygen", "-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]) + self.gen_log.append(e2.strip()) + if c2 == 0: + self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}") + else: + self.gen_log.append("\nFailed.") self.gen_btn.setEnabled(True) def _export(self): @@ -296,7 +342,11 @@ class CompressTab(QWidget): v.addLayout(row); v.addWidget(Sep()) 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);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq) + 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) 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) @@ -319,7 +369,9 @@ class CompressTab(QWidget): if self.dedup.isChecked(): cmd.append("--dedup") if self.solid.isChecked(): cmd.append("--solid") if self.pw.text(): cmd += ["-p", self.pw.text()] - if self.pq.path(): cmd += ["--pq", self.pq.path()] + if self.pq.path(): + flag = "--pq-sdk" if self.sdk.isChecked() else "--pq" + cmd += [flag, self.pq.path()] cmd.append(dst); cmd.extend(srcs) run_async(self, cmd, self.btn, self.log, self.progress) @@ -337,6 +389,10 @@ 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) 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) @@ -350,7 +406,12 @@ class ExtractTab(QWidget): cmd = ["extract"] if self.out.path(): cmd += ["-o", self.out.path()] if self.pw.text(): cmd += ["-p", self.pw.text()] - if self.pq.path(): cmd += ["--pq", self.pq.path()] + 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" + cmd += [flag, self.pq.path()] cmd.append(arc) run_async(self, cmd, self.btn, self.log, self.progress) @@ -433,7 +494,8 @@ class DiskTab(QWidget): def _restore(self): a, t = self.rarc.path(), self.rtgt.path() if not a or not t: QMessageBox.warning(self, "Zupt", "Set archive and target."); return - if QMessageBox.warning(self, "Confirm", f"OVERWRITE {t}?", QMessageBox.Yes|QMessageBox.Cancel) != QMessageBox.Yes: return + SB = QMessageBox.StandardButton + if QMessageBox.warning(self, "Confirm", f"OVERWRITE {t}?", SB.Yes|SB.Cancel) != SB.Yes: return cmd = ["disk", "restore"] if self.rpw.text(): cmd += ["-p", self.rpw.text()] cmd += [a, t]; run_async(self, cmd, self.rbtn, self.rlog) @@ -462,11 +524,11 @@ class AboutTab(QWidget): ("SHA3/SHAKE FIPS 202 Hash / XOF", "color:#5a7a88;font-size:12px;font-family:monospace;"), ("", ""), ("CREDITS", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), - ("zupt Cristian Cezar Moises AGPL-3.0", "color:#5a7a88;font-size:12px;font-family:monospace;"), - ("github.com/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"), + ("zupt Cristian Cezar Moises MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("git.securityops.co/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"), ("", ""), - ("libzupt Alessandro de Oliveira Faria MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"), - ("github.com/cabelo/libzupt", "color:#3a5868;font-size:11px;font-family:monospace;"), + ("zupt Cristian Cezar Moises AGPL-3.0+", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("git.securityops.co/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"), ("", ""), ("WEBSITE", "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;"), @@ -555,10 +617,13 @@ def main(): app.setStyle("Fusion") app.setStyleSheet(STYLE) pal = QPalette() - for role, c in [(QPalette.Window,"#0a0a0a"),(QPalette.WindowText,"#90acb8"), - (QPalette.Base,"#0e1820"),(QPalette.Text,"#c0dce8"), - (QPalette.Button,"#0a2028"),(QPalette.ButtonText,"#00dde0"), - (QPalette.Highlight,"#004858"),(QPalette.HighlightedText,"#00dde0")]: + # PyQt6 requires scoped enums (QPalette.ColorRole.Window); PySide6 supports + # both forms but we use the scoped form for cross-binding compatibility. + CR = QPalette.ColorRole + for role, c in [(CR.Window,"#0a0a0a"),(CR.WindowText,"#90acb8"), + (CR.Base,"#0e1820"),(CR.Text,"#c0dce8"), + (CR.Button,"#0a2028"),(CR.ButtonText,"#00dde0"), + (CR.Highlight,"#004858"),(CR.HighlightedText,"#00dde0")]: pal.setColor(role, QColor(c)) app.setPalette(pal) win = ZuptWindow(compress_files=compress_files, extract_file=extract_file) diff --git a/gui/zupt-gui b/gui/zupt-gui old mode 100644 new mode 100755 diff --git a/include/vaptvupt.h b/include/vaptvupt.h index 1e5e401..55d994c 100644 --- a/include/vaptvupt.h +++ b/include/vaptvupt.h @@ -3,8 +3,7 @@ * Public API and data structures * * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright 2026 Cristian. * Zero dependencies. Pure C11. */ #ifndef VAPTVUPT_H diff --git a/include/vaptvupt_api.h b/include/vaptvupt_api.h index a551ae8..f19c85f 100644 --- a/include/vaptvupt_api.h +++ b/include/vaptvupt_api.h @@ -1,8 +1,7 @@ /* * VaptVupt — Zupt Integration API * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright 2026 Cristian. * * ZUPT-COMPAT: This is the API that Zupt calls. It wraps the internal * VaptVupt API with sensible defaults for backup workloads: diff --git a/include/vv_ans.h b/include/vv_ans.h index b906e82..835d0f4 100644 --- a/include/vv_ans.h +++ b/include/vv_ans.h @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — tANS Entropy Codec (v2: sparse header + 4-way interleaved) * diff --git a/include/vv_huffman.h b/include/vv_huffman.h index 02abda3..ef9697b 100644 --- a/include/vv_huffman.h +++ b/include/vv_huffman.h @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — Canonical Huffman Codec * diff --git a/include/vv_platform.h b/include/vv_platform.h index 51f0f61..2d67b91 100644 --- a/include/vv_platform.h +++ b/include/vv_platform.h @@ -5,8 +5,6 @@ * the codebase. Supports GCC, Clang, MSVC, and Intel compilers. * * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co */ #ifndef VV_PLATFORM_H diff --git a/include/zupt.h b/include/zupt.h index cb8ea8b..c598916 100644 --- a/include/zupt.h +++ b/include/zupt.h @@ -30,7 +30,7 @@ #define zupt_mkdir(p) mkdir(p, 0755) #endif -#define ZUPT_VERSION_STRING "2.1.7" +#define ZUPT_VERSION_STRING "2.2.2" #define ZUPT_FORMAT_MAJOR 1 #define ZUPT_FORMAT_MINOR 4 @@ -57,10 +57,13 @@ #define ZUPT_FLAG_PQ_HYBRID (1u << 3) /* Post-quantum hybrid encryption */ #define ZUPT_FLAG_FORMAT_STABLE (1u << 4) /* v1.0: format frozen */ #define ZUPT_FLAG_DEDUP (1u << 7) /* Block-level deduplication enabled */ +#define ZUPT_FLAG_AAD_SEQ (1u << 8) /* MAC binds block_seq as AAD (anti-reorder) */ /* Encryption types (stored in encryption header block) */ #define ZUPT_ENC_PBKDF2 0x01 /* Password-based: PBKDF2 → AES-256-CTR + HMAC */ -#define ZUPT_ENC_PQ_HYBRID 0x02 /* ML-KEM-768 + X25519 hybrid KEM */ +#define ZUPT_ENC_PQ_HYBRID 0x02 /* ML-KEM-768 + X25519 hybrid KEM (legacy XOR+SHA3) */ +#define ZUPT_ENC_PQ_SDK_V2 0x03 /* libzuptsdk v2 header: HKDF combiner + commitment + HPKE binding */ +#define ZUPT_ENC_PW_ARGON2 0x04 /* Password-based via libzuptsdk: Argon2id + XChaCha20-Poly1305 */ /* Block types */ #define ZUPT_BLOCK_DATA 0x00 @@ -170,6 +173,7 @@ typedef struct { int level; uint32_t block_size; uint16_t codec_id; int verbose, encrypt, quiet, solid, threads; int pq_mode; /* 1 = post-quantum hybrid KEM mode */ + int sdk_mode; /* 1 = use libzuptsdk-backed v3 crypto (HKDF combiner + commitment + HPKE) */ int dedup; /* 1 = block-level deduplication enabled */ char password[256]; char keyfile[ZUPT_MAX_PATH]; /* Path to .zupt-key file */ @@ -325,6 +329,17 @@ int zupt_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile, int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile, const uint8_t *enc_hdr, size_t enc_hdr_len); +/* ─── SDK-backed crypto (zupt v2.2+, libzuptsdk under the hood) ─── */ +int zupt_sdk_hybrid_keygen(const char *privkeyfile, const char *pubkeyfile); +int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile, + uint8_t *enc_hdr, size_t *enc_hdr_len); +int zupt_sdk_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile, + const uint8_t *enc_hdr, size_t enc_hdr_len); +int zupt_sdk_password_encrypt_init(zupt_keyring_t *kr, const char *password, + uint8_t *enc_hdr, size_t *enc_hdr_len); +int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password, + const uint8_t *enc_hdr, size_t enc_hdr_len); + const char *zupt_strerror(zupt_error_t e); const char *zupt_codec_name(uint16_t id); void zupt_default_options(zupt_options_t *o); diff --git a/include/zupt_acsl.h b/include/zupt_acsl.h index b9c1004..8973783 100644 --- a/include/zupt_acsl.h +++ b/include/zupt_acsl.h @@ -1,7 +1,8 @@ /* - * Zupt — ACSL Custom Predicates for Frama-C/WP * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt — ACSL Custom Predicates for Frama-C/WP + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Usage: frama-c -wp -wp-rte -wp-model Typed+Cast * -cpp-extra-args="-Iinclude -Isrc" src/zupt_crypto.c diff --git a/include/zupt_cpuid.h b/include/zupt_cpuid.h index 7dcb3f1..3328673 100644 --- a/include/zupt_cpuid.h +++ b/include/zupt_cpuid.h @@ -1,7 +1,8 @@ /* - * Zupt — CPU Feature Detection * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt — CPU Feature Detection + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later */ #ifndef ZUPT_CPUID_H #define ZUPT_CPUID_H diff --git a/include/zupt_jasmin.h b/include/zupt_jasmin.h index a6b785b..f0b8278 100644 --- a/include/zupt_jasmin.h +++ b/include/zupt_jasmin.h @@ -1,7 +1,8 @@ /* - * Zupt — Jasmin Verified Crypto Declarations * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt — Jasmin Verified Crypto Declarations + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Extern declarations for Jasmin-compiled assembly functions. * These replace C fallbacks when built with -DZUPT_USE_JASMIN. diff --git a/install.sh b/install.sh index 84fd93d..98412a1 100644 --- a/install.sh +++ b/install.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés # Fast Installer for Zupt - GNU/Linux set -e @@ -9,7 +11,7 @@ echo "🔧 Installing Zupt..." TMP_DIR=$(mktemp -d) # Clone and build -git clone https://github.com/cristiancmoises/zupt.git "$TMP_DIR/zupt" +git clone https://git.securityops.co/cristiancmoises/zupt.git "$TMP_DIR/zupt" cd "$TMP_DIR/zupt" make clean diff --git a/jasmin/zupt_aes_ctr.jazz b/jasmin/zupt_aes_ctr.jazz index ae9895a..6f017d3 100644 --- a/jasmin/zupt_aes_ctr.jazz +++ b/jasmin/zupt_aes_ctr.jazz @@ -1,5 +1,6 @@ /* Zupt — AES-256 Single Block Encrypt via AES-NI (Jasmin) - * Copyright (c) 2026 Cristian Cezar Moisés — MIT License + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later * * CT-REQUIRED: AES-NI has no data-dependent timing. * diff --git a/jasmin/zupt_aes_ctr.s b/jasmin/zupt_aes_ctr.s index 6d121a2..5c08b65 100644 --- a/jasmin/zupt_aes_ctr.s +++ b/jasmin/zupt_aes_ctr.s @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +# Generated from jasmin/zupt_aes_ctr.jazz by jasminc. .intel_syntax noprefix .text .p2align 5 diff --git a/jasmin/zupt_aes_ctr4.jazz b/jasmin/zupt_aes_ctr4.jazz index be87368..bb886f7 100644 --- a/jasmin/zupt_aes_ctr4.jazz +++ b/jasmin/zupt_aes_ctr4.jazz @@ -1,5 +1,6 @@ /* Zupt — AES-256-CTR 4-Block Pipeline via AES-NI (Jasmin) - * Copyright (c) 2026 Cristian Cezar Moisés — MIT License + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later * * CT-REQUIRED: AES-NI has no data-dependent timing. * diff --git a/jasmin/zupt_aes_ctr4.s b/jasmin/zupt_aes_ctr4.s index 8c1fe08..eb0f91d 100644 --- a/jasmin/zupt_aes_ctr4.s +++ b/jasmin/zupt_aes_ctr4.s @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +# Generated from jasmin/zupt_aes_ctr4.jazz by jasminc. .intel_syntax noprefix .text .p2align 5 diff --git a/jasmin/zupt_mac_verify.jazz b/jasmin/zupt_mac_verify.jazz index 39e95b6..6f6884a 100644 --- a/jasmin/zupt_mac_verify.jazz +++ b/jasmin/zupt_mac_verify.jazz @@ -1,5 +1,6 @@ /* Zupt — Constant-Time MAC Comparison (Jasmin) - * Copyright (c) 2026 Cristian Cezar Moisés — MIT License + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later * * CT-REQUIRED: Timing independent of input byte values. * Compare 32 bytes as 4 × u64 — no byte-level access needed. diff --git a/jasmin/zupt_mac_verify.s b/jasmin/zupt_mac_verify.s index 815c449..ac2b132 100644 --- a/jasmin/zupt_mac_verify.s +++ b/jasmin/zupt_mac_verify.s @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +# Generated from jasmin/zupt_mac_verify.jazz by jasminc. .intel_syntax noprefix .text .p2align 5 diff --git a/jasmin/zupt_mlkem_select.jazz b/jasmin/zupt_mlkem_select.jazz index 49997c7..1004d02 100644 --- a/jasmin/zupt_mlkem_select.jazz +++ b/jasmin/zupt_mlkem_select.jazz @@ -1,5 +1,6 @@ /* Zupt — ML-KEM Constant-Time Select (Jasmin) - * Copyright (c) 2026 Cristian Cezar Moisés — MIT License + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later * * CT-REQUIRED: FO implicit rejection must not leak via timing. * Operates in u64 chunks: 4 × 8 = 32 bytes, no byte access. diff --git a/jasmin/zupt_mlkem_select.s b/jasmin/zupt_mlkem_select.s index 8565d7f..3cd18dd 100644 --- a/jasmin/zupt_mlkem_select.s +++ b/jasmin/zupt_mlkem_select.s @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +# Generated from jasmin/zupt_mlkem_select.jazz by jasminc. .intel_syntax noprefix .text .p2align 5 diff --git a/jasmin/zupt_x25519_fe.jazz b/jasmin/zupt_x25519_fe.jazz index 4fedcb6..461db22 100644 --- a/jasmin/zupt_x25519_fe.jazz +++ b/jasmin/zupt_x25519_fe.jazz @@ -1,5 +1,6 @@ /* Zupt — X25519 Constant-Time Conditional Swap (Jasmin) - * Copyright (c) 2026 Cristian Cezar Moisés — MIT License + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later * * CT-REQUIRED: fe_cswap must not leak cond via timing. * This is the only CT-critical field operation in X25519. diff --git a/jasmin/zupt_x25519_fe.s b/jasmin/zupt_x25519_fe.s index e2d0edb..fb77e86 100644 --- a/jasmin/zupt_x25519_fe.s +++ b/jasmin/zupt_x25519_fe.s @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +# Generated from jasmin/zupt_x25519_fe.jazz by jasminc. .intel_syntax noprefix .text .p2align 5 diff --git a/packaging/build-appimage.sh b/packaging/build-appimage.sh new file mode 100755 index 0000000..9eb0e80 --- /dev/null +++ b/packaging/build-appimage.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Build zupt as AppImage (portable single-file binary). +set -e +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-2.2.2}" +ARCH="${ARCH:-x86_64}" +NAME="zupt-$VERSION-$ARCH" +OUT="/tmp/${NAME}.AppDir" + +rm -rf "$OUT" +mkdir -p "$OUT/usr/bin" "$OUT/usr/lib" "$OUT/usr/share/applications" "$OUT/usr/share/icons/hicolor/256x256/apps" + +install -m 755 zupt "$OUT/usr/bin/" +install -m 644 vendor/zuptsdk/libzuptsdk.so.2.0.0 "$OUT/usr/lib/" +ln -sf libzuptsdk.so.2.0.0 "$OUT/usr/lib/libzuptsdk.so.2" +ln -sf libzuptsdk.so.2 "$OUT/usr/lib/libzuptsdk.so" + +cat > "$OUT/AppRun" <<'APPRUN' +#!/bin/bash +HERE="$(dirname "$(readlink -f "${0}")")" +export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH" +export PATH="$HERE/usr/bin:$PATH" +exec "$HERE/usr/bin/zupt" "$@" +APPRUN +chmod +x "$OUT/AppRun" + +cat > "$OUT/zupt.desktop" <<'DESK' +[Desktop Entry] +Name=Zupt +Comment=Post-quantum backup compression utility +Exec=zupt +Terminal=true +Type=Application +Categories=Utility;Archiving; +Icon=zupt +DESK +cp "$OUT/zupt.desktop" "$OUT/usr/share/applications/" + +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' > "$OUT/zupt.png" +cp "$OUT/zupt.png" "$OUT/usr/share/icons/hicolor/256x256/apps/zupt.png" + +if command -v appimagetool >/dev/null 2>&1; then + ARCH=$ARCH appimagetool "$OUT" "/tmp/${NAME}.AppImage" + echo "Built: /tmp/${NAME}.AppImage" +else + cd /tmp + tar -czf "${NAME}.AppDir.tar.gz" "$(basename "$OUT")" + echo "appimagetool not present; built AppDir tarball: /tmp/${NAME}.AppDir.tar.gz" + echo "Users can run: tar xzf ${NAME}.AppDir.tar.gz && ./${NAME}.AppDir/AppRun" +fi diff --git a/packaging/build-deb.sh b/packaging/build-deb.sh new file mode 100755 index 0000000..af2591a --- /dev/null +++ b/packaging/build-deb.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Build zupt CLI .deb package +set -e +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-2.2.2}" +ARCH="${ARCH:-amd64}" + +# Multi-arch lib path mapping (Debian convention) +case "$ARCH" in + amd64) MULTIARCH="x86_64-linux-gnu" ;; + arm64) MULTIARCH="aarch64-linux-gnu" ;; + armhf) MULTIARCH="arm-linux-gnueabihf" ;; + i386) MULTIARCH="i386-linux-gnu" ;; + *) MULTIARCH="$ARCH-linux-gnu" ;; +esac + +PKG="zupt_${VERSION}_${ARCH}" +ROOT="/tmp/$PKG" + +rm -rf "$ROOT" +mkdir -p "$ROOT/DEBIAN" \ + "$ROOT/usr/bin" \ + "$ROOT/usr/share/doc/zupt" \ + "$ROOT/usr/share/man/man1" + +# Binary +install -m 755 zupt "$ROOT/usr/bin/zupt" + +# Note: libzuptsdk.so.2 is provided by the libzuptsdk2 package (separate +# project). We do NOT bundle it. The user must install libzuptsdk2 to +# satisfy the Depends: line below, which gives them the SDK on its own +# upgrade cycle. + +# Docs +install -m 644 README.md CHANGELOG.md SECURITY.md AUDIT.md "$ROOT/usr/share/doc/zupt/" +gzip -9n -c CHANGELOG.md > "$ROOT/usr/share/doc/zupt/changelog.gz" + +# Man page (use the real one from doc/zupt.1) +if [ -f doc/zupt.1 ]; then + install -m 644 doc/zupt.1 "$ROOT/usr/share/man/man1/zupt.1" +else + cat > "$ROOT/usr/share/man/man1/zupt.1" <<'MAN' +.TH ZUPT 1 "April 2026" "zupt 2.2.2" "User Commands" +.SH NAME +zupt \- post-quantum backup compression utility +.SH SYNOPSIS +.B zupt +[\fIcommand\fR] [\fIoptions\fR] \fIarchive\fR [\fIfiles...\fR] +.SH SEE ALSO +Run \fBzupt help\fR for the full options reference. +MAN +fi +gzip -9n "$ROOT/usr/share/man/man1/zupt.1" + +# Copyright +cat > "$ROOT/usr/share/doc/zupt/copyright" <<'COPYRIGHT' +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: zupt +Upstream-Contact: Cristian Cezar Moisés +Source: https://git.securityops.co/cristiancmoises/zupt + +Files: * +Copyright: 2025-2026 Cristian Cezar Moisés +License: AGPL-3.0+ + This program 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. + . + On Debian systems, the complete text of the GNU Affero General Public + License version 3 can be found in /usr/share/common-licenses/AGPL-3. + +Files: src/vv_*.c src/vaptvupt_api.c include/vv_*.h include/vaptvupt*.h +Copyright: 2025-2026 Cristian Cezar Moisés +License: GPL-3.0+ + The VaptVupt LZ codec is licensed under the GNU General Public License + version 3 or later (NOT AGPL like the rest of the project). VaptVupt + is GPL so that, with sufficient maturity, it can be considered for + upstreaming into the Linux or BSD kernels. + . + On Debian systems, the complete text of the GNU General Public + License version 3 can be found in /usr/share/common-licenses/GPL-3. + +Comment: + Commercial licenses (relief from AGPL/GPL copyleft terms) are + available for both components. Contact sac@securityops.co for + commercial inquiries. +COPYRIGHT + +# Control +INSTALLED_SIZE=$(du -sk "$ROOT" | cut -f1) +cat > "$ROOT/DEBIAN/control" <= 2.28), libargon2-1, libssl3, libzuptsdk2 (>= 2.0.0) +Maintainer: Cristian Cezar Moisés +Installed-Size: $INSTALLED_SIZE +Homepage: https://git.securityops.co/cristiancmoises/zupt +Description: Post-quantum backup compression utility + Zupt is a backup-oriented compression utility with hybrid post-quantum + encryption (ML-KEM-768 + X25519). It provides authenticated encryption, + multi-threaded compression, full-disk backup/restore, and integrates + libzuptsdk for state-of-the-art crypto (HKDF combiner, key commitment, + HPKE binding, anti-fault decapsulation). +EOF + +# Postinst: ldconfig +cat > "$ROOT/DEBIAN/postinst" <<'POSTINST' +#!/bin/sh +set -e +ldconfig +POSTINST +chmod 755 "$ROOT/DEBIAN/postinst" + +cat > "$ROOT/DEBIAN/postrm" <<'POSTRM' +#!/bin/sh +set -e +if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then + ldconfig +fi +POSTRM +chmod 755 "$ROOT/DEBIAN/postrm" + +# Build +dpkg-deb -Zxz --build --root-owner-group "$ROOT" "/tmp/$PKG.deb" +echo "Built: /tmp/$PKG.deb" +dpkg-deb --info "/tmp/$PKG.deb" | head -15 diff --git a/packaging/build-gui-appimage.sh b/packaging/build-gui-appimage.sh new file mode 100755 index 0000000..53cda78 --- /dev/null +++ b/packaging/build-gui-appimage.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Build zupt-gui AppImage. Since the GUI is pure Python + Qt, the AppDir +# bundles only the Python source and metadata; it relies on system +# python3 + PyQt6/PySide6 at runtime. This keeps the AppImage tiny +# (~50 KB) and lets it work on any Linux with Qt6 Python bindings. +# +# For a true self-contained AppImage with bundled Python interpreter, +# use python-appimage (https://github.com/niess/python-appimage) on +# the build host — it produces a ~80 MB AppImage. The portable variant +# below is the better tradeoff for most distributions. +set -e +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-1.1.1}" +APPDIR="/tmp/zupt-gui.AppDir" + +rm -rf "$APPDIR" +mkdir -p "$APPDIR/usr/bin" \ + "$APPDIR/usr/lib/zupt-gui" \ + "$APPDIR/usr/share/applications" \ + "$APPDIR/usr/share/icons/hicolor/256x256/apps" + +# Python source +install -m 644 gui/src/zupt_gui.py "$APPDIR/usr/lib/zupt-gui/" + +# Wrapper +cat > "$APPDIR/usr/bin/zupt-gui" <<'WRAP' +#!/bin/sh +exec python3 "$(dirname "$0")/../lib/zupt-gui/zupt_gui.py" "$@" +WRAP +chmod 755 "$APPDIR/usr/bin/zupt-gui" + +# Desktop file +cat > "$APPDIR/zupt-gui.desktop" <<'DESKTOP' +[Desktop Entry] +Type=Application +Name=Zupt GUI +GenericName=Backup and Compression Utility +Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding +Exec=zupt-gui %f +Icon=zupt-gui +Terminal=false +Categories=Utility;Archiving;Compression;Security; +StartupNotify=true +DESKTOP +cp "$APPDIR/zupt-gui.desktop" "$APPDIR/usr/share/applications/" + +# Icon +if [ -f gui/assets/zupt-icon.png ]; then + cp gui/assets/zupt-icon.png "$APPDIR/zupt-gui.png" + cp gui/assets/zupt-icon.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" +else + python3 -c " +import struct, zlib +def png(w, h, color): + raw = b''.join(b'\\0' + bytes(color) * w for _ in range(h)) + def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t+d) & 0xffffffff) + return b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'') +open('$APPDIR/zupt-gui.png','wb').write(png(256, 256, (88, 92, 215))) +" + cp "$APPDIR/zupt-gui.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" +fi + +# AppRun — sets PATH so zupt-gui finds the bundled wrapper, falls +# back to system zupt CLI if not present in /usr/bin alongside. +cat > "$APPDIR/AppRun" <<'APPRUN' +#!/bin/sh +HERE="$(dirname "$(readlink -f "$0")")" +export PATH="$HERE/usr/bin:$PATH" + +# Pre-flight check: is python3 available? Is a Qt6 binding installed? +if ! command -v python3 >/dev/null 2>&1; then + cat >&2 </dev/null \ + && ! python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then + cat >&2 </dev/null 2>&1; then + cat >&2 </dev/null 2>&1; then + ARCH=x86_64 appimagetool "$APPDIR" "/tmp/Zupt-GUI-$VERSION-x86_64.AppImage" 2>&1 | tail -5 + echo "Built: /tmp/Zupt-GUI-$VERSION-x86_64.AppImage" +else + cd /tmp + rm -f "Zupt-GUI-$VERSION-x86_64.AppDir.tar.gz" + tar -czf "Zupt-GUI-$VERSION-x86_64.AppDir.tar.gz" zupt-gui.AppDir + cd - >/dev/null + echo "appimagetool unavailable; portable AppDir tarball at:" + echo " /tmp/Zupt-GUI-$VERSION-x86_64.AppDir.tar.gz" + echo "Run via: tar -xzf ... && ./zupt-gui.AppDir/AppRun" + echo "Convert to AppImage on a host with appimagetool:" + echo " ARCH=x86_64 appimagetool zupt-gui.AppDir Zupt-GUI-$VERSION-x86_64.AppImage" +fi diff --git a/packaging/build-gui-deb.sh b/packaging/build-gui-deb.sh new file mode 100755 index 0000000..15a05a9 --- /dev/null +++ b/packaging/build-gui-deb.sh @@ -0,0 +1,176 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Build zupt-gui .deb (Python/Qt GUI). Works with PyQt6 OR PySide6. +set -e +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-1.1.1}" +ARCH="all" +PKG="zupt-gui_${VERSION}_${ARCH}" +ROOT="/tmp/$PKG" + +rm -rf "$ROOT" +mkdir -p "$ROOT/DEBIAN" \ + "$ROOT/usr/bin" \ + "$ROOT/usr/lib/zupt-gui" \ + "$ROOT/usr/share/applications" \ + "$ROOT/usr/share/icons/hicolor/256x256/apps" \ + "$ROOT/usr/share/man/man1" \ + "$ROOT/usr/share/doc/zupt-gui" + +# Source files +install -m 644 gui/src/zupt_gui.py "$ROOT/usr/lib/zupt-gui/" + +# Wrapper script in /usr/bin +cat > "$ROOT/usr/bin/zupt-gui" <<'WRAP' +#!/bin/sh +exec python3 /usr/lib/zupt-gui/zupt_gui.py "$@" +WRAP +chmod 755 "$ROOT/usr/bin/zupt-gui" + +# Desktop entry +cat > "$ROOT/usr/share/applications/zupt-gui.desktop" <<'DESKTOP' +[Desktop Entry] +Type=Application +Name=Zupt GUI +GenericName=Backup and Compression Utility +Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding +Exec=zupt-gui %f +Icon=zupt-gui +Terminal=false +Categories=Utility;Archiving;Compression;Security; +StartupNotify=true +MimeType=application/x-zupt; +Keywords=archive;compression;encryption;post-quantum;backup; +DESKTOP + +# Man page +if [ -f doc/zupt-gui.1 ]; then + install -m 644 doc/zupt-gui.1 "$ROOT/usr/share/man/man1/zupt-gui.1" + gzip -9n "$ROOT/usr/share/man/man1/zupt-gui.1" +fi + +# Icon +if [ -f gui/assets/zupt-icon.png ]; then + cp gui/assets/zupt-icon.png "$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" +else + python3 -c " +import struct, zlib +def png(w, h, color): + raw = b''.join(b'\\0' + bytes(color) * w for _ in range(h)) + def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t+d) & 0xffffffff) + return b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'') +open('$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png','wb').write(png(256, 256, (88, 92, 215))) +" +fi + +# Docs +install -m 644 gui/README.md "$ROOT/usr/share/doc/zupt-gui/" 2>/dev/null || true +gzip -9n -c CHANGELOG.md > "$ROOT/usr/share/doc/zupt-gui/changelog.gz" + +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 +Source: https://git.securityops.co/cristiancmoises/zupt + +Files: * +Copyright: 2025-2026 Cristian Cezar Moisés +License: AGPL-3.0+ + This program 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. + . + On Debian systems, the complete text of the GNU Affero General Public + License version 3 can be found in /usr/share/common-licenses/AGPL-3. +COPYRIGHT + +# Control +INSTALLED_SIZE=$(du -sk "$ROOT" | cut -f1) +cat > "$ROOT/DEBIAN/control" <= 3.9), python3-pyqt6 | python3-pyside6, zupt (>= 2.2.2) +Maintainer: Cristian Cezar Moisés +Installed-Size: $INSTALLED_SIZE +Homepage: https://git.securityops.co/cristiancmoises/zupt +Description: Graphical interface for the Zupt post-quantum backup utility + PySide6/PyQt6 frontend for Zupt. Supports compression, extraction, key + management, and full disk backup/restore. Exposes both legacy --pq + and new --pq-sdk (libzuptsdk: HKDF combiner, key commitment, HPKE + binding, Argon2id) encryption modes. +EOF + +# Postinst: refresh icon cache + desktop database, print first-run guidance +cat > "$ROOT/DEBIAN/postinst" <<'POSTINST' +#!/bin/sh +set -e +if [ -x /usr/bin/update-desktop-database ]; then + update-desktop-database -q /usr/share/applications || true +fi +if [ -x /usr/bin/gtk-update-icon-cache ]; then + gtk-update-icon-cache -q /usr/share/icons/hicolor || true +fi + +# Friendly first-run check: warn the user if no Qt6 binding is installed. +# We don't fail the install (deb deps already enforce this); we just print +# clear guidance for users who saw "unmet dependencies" earlier. +if ! python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \ + && ! python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then + cat << 'MSG' + +────────────────────────────────────────────────────────────────────── +zupt-gui installed, but no Qt6 Python binding is available. + +Install one of the following to enable the GUI: + + Debian/Ubuntu/Mint: sudo apt install python3-pyqt6 + Fedora/RHEL/Rocky: sudo dnf install python3-pyqt6 + Arch/Manjaro: sudo pacman -S python-pyqt6 + pip (any distro): pip install --user PySide6 + +After installing the binding, launch with: zupt-gui +────────────────────────────────────────────────────────────────────── + +MSG +fi + +# Same friendly warning if zupt CLI not installed. +if ! command -v zupt >/dev/null 2>&1; then + cat << 'MSG' + +────────────────────────────────────────────────────────────────────── +zupt-gui needs the 'zupt' CLI to function. Install it: + + Debian/Ubuntu/Mint: sudo dpkg -i zupt_2.2.2_amd64.deb + (followed by: sudo apt --fix-broken install) +────────────────────────────────────────────────────────────────────── + +MSG +fi +exit 0 +POSTINST +chmod 755 "$ROOT/DEBIAN/postinst" + +cat > "$ROOT/DEBIAN/postrm" <<'POSTRM' +#!/bin/sh +set -e +if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then + if [ -x /usr/bin/update-desktop-database ]; then + update-desktop-database -q /usr/share/applications || true + fi + if [ -x /usr/bin/gtk-update-icon-cache ]; then + gtk-update-icon-cache -q /usr/share/icons/hicolor || true + fi +fi +POSTRM +chmod 755 "$ROOT/DEBIAN/postrm" + +dpkg-deb -Zxz --build --root-owner-group "$ROOT" "/tmp/$PKG.deb" +echo "Built: /tmp/$PKG.deb" +dpkg-deb --info "/tmp/$PKG.deb" | head -12 diff --git a/packaging/build-gui-rpm.sh b/packaging/build-gui-rpm.sh new file mode 100755 index 0000000..b399339 --- /dev/null +++ b/packaging/build-gui-rpm.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Build zupt-gui RPM. Falls back to SRPM-equivalent tarball if rpmbuild absent. +set -e +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-1.1.1}" +RPMROOT="/tmp/rpmbuild-zupt-gui" +rm -rf "$RPMROOT" +mkdir -p "$RPMROOT"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +TMP="/tmp/zupt-gui-$VERSION" +rm -rf "$TMP" && mkdir -p "$TMP/src" "$TMP/doc" "$TMP/assets" +cp gui/src/zupt_gui.py "$TMP/src/" +cp doc/zupt-gui.1 "$TMP/doc/" 2>/dev/null || true +cp gui/README.md "$TMP/" 2>/dev/null || true +cp LICENSE "$TMP/" 2>/dev/null || true +[ -f gui/assets/zupt-icon.png ] && cp gui/assets/zupt-icon.png "$TMP/assets/" +tar -czf "$RPMROOT/SOURCES/zupt-gui-$VERSION.tar.gz" -C /tmp "zupt-gui-$VERSION" + +cat > "$RPMROOT/SPECS/zupt-gui.spec" <= 3.9 +Requires: python3 >= 3.9 +Requires: (python3-qt6 or python3-pyside6 or python3-pyqt6) +Requires: zupt >= 2.2.2 + +%description +PySide6/PyQt6 frontend for Zupt. Supports compression, extraction, key +management, and full disk backup/restore. Exposes both legacy --pq and +new --pq-sdk (libzuptsdk: HKDF combiner, key commitment, HPKE binding, +Argon2id) encryption modes. Auto-detects whichever Qt6 binding is +installed at startup. + +%prep +%autosetup + +%build +# nothing to build; pure Python + +%install +mkdir -p %{buildroot}%{_bindir} +mkdir -p %{buildroot}%{_libdir}/zupt-gui +mkdir -p %{buildroot}%{_datadir}/applications +mkdir -p %{buildroot}%{_datadir}/icons/hicolor/256x256/apps +mkdir -p %{buildroot}%{_mandir}/man1 + +install -m 644 src/zupt_gui.py %{buildroot}%{_libdir}/zupt-gui/ + +cat > %{buildroot}%{_bindir}/zupt-gui <<'WRAP' +#!/bin/sh +exec python3 %{_libdir}/zupt-gui/zupt_gui.py "\$@" +WRAP +chmod 755 %{buildroot}%{_bindir}/zupt-gui + +cat > %{buildroot}%{_datadir}/applications/zupt-gui.desktop <<'DESKTOP' +[Desktop Entry] +Type=Application +Name=Zupt GUI +GenericName=Backup and Compression Utility +Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding +Exec=zupt-gui %f +Icon=zupt-gui +Terminal=false +Categories=Utility;Archiving;Compression;Security; +StartupNotify=true +DESKTOP + +[ -f doc/zupt-gui.1 ] && install -m 644 doc/zupt-gui.1 %{buildroot}%{_mandir}/man1/ +[ -f assets/zupt-icon.png ] && install -m 644 assets/zupt-icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png || true + +# Generate placeholder icon if no real one exists +if [ ! -f %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png ]; then + python3 -c " +import struct, zlib +def png(w, h, color): + raw = b''.join(b'\\0' + bytes(color) * w for _ in range(h)) + def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t+d) & 0xffffffff) + return b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'') +open('%{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png','wb').write(png(256, 256, (88, 92, 215))) +" +fi + +%post +if [ -x /usr/bin/update-desktop-database ]; then + update-desktop-database -q /usr/share/applications || : +fi +if [ -x /usr/bin/gtk-update-icon-cache ]; then + gtk-update-icon-cache -q /usr/share/icons/hicolor || : +fi + +%postun +if [ \$1 -eq 0 ]; then + if [ -x /usr/bin/update-desktop-database ]; then + update-desktop-database -q /usr/share/applications || : + fi +fi + +%files +%doc README.md +%license LICENSE +%{_bindir}/zupt-gui +%{_libdir}/zupt-gui/zupt_gui.py +%{_datadir}/applications/zupt-gui.desktop +%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png +%{_mandir}/man1/zupt-gui.1* + +%changelog +* Mon Apr 27 2026 Cristian Cezar Moisés - $VERSION-1 +- Cross-binding (PySide6 OR PyQt6 auto-detected) +- SDK v2 mode toggles in compress/extract/keygen tabs +- Man page added +EOF + +if command -v rpmbuild >/dev/null 2>&1; then + rpmbuild --define "_topdir $RPMROOT" -bb "$RPMROOT/SPECS/zupt-gui.spec" 2>&1 | tail -3 + cp "$RPMROOT/RPMS/noarch/zupt-gui-$VERSION-1."*.rpm /tmp/ 2>/dev/null || true + ls /tmp/zupt-gui-$VERSION-*.rpm 2>/dev/null +else + SRPM_TAR="/tmp/zupt-gui-$VERSION.srpm.tar.gz" + tar -czf "$SRPM_TAR" -C "$RPMROOT" SPECS SOURCES + echo "rpmbuild unavailable; SRPM-equivalent at: $SRPM_TAR" +fi diff --git a/packaging/build-rpm-manual.py b/packaging/build-rpm-manual.py new file mode 100644 index 0000000..9a7ce40 --- /dev/null +++ b/packaging/build-rpm-manual.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +""" +Build a binary RPM for zupt without rpmbuild. +Constructs an RPM-format file directly from the file tree we have for deb. + +This is intentionally minimal but produces a valid RPM that: +- Can be installed via `rpm -i` on RHEL/Fedora and other RPM-based distributions +- Contains correct dependency info +- Has working pre/post scripts +- Includes the binary, library, headers, docs, license +""" +import struct, os, sys, hashlib, gzip, io, time, subprocess + +VERSION = os.environ.get('VERSION', '2.2.1') +RELEASE = '1' +ARCH = 'x86_64' +NAME = 'zupt' + +# RPM tag values (from rpmtag.h) +RPMTAG_NAME = 1000 +RPMTAG_VERSION = 1001 +RPMTAG_RELEASE = 1002 +RPMTAG_SUMMARY = 1004 +RPMTAG_DESCRIPTION = 1005 +RPMTAG_BUILDTIME = 1006 +RPMTAG_BUILDHOST = 1007 +RPMTAG_SIZE = 1009 +RPMTAG_DISTRIBUTION = 1010 +RPMTAG_VENDOR = 1011 +RPMTAG_LICENSE = 1014 +RPMTAG_PACKAGER = 1015 +RPMTAG_GROUP = 1016 +RPMTAG_URL = 1020 +RPMTAG_OS = 1021 +RPMTAG_ARCH = 1022 +RPMTAG_PREIN = 1023 +RPMTAG_POSTIN = 1024 +RPMTAG_PREUN = 1025 +RPMTAG_POSTUN = 1026 +RPMTAG_FILESIZES = 1028 +RPMTAG_FILEMODES = 1030 +RPMTAG_FILERDEVS = 1033 +RPMTAG_FILEMTIMES = 1034 +RPMTAG_FILEDIGESTS = 1035 +RPMTAG_FILELINKTOS = 1036 +RPMTAG_FILEFLAGS = 1037 +RPMTAG_FILEUSERNAME = 1039 +RPMTAG_FILEGROUPNAME = 1040 +RPMTAG_PROVIDENAME = 1047 +RPMTAG_REQUIREFLAGS = 1048 +RPMTAG_REQUIRENAME = 1049 +RPMTAG_REQUIREVERSION = 1050 +RPMTAG_BASENAMES = 1117 +RPMTAG_DIRNAMES = 1118 +RPMTAG_DIRINDEXES = 1116 +RPMTAG_PAYLOADFORMAT = 1124 +RPMTAG_PAYLOADCOMPRESSOR = 1125 +RPMTAG_FILEDIGESTALGO = 5011 + +# Type codes +RPM_NULL_TYPE = 0 +RPM_CHAR_TYPE = 1 +RPM_INT8_TYPE = 2 +RPM_INT16_TYPE = 3 +RPM_INT32_TYPE = 4 +RPM_INT64_TYPE = 5 +RPM_STRING_TYPE = 6 +RPM_BIN_TYPE = 7 +RPM_STRING_ARRAY_TYPE = 8 + +class Header: + def __init__(self): + self.entries = [] # (tag, type, value) + + def add(self, tag, typ, value): + self.entries.append((tag, typ, value)) + + def serialize(self): + # Build store + index + store = bytearray() + index = [] + for tag, typ, value in self.entries: + if typ == RPM_STRING_TYPE: + count = 1 + data = value.encode('utf-8') + b'\x00' + offset = len(store) + store.extend(data) + elif typ == RPM_STRING_ARRAY_TYPE: + count = len(value) + data = b''.join(s.encode('utf-8') + b'\x00' for s in value) + offset = len(store) + store.extend(data) + elif typ == RPM_INT32_TYPE: + if not isinstance(value, list): + value = [value] + count = len(value) + # align to 4 + while len(store) % 4: store.append(0) + offset = len(store) + for v in value: + store.extend(struct.pack('>I', v & 0xFFFFFFFF)) + elif typ == RPM_INT16_TYPE: + if not isinstance(value, list): + value = [value] + count = len(value) + while len(store) % 2: store.append(0) + offset = len(store) + for v in value: + store.extend(struct.pack('>H', v & 0xFFFF)) + elif typ == RPM_BIN_TYPE: + count = len(value) + offset = len(store) + store.extend(value) + elif typ == RPM_NULL_TYPE: + count = 1 + offset = 0 + else: + raise ValueError(f"Unsupported type {typ}") + index.append(struct.pack('>IIII', tag, typ, offset, count)) + + index_bytes = b''.join(index) + # Header magic + reserved + index count + store size + out = struct.pack('>3sBI4sII', b'\x8e\xad\xe8', 1, 0, b'\x00\x00\x00\x00', + len(self.entries), len(store)) + out += index_bytes + bytes(store) + return out + +def make_cpio(file_list, source_root, payload_size_out): + """Build a cpio archive (newc format) of the files.""" + out = io.BytesIO() + inode = 1 + total = 0 + for arc_path, src_path, mode, is_dir, link_target in file_list: + if is_dir: + data = b'' + file_size = 0 + elif link_target is not None: + data = link_target.encode('utf-8') + file_size = len(data) + else: + with open(src_path, 'rb') as f: + data = f.read() + file_size = len(data) + total += file_size + + name = ('.' + arc_path).encode('utf-8') + b'\x00' + # newc header: 110 bytes + header = ( + b'070701' + + format(inode, '08x').encode('ascii') + + format(mode, '08x').encode('ascii') + + b'00000000' # uid + + b'00000000' # gid + + b'00000001' # nlink + + format(int(time.time()), '08x').encode('ascii') + + format(file_size, '08x').encode('ascii') + + b'00000000' * 4 # devmajor/minor + rdevmajor/minor + + format(len(name), '08x').encode('ascii') + + b'00000000' # check + ) + out.write(header) + out.write(name) + # pad to 4 + pad = (4 - ((len(header) + len(name)) % 4)) % 4 + out.write(b'\x00' * pad) + out.write(data) + # pad data to 4 + pad = (4 - (file_size % 4)) % 4 + out.write(b'\x00' * pad) + inode += 1 + + # Trailer + trailer_name = b'TRAILER!!!\x00' + out.write(b'070701' + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'00000001' + + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + + format(len(trailer_name), '08x').encode('ascii') + b'0' * 8) + out.write(trailer_name) + pad = (4 - ((110 + len(trailer_name)) % 4)) % 4 + out.write(b'\x00' * pad) + payload_size_out[0] = total + return out.getvalue() + +def main(): + # Files to include (source_path inside our deb tree) + deb_root = '/tmp/zupt_2.2.1_amd64' + files = [] # (arc_path, source_path, mode, is_dir, link_target) + + for root, dirs, fnames in os.walk(deb_root): + for d in sorted(dirs): + full = os.path.join(root, d) + arc = full[len(deb_root):] + files.append((arc, full, 0o40755, True, None)) + for fn in sorted(fnames): + full = os.path.join(root, fn) + arc = full[len(deb_root):] + if 'DEBIAN' in arc: + continue + if os.path.islink(full): + files.append((arc, full, 0o120777, False, os.readlink(full))) + else: + mode = 0o100755 if os.access(full, os.X_OK) else 0o100644 + files.append((arc, full, mode, False, None)) + + # Sort and build basename/dirname/dirindex arrays + files.sort(key=lambda x: x[0]) + + basenames = [] + dirnames_set = [] + dirname_to_idx = {} + dirindexes = [] + filesizes = [] + filemodes = [] + filemtimes = [] + filedigests = [] + filelinktos = [] + filerdevs = [] + fileflags = [] + fileuser = [] + filegroup = [] + + for arc, src, mode, is_dir, link in files: + d, b = os.path.split(arc) + d = d + '/' + if d not in dirname_to_idx: + dirname_to_idx[d] = len(dirnames_set) + dirnames_set.append(d) + basenames.append(b or '.') + dirindexes.append(dirname_to_idx[d]) + if is_dir: + filesizes.append(0) + filedigests.append('') + filelinktos.append('') + elif link: + filesizes.append(len(link)) + filedigests.append('') + filelinktos.append(link) + else: + filesizes.append(os.path.getsize(src)) + with open(src, 'rb') as f: + filedigests.append(hashlib.sha256(f.read()).hexdigest()) + filelinktos.append('') + filemodes.append(mode) + filemtimes.append(int(time.time())) + filerdevs.append(0) + fileflags.append(0) + fileuser.append('root') + filegroup.append('root') + + payload_size = [0] + cpio_data = make_cpio(files, deb_root, payload_size) + # Compress payload with gzip + gz_payload = gzip.compress(cpio_data) + + # Build main header + h = Header() + h.add(RPMTAG_NAME, RPM_STRING_TYPE, NAME) + h.add(RPMTAG_VERSION, RPM_STRING_TYPE, VERSION) + h.add(RPMTAG_RELEASE, RPM_STRING_TYPE, RELEASE) + h.add(RPMTAG_SUMMARY, RPM_STRING_ARRAY_TYPE, ['Post-quantum backup compression utility']) + h.add(RPMTAG_DESCRIPTION, RPM_STRING_ARRAY_TYPE, [ + 'Zupt provides hybrid post-quantum encryption (ML-KEM-768 + X25519)\n' + 'with multi-threaded compression and full-disk backup support.\n' + 'Bundled with libzuptsdk for HKDF-SHA3 hybrid KDF, key commitment,\n' + 'HPKE binding, and anti-fault decapsulation.' + ]) + h.add(RPMTAG_BUILDTIME, RPM_INT32_TYPE, int(time.time())) + h.add(RPMTAG_BUILDHOST, RPM_STRING_TYPE, 'localhost') + h.add(RPMTAG_SIZE, RPM_INT32_TYPE, sum(filesizes)) + h.add(RPMTAG_LICENSE, RPM_STRING_TYPE, 'AGPL-3.0-or-later') + h.add(RPMTAG_PACKAGER, RPM_STRING_TYPE, 'Cristian Cezar Moises ') + h.add(RPMTAG_GROUP, RPM_STRING_ARRAY_TYPE, ['Applications/Archiving']) + h.add(RPMTAG_URL, RPM_STRING_TYPE, 'https://git.securityops.co/cristiancmoises/zupt') + h.add(RPMTAG_OS, RPM_STRING_TYPE, 'linux') + h.add(RPMTAG_ARCH, RPM_STRING_TYPE, ARCH) + h.add(RPMTAG_POSTIN, RPM_STRING_TYPE, '/sbin/ldconfig\n') + h.add(RPMTAG_POSTUN, RPM_STRING_TYPE, '/sbin/ldconfig\n') + h.add(RPMTAG_BASENAMES, RPM_STRING_ARRAY_TYPE, basenames) + h.add(RPMTAG_DIRNAMES, RPM_STRING_ARRAY_TYPE, dirnames_set) + h.add(RPMTAG_DIRINDEXES, RPM_INT32_TYPE, dirindexes) + h.add(RPMTAG_FILESIZES, RPM_INT32_TYPE, filesizes) + h.add(RPMTAG_FILEMODES, RPM_INT16_TYPE, filemodes) + h.add(RPMTAG_FILEMTIMES, RPM_INT32_TYPE, filemtimes) + h.add(RPMTAG_FILEDIGESTS, RPM_STRING_ARRAY_TYPE, filedigests) + h.add(RPMTAG_FILELINKTOS, RPM_STRING_ARRAY_TYPE, filelinktos) + h.add(RPMTAG_FILEFLAGS, RPM_INT32_TYPE, fileflags) + h.add(RPMTAG_FILERDEVS, RPM_INT16_TYPE, filerdevs) + h.add(RPMTAG_FILEUSERNAME, RPM_STRING_ARRAY_TYPE, fileuser) + h.add(RPMTAG_FILEGROUPNAME, RPM_STRING_ARRAY_TYPE, filegroup) + h.add(RPMTAG_PROVIDENAME, RPM_STRING_ARRAY_TYPE, [NAME]) + h.add(RPMTAG_REQUIRENAME, RPM_STRING_ARRAY_TYPE, ['libargon2.so.1()(64bit)', 'libcrypto.so.3()(64bit)', 'libc.so.6()(64bit)']) + h.add(RPMTAG_REQUIREFLAGS, RPM_INT32_TYPE, [0, 0, 0]) + h.add(RPMTAG_REQUIREVERSION, RPM_STRING_ARRAY_TYPE, ['', '', '']) + h.add(RPMTAG_PAYLOADFORMAT, RPM_STRING_TYPE, 'cpio') + h.add(RPMTAG_PAYLOADCOMPRESSOR, RPM_STRING_TYPE, 'gzip') + h.add(RPMTAG_FILEDIGESTALGO, RPM_INT32_TYPE, 8) # SHA-256 + + main_hdr = h.serialize() + + # Signature header (minimal: just size of payload after sig hdr) + sig = Header() + sig_payload = main_hdr + gz_payload + sig.add(1000, RPM_INT32_TYPE, len(sig_payload)) # SIZE + sig.add(1004, RPM_BIN_TYPE, hashlib.md5(sig_payload).digest()) # MD5 + sig_bytes = sig.serialize() + # Pad sig hdr to 8-byte boundary + pad = (8 - (len(sig_bytes) % 8)) % 8 + sig_bytes += b'\x00' * pad + + # Lead (96 bytes) + lead = struct.pack('>4sBBhh66sHH16s', + b'\xed\xab\xee\xdb', # magic + 3, 0, # major, minor + 0, # type (binary) + 1, # archnum + NAME.encode().ljust(66, b'\x00'), + 1, # osnum + 5, # signature_type + b'\x00' * 16) + + out_path = f'/tmp/{NAME}-{VERSION}-{RELEASE}.{ARCH}.rpm' + with open(out_path, 'wb') as f: + f.write(lead) + f.write(sig_bytes) + f.write(main_hdr) + f.write(gz_payload) + + print(f'Built: {out_path} ({os.path.getsize(out_path)} bytes)') + # Try rpm -Kvv to verify if rpm is installed + try: + result = subprocess.run(['rpm', '-qpi', out_path], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print(result.stdout[:500]) + except Exception: + pass + +if __name__ == '__main__': + main() diff --git a/packaging/build-rpm.sh b/packaging/build-rpm.sh new file mode 100755 index 0000000..72fc935 --- /dev/null +++ b/packaging/build-rpm.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Build zupt RPM. Falls back to SRPM-equivalent tarball if rpmbuild absent. +set -e +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-2.2.2}" +RPMROOT="/tmp/rpmbuild-zupt" +rm -rf "$RPMROOT" +mkdir -p "$RPMROOT"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +TMP="/tmp/zupt-$VERSION" +rm -rf "$TMP" && mkdir -p "$TMP" +cp -r include src tests jasmin doc vendor Makefile CMakeLists.txt \ + README.md CHANGELOG.md SECURITY.md AUDIT.md LICENSE "$TMP/" 2>/dev/null || true +tar -czf "$RPMROOT/SOURCES/zupt-$VERSION.tar.gz" -C /tmp "zupt-$VERSION" + +cat > "$RPMROOT/SPECS/zupt.spec" <= 3.0 libzuptsdk-devel >= 2.0.0 +Requires: libargon2 openssl-libs >= 3.0 libzuptsdk2 >= 2.0.0 + +%description +Backup-oriented compression utility with hybrid post-quantum encryption +(ML-KEM-768 + X25519). Integrates libzuptsdk for state-of-the-art crypto: +HKDF combiner with domain separation, key commitment, HPKE binding, +anti-fault decapsulation, Argon2id password derivation. + +%prep +%autosetup + +%build +make %{?_smp_mflags} + +%install +mkdir -p %{buildroot}%{_bindir} +install -m 755 zupt %{buildroot}%{_bindir}/zupt + +%files +%license LICENSE +%doc README.md CHANGELOG.md SECURITY.md AUDIT.md +%{_bindir}/zupt + +%changelog +* Mon Apr 27 2026 Cristian Cezar Moisés - $VERSION-1 +- Audit-driven release: 6 bugs fixed (varint truncation, unchecked fwrites, + mac_key reuse, LZ length overflow, dedup ref recursion, partial archive + cleanup on encrypt failure). 30/30 tests pass. +EOF + +if command -v rpmbuild >/dev/null 2>&1; then + rpmbuild --define "_topdir $RPMROOT" -bb "$RPMROOT/SPECS/zupt.spec" 2>&1 | tail -5 + cp "$RPMROOT/RPMS/x86_64/zupt-$VERSION-1."*.rpm /tmp/ 2>/dev/null || true + ls /tmp/zupt-$VERSION-*.rpm 2>/dev/null +else + SRPM_TAR="/tmp/zupt-$VERSION.srpm.tar.gz" + tar -czf "$SRPM_TAR" -C "$RPMROOT" SPECS SOURCES + echo "rpmbuild unavailable; SRPM-equivalent at: $SRPM_TAR" + echo "Distros: tar -xzf $SRPM_TAR && rpmbuild -bb SPECS/zupt.spec" +fi diff --git a/packaging/install-zupt-gui.sh b/packaging/install-zupt-gui.sh new file mode 100755 index 0000000..8c8ca17 --- /dev/null +++ b/packaging/install-zupt-gui.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Zupt + Zupt GUI all-in-one installer for Linux +# Detects your distro, installs all dependencies, then installs +# zupt and zupt-gui. Run as root or with sudo. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ZUPT_CLI_DEB="$SCRIPT_DIR/zupt_2.2.2_amd64.deb" +ZUPT_GUI_DEB="$SCRIPT_DIR/zupt-gui_1.1.1_all.deb" + +print_step() { echo ""; echo "═══ $* ═══"; } +print_err() { echo "ERROR: $*" >&2; exit 1; } + +# Must be root +if [ "$EUID" -ne 0 ]; then + print_err "Run with sudo: sudo bash $0" +fi + +# Detect distro +if [ -f /etc/os-release ]; then + . /etc/os-release + DISTRO="$ID" + DISTRO_LIKE="${ID_LIKE:-}" +else + print_err "Cannot detect distribution (no /etc/os-release)" +fi + +print_step "Detected: $PRETTY_NAME" + +# 1. Install Python 3 + Qt6 binding +print_step "Step 1/3: Installing Python 3 and Qt6 binding" +case "$DISTRO" in + debian|ubuntu|linuxmint|pop) + apt-get update + apt-get install -y python3 python3-pyqt6 || \ + apt-get install -y python3 python3-pyside6 + ;; + fedora|rhel|centos|rocky|almalinux) + if command -v dnf >/dev/null; then + dnf install -y python3 python3-pyqt6 || dnf install -y python3 python3-pyside6 + else + yum install -y python3 python3-pyqt6 || yum install -y python3 python3-pyside6 + fi + ;; + opensuse*|suse) + zypper install -y python3 python3-pyqt6 || zypper install -y python3 python3-PyQt6 \ + || zypper install -y python3 python3-pyside6 + ;; + arch|manjaro|endeavouros) + pacman -S --noconfirm python python-pyqt6 || pacman -S --noconfirm python python-pyside6 + ;; + alpine) + apk add python3 py3-pyqt6 || apk add python3 py3-pyside6 + ;; + *) + # Fallback: try pip + echo "Unknown distribution '$DISTRO'. Trying pip fallback..." + if command -v pip3 >/dev/null; then + pip3 install --break-system-packages PySide6 || pip3 install PySide6 + else + print_err "No pip3 available. Install python3-pyqt6 manually for your distro." + fi + ;; +esac + +# Verify Qt6 binding works +if ! python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \ + && ! python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then + print_err "Failed to install Qt6 Python binding. Install manually with your package manager." +fi +echo "✓ Python 3 + Qt6 binding installed" + +# 2. Install zupt CLI +print_step "Step 2/3: Installing zupt CLI 2.2.2" +case "$DISTRO" in + debian|ubuntu|linuxmint|pop) + if [ ! -f "$ZUPT_CLI_DEB" ]; then + print_err "Cannot find $ZUPT_CLI_DEB next to this script" + fi + # Force-replace any older zupt + dpkg -i "$ZUPT_CLI_DEB" || apt-get -f install -y + ;; + fedora|rhel|centos|rocky|almalinux|opensuse*|suse) + ZUPT_CLI_RPM="$SCRIPT_DIR/zupt-2.2.2-1.x86_64.rpm" + if [ -f "$ZUPT_CLI_RPM" ]; then + rpm -Uvh --force "$ZUPT_CLI_RPM" + else + print_err "RPM build not provided. Build from source tarball or install via SRPM." + fi + ;; + *) + # Fallback: tarball install + ZUPT_CLI_TAR="$SCRIPT_DIR/zupt-2.2.2-linux-x86_64.tar.gz" + if [ -f "$ZUPT_CLI_TAR" ]; then + tar -xzf "$ZUPT_CLI_TAR" -C /opt/ + ln -sf /opt/zupt-2.2.2-linux-x86_64/zupt /usr/local/bin/zupt + else + print_err "No suitable installer for $DISTRO" + fi + ;; +esac +echo "✓ zupt CLI installed" + +# 3. Install zupt-gui +print_step "Step 3/3: Installing zupt-gui" +case "$DISTRO" in + debian|ubuntu|linuxmint|pop) + dpkg -i "$ZUPT_GUI_DEB" || apt-get -f install -y + ;; + fedora|rhel|centos|rocky|almalinux|opensuse*|suse) + ZUPT_GUI_RPM="$SCRIPT_DIR/zupt-gui-1.1.1-1.noarch.rpm" + if [ -f "$ZUPT_GUI_RPM" ]; then + rpm -Uvh --force "$ZUPT_GUI_RPM" + fi + ;; + *) + # Manual fallback + mkdir -p /opt/zupt-gui /usr/local/bin + cp "$SCRIPT_DIR/zupt_gui.py" /opt/zupt-gui/ 2>/dev/null || true + cat > /usr/local/bin/zupt-gui <<'WRAP' +#!/bin/sh +exec python3 /opt/zupt-gui/zupt_gui.py "$@" +WRAP + chmod +x /usr/local/bin/zupt-gui + ;; +esac +echo "✓ zupt-gui installed" + +print_step "Installation complete" +echo "" +echo "Run:" +echo " zupt help # CLI help" +echo " zupt-gui # Graphical interface" +echo "" +echo "If you encounter issues, check that your zupt version is correct:" +echo " zupt version # should show 2.2.2" diff --git a/packaging/zupt-installer-header.sh b/packaging/zupt-installer-header.sh new file mode 100644 index 0000000..7adf80d --- /dev/null +++ b/packaging/zupt-installer-header.sh @@ -0,0 +1,312 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# ╔════════════════════════════════════════════════════════════════════╗ +# ║ ZUPT 2.2.2 + ZUPT-GUI 1.1.1 — UNIVERSAL LINUX INSTALLER ║ +# ║ ║ +# ║ One script, all distributions. Self-extracting. No internet ║ +# ║ needed for the package install (only for Qt6 dependency). ║ +# ║ ║ +# ║ Usage: sudo bash zupt-installer.sh ║ +# ║ Or: sudo bash zupt-installer.sh --gui-only ║ +# ║ Or: sudo bash zupt-installer.sh --cli-only ║ +# ║ Or: sudo bash zupt-installer.sh --appimage ║ +# ║ Or: sudo bash zupt-installer.sh --uninstall ║ +# ╚════════════════════════════════════════════════════════════════════╝ +set -e + +VERSION="2.2.2" +GUI_VERSION="1.1.1" +EXTRACT_DIR="" + +cleanup() { + [ -n "$EXTRACT_DIR" ] && [ -d "$EXTRACT_DIR" ] && rm -rf "$EXTRACT_DIR" +} +trap cleanup EXIT + +# ── Color output (if terminal supports) ───────────────────────────── +if [ -t 1 ]; then + BOLD='\033[1m'; CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m' +else + BOLD=''; CYAN=''; GREEN=''; YELLOW=''; RED=''; RESET='' +fi + +step() { echo -e "${CYAN}${BOLD}═══ $* ═══${RESET}"; } +ok() { echo -e "${GREEN}✓${RESET} $*"; } +warn() { echo -e "${YELLOW}⚠${RESET} $*"; } +err() { echo -e "${RED}✗${RESET} $*" >&2; } +die() { err "$*"; exit 1; } + +# ── Parse arguments ───────────────────────────────────────────────── +MODE="full" +case "${1:-}" in + --cli-only) MODE="cli" ;; + --gui-only) MODE="gui" ;; + --appimage) MODE="appimage" ;; + --uninstall) MODE="uninstall" ;; + --help|-h) + sed -n '2,15p' "$0" | sed 's/^# //' + exit 0 ;; + "") MODE="full" ;; + *) die "Unknown option: $1. Use --help for options." ;; +esac + +# ── Root check (except for AppImage) ──────────────────────────────── +if [ "$MODE" != "appimage" ] && [ "$EUID" -ne 0 ]; then + die "Run with sudo: sudo bash $0 ${1:-}" +fi + +# ── Distro detection ──────────────────────────────────────────────── +detect_distro() { + if [ -f /etc/os-release ]; then + # Use subshell to prevent /etc/os-release VERSION from clobbering ours + DISTRO=$(. /etc/os-release; echo "${ID:-unknown}") + DISTRO_LIKE=$(. /etc/os-release; echo "${ID_LIKE:-}") + DISTRO_NAME=$(. /etc/os-release; echo "${PRETTY_NAME:-$DISTRO}") + else + DISTRO="unknown"; DISTRO_LIKE=""; DISTRO_NAME="Unknown Linux" + fi +} +detect_distro + +# Categorize +DEB_BASED=0; RPM_BASED=0; ARCH_BASED=0; ALPINE=0 +case "$DISTRO" in + debian|ubuntu|linuxmint|pop|elementary|kali|raspbian|deepin|zorin) DEB_BASED=1 ;; + fedora|rhel|centos|rocky|almalinux|ol) RPM_BASED=1 ;; + opensuse*|suse|sles) RPM_BASED=1 ;; + arch|manjaro|endeavouros|garuda|artix) ARCH_BASED=1 ;; + alpine) ALPINE=1 ;; + *) + case "$DISTRO_LIKE" in + *debian*|*ubuntu*) DEB_BASED=1 ;; + *fedora*|*rhel*|*suse*) RPM_BASED=1 ;; + *arch*) ARCH_BASED=1 ;; + esac ;; +esac + +# ── Self-extract embedded payload ─────────────────────────────────── +extract_payload() { + EXTRACT_DIR=$(mktemp -d -t zupt-installer.XXXXXX) + # Find the line number where the payload starts (marker: __PAYLOAD_BELOW__) + local marker_line + marker_line=$(grep -an '^__PAYLOAD_BELOW__$' "$0" | head -1 | cut -d: -f1) + [ -z "$marker_line" ] && die "Installer is corrupt — no payload marker." + # Skip past marker line, decode base64 → tar + tail -n +$((marker_line + 1)) "$0" | base64 -d | tar -xzC "$EXTRACT_DIR" + [ -f "$EXTRACT_DIR/zupt_${VERSION}_amd64.deb" ] || die "Payload extraction failed." +} + +# ── Install Qt6 binding (needs network) ───────────────────────────── +install_qt6() { + if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \ + || python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then + ok "Qt6 binding already installed" + return 0 + fi + step "Installing Python 3 + Qt6 binding" + if [ $DEB_BASED -eq 1 ]; then + apt-get update -qq || warn "apt-get update failed (network?); continuing anyway" + apt-get install -y python3 python3-pyqt6 \ + || apt-get install -y python3 python3-pyside6 \ + || warn "Could not install Qt6 binding via apt" + elif [ $RPM_BASED -eq 1 ]; then + case "$DISTRO" in + opensuse*|suse|sles) + zypper --non-interactive install python3 python3-pyqt6 \ + || zypper --non-interactive install python3 python3-PyQt6 \ + || zypper --non-interactive install python3 python3-pyside6 ;; + *) + if command -v dnf >/dev/null; then + dnf install -y python3 python3-pyqt6 \ + || dnf install -y python3 python3-pyside6 + else + yum install -y python3 python3-pyqt6 \ + || yum install -y python3 python3-pyside6 + fi ;; + esac + elif [ $ARCH_BASED -eq 1 ]; then + pacman -Sy --noconfirm python python-pyqt6 \ + || pacman -Sy --noconfirm python python-pyside6 + elif [ $ALPINE -eq 1 ]; then + apk add python3 py3-pyqt6 || apk add python3 py3-pyside6 + else + warn "Unknown distribution. Trying pip fallback..." + if command -v pip3 >/dev/null; then + pip3 install --break-system-packages PySide6 2>/dev/null \ + || pip3 install --user PySide6 + else + warn "No pip3. Install python3-pyqt6 manually." + fi + fi + if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \ + || python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then + ok "Qt6 binding installed" + else + warn "Qt6 binding install failed. The CLI will still work; the GUI won't." + fi +} + +# ── Install zupt CLI ──────────────────────────────────────────────── +install_cli() { + step "Installing zupt CLI ${VERSION}" + if [ $DEB_BASED -eq 1 ]; then + dpkg -i "$EXTRACT_DIR/zupt_${VERSION}_amd64.deb" 2>&1 \ + | grep -v '^Selecting\|^Preparing\|^Unpacking\|^Setting up\|^Processing' || true + # Resolve any missing libs from apt + apt-get -f install -y 2>/dev/null || true + ok "zupt CLI installed: $(zupt version 2>&1 | head -1)" + elif [ $RPM_BASED -eq 1 ]; then + local rpmtar="$EXTRACT_DIR/zupt-${VERSION}.srpm.tar.gz" + if command -v rpmbuild >/dev/null; then + local rpmroot=$(mktemp -d) + tar -xzC "$rpmroot" -f "$rpmtar" + rpmbuild --define "_topdir $rpmroot" -bb "$rpmroot/SPECS/zupt.spec" + rpm -Uvh --force "$rpmroot"/RPMS/x86_64/zupt-*.rpm + rm -rf "$rpmroot" + else + # rpmbuild not available — fall back to tarball + warn "rpmbuild missing — using portable binary install" + local appdir="$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz" + mkdir -p /opt + tar -xzC /opt -f "$appdir" + ln -sf "/opt/zupt-${VERSION}-x86_64.AppDir/AppRun" /usr/local/bin/zupt + ok "zupt CLI installed (portable mode)" + fi + else + # Universal fallback: portable AppDir tarball + warn "No native package format for $DISTRO. Using portable binary." + mkdir -p /opt /usr/local/bin + tar -xzC /opt -f "$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz" + ln -sf "/opt/zupt-${VERSION}-x86_64.AppDir/AppRun" /usr/local/bin/zupt + ok "zupt CLI installed (portable mode)" + fi +} + +# ── Install zupt-gui ──────────────────────────────────────────────── +install_gui() { + step "Installing zupt-gui ${GUI_VERSION}" + if [ $DEB_BASED -eq 1 ]; then + dpkg -i "$EXTRACT_DIR/zupt-gui_${GUI_VERSION}_all.deb" 2>&1 \ + | grep -v '^Selecting\|^Preparing\|^Unpacking\|^Setting up\|^Processing' || true + apt-get -f install -y 2>/dev/null || true + ok "zupt-gui installed" + elif [ $RPM_BASED -eq 1 ]; then + local rpmtar="$EXTRACT_DIR/zupt-gui-${GUI_VERSION}.srpm.tar.gz" + if command -v rpmbuild >/dev/null; then + local rpmroot=$(mktemp -d) + tar -xzC "$rpmroot" -f "$rpmtar" + rpmbuild --define "_topdir $rpmroot" -bb "$rpmroot/SPECS/zupt-gui.spec" + rpm -Uvh --force "$rpmroot"/RPMS/noarch/zupt-gui-*.rpm + rm -rf "$rpmroot" + else + warn "rpmbuild missing — using portable mode" + mkdir -p /opt /usr/local/bin + tar -xzC /opt -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz" + ln -sf "/opt/zupt-gui.AppDir/AppRun" /usr/local/bin/zupt-gui + ok "zupt-gui installed (portable)" + fi + else + # Portable + mkdir -p /opt /usr/local/bin + tar -xzC /opt -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz" + ln -sf "/opt/zupt-gui.AppDir/AppRun" /usr/local/bin/zupt-gui + # Desktop integration if possible + if [ -d /usr/share/applications ]; then + cp /opt/zupt-gui.AppDir/zupt-gui.desktop /usr/share/applications/ 2>/dev/null || true + fi + ok "zupt-gui installed (portable)" + fi +} + +# ── AppImage extract (no install) ─────────────────────────────────── +install_appimage() { + step "Extracting AppImage to current directory" + local target="${PWD}/zupt-portable" + mkdir -p "$target" + tar -xzC "$target" -f "$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz" + tar -xzC "$target" -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz" + cat > "$target/zupt" < "$target/zupt-gui" </dev/null || true + dpkg -r zupt 2>/dev/null || true + elif [ $RPM_BASED -eq 1 ]; then + rpm -e zupt-gui 2>/dev/null || true + rpm -e zupt 2>/dev/null || true + fi + rm -rf /opt/zupt-2.2.2-x86_64.AppDir /opt/zupt-gui.AppDir 2>/dev/null + rm -f /usr/local/bin/zupt /usr/local/bin/zupt-gui 2>/dev/null + rm -f /usr/share/applications/zupt-gui.desktop 2>/dev/null + ok "Uninstall complete" +} + +# ───────────────────────────────────────────────────────────────────── +# MAIN +# ───────────────────────────────────────────────────────────────────── + +cat <
+ + libzuptsdk 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. + + libzuptsdk 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. + + You should have received a copy of the GNU Affero General Public + License along with this program. If not, see: + + https://www.gnu.org/licenses/agpl-3.0.txt + https://www.gnu.org/licenses/agpl-3.0.html + + SPDX-License-Identifier: AGPL-3.0-or-later + + ───────────────────────────────────────────────────────────────────── + + ABOUT THIS LICENSE + + The GNU Affero General Public License v3 (AGPLv3) is a copyleft + license designed for software that may be run as a network service. + It is identical to the GNU General Public License v3, with one + additional requirement (Section 13): if you modify libzuptsdk and + make the modified version available to users over a computer network, + you must offer those users access to the corresponding modified + source code. + + This protects libzuptsdk against being adopted by SaaS providers as + a private fork without contributing back, while keeping it freely + usable by individuals, small businesses, and the broader open-source + community. + + If you write a separate program that is distributed alongside + libzuptsdk (for example, statically linking it into your own + application), the AGPL requires you to license that combined work + under the AGPL as well — which means you must publish the source. + If this is not acceptable for your use case, please contact the + author for commercial licensing options: + + zupt@riseup.net + https://github.com/cristiancmoises/zupt + + ───────────────────────────────────────────────────────────────────── + + The full text of the GNU Affero General Public License version 3 + should accompany this distribution as a separate file (or you may + download it from the URLs above). It is approximately 35 KB / 619 + lines of plain text. diff --git a/sdk/Makefile.sdk b/sdk/Makefile.sdk new file mode 100644 index 0000000..9fc8059 --- /dev/null +++ b/sdk/Makefile.sdk @@ -0,0 +1,141 @@ +# ───────────────────────────────────────────────────────────────────── +# libzuptsdk — public C ABI for Zupt +# ───────────────────────────────────────────────────────────────────── + +SDK_VERSION_MAJOR = 1 +SDK_VERSION_MINOR = 0 +SDK_VERSION_PATCH = 0 +SDK_SOVERSION = $(SDK_VERSION_MAJOR) +SDK_FULLVERSION = $(SDK_VERSION_MAJOR).$(SDK_VERSION_MINOR).$(SDK_VERSION_PATCH) + +SDK_HDR = sdk/include/zuptsdk.h +SDK_SRC = sdk/src/zuptsdk.c +SDK_MAP = sdk/zuptsdk.map +SDK_PREFIX ?= /usr/local + +# All zupt sources except main.c get rebuilt with -fPIC for the SDK. +# Object files go to sdk/build/ to avoid colliding with the CLI build. +SDK_BUILD_DIR = sdk/build +SDK_PIC_OBJS = $(patsubst src/%.c,$(SDK_BUILD_DIR)/%.o,$(filter-out src/zupt_main.c,$(ZUPT_SOURCES))) +SDK_PIC_OBJS += $(patsubst src/%.c,$(SDK_BUILD_DIR)/%.o,$(VV_SOURCES)) +SDK_PIC_OBJS += $(SDK_BUILD_DIR)/zuptsdk.o + +SDK_PIC_FLAGS = -fPIC -DZUPT_BUILDING_SDK=1 + +# VV files need SIMD flags too +SDK_PIC_VV_FLAGS = $(SDK_PIC_FLAGS) $(VV_SIMD_FLAGS) + +SDK_SHARED = sdk/build/libzuptsdk.so.$(SDK_FULLVERSION) +SDK_SHARED_SO = sdk/build/libzuptsdk.so.$(SDK_SOVERSION) +SDK_SHARED_LINK = sdk/build/libzuptsdk.so +SDK_STATIC = sdk/build/libzuptsdk.a + +SDK_PC = sdk/build/zuptsdk.pc + +# Compile rule for SDK PIC objects (vv_* files need SIMD flags) +$(SDK_BUILD_DIR)/vv_%.o: src/vv_%.c | $(SDK_BUILD_DIR) + $(Q)$(CC) $(CFLAGS) $(SDK_PIC_VV_FLAGS) -I include -c $< -o $@ + +$(SDK_BUILD_DIR)/vaptvupt_api.o: src/vaptvupt_api.c | $(SDK_BUILD_DIR) + $(Q)$(CC) $(CFLAGS) $(SDK_PIC_FLAGS) -I include -c $< -o $@ + +$(SDK_BUILD_DIR)/%.o: src/%.c | $(SDK_BUILD_DIR) + $(Q)$(CC) $(CFLAGS) $(SDK_PIC_FLAGS) -I include -c $< -o $@ + +$(SDK_BUILD_DIR)/zuptsdk.o: $(SDK_SRC) $(SDK_HDR) | $(SDK_BUILD_DIR) + $(Q)$(CC) $(CFLAGS) $(SDK_PIC_FLAGS) -I sdk/include -I include -I src -c $< -o $@ + +$(SDK_BUILD_DIR): + $(Q)mkdir -p $(SDK_BUILD_DIR) + +# Shared library +$(SDK_SHARED): $(SDK_PIC_OBJS) $(SDK_MAP) $(JAZZ_O) + @echo "[sdk-shared] $@" + $(Q)$(CC) -shared -fPIC \ + -Wl,-soname,libzuptsdk.so.$(SDK_SOVERSION) \ + -Wl,--version-script,$(SDK_MAP) \ + $(LDFLAGS) \ + $(SDK_PIC_OBJS) $(JAZZ_O) \ + -o $@ $(LDLIBS) + $(Q)cd $(SDK_BUILD_DIR) && ln -sf $(notdir $(SDK_SHARED)) libzuptsdk.so.$(SDK_SOVERSION) + $(Q)cd $(SDK_BUILD_DIR) && ln -sf libzuptsdk.so.$(SDK_SOVERSION) libzuptsdk.so + +# Static library +$(SDK_STATIC): $(SDK_PIC_OBJS) $(JAZZ_O) + @echo "[sdk-static] $@" + $(Q)$(AR) rcs $@ $(SDK_PIC_OBJS) $(JAZZ_O) + +# pkg-config file +$(SDK_PC): $(SDK_HDR) + @echo "[sdk-pc] $@" + $(Q)mkdir -p $(SDK_BUILD_DIR) + $(Q)printf 'prefix=$(SDK_PREFIX)\n' > $@ + $(Q)printf 'exec_prefix=$${prefix}\n' >> $@ + $(Q)printf 'libdir=$${exec_prefix}/lib\n' >> $@ + $(Q)printf 'includedir=$${prefix}/include\n\n' >> $@ + $(Q)printf 'Name: zuptsdk\n' >> $@ + $(Q)printf 'Description: Zupt backup compression SDK\n' >> $@ + $(Q)printf 'URL: https://git.securityops.co/cristiancmoises/zupt\n' >> $@ + $(Q)printf 'Version: $(SDK_FULLVERSION)\n' >> $@ + $(Q)printf 'Libs: -L$${libdir} -lzuptsdk\n' >> $@ + $(Q)printf 'Libs.private: -lpthread\n' >> $@ + $(Q)printf 'Cflags: -I$${includedir}\n' >> $@ + +# Convenience targets +.PHONY: sdk sdk-shared sdk-static sdk-pkgconfig sdk-clean sdk-install \ + sdk-verify-symbols sdk-test + +sdk: sdk-shared sdk-static sdk-pkgconfig + +sdk-shared: $(SDK_SHARED) + +sdk-static: $(SDK_STATIC) + +sdk-pkgconfig: $(SDK_PC) + +sdk-clean: + $(Q)rm -rf $(SDK_BUILD_DIR) + +# Symbol leakage verification. +# Pass: every exported text symbol starts with `zuptsdk_`. +# Fail: any symbol that doesn't. +sdk-verify-symbols: $(SDK_SHARED) + @echo "[sdk-verify] checking exported symbols in $(SDK_SHARED)" + $(Q)leaked=$$(nm -D --defined-only $(SDK_SHARED) | grep ' T ' | awk '{print $$3}' | grep -v '^zuptsdk_' || true); \ + if [ -n "$$leaked" ]; then \ + echo "FAIL: non-zuptsdk symbols exported:"; \ + echo "$$leaked"; \ + exit 1; \ + fi; \ + expected=$$(grep -c '^ zuptsdk_' $(SDK_MAP)); \ + exported=$$(nm -D --defined-only $(SDK_SHARED) | grep ' T ' | grep -c '^.* T zuptsdk_' || true); \ + echo " $$exported exported / $$expected declared in version script"; \ + if [ "$$exported" -lt "$$expected" ]; then \ + echo "FAIL: $$((expected - exported)) declared symbols are missing from the .so"; \ + nm -D --defined-only $(SDK_SHARED) | grep ' T ' | grep '^.* T zuptsdk_' | awk '{print $$3}' | sort > /tmp/exp; \ + grep '^ zuptsdk_' $(SDK_MAP) | tr -d ' ;' | sort > /tmp/decl; \ + diff /tmp/decl /tmp/exp; \ + exit 1; \ + fi; \ + echo " PASS: no symbol leakage, all declared symbols exported" + +# Build & run roundtrip test +sdk-test: $(SDK_SHARED) + @echo "[sdk-test] building and running roundtrip" + $(Q)$(CC) $(CFLAGS) -I sdk/include sdk/tests/test_sdk_roundtrip.c \ + -Lsdk/build -lzuptsdk \ + -Wl,-rpath,'$$ORIGIN/build' \ + -o sdk/build/test_sdk_roundtrip $(LDLIBS) + $(Q)cd sdk && LD_LIBRARY_PATH=build ./build/test_sdk_roundtrip + +sdk-install: sdk + install -d $(DESTDIR)$(SDK_PREFIX)/lib + install -d $(DESTDIR)$(SDK_PREFIX)/include + install -d $(DESTDIR)$(SDK_PREFIX)/lib/pkgconfig + install -m 0644 $(SDK_HDR) $(DESTDIR)$(SDK_PREFIX)/include/ + install -m 0755 $(SDK_SHARED) $(DESTDIR)$(SDK_PREFIX)/lib/ + cd $(DESTDIR)$(SDK_PREFIX)/lib && \ + ln -sf libzuptsdk.so.$(SDK_FULLVERSION) libzuptsdk.so.$(SDK_SOVERSION) && \ + ln -sf libzuptsdk.so.$(SDK_SOVERSION) libzuptsdk.so + install -m 0644 $(SDK_STATIC) $(DESTDIR)$(SDK_PREFIX)/lib/ + install -m 0644 $(SDK_PC) $(DESTDIR)$(SDK_PREFIX)/lib/pkgconfig/ diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..bf143af --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,183 @@ +# libzuptsdk + +Public C ABI for the [Zupt](https://git.securityops.co/cristiancmoises/zupt) backup compression library. + +Provides post-quantum encrypted compression as a stable, embeddable shared library — completely independent of the `zupt` CLI.No dependency on any other compression library; everything is built from Zupt's own implementations. + +- **Version:** 1.0.0 +- **License:** AGPL-3.0-or-later +- **ABI:** Stable across 1.x via versioned symbols (`ZUPTSDK_1.0`) +- **C standard:** Public header is C99; C11 implementation; works in C++17 + +## Features + +- **Hybrid post-quantum encryption** — ML-KEM-768 + X25519 KEM +- **Authenticated encryption** — AES-256-CTR + HMAC-SHA256, PBKDF2-SHA256 KDF +- **Hardware-adaptive compression** — VaptVupt on AVX2/NEON, LZHP elsewhere +- **Streaming I/O** — read/write callbacks for sockets, pipes, encrypted volumes +- **Secure memory** — mlock-backed buffers for passwords and keys, zeroed on destroy +- **Constant-time crypto** — Jasmin-verified assembly on x86_64 +- **Per-context state** — no globals; safe to use from any thread on distinct contexts +- **Custom allocator hooks** — embed cleanly in any runtime + +## Quick start (C) + +```c +#include + +zuptsdk_ctx_t *ctx; +zuptsdk_ctx_create(&ctx); + +zuptsdk_secure_buf_t *pw; +zuptsdk_secure_buf_from_data((const uint8_t *)"mypassword", 10, &pw); + +uint8_t *archive; +size_t archive_sz; +zuptsdk_compress_buffer(ctx, NULL, "data.bin", + my_data, my_data_size, + pw, NULL, + &archive, &archive_sz); + +/* ... store archive somewhere ... */ + +uint8_t *out; +size_t out_sz; +zuptsdk_extract_buffer(ctx, archive, archive_sz, pw, NULL, &out, &out_sz); + +zuptsdk_free(archive); +zuptsdk_free(out); +zuptsdk_secure_buf_destroy(pw); +zuptsdk_ctx_destroy(ctx); +``` + +Build with `pkg-config`: + +```sh +gcc myapp.c $(pkg-config --cflags --libs zuptsdk) -o myapp +``` + +## Quick start (Python) + +```python +import zuptsdk + +with zuptsdk.Context() as ctx: + archive = ctx.compress_buffer(b"hello world", + name="hello.txt", + password=b"secret") + + data = ctx.extract_buffer(archive, password=b"secret") + assert data == b"hello world" +``` + +Post-quantum mode: + +```python +with zuptsdk.Context() as ctx: + kp = ctx.generate_keypair() + kp.save("/tmp/mykey") # writes mykey.key (priv, 0600) + mykey.pub (pub, 0644) + + archive = ctx.compress_buffer(b"secret data", + name="s.txt", + public_key=kp.public) + + data = ctx.extract_buffer(archive, private_key=kp.private) +``` + +## Build & install + +```sh +git clone https://git.securityops.co/cristiancmoises/zupt +cd zupt +make # builds CLI (required: produces jasmin/*.o assembly objects) +make sdk # builds libzuptsdk.so.1.0.0 + libzuptsdk.a + zuptsdk.pc +make sdk-test # runs C roundtrip suite (15 tests) +sudo make sdk-install PREFIX=/usr/local +``` + +This installs: +- `/usr/local/include/zuptsdk.h` +- `/usr/local/lib/libzuptsdk.so.1.0.0` (with versioned `.so.1` and `.so` symlinks) +- `/usr/local/lib/libzuptsdk.a` +- `/usr/local/lib/pkgconfig/zuptsdk.pc` + +## Symbol visibility + +The shared library exports **only** the 55 documented public symbols, all prefixed with `zuptsdk_`. Internal `zupt_*` and `vv_*` symbols are hidden via a linker version script. + +Verify yourself: + +```sh +make sdk-verify-symbols +# [sdk-verify] checking exported symbols in sdk/build/libzuptsdk.so.1.0.0 +# 55 exported / 55 declared in version script +# PASS: no symbol leakage, all declared symbols exported +``` + +## ABI stability policy + +Every symbol declared in `zuptsdk.h` is part of the stable v1.0 ABI and is gated under the linker tag `ZUPTSDK_1.0`. + +- **New symbols** in v1.x get added under new tags (`ZUPTSDK_1.1`, ...) +- **Existing symbols** never change signature within v1.x +- **Breaking changes** require a major bump (`libzuptsdk.so.2`) + +Do not link against internal `zupt_*` symbols even if you find them in the static archive — they will disappear without notice. + +## Thread safety + +- Concurrent calls on **distinct contexts** are safe (MT-Safe). +- Concurrent calls on the **same context** are not safe. + +For parallel work, create one context per worker thread. + +## Memory ownership + +Every output pointer documents its destroyer. Always use the documented function (`zuptsdk_free`, `zuptsdk_*_destroy`) to release memory — never bare `free()` — because the library may have been built with a custom allocator. + +Function parameter conventions: +- `[in]` — caller owns, library reads only +- `[out]` — caller owns, library writes +- `[in,out]` — caller owns, library reads and writes +- `[transfers]` — ownership moves caller ↔ library +- `[borrowed]` — pointer valid only for the duration of the call + +## Error handling + +Functions return `int` where 0 = success, negative = `zuptsdk_error_t` code. + +```c +int rc = zuptsdk_compress_buffer(...); +if (rc != ZUPTSDK_OK) { + fprintf(stderr, "%s\n", zuptsdk_strerror(rc)); + fprintf(stderr, " detail: %s\n", zuptsdk_last_error_detail()); + return rc; +} +``` + +`zuptsdk_last_error_detail()` returns a thread-local string with file:line context. + +## Components + +``` +sdk/ +├── include/zuptsdk.h # Public C99 header (55 functions, 7 opaque types) +├── src/zuptsdk.c # Implementation (wraps zupt internals) +├── zuptsdk.map # Linker version script (gates exports) +├── Makefile.sdk # Build integration +├── bindings/python/zuptsdk.py # Python cffi reference bindings +├── tests/test_sdk_roundtrip.c # C roundtrip suite +└── tests/test_python.py # Python test suite +``` + +## License + +libzuptsdk is licensed under **AGPL-3.0-or-later** (see `sdk/LICENSE`). + +The AGPL allows everyone to use the library freely, but anyone running it as a network service must publish their source code modifications. This protects the project from enterprise exploitation while keeping it usable by individuals, small businesses, and the open-source community. + +## Contact + +- Repository: https://git.securityops.co/cristiancmoises/zupt +- Website: https://zupt.securityops.co +- Email: zupt@riseup.net diff --git a/sdk/bindings/python/__pycache__/zuptsdk.cpython-312.pyc b/sdk/bindings/python/__pycache__/zuptsdk.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39eb391fc225b054345916c389d31c57ad06ae2c GIT binary patch literal 27397 zcmdUYdvFx@nPB(4-=i0L@N!GYpb>gt%){8mSO@{)AqfQ7$hn=2rdwjfNHgvk0W%Sf z?2wIc*$3<<3)I>NsrVA9$QE+Ncb4n6&SaCyO=@esce@3M4PKJV z-uL~wf89N!*2A&0wP{;l|K{u8`}h66*YE4*FTGwD1J~Jq{Kv?hqYU$N{6R0Pl9+qM zGR#$mXSx`k2uQxv^#IP1$E%XGQ9Ny(`AEF zHno)9#Oz~u+XaTV-!jShx*P=O0Gw0BISI}MIJe-sWma;!3DyHxui#d(9)k4&)~~~Q z3AO;Rg*vQ{V2c1-ti$>Vwgj*%bl3vE6#8FCo|VC~a_R9q>+RRXR`#g!41${xN?~DC*`@&L&4s+RsUN~(>bT>(YJkm z9{zFqv}<+B6j(p_XE4>S^k6)h;7@aZGyF}iBb7WA8{oPl1AJtlH^KG9L@pZXR4^y9(_GWOjzjfNHf*YoiS^M?QV?CPqe71$3IkzbUi-bmfFMF9o;wo>ap9hx zNCVd)#?C}|A;E=|?2$0n)Y0mKekWsbF4n^(PYGNz*R;F!87?7&MIo8sYT}_p;%rRh z6Ls8aA(aSlAyMG^Lj$2+s5~;@>W{|4r|Y{z;nVQDd$0$3lz@stNiHpjF%By44T;^5 zn+wOHQ6Zd+#0DB%t|Nny@M$iQ42j97T^wAI{&-A;Lh_^%)Y(Y#6h$;N#|DzZxnvD= zAi;%`=j2jYG9-piMa~G^PDD3^WBqYaNF;)CUuwFmP6^RyjDs;m`Kmf@Ak;7HlyMEo zbIGbY(iGOS_(kVoXLpr6H&p?(Bp*tKl;)v}Vkn%Xtt)-2;}$CpGc3T+V~+i zN!F(KvrX+_cQg{le(j_^i4&C8!{yAd7&!wJpIeQL3%EYPP2Cn?CG+0Bt)agvgzbyV zolM0AUg!Zj6bHj9@dbg7G-6OF5|P2=Gm90A#gVuZa#31LK`v2LnciSzpeII2KBz}7 zQ65NgP!y2rpmo}`hFqewGV8e=SCC5-g@uBQ%52ewW#m#NVQti+YDpQ1MafKuVLp>w z_!Kb6TAJq!a2S~9AS@d2+L|-52p^EwB{HC`Ae3)F0fPm4t)asr^u^WI z;ypH)RNBGO<&**jo)lxL{9;f_Hwif<5<)VVJOz#M34>DdgecFUiurw|6r9&zicUhG zA)Q#L(==ax$0;7lV{xP2(=0fGaEuqiYX9jIrLa5*l4YrSqQV&=x_CV#%To350zVjE zydGg7)EyP{#Hp?e?LUqz5sOCn<<>NRd33v6S)oKEEd-Ms-s;pAb=h#mfKx2AV7i?7 zYo>byl|UUvv!u(^DCkf7r6unm?3t%vB(=53|FOSyWK@VEgVgp(&hFAR)g#9G zgIKyo-rQ=Hh*N7LQ$?m$;be>4=zi5Tf<4qGbp8@iR!T*%Iw#7Whnu!gG&5FuI=;{% z(Omf}MDbAiblhllXaV=$Z zUMJMTav$c?P`+ME7fVM%OB1lXl(Z;iLNIbjCYIihIiwJc^+v*>Xb=_ZWfwKxeq?ti z(#xoYc0G6VQRR9W739tURaCfIQ3yxk5wNy`@zcwwMjb~jRnl^p6GbM{Ku`16GS5h$ zgZeB>na9nP233Zpbztei(B^;JInA3=s=?{!G_j01)uwblvsVaY(aB84f_y|YVy#Q= zi!X38<*H;hg_|7v^lxMd3}+A2uh@9@BAS3-;K!)UG&X*)P51}Tk2xbJ^BoZ4FhOL6jsy~zr5-TZBcF(Kx zGgXMT)-o&9?;gWW4i18}~a4tgltEX!Vq#VeCdsii9;M4Y43^@xH% zjBKtpo#_2mW@gnoDU>DvhR}4@Dc{3H~W8Le5?; zKR0+{YG{zG-Gh-RpXQolgHfIvh$T5RG<2s2q!O+sy)DqdbqYx?uX~&@aE3br4l+1$ zh=8RQibjbeA9^8%#8gATBbj0e$qv~fVr)P%gHbM7(1eogpq!xkF4;rf34(D1gFTU` z5DZGTP&_US@RFr378#H%K{!H?Y3`nei3eUn{;+Fu1nO`_ums}TJmwXp}lVvqoM_|TLG2s_wdNMYiDs z4+)ocjKw!?S&^}<_-6Oj-YdOV zqgSFgw~UsICdXfxe6BrP+A(E0vPhnZShlon%F_PV{^H^NGv2cMjLljx!hY;8nDLa{ zHJP0iv&CgspSkkPwU__+xsN=Rcb)M3fsb()O_ppKe{uZ4U54GYp9NUl%FZ#Y`v9h` z2iQ*^SeXK-j&)Ydl$F1^|5ta-kbIOVhoS$dq@?*7(~q8U?kTYT*kan_w*J^{g>()N zRQMM&R}CeuG6DlY2smsEfyR5vW%f?%~?7XeZk+KIIlC>FqZ6Y$sPq6$a zB!!&x6AJ=d(YAn70%Aex0b+3lgZ(jnkPwb17=*)$sPe?pbo^MmhbxGFFW9>&<#+W&%k`ZxK;wlRM1QH1jzy)T;UjSAueOLA zX%XY0?}x`r51qGnIeAByi+6Upc~_T*cXxStPnVDPcKLZ}VMDR-l|AgQHf;j~D0kj2*#7Z1Q6@qmDBvboA z$=%v^qUliUo?z3_eUh`SJ$Sg~aQo4dlBu;_a_??JeDnU+6D^Vle{?i;cD~SlbdO}m zUk6)G%8A2G&2r`wEk`?B+uP*K&HG!L4|X0uEEViNzISiS(crQ6_F(7XrbCA$Z(IAZ zU`t!`(UTp=TJ}hVwBF#+mLtboj{?fyaRlqv5bon`oyR*m+K)m3u@=WbmV~TlJ^n;+ z1O5{20AM}WtcA5(^Vsk~1g!|PpNH`0J{6EGfa4}DB>;yHljI6awgY(epxhR=khA{#?6Qn7=h!Z5Cljzp&Msg|WOdUuyF&?q6V8PNa;ig4bVCCv z9be{%tG^*k8^XqLYvAv>_W*p0y~+%MxDG;EzLKiqVWe&(Rpr1)i5OCq5h?-=0}EhK z^dKCBs!6mDnL#K*>Q9-s&F9UUVsX(j#JstNVKkA-GAWpZwnx^%WKaE5l3f(y(NI`O zo1W%?D_GFIE*8K*B%6Hn0#X5ZvQ$9zVR}>oazen&k~J|H7ewR}k%34u7zBA?NeIy% zaUZEgJ|vOM{fS=584QA4fMeUBDlmp+esCD_ri;{tX`q$85BNj}fKP{+nUeAm%Nx#F zU(u!07f-)cJ-xCqv$ApAl3lqq>)ST%+nMq0oanui%KDCsm_K$G&a5cE+;Xkx+TLqV zO%_#;G|xJ`7ws8G`L*JoIo1$*8e`i6w){P54+i;II2aVMPau}$E{EZK8OFwnN3ci8 zFrSxu-V8|*!GFRBICGy~U_Q2NerU2<*WP0Q$fS!|!tkeFD$_6&&f~aM;Qb1?!sPOV&L=<@Aw*CN{K=2p2Z6RveZFxJ6 zz?3cC(3i1(1V6%rHYh}eDHT8(^jw9n{4HRCtLTjom8W1v`8r(#)v+lyiRH-EXQwIc zo^;T(5huB&JXE=jm*&>)Xcnp51~{G02=QsTXo~d8lT=NSA>fX*^lQ1LRU?3wWf-Nj zY7{I|x>PGgQ%Y&n9m%yw>6J8(c2`ORx0J6%O0T7Pw7XKel45d8C)XL{1D5pGqoD{0 zAq>lrgNNpeqk-5!efvQUVs!?^0dP+7LPML_0yTg*fzP6 zDm&>@*#~_FDh!(}roS{b349o;7ll4$7#a_G-jvi>iP|Rdd>EAC&B!^RM{ZgZQI*mw z)P>@bq0gO5$;O^R6~%Kt1Y~%u!*0>@?8Opj5ytwn4Wg3m&70NURB5P#_Dv zhNX@~)v*32=KL~%VP@7}ajp8D72kJGa+|08PtF#tnq2*4wrIRB82ENpn1jNHpY@s5(P|QI`-fxApEG1F4PT9 zM*SR0BygcYdGr@wN?lA%I?Ctw1N%Vw@rRIDs2}21Z6g@z^~SxYWX>`=yqvDR((Z8mH5{Q}lnao=XPR^4|o*2g7F&78?(l_e`F z6@~)pMW=8M6)nAZ1>p%UEr5!EO6S(*PpD$%K}h|Rk+*#$BK z4SaIxHm!lje~2|wsdchVm69487y@Nlkupf@d7H3m|`GcboVpIP-aW!*#?Vu;AKU#b3BK1eLSFsn#?nYg)Od zT$)NC*wE^xdSI6weuMq3{&o;M+)2*B?6CXs<(c%Ued?=J9Y;7hiULh;eAvLfAco=z z46G60AQjGC1$>JMPF*=XMNPp7}=O`$s01yS` zElA{qE_`7M%8;=NVp3EPRng(m>{S&K@=G=AK|L|IrB$AxOpDIcbr`B9<&-ti0yHpM zG6zq`;WQCRG)NAlok7rfNRnEyE)0{Dx2$*xX2*aV6&zI7hn^LbYP72AEIF*GF5|Bo z?V2dc`k$S0?3^iHaXEBlD;TqmnU!nd=AWrp1B#1JW(}0upm7EbHnLEwp$X9O;Z0D~ zg(0#54q`~kta(f{?S26>QJrS2TE2!%Mu$P*c7^uvq7GF08_Y@N38S+njI>d(l8o9j zf7bSfOhe`{L~>|E0%?^*@M(_;PuJ4@oe*rEN|rty;Z%ik@Z*z(~sA3SsC zZ1$P$yWc}PKtON+JV6m;u?oE z5qJ4&jAU2#O4_duQrtD?sB{=eKG+uAa`fDd0wev@b#7ai_T}b?X8A9K| z$!Y~4sk)9rQJ}Gqx8`JNw1g$3PN?EXc@ifyPtiH|`Z;0;DT>aEV=qpD9klAP8#}J= z06WNmc2Ieq9V9C{i}_Rj4Eyegix6ZbCR5_OIG!IM_&S0-<>)5j5ENnmo`Zz!RB60# zgOla^K8Lm9ZV6-a-2)RTFlUAo1R_}jcU=l@ql()&=TdDatS}6J>IIdPi@1%9{Vgdo zgig4X)7w^vH_lOIkQ)$!`VGiUl#qIdrp7_E69l89O3kZJ1;B8rHIr zKGp7qwUpB5hP5=(r`p}H7Id4EOE()&Qk-_z%EFgDP<2)x5oMWZA`%l=DVc(&Br{kJ zGRLZtiOP-ux z#tmRxJVgBv*d2>S7pPYs^)7jGei?TJvZzt*q=5Eojp1C6B+WTQl~;q(QKys0HR9;v!Xo=5iW1^_sKz_HhGd_Ic6g>8fs7+Cx_-*>#7t2s*je`ZY6$Pws7&6cfupwB|9In&w*3-ze0`T9Rz=Y;J+jIO9Veb zkgxEiOJXwB(V?_L=xj}0&Q*1RBoQ+o*aK-{Mhu?y_v0hC%5c_h~xci z`HXzDclj$fH;(et4ZAW8yC&Cz3E(hZX6;1{X}gy{YbI`yLjZ!6pXY-ytWOp9&`aeqCP@%UipxT4#jr+I^wat<q zm47=`gcw_o?HHd4_e-R`OeE(f9sUvz`Rl)w2b4UpE(YY zEO4JMHK6PA23`q?h5Q&=7|AT;%IB@}v0{NXEwYs7aaNH1lT*ds>%Es>ncqC}DZ)R# zOPc?EI#eW6k!wp+Li;SXv~J$yQ-dCyw>)F|?CWRs#`FtgFH99|on5*1M&Nn?jA=U> z(`ClSv?E{8khe#waUF@s!NVRy>v;wrAn3vlJq8JUE1~K}*Y&Q^mnW=uj?Xc6>p}K@ z5o29HXHm^#ERiFMsGoE00zA<~5sCIIf^Y8oY7?iIt$n1WB*oq=*VPIy3%UkdZVVdU&pCFH?Ush6L@a)QO z$rLuS5DJu~Q@QUTl4l3ZypH?UWW53P5OsaVzy2nioYqb`HmWD5Z#X}87tMN#zPV+( zxG__VE@9`p&WY{W;^v=u_S`iyp5po33R;r66$%DqmWzdKk%*skq1Ig`erpJ75x)Wz z%&#AH5Zo@GEl5i|kCnXwKp&kY4g(^sn=Lt1>pZspDi%QBK+YL1@?pY@{}2)jiGqLK z@D3Wu_Yc@%!fVx-9i*Q5d^S(*C!tqqFI@u?z4uj^0fXqh>WruQ=CSXc{PxKyPvg?j zd#d2tO(zSg&A)Cb2#lv&M&z3KHON0tx}s+@?WaRfJgygT8F8J&ZvZg%Ci)-`dJ~5W zkjKlHk=@tHXgmYrgpsz%MO8ns0AjVA6sz6o=<>>|7N7Pal@{?VKB+}KC*8Cz%_Btl z8+e7N+&~4}M{{cTC3>W))tV>qHF!BFy}mgnh+zm7$B^PiHEgl5EjA!<_0Ms#?$VI1 zr2*tz{~(-2cPl08;Qe9@HHWzKI*csGVgaYlX?_ZNNJ8@D(7y=&>;nMN$?(z0VEn9j z8h!YE$ZycI|w{4%j^6Yf+hD`B>Y;j=H z6PWdtO_uM;`dTI(@(ZJB2Zlp8zXbL-msnuP^J)eu@~4DNIjMQf9t4swe}(F zkZs65B#_NINGk;j_pLnS=#8gec+A~^dt@LKb#(F|*Wnj5*aT{h$z zau0ciytkAYr@IbL#&Jc-r5j%*#cB5;4{sx35MKFCc?GTMyiYgQby_)^qUC9RFzS8a z5wfSBZdSfd%Z0$%Gte6qAa0DihqOP=LmiQ;E_xFl+q&y`jjvrgE*e$_YaChvX9l>OExW0hQ)uzN{G@-3|w+j zmS^RvXKQ(1Bom1kBavb;s712Ld6SHh2V1pIQy>C2eILcTrVpd3JQ$;Y%9nrbvI z$pRJ#d}Sbh7Ch&c_-TMv_(D!FF4^#wNJ}>O@b6$WnLs}VF>Ey;pS}C@`kXGKk|*!M zA0s#VJE&%udEj6QR?d6*VN?Nrte2kjqk zcw!_y?XQH>{3-vs_mc0Y-c5~Ieo?sMYWbD&YjxSe4I|cBo`>{%>&E#X1Ryb9Jz|-A=X6Jqu`dd`? z&E(auT=~k)t)qqCeroi@c+=R)Y}xi~;ZxWG_**M7j*4rK-(;`XOgYxg_*aa0Wra!M zM=)-NL4Jh$W$y1`F1`p32s}>Zf!Kh2X(wgyZ4NW=II|Ha)bTj<5t7SDZl1Sla@puj zAuR&?ylsh`YJ|;syY_ZbvP5mYjHa|ZjIdn$MN`0~&?+UDj!Ri&IdDtS1TsxRgWJTa{1Jo`7^w%4;nTIe<%9({(NKN32yQRgckZ8b1Pq5hK}T zzVv7Xy)D_oI0-w?RfD{szInU?Wc1@nu(fB(gxc zl3NLsAZ!H#3E?yfc!y|%q-aOr0FXe^RM~~hEqu96rb{Wa$i;sTi+>2k;aeo|n$(7O zTE4$ya^2P`|2F+=QhA)A=E;$-O#7=c{;DZ|^`Dn~Sn)x{vN!{Awr#n+V{FHG@1G~` zZ2vDqGaDPnOU4H$dgjb*!CqhjtaBgxi^9_1k_G7f->B>o7O)M5_&a0)+E%kFYL0<2 zv}_h&)7~&%;$uRLk<$gXEAA?C>9(t>Pu;}yDT5wlm>IlqGv88-3yLG(Q!uxQZpcH# zZ@{;Mr>C6R>C`XQA-hNDn= z9-YfI&!=-4|DUn&AQYBg2mBm#u1Gl}T7L5OuCZO19n8NB#0!?myL7aria?_SK@cyh2-9B8Fj8TS%{rhh)pXO*7KnoRpU1+l zJ*2mtAm3+%cu2~AH^||}y%6-6(sadR;-G*Qn}{!rCmN0cD>UM*8ddqIRwjP{Qllp2 z^gu*HG9(`)a+!Q&>aoc+qaX!Jf-iiVRtwLK4UjZ*KYCj&JW6?R_sX zvHb@_caHsZiyYzz98@L(6e#pkiC`QszZ37I@u3o%R4p;xLaG*>#sZS+p&IpR(q|xR z&G3ppdSj^wJq14mk?V)A#}nfeEO7N-j%Wn6m>Uq_-z(r7((37tQj+|`0ZYbR8<#?V zu(tl>v{eadAVeV9$hRnSy_J#$k!cTIbNuxaT=Cto3?K((rFIfNzf!nO)oPWapvYKc zqU^`(?)3hsZnEP<*8jqkQ;x1rl$vmF0?t8 zoopr&DJPFI&x#v-2RFWPCSXtRI zwax#alh_Hx0v*1U!Ns1Q!upM{pCt1cDg^WWf6& zC8-h;IFIgMh9IA@kleu_MF;aH?vdkJ`Kx8miB&Lj@IRA4wH3ax#tw8yvRGFqUsPUv=JPj<9aa1Awgc{wY%_dw{*SiE*s>+E<4y4}CVa@E!x; zVUeG$yvG1|P-kb?UUoiU;P#+|Vb@;{OfiqoGLMg(l<$`-$bG}*)D*J;@RwWR_kEj- zFQi k@O`9f@O`AM8Q7aEw`YA%P5X9be7o-0v%dYWy2%3nf1L+>wg3PC literal 0 HcmV?d00001 diff --git a/sdk/bindings/python/zuptsdk.py b/sdk/bindings/python/zuptsdk.py new file mode 100644 index 0000000..2402d86 --- /dev/null +++ b/sdk/bindings/python/zuptsdk.py @@ -0,0 +1,468 @@ +""" +zuptsdk — Python bindings for libzuptsdk + +Copyright (c) 2026 Cristian Cezar Moisés +SPDX-License-Identifier: AGPL-3.0-or-later + +Reference Python bindings generated via cffi. Provides a Pythonic API +on top of the C ABI; secrets (passwords, keys) are managed in +mlock-backed buffers that zero on garbage collection. + +Quick start: + import zuptsdk + with zuptsdk.Context() as ctx: + archive = ctx.compress_buffer(b"hello world", name="hello.txt", + password=b"secret") + data = ctx.extract_buffer(archive, password=b"secret") + assert data == b"hello world" + +Post-quantum: + with zuptsdk.Context() as ctx: + keypair = ctx.generate_keypair() + keypair.save("/tmp/mykey") + archive = ctx.compress_buffer(b"hello", name="h.txt", + public_key=keypair.public) + data = ctx.extract_buffer(archive, private_key=keypair.private) +""" + +import os +from cffi import FFI + +# ───────────────────────────────────────────────────────────────────── +# CFFI setup +# ───────────────────────────────────────────────────────────────────── +_ffi = FFI() +_ffi.cdef(""" + typedef struct zuptsdk_ctx zuptsdk_ctx_t; + typedef struct zuptsdk_options zuptsdk_options_t; + typedef struct zuptsdk_archive_info zuptsdk_archive_info_t; + typedef struct zuptsdk_secure_buf zuptsdk_secure_buf_t; + typedef struct zuptsdk_keypair zuptsdk_keypair_t; + typedef struct zuptsdk_pubkey zuptsdk_pubkey_t; + typedef struct zuptsdk_privkey zuptsdk_privkey_t; + + const char *zuptsdk_version_string(void); + int zuptsdk_version_check(int major, int minor, int patch); + const char *zuptsdk_strerror(int err); + const char *zuptsdk_last_error_detail(void); + + int zuptsdk_ctx_create(zuptsdk_ctx_t **ctx_out); + void zuptsdk_ctx_destroy(zuptsdk_ctx_t *ctx); + int zuptsdk_ctx_set_threads(zuptsdk_ctx_t *ctx, int threads); + + int zuptsdk_options_create(zuptsdk_options_t **opts_out); + void zuptsdk_options_destroy(zuptsdk_options_t *opts); + int zuptsdk_options_set_codec(zuptsdk_options_t *opts, int codec); + int zuptsdk_options_set_level(zuptsdk_options_t *opts, int level); + int zuptsdk_options_set_dedup(zuptsdk_options_t *opts, int enabled); + int zuptsdk_options_set_solid(zuptsdk_options_t *opts, int enabled); + + int zuptsdk_secure_buf_create(size_t size, zuptsdk_secure_buf_t **buf_out); + void zuptsdk_secure_buf_destroy(zuptsdk_secure_buf_t *buf); + int zuptsdk_secure_buf_get(zuptsdk_secure_buf_t *buf, + uint8_t **data_out, size_t *size_out); + int zuptsdk_secure_buf_from_data(const uint8_t *data, size_t size, + zuptsdk_secure_buf_t **buf_out); + + int zuptsdk_keypair_generate(zuptsdk_ctx_t *ctx, zuptsdk_keypair_t **kp_out); + void zuptsdk_keypair_destroy(zuptsdk_keypair_t *kp); + int zuptsdk_keypair_save_private(const zuptsdk_keypair_t *kp, const char *path); + int zuptsdk_keypair_save_public(const zuptsdk_keypair_t *kp, const char *path); + int zuptsdk_privkey_load(const char *path, zuptsdk_privkey_t **key_out); + void zuptsdk_privkey_destroy(zuptsdk_privkey_t *key); + int zuptsdk_pubkey_load(const char *path, zuptsdk_pubkey_t **key_out); + void zuptsdk_pubkey_destroy(zuptsdk_pubkey_t *key); + + int zuptsdk_compress_buffer(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *logical_name, + const uint8_t *data, size_t data_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, size_t *archive_sz); + + int zuptsdk_extract_buffer(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk, + uint8_t **data_out, size_t *data_sz); + + int zuptsdk_extract_to_dir(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + const char *dest_dir, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + + int zuptsdk_verify(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + + int zuptsdk_archive_info_read(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_archive_info_t **info_out); + void zuptsdk_archive_info_destroy(zuptsdk_archive_info_t *info); + int zuptsdk_archive_info_format_major(const zuptsdk_archive_info_t *i); + int zuptsdk_archive_info_format_minor(const zuptsdk_archive_info_t *i); + const char *zuptsdk_archive_info_uuid(const zuptsdk_archive_info_t *i); + int64_t zuptsdk_archive_info_created_unix(const zuptsdk_archive_info_t *i); + uint64_t zuptsdk_archive_info_size(const zuptsdk_archive_info_t *i); + int zuptsdk_archive_info_is_encrypted(const zuptsdk_archive_info_t *i); + int zuptsdk_archive_info_is_pq_hybrid(const zuptsdk_archive_info_t *i); + int zuptsdk_archive_info_is_solid(const zuptsdk_archive_info_t *i); + int zuptsdk_archive_info_is_dedup(const zuptsdk_archive_info_t *i); + + void zuptsdk_free(void *ptr); +""") + +# Try multiple paths to find the library +def _load_library(): + candidates = [ + os.environ.get("ZUPTSDK_LIBRARY"), + "libzuptsdk.so.1", + "libzuptsdk.so", + "/usr/local/lib/libzuptsdk.so.1", + "/usr/lib/libzuptsdk.so.1", + ] + # Also try ../build relative to this file (dev mode) + here = os.path.dirname(os.path.abspath(__file__)) + candidates.append(os.path.join(here, "..", "..", "build", "libzuptsdk.so.1")) + candidates.append(os.path.join(here, "..", "..", "build", "libzuptsdk.so")) + + last_err = None + for c in candidates: + if not c: + continue + try: + return _ffi.dlopen(c) + except OSError as e: + last_err = e + raise OSError(f"Could not load libzuptsdk.so.1 ({last_err}). " + f"Set ZUPTSDK_LIBRARY env var or install the library.") + +_lib = _load_library() + + +# ───────────────────────────────────────────────────────────────────── +# Constants +# ───────────────────────────────────────────────────────────────────── + +class Codec: + AUTO = 0 + VAPTVUPT = 1 + LZHP = 2 + LZH = 3 + LZ = 4 + STORE = 5 + + +class _Errors: + OK = 0 + INVALID_ARG = -1 + NO_MEMORY = -2 + IO = -3 + BAD_ARCHIVE = -4 + BAD_PASSWORD = -5 + BAD_KEY = -6 + BAD_MAC = -7 + BAD_VERSION = -8 + BAD_CHECKSUM = -9 + BUFFER_TOO_SMALL = -10 + NOT_ENCRYPTED = -11 + PASSWORD_REQUIRED = -12 + PQ_KEY_REQUIRED = -13 + UNSUPPORTED = -14 + + +# ───────────────────────────────────────────────────────────────────── +# Exceptions +# ───────────────────────────────────────────────────────────────────── + +class ZuptError(Exception): + """Base exception for any libzuptsdk error.""" + def __init__(self, code, detail=None): + self.code = code + msg = _ffi.string(_lib.zuptsdk_strerror(code)).decode("utf-8", "replace") + if detail: + msg = f"{msg}: {detail}" + super().__init__(msg) + + +class BadPassword(ZuptError): pass +class BadKey(ZuptError): pass +class BadArchive(ZuptError): pass +class PasswordRequired(ZuptError): pass +class PQKeyRequired(ZuptError): pass + + +def _check(rc): + """Raise the right exception for any non-OK return code.""" + if rc == _Errors.OK: + return + detail = _ffi.string(_lib.zuptsdk_last_error_detail()).decode("utf-8", "replace") + if rc == _Errors.BAD_PASSWORD: raise BadPassword(rc, detail) + if rc == _Errors.BAD_KEY: raise BadKey(rc, detail) + if rc == _Errors.BAD_ARCHIVE: raise BadArchive(rc, detail) + if rc == _Errors.PASSWORD_REQUIRED: raise PasswordRequired(rc, detail) + if rc == _Errors.PQ_KEY_REQUIRED: raise PQKeyRequired(rc, detail) + raise ZuptError(rc, detail) + + +# ───────────────────────────────────────────────────────────────────── +# SecureBuf — helper to wrap a Python bytes into an mlock'd buffer +# ───────────────────────────────────────────────────────────────────── + +class SecureBuf: + """A secure buffer for passwords or key material. Memory is mlock'd + and zeroed on close. Use as a context manager.""" + + def __init__(self, data): + if not isinstance(data, (bytes, bytearray)): + raise TypeError("SecureBuf takes bytes") + self._buf_pp = _ffi.new("zuptsdk_secure_buf_t **") + rc = _lib.zuptsdk_secure_buf_from_data(data, len(data), self._buf_pp) + _check(rc) + self._buf = self._buf_pp[0] + + def _handle(self): + return self._buf + + def close(self): + if self._buf: + _lib.zuptsdk_secure_buf_destroy(self._buf) + self._buf = _ffi.NULL + + def __enter__(self): return self + def __exit__(self, *a): self.close() + def __del__(self): + try: self.close() + except Exception: pass + + +# ───────────────────────────────────────────────────────────────────── +# Keypair, PublicKey, PrivateKey +# ───────────────────────────────────────────────────────────────────── + +class PublicKey: + def __init__(self, path): + kp = _ffi.new("zuptsdk_pubkey_t **") + _check(_lib.zuptsdk_pubkey_load(path.encode("utf-8"), kp)) + self._key = kp[0] + def _handle(self): return self._key + def __del__(self): + try: + if self._key: + _lib.zuptsdk_pubkey_destroy(self._key) + self._key = _ffi.NULL + except Exception: pass + + +class PrivateKey: + def __init__(self, path): + kp = _ffi.new("zuptsdk_privkey_t **") + _check(_lib.zuptsdk_privkey_load(path.encode("utf-8"), kp)) + self._key = kp[0] + def _handle(self): return self._key + def __del__(self): + try: + if self._key: + _lib.zuptsdk_privkey_destroy(self._key) + self._key = _ffi.NULL + except Exception: pass + + +class Keypair: + """Generated PQ hybrid keypair. Wraps internal temporary files; + use save() to persist the key material.""" + + def __init__(self, ctx_handle): + kp = _ffi.new("zuptsdk_keypair_t **") + _check(_lib.zuptsdk_keypair_generate(ctx_handle, kp)) + self._kp = kp[0] + self._priv_path = None + self._pub_path = None + + def save(self, base_path): + """Save keypair as .key (private) and .pub. + Private key is written with mode 0600.""" + priv = base_path + ".key" + pub = base_path + ".pub" + _check(_lib.zuptsdk_keypair_save_private(self._kp, priv.encode("utf-8"))) + _check(_lib.zuptsdk_keypair_save_public(self._kp, pub.encode("utf-8"))) + self._priv_path, self._pub_path = priv, pub + return priv, pub + + @property + def public(self): + if not self._pub_path: + raise RuntimeError("save() the keypair before accessing keys") + return PublicKey(self._pub_path) + + @property + def private(self): + if not self._priv_path: + raise RuntimeError("save() the keypair before accessing keys") + return PrivateKey(self._priv_path) + + def __del__(self): + try: + if self._kp: + _lib.zuptsdk_keypair_destroy(self._kp) + self._kp = _ffi.NULL + except Exception: pass + + +# ───────────────────────────────────────────────────────────────────── +# ArchiveInfo +# ───────────────────────────────────────────────────────────────────── + +class ArchiveInfo: + def __init__(self, handle): + self._h = handle + # Snapshot fields immediately so handle can be freed + self.format_major = int(_lib.zuptsdk_archive_info_format_major(handle)) + self.format_minor = int(_lib.zuptsdk_archive_info_format_minor(handle)) + uuid_p = _lib.zuptsdk_archive_info_uuid(handle) + self.uuid = _ffi.string(uuid_p).decode("utf-8") if uuid_p else "" + self.created_unix = int(_lib.zuptsdk_archive_info_created_unix(handle)) + self.size = int(_lib.zuptsdk_archive_info_size(handle)) + self.is_encrypted = bool(_lib.zuptsdk_archive_info_is_encrypted(handle)) + self.is_pq_hybrid = bool(_lib.zuptsdk_archive_info_is_pq_hybrid(handle)) + self.is_solid = bool(_lib.zuptsdk_archive_info_is_solid(handle)) + self.is_dedup = bool(_lib.zuptsdk_archive_info_is_dedup(handle)) + _lib.zuptsdk_archive_info_destroy(handle) + self._h = None + + def __repr__(self): + return (f"") + + +# ───────────────────────────────────────────────────────────────────── +# Options +# ───────────────────────────────────────────────────────────────────── + +class _Options: + def __init__(self, codec=Codec.AUTO, level=7, dedup=False, solid=False): + op = _ffi.new("zuptsdk_options_t **") + _check(_lib.zuptsdk_options_create(op)) + self._o = op[0] + _check(_lib.zuptsdk_options_set_codec(self._o, codec)) + _check(_lib.zuptsdk_options_set_level(self._o, level)) + _check(_lib.zuptsdk_options_set_dedup(self._o, 1 if dedup else 0)) + _check(_lib.zuptsdk_options_set_solid(self._o, 1 if solid else 0)) + + def _handle(self): return self._o + + def __del__(self): + try: + if self._o: + _lib.zuptsdk_options_destroy(self._o) + self._o = _ffi.NULL + except Exception: pass + + +# ───────────────────────────────────────────────────────────────────── +# Context — main entry point +# ───────────────────────────────────────────────────────────────────── + +class Context: + """SDK context. Holds thread pool, callbacks, error state.""" + + def __init__(self, threads=0): + cp = _ffi.new("zuptsdk_ctx_t **") + _check(_lib.zuptsdk_ctx_create(cp)) + self._ctx = cp[0] + if threads: + _check(_lib.zuptsdk_ctx_set_threads(self._ctx, threads)) + + def __enter__(self): return self + def __exit__(self, *a): self.close() + + def close(self): + if self._ctx: + _lib.zuptsdk_ctx_destroy(self._ctx) + self._ctx = _ffi.NULL + + def __del__(self): + try: self.close() + except Exception: pass + + # ──── helpers ──── + def _password(self, pw): + """Coerce bytes/SecureBuf/None -> handle.""" + if pw is None: return _ffi.NULL + if isinstance(pw, SecureBuf): return pw._handle() + if isinstance(pw, (bytes, bytearray)): + self._tmp_pw = SecureBuf(bytes(pw)) + return self._tmp_pw._handle() + raise TypeError("password must be bytes, SecureBuf, or None") + + # ──── operations ──── + def compress_buffer(self, data, name="data.bin", + password=None, public_key=None, + codec=Codec.AUTO, level=7, dedup=False, solid=False): + """Compress a single in-memory buffer. Returns archive bytes.""" + if not isinstance(data, (bytes, bytearray)): + raise TypeError("data must be bytes") + opts = _Options(codec=codec, level=level, dedup=dedup, solid=solid) + pw_h = self._password(password) + pk_h = public_key._handle() if public_key else _ffi.NULL + + out_p = _ffi.new("uint8_t **") + out_sz = _ffi.new("size_t *") + _check(_lib.zuptsdk_compress_buffer(self._ctx, opts._handle(), + name.encode("utf-8"), + data, len(data), + pw_h, pk_h, out_p, out_sz)) + result = bytes(_ffi.buffer(out_p[0], out_sz[0])) + _lib.zuptsdk_free(out_p[0]) + return result + + def extract_buffer(self, archive, password=None, private_key=None): + """Extract a single-file archive back to bytes.""" + if not isinstance(archive, (bytes, bytearray)): + raise TypeError("archive must be bytes") + pw_h = self._password(password) + sk_h = private_key._handle() if private_key else _ffi.NULL + + out_p = _ffi.new("uint8_t **") + out_sz = _ffi.new("size_t *") + _check(_lib.zuptsdk_extract_buffer(self._ctx, archive, len(archive), + pw_h, sk_h, out_p, out_sz)) + result = bytes(_ffi.buffer(out_p[0], out_sz[0])) + _lib.zuptsdk_free(out_p[0]) + return result + + def extract_to_dir(self, archive, dest_dir, password=None, private_key=None): + if not isinstance(archive, (bytes, bytearray)): + raise TypeError("archive must be bytes") + pw_h = self._password(password) + sk_h = private_key._handle() if private_key else _ffi.NULL + _check(_lib.zuptsdk_extract_to_dir(self._ctx, archive, len(archive), + dest_dir.encode("utf-8"), + pw_h, sk_h)) + + def verify(self, archive, password=None, private_key=None): + """Verify an archive's integrity. Returns True or raises.""" + pw_h = self._password(password) + sk_h = private_key._handle() if private_key else _ffi.NULL + _check(_lib.zuptsdk_verify(self._ctx, archive, len(archive), pw_h, sk_h)) + return True + + def info(self, archive): + """Read archive header metadata. No password/key needed.""" + info_p = _ffi.new("zuptsdk_archive_info_t **") + _check(_lib.zuptsdk_archive_info_read(self._ctx, archive, len(archive), info_p)) + return ArchiveInfo(info_p[0]) + + def generate_keypair(self): + return Keypair(self._ctx) + + +# ───────────────────────────────────────────────────────────────────── +# Module info +# ───────────────────────────────────────────────────────────────────── + +__version__ = _ffi.string(_lib.zuptsdk_version_string()).decode("utf-8") diff --git a/sdk/doc/example.c b/sdk/doc/example.c new file mode 100644 index 0000000..4c88a49 --- /dev/null +++ b/sdk/doc/example.c @@ -0,0 +1,80 @@ +/* + * libzuptsdk example: compress a file with a password, then extract it. + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Build: gcc example.c $(pkg-config --cflags --libs zuptsdk) -o example + * # or, before installation: + * gcc example.c -I sdk/include -L sdk/build -lzuptsdk \ + * -Wl,-rpath,'$ORIGIN/../build' -o example + */ +#include +#include +#include +#include + +static int die(const char *what, int rc) { + fprintf(stderr, "ERROR (%s): %s\n", what, zuptsdk_strerror(rc)); + fprintf(stderr, " detail: %s\n", zuptsdk_last_error_detail()); + return 1; +} + +int main(void) { + /* 1. Always check ABI compatibility at startup */ + int rc = zuptsdk_version_check(1, 0, 0); + if (rc) return die("version_check", rc); + printf("libzuptsdk %s\n\n", zuptsdk_version_string()); + + /* 2. Create a context (cheap; one per worker thread is fine) */ + zuptsdk_ctx_t *ctx = NULL; + rc = zuptsdk_ctx_create(&ctx); + if (rc) return die("ctx_create", rc); + + /* 3. Wrap a password in a secure buffer (mlock + zero on destroy) */ + zuptsdk_secure_buf_t *pw = NULL; + const char *password = "correct horse battery staple"; + rc = zuptsdk_secure_buf_from_data((const uint8_t *)password, + strlen(password), &pw); + if (rc) { zuptsdk_ctx_destroy(ctx); return die("secure_buf", rc); } + + /* 4. Compress some data into a memory archive */ + const char *plaintext = "This is the secret data we want to protect.\n"; + uint8_t *archive = NULL; + size_t archive_sz = 0; + rc = zuptsdk_compress_buffer(ctx, NULL, + "secret.txt", + (const uint8_t *)plaintext, strlen(plaintext), + pw, NULL, + &archive, &archive_sz); + if (rc) { + zuptsdk_secure_buf_destroy(pw); + zuptsdk_ctx_destroy(ctx); + return die("compress_buffer", rc); + } + printf("Compressed %zu bytes -> %zu byte archive\n", strlen(plaintext), archive_sz); + + /* 5. Extract it back, verifying byte-for-byte */ + uint8_t *extracted = NULL; + size_t extracted_sz = 0; + rc = zuptsdk_extract_buffer(ctx, archive, archive_sz, + pw, NULL, + &extracted, &extracted_sz); + if (rc) { + zuptsdk_free(archive); + zuptsdk_secure_buf_destroy(pw); + zuptsdk_ctx_destroy(ctx); + return die("extract_buffer", rc); + } + + /* 6. Verify */ + int ok = (extracted_sz == strlen(plaintext)) && + (memcmp(extracted, plaintext, extracted_sz) == 0); + printf("Roundtrip: %s\n", ok ? "OK (byte-exact)" : "FAILED"); + printf("Extracted: %.*s", (int)extracted_sz, extracted); + + /* 7. Clean up — always use zuptsdk_free for transferred memory */ + zuptsdk_free(extracted); + zuptsdk_free(archive); + zuptsdk_secure_buf_destroy(pw); + zuptsdk_ctx_destroy(ctx); + return ok ? 0 : 1; +} diff --git a/sdk/include/zuptsdk.h b/sdk/include/zuptsdk.h new file mode 100644 index 0000000..f30ea6c --- /dev/null +++ b/sdk/include/zuptsdk.h @@ -0,0 +1,605 @@ +/* + * libzuptsdk — Public C ABI for the Zupt backup compression library + * + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Repository: https://git.securityops.co/cristiancmoises/zupt + * Website: https://zupt.securityops.co + * Contact: zupt@riseup.net + * + * -------------------------------------------------------------------------- + * STABILITY GUARANTEE + * -------------------------------------------------------------------------- + * Every symbol declared in this header is part of the stable v1.0 ABI and + * is gated behind the linker version tag ZUPTSDK_1.0. New symbols may be + * added in minor versions (1.1, 1.2, ...) under new tags (ZUPTSDK_1.1, ...). + * Existing symbols will never change signature within v1.x. Breaking + * changes require a major version bump (libzuptsdk.so.2). + * + * No symbol prefixed with anything other than `zuptsdk_` or `ZUPTSDK_` is + * part of this ABI. Do not link against internal `zupt_*` symbols even if + * they appear in the static archive — they will disappear without notice. + * + * -------------------------------------------------------------------------- + * THREAD SAFETY + * -------------------------------------------------------------------------- + * Every function that takes a `zuptsdk_ctx_t *` operates only on that + * context's state and on caller-provided buffers. Concurrent calls on + * DISTINCT contexts are safe (MT-Safe). Concurrent calls on the SAME + * context are NOT safe (MT-Unsafe-Same-Context) unless explicitly + * documented otherwise. + * + * -------------------------------------------------------------------------- + * MEMORY OWNERSHIP + * -------------------------------------------------------------------------- + * Every function documents ownership using these conventions in the param + * comments: + * [in] caller owns, library reads only + * [out] caller owns, library writes + * [in,out] caller owns, library reads and writes + * [transfers] ownership moves caller -> library (or library -> caller) + * [borrowed] pointer valid only for the duration of the call + * + * Any function that returns a heap-allocated value via an output pointer + * documents the corresponding zuptsdk_*_destroy() or zuptsdk_free() call + * the caller must invoke. Calling free() on libc-allocated memory from a + * different allocator is undefined; always use the documented destroyer. + * + * -------------------------------------------------------------------------- + * ERROR HANDLING + * -------------------------------------------------------------------------- + * Functions return `int` where 0 == ZUPTSDK_OK and negative values are + * `zuptsdk_error_t` codes. Use zuptsdk_strerror() for a static description + * and zuptsdk_last_error_detail(ctx) for a thread-local detailed message + * including filename, line number, and underlying errno where applicable. + * + * The library never calls abort(), exit(), or _exit(). It never writes to + * stdout or stderr unless the caller explicitly enables logging via + * zuptsdk_ctx_set_log_callback(). + * + * -------------------------------------------------------------------------- + * SECURE MEMORY + * -------------------------------------------------------------------------- + * Inputs and outputs containing secret material (passwords, raw keys, + * decrypted plaintext keys) MUST be passed via `zuptsdk_secure_buffer_t` + * to ensure mlock()-backed storage and explicit_bzero() on destroy. + * Passing such material via plain `const uint8_t *` is allowed for + * convenience but the library cannot guarantee zeroization of caller + * memory in that case. + */ + +#ifndef ZUPTSDK_H +#define ZUPTSDK_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ════════════════════════════════════════════════════════════════════════ + * VERSION + * ════════════════════════════════════════════════════════════════════════ */ + +#define ZUPTSDK_VERSION_MAJOR 1 +#define ZUPTSDK_VERSION_MINOR 0 +#define ZUPTSDK_VERSION_PATCH 0 +#define ZUPTSDK_VERSION_STRING "1.0.0" + +/* Compile-time version check helper (negative if header older than required) */ +#define ZUPTSDK_VERSION_AT_LEAST(maj, min, pat) \ + ((ZUPTSDK_VERSION_MAJOR > (maj)) || \ + (ZUPTSDK_VERSION_MAJOR == (maj) && ZUPTSDK_VERSION_MINOR > (min)) || \ + (ZUPTSDK_VERSION_MAJOR == (maj) && ZUPTSDK_VERSION_MINOR == (min) && \ + ZUPTSDK_VERSION_PATCH >= (pat))) + +/** + * Return the runtime version string of the linked library, e.g. "1.0.0". + * The returned pointer is to static storage and must NOT be freed. + * + * Use this with the compile-time ZUPTSDK_VERSION_STRING to detect mismatch + * between header and library at runtime. + */ +const char *zuptsdk_version_string(void); + +/** + * Verify that the linked library is at least the requested version. + * Returns 0 if compatible, ZUPTSDK_ERR_VERSION_MISMATCH otherwise. + * Call this once at startup before any other zuptsdk_* function. + */ +int zuptsdk_version_check(int major, int minor, int patch); + +/* ════════════════════════════════════════════════════════════════════════ + * ERRORS + * ════════════════════════════════════════════════════════════════════════ */ + +typedef enum { + ZUPTSDK_OK = 0, + ZUPTSDK_ERR_INVALID_ARG = -1, /* NULL pointer, bad size, bad enum value */ + ZUPTSDK_ERR_NO_MEMORY = -2, /* malloc/calloc/realloc returned NULL */ + ZUPTSDK_ERR_IO = -3, /* read/write error; see errno detail */ + ZUPTSDK_ERR_BAD_ARCHIVE = -4, /* Magic mismatch or truncated header */ + ZUPTSDK_ERR_BAD_PASSWORD = -5, /* MAC verification failed */ + ZUPTSDK_ERR_BAD_KEY = -6, /* PQ key file malformed or wrong type */ + ZUPTSDK_ERR_BAD_MAC = -7, /* HMAC mismatch — archive corrupted or tampered */ + ZUPTSDK_ERR_BAD_VERSION = -8, /* Archive format version not supported */ + ZUPTSDK_ERR_BAD_CHECKSUM = -9, /* Block checksum mismatch */ + ZUPTSDK_ERR_BUFFER_TOO_SMALL = -10, /* Output buffer insufficient */ + ZUPTSDK_ERR_NOT_ENCRYPTED = -11, /* Tried to decrypt unencrypted archive */ + ZUPTSDK_ERR_PASSWORD_REQUIRED = -12, /* Archive needs password but none supplied */ + ZUPTSDK_ERR_PQ_KEY_REQUIRED = -13, /* Archive needs PQ key but none supplied */ + ZUPTSDK_ERR_UNSUPPORTED = -14, /* Feature not supported on this platform */ + ZUPTSDK_ERR_VERSION_MISMATCH = -15, /* Library older than requested */ + ZUPTSDK_ERR_PATH_TRAVERSAL = -16, /* "../" or absolute path in archive */ + ZUPTSDK_ERR_TOO_LARGE = -17, /* Decompressed size exceeds limit */ + ZUPTSDK_ERR_CRYPTO_FAIL = -18, /* Underlying crypto primitive failed */ + ZUPTSDK_ERR_CANCELLED = -19, /* Caller cancelled via progress callback */ + ZUPTSDK_ERR_INTERNAL = -99 /* Bug in library — please report */ +} zuptsdk_error_t; + +/** + * Static error description for a zuptsdk_error_t value. + * Returned pointer is static and must not be freed. Always non-NULL. + */ +const char *zuptsdk_strerror(int err); + +/** + * Thread-local detailed error message from the most recent failed call. + * The string includes file:line of the failure point and underlying errno + * description where applicable. Returned pointer is to thread-local + * storage, valid until the next failed zuptsdk_* call on this thread. + * Returns "" if no error has been recorded on this thread. + */ +const char *zuptsdk_last_error_detail(void); + +/* ════════════════════════════════════════════════════════════════════════ + * OPAQUE TYPES (forward declarations only — no struct layout exposed) + * ════════════════════════════════════════════════════════════════════════ */ + +typedef struct zuptsdk_ctx zuptsdk_ctx_t; +typedef struct zuptsdk_options zuptsdk_options_t; +typedef struct zuptsdk_archive_info zuptsdk_archive_info_t; +typedef struct zuptsdk_secure_buf zuptsdk_secure_buf_t; +typedef struct zuptsdk_keypair zuptsdk_keypair_t; +typedef struct zuptsdk_pubkey zuptsdk_pubkey_t; +typedef struct zuptsdk_privkey zuptsdk_privkey_t; + +/* ════════════════════════════════════════════════════════════════════════ + * ENUMS + * ════════════════════════════════════════════════════════════════════════ */ + +typedef enum { + ZUPTSDK_CODEC_AUTO = 0, /* Hardware-adaptive (VaptVupt on AVX2, LZHP otherwise) */ + ZUPTSDK_CODEC_VAPTVUPT = 1, /* VaptVupt LZ + ANS entropy */ + ZUPTSDK_CODEC_LZHP = 2, /* LZ77 + Huffman + Byte Prediction */ + ZUPTSDK_CODEC_LZH = 3, /* LZ77 + Huffman */ + ZUPTSDK_CODEC_LZ = 4, /* LZ77 only */ + ZUPTSDK_CODEC_STORE = 5 /* No compression */ +} zuptsdk_codec_t; + +typedef enum { + ZUPTSDK_ENC_NONE = 0, /* No encryption */ + ZUPTSDK_ENC_PASSWORD = 1, /* PBKDF2 → AES-256-CTR + HMAC-SHA256 */ + ZUPTSDK_ENC_PQ_HYBRID = 2 /* ML-KEM-768 + X25519 hybrid KEM */ +} zuptsdk_encryption_t; + +typedef enum { + ZUPTSDK_LOG_ERROR = 0, + ZUPTSDK_LOG_WARN = 1, + ZUPTSDK_LOG_INFO = 2, + ZUPTSDK_LOG_DEBUG = 3 +} zuptsdk_log_level_t; + +/* ════════════════════════════════════════════════════════════════════════ + * CALLBACKS + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Streaming read callback. Library calls this to obtain input bytes. + * @param userdata [in] opaque pointer supplied at stream init + * @param buf [out] destination buffer + * @param max_bytes max bytes to read into buf + * @return Number of bytes actually read (0 == EOF, < 0 == error). + */ +typedef int64_t (*zuptsdk_read_fn)(void *userdata, uint8_t *buf, size_t max_bytes); + +/** + * Streaming write callback. Library calls this to deliver output bytes. + * @param userdata [in] opaque pointer supplied at stream init + * @param buf [in] data to write + * @param bytes number of bytes in buf + * @return Number of bytes actually written (must equal `bytes` on success). + */ +typedef int64_t (*zuptsdk_write_fn)(void *userdata, const uint8_t *buf, size_t bytes); + +/** + * Progress callback. Library invokes periodically during long operations. + * Return non-zero to cancel the operation; the in-flight call will then + * return ZUPTSDK_ERR_CANCELLED. + * @param userdata [in] opaque pointer set via zuptsdk_ctx_set_progress_callback + * @param processed bytes processed so far + * @param total total bytes (0 if unknown) + * @return 0 to continue, non-zero to cancel. + */ +typedef int (*zuptsdk_progress_fn)(void *userdata, uint64_t processed, uint64_t total); + +/** + * Log callback. Receives diagnostic messages from the library. + * Set via zuptsdk_ctx_set_log_callback(). NULL means no logging (default). + * The string is null-terminated and valid only for the duration of the call. + */ +typedef void (*zuptsdk_log_fn)(void *userdata, zuptsdk_log_level_t level, const char *msg); + +/** + * Custom allocator hooks. Set globally via zuptsdk_set_allocator(). + * If any function is NULL, libc malloc/free/realloc is used. + * realloc_fn must accept (NULL, n) as malloc(n) and (p, 0) as free(p). + */ +typedef struct { + void *(*malloc_fn)(void *userdata, size_t size); + void (*free_fn)(void *userdata, void *ptr); + void *(*realloc_fn)(void *userdata, void *ptr, size_t size); + void *userdata; +} zuptsdk_allocator_t; + +/* ════════════════════════════════════════════════════════════════════════ + * GLOBAL CONFIG + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Install a custom allocator. Must be called before any other zuptsdk_* + * function. Calling after contexts have been created is undefined. + * Pass NULL to revert to libc allocator (only valid before first use). + * + * @param alloc [in,borrowed] allocator hooks; copied internally + * @return ZUPTSDK_OK or ZUPTSDK_ERR_INVALID_ARG + */ +int zuptsdk_set_allocator(const zuptsdk_allocator_t *alloc); + +/* ════════════════════════════════════════════════════════════════════════ + * CONTEXT + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Create a new SDK context. Each context holds its own thread pool, + * progress callback, log callback, and error state. Contexts are + * cheap to create — a few KB plus the configured thread count. + * + * @param ctx_out [out,transfers] pointer to receive new context + * @return ZUPTSDK_OK on success, ZUPTSDK_ERR_NO_MEMORY on alloc failure. + * On error, *ctx_out is set to NULL. + */ +int zuptsdk_ctx_create(zuptsdk_ctx_t **ctx_out); + +/** + * Destroy a context. Frees all owned resources including thread pool. + * Safe to call with NULL. After this call, the pointer is invalid. + */ +void zuptsdk_ctx_destroy(zuptsdk_ctx_t *ctx); + +/** + * Set worker thread count. 0 == auto (one per CPU). Default is auto. + * Returns ZUPTSDK_ERR_INVALID_ARG if ctx is NULL or threads > 256. + */ +int zuptsdk_ctx_set_threads(zuptsdk_ctx_t *ctx, int threads); + +/** + * Set progress callback for long-running operations on this context. + * Pass NULL fn to clear. userdata is opaque to the library. + */ +int zuptsdk_ctx_set_progress_callback(zuptsdk_ctx_t *ctx, + zuptsdk_progress_fn fn, + void *userdata); + +/** + * Set log callback for diagnostic messages on this context. + * Pass NULL fn to disable logging (default). + */ +int zuptsdk_ctx_set_log_callback(zuptsdk_ctx_t *ctx, + zuptsdk_log_fn fn, + zuptsdk_log_level_t min_level, + void *userdata); + +/* ════════════════════════════════════════════════════════════════════════ + * OPTIONS + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Create a default-initialized options bag for compress/encrypt operations. + * Defaults: codec=AUTO, level=7, no encryption, no dedup, no solid mode. + */ +int zuptsdk_options_create(zuptsdk_options_t **opts_out); +void zuptsdk_options_destroy(zuptsdk_options_t *opts); + +int zuptsdk_options_set_codec(zuptsdk_options_t *opts, zuptsdk_codec_t codec); +int zuptsdk_options_set_level(zuptsdk_options_t *opts, int level /* 1..9 */); +int zuptsdk_options_set_dedup(zuptsdk_options_t *opts, int enabled); +int zuptsdk_options_set_solid(zuptsdk_options_t *opts, int enabled); +int zuptsdk_options_set_block_size(zuptsdk_options_t *opts, size_t bytes); + +/** + * Maximum decompressed output size. Decompression aborts with + * ZUPTSDK_ERR_TOO_LARGE if exceeded. 0 == unlimited (NOT recommended + * for untrusted input — zip-bomb attack vector). Default: 16 GiB. + */ +int zuptsdk_options_set_max_decompressed(zuptsdk_options_t *opts, + uint64_t max_bytes); + +/* ════════════════════════════════════════════════════════════════════════ + * SECURE BUFFERS (for passwords and key material) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Allocate a secure buffer: backing memory is mlock()ed (locked into RAM, + * never swapped to disk) and explicit_bzero()ed on destroy. + * + * @param size requested size in bytes (1..65536) + * @param buf_out [out,transfers] receives buffer handle + * @return ZUPTSDK_OK on success. + */ +int zuptsdk_secure_buf_create(size_t size, zuptsdk_secure_buf_t **buf_out); + +/** + * Destroy a secure buffer. Memory is zeroed and unlocked before free. + * Safe to call with NULL. + */ +void zuptsdk_secure_buf_destroy(zuptsdk_secure_buf_t *buf); + +/** + * Get raw pointer to the secure buffer's storage. Pointer is valid until + * zuptsdk_secure_buf_destroy() is called. Caller may read or write up to + * the buffer's size. + * + * @param buf [in] + * @param data_out [out,borrowed] receives pointer to storage + * @param size_out [out] receives buffer size + */ +int zuptsdk_secure_buf_get(zuptsdk_secure_buf_t *buf, + uint8_t **data_out, size_t *size_out); + +/** + * Convenience: copy data into a new secure buffer. + * Useful when migrating an existing plain buffer to secure storage. + */ +int zuptsdk_secure_buf_from_data(const uint8_t *data, size_t size, + zuptsdk_secure_buf_t **buf_out); + +/* ════════════════════════════════════════════════════════════════════════ + * KEYS (PQ hybrid: ML-KEM-768 + X25519) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Generate a fresh hybrid keypair. Uses the system CSPRNG. + * + * @param ctx [in] + * @param kp_out [out,transfers] receives new keypair + * @return ZUPTSDK_OK on success, ZUPTSDK_ERR_CRYPTO_FAIL on RNG failure. + */ +int zuptsdk_keypair_generate(zuptsdk_ctx_t *ctx, zuptsdk_keypair_t **kp_out); + +void zuptsdk_keypair_destroy(zuptsdk_keypair_t *kp); + +/** + * Save private key to a file. The file is written with mode 0600 on POSIX. + * Recommended extension: ".key". + */ +int zuptsdk_keypair_save_private(const zuptsdk_keypair_t *kp, const char *path); + +/** + * Save public key to a file. World-readable. + * Recommended extension: ".pub" or "_public.key". + */ +int zuptsdk_keypair_save_public(const zuptsdk_keypair_t *kp, const char *path); + +/** + * Load a private key from a file. + * @param path [in] + * @param key_out [out,transfers] + */ +int zuptsdk_privkey_load(const char *path, zuptsdk_privkey_t **key_out); +void zuptsdk_privkey_destroy(zuptsdk_privkey_t *key); + +/** + * Load a public key from a file. + */ +int zuptsdk_pubkey_load(const char *path, zuptsdk_pubkey_t **key_out); +void zuptsdk_pubkey_destroy(zuptsdk_pubkey_t *key); + +/** + * Derive public key from private key (no I/O). + */ +int zuptsdk_privkey_get_public(const zuptsdk_privkey_t *priv, + zuptsdk_pubkey_t **pub_out); + +/* ════════════════════════════════════════════════════════════════════════ + * COMPRESS / DECOMPRESS — buffer mode (for small archives) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Compress an in-memory file list into a single archive buffer. + * + * @param ctx [in] + * @param opts [in,borrowed] compression and encryption options + * @param file_paths [in] array of filesystem paths to add + * @param file_count number of paths in file_paths + * @param password [in,nullable] password as a secure buffer; NULL for no pw + * @param recipient_pk [in,nullable] PQ public key for encryption; NULL for no PQ + * @param archive_out [out,transfers] receives malloc'd archive bytes; + * caller must free with zuptsdk_free() + * @param archive_sz [out] size of returned archive + * @return ZUPTSDK_OK on success. + */ +int zuptsdk_compress_files(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *const *file_paths, + size_t file_count, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, + size_t *archive_sz); + +/** + * Compress a single in-memory data buffer. Useful for SDK consumers that + * have data in memory and want a self-contained archive. + * + * @param logical_name [in] name to record inside the archive (e.g. "data.bin") + */ +int zuptsdk_compress_buffer(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *logical_name, + const uint8_t *data, size_t data_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, + size_t *archive_sz); + +/** + * Extract an archive into a directory. + * + * @param dest_dir [in] target directory; created if missing + * @param password [in,nullable] + * @param recipient_sk [in,nullable] PQ private key + */ +int zuptsdk_extract_to_dir(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + const char *dest_dir, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/** + * Extract a single-file archive (one created with zuptsdk_compress_buffer) + * back into a memory buffer. + * + * @param data_out [out,transfers] caller frees with zuptsdk_free() + * @param data_sz [out] + */ +int zuptsdk_extract_buffer(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk, + uint8_t **data_out, size_t *data_sz); + +/* ════════════════════════════════════════════════════════════════════════ + * COMPRESS / DECOMPRESS — streaming mode (for large archives) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Compress from a read callback to a write callback. Streaming version + * with no archive size limit — suitable for piping to network sockets, + * encrypted volumes, or any backend with a write_fn. + * + * @param input [in] read callback supplying source bytes + * @param input_ud [in] userdata passed to read callback + * @param input_name [in] logical filename to record in archive + * @param input_total total bytes to read; 0 if unknown + * @param output [in] write callback receiving archive bytes + * @param output_ud [in] userdata passed to write callback + */ +int zuptsdk_compress_stream(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + zuptsdk_read_fn input, void *input_ud, + const char *input_name, uint64_t input_total, + zuptsdk_write_fn output, void *output_ud, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk); + +/** + * Decompress an archive read from a callback, writing extracted single-file + * content to a write callback. + */ +int zuptsdk_decompress_stream(zuptsdk_ctx_t *ctx, + zuptsdk_read_fn input, void *input_ud, + zuptsdk_write_fn output, void *output_ud, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/* ════════════════════════════════════════════════════════════════════════ + * VERIFY / INFO + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Verify all block checksums and (if encrypted) HMAC of an archive. + * No data is written to disk. Returns ZUPTSDK_OK if every block validates. + */ +int zuptsdk_verify(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/** + * Read archive metadata without password or key. Returns header info only; + * does not decrypt block contents. + * + * @param info_out [out,transfers] receives info object; + * caller must zuptsdk_archive_info_destroy() + */ +int zuptsdk_archive_info_read(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_archive_info_t **info_out); + +void zuptsdk_archive_info_destroy(zuptsdk_archive_info_t *info); + +/* Getters — opaque struct, all fields accessed via these functions. */ +int zuptsdk_archive_info_format_major(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_format_minor(const zuptsdk_archive_info_t *info); +const char *zuptsdk_archive_info_uuid(const zuptsdk_archive_info_t *info); +int64_t zuptsdk_archive_info_created_unix(const zuptsdk_archive_info_t *info); +uint64_t zuptsdk_archive_info_size(const zuptsdk_archive_info_t *info); +uint32_t zuptsdk_archive_info_block_count(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_encrypted(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_pq_hybrid(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_solid(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_dedup(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_disk_image(const zuptsdk_archive_info_t *info); + +/* ════════════════════════════════════════════════════════════════════════ + * DISK BACKUP / RESTORE + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Backup a block device or disk image file to an archive. + * REQUIRES root/admin privileges to read raw block devices on most OSes. + */ +int zuptsdk_disk_backup(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *source_device_or_image, + const char *output_archive_path, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk); + +/** + * Restore a disk backup archive to a block device or image file. + * DESTRUCTIVE: target is overwritten without confirmation. + */ +int zuptsdk_disk_restore(zuptsdk_ctx_t *ctx, + const char *archive_path, + const char *target_device_or_image, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/* ════════════════════════════════════════════════════════════════════════ + * MISC + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Free memory returned by the library via [transfers] output pointers. + * Safe to call with NULL. + * + * Always use this — never free() — for SDK-allocated memory, since the + * library may have been built with a custom allocator. + */ +void zuptsdk_free(void *ptr); + +/** + * Best-effort secure zero of a buffer. Resistant to dead-store elimination + * by the optimizer. Use for caller-managed sensitive memory. + */ +void zuptsdk_secure_zero(void *buf, size_t bytes); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* ZUPTSDK_H */ diff --git a/sdk/src/zuptsdk.c b/sdk/src/zuptsdk.c new file mode 100644 index 0000000..d886aa0 --- /dev/null +++ b/sdk/src/zuptsdk.c @@ -0,0 +1,1149 @@ +/* + * libzuptsdk implementation — wraps zupt's internal API + * + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#define _DEFAULT_SOURCE 1 + +#include "zuptsdk.h" +#include "zupt.h" +#include "zupt_keccak.h" +#include "zupt_mlkem.h" +#include "zupt_x25519.h" + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #define ZSDK_THREAD_LOCAL __declspec(thread) +#else + #include + #include + #include + #include + #define ZSDK_THREAD_LOCAL __thread +#endif + +/* ════════════════════════════════════════════════════════════════════════ + * Internal: allocator routing + * ════════════════════════════════════════════════════════════════════════ */ + +static zuptsdk_allocator_t g_alloc = { NULL, NULL, NULL, NULL }; + +static void *zsdk_malloc(size_t n) { + if (g_alloc.malloc_fn) return g_alloc.malloc_fn(g_alloc.userdata, n); + return malloc(n); +} + +static void zsdk_free_internal(void *p) { + if (!p) return; + if (g_alloc.free_fn) { g_alloc.free_fn(g_alloc.userdata, p); return; } + free(p); +} + +static void *zsdk_calloc(size_t count, size_t size) { + if (count && size > SIZE_MAX / count) { errno = ENOMEM; return NULL; } + size_t total = count * size; + void *p = zsdk_malloc(total); + if (p) memset(p, 0, total); + return p; +} + +static char *zsdk_strdup(const char *s) { + if (!s) return NULL; + size_t n = strlen(s) + 1; + char *p = (char *)zsdk_malloc(n); + if (p) memcpy(p, s, n); + return p; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Internal: thread-local error detail + * ════════════════════════════════════════════════════════════════════════ */ + +static ZSDK_THREAD_LOCAL char g_err_detail[512] = {0}; + +static int zsdk_set_error(int code, const char *file, int line, const char *fmt, ...) { + char msg[400] = {0}; + if (fmt) { + va_list ap; + va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + } + if (file) { + const char *base = strrchr(file, '/'); + base = base ? base + 1 : file; + snprintf(g_err_detail, sizeof(g_err_detail), + "[%s] %s:%d: %s%s%s", + zuptsdk_strerror(code), base, line, msg, + errno ? " (errno: " : "", + errno ? strerror(errno) : ""); + if (errno) strncat(g_err_detail, ")", + sizeof(g_err_detail) - strlen(g_err_detail) - 1); + } else { + snprintf(g_err_detail, sizeof(g_err_detail), + "[%s] %s", zuptsdk_strerror(code), msg); + } + return code; +} + +#define ZSDK_FAIL(code, ...) zsdk_set_error((code), __FILE__, __LINE__, __VA_ARGS__) +#define ZSDK_OK(...) (g_err_detail[0] = 0, ZUPTSDK_OK) + +/* ════════════════════════════════════════════════════════════════════════ + * Internal: error mapping zupt_error_t -> zuptsdk_error_t + * ════════════════════════════════════════════════════════════════════════ */ + +static int zsdk_map_zupt_error(zupt_error_t e) { + switch (e) { + case ZUPT_OK: return ZUPTSDK_OK; + case ZUPT_ERR_IO: return ZUPTSDK_ERR_IO; + case ZUPT_ERR_CORRUPT: return ZUPTSDK_ERR_BAD_ARCHIVE; + case ZUPT_ERR_BAD_MAGIC: return ZUPTSDK_ERR_BAD_ARCHIVE; + case ZUPT_ERR_BAD_VERSION: return ZUPTSDK_ERR_BAD_VERSION; + case ZUPT_ERR_BAD_CHECKSUM: return ZUPTSDK_ERR_BAD_CHECKSUM; + case ZUPT_ERR_NOMEM: return ZUPTSDK_ERR_NO_MEMORY; + case ZUPT_ERR_OVERFLOW: return ZUPTSDK_ERR_TOO_LARGE; + case ZUPT_ERR_INVALID: return ZUPTSDK_ERR_INVALID_ARG; + case ZUPT_ERR_NOT_FOUND: return ZUPTSDK_ERR_IO; + case ZUPT_ERR_UNSUPPORTED: return ZUPTSDK_ERR_UNSUPPORTED; + case ZUPT_ERR_AUTH_FAIL: return ZUPTSDK_ERR_BAD_PASSWORD; + default: return ZUPTSDK_ERR_INTERNAL; + } +} + +/* ════════════════════════════════════════════════════════════════════════ + * Public: version + * ════════════════════════════════════════════════════════════════════════ */ + +const char *zuptsdk_version_string(void) { + return ZUPTSDK_VERSION_STRING; +} + +int zuptsdk_version_check(int major, int minor, int patch) { + if (major > ZUPTSDK_VERSION_MAJOR) return ZSDK_FAIL(ZUPTSDK_ERR_VERSION_MISMATCH, + "library is %d.%d.%d, caller required %d.%d.%d", + ZUPTSDK_VERSION_MAJOR, ZUPTSDK_VERSION_MINOR, ZUPTSDK_VERSION_PATCH, + major, minor, patch); + if (major == ZUPTSDK_VERSION_MAJOR && minor > ZUPTSDK_VERSION_MINOR) + return ZSDK_FAIL(ZUPTSDK_ERR_VERSION_MISMATCH, "minor version too low"); + if (major == ZUPTSDK_VERSION_MAJOR && minor == ZUPTSDK_VERSION_MINOR && + patch > ZUPTSDK_VERSION_PATCH) + return ZSDK_FAIL(ZUPTSDK_ERR_VERSION_MISMATCH, "patch version too low"); + return ZUPTSDK_OK; +} + +/* Helper to copy a path into options' fixed keyfile buffer */ +static int zsdk_apply_pubkey(zupt_options_t *zopts, const zuptsdk_pubkey_t *pk); +static int zsdk_apply_privkey(zupt_options_t *zopts, const zuptsdk_privkey_t *sk); + +/* ════════════════════════════════════════════════════════════════════════ + * Forward struct definitions (must come before helper functions) + * ════════════════════════════════════════════════════════════════════════ */ + +struct zuptsdk_pubkey { char *path; }; +struct zuptsdk_privkey { char *path; }; + +struct zuptsdk_keypair { + char *priv_path; /* persistent path; library generated key files */ + char *pub_path; +}; + +/* ════════════════════════════════════════════════════════════════════════ + * Public: errors + * ════════════════════════════════════════════════════════════════════════ */ + +const char *zuptsdk_strerror(int err) { + switch (err) { + case ZUPTSDK_OK: return "Success"; + case ZUPTSDK_ERR_INVALID_ARG: return "Invalid argument"; + case ZUPTSDK_ERR_NO_MEMORY: return "Out of memory"; + case ZUPTSDK_ERR_IO: return "I/O error"; + case ZUPTSDK_ERR_BAD_ARCHIVE: return "Malformed archive"; + case ZUPTSDK_ERR_BAD_PASSWORD: return "Wrong password"; + case ZUPTSDK_ERR_BAD_KEY: return "Invalid key file"; + case ZUPTSDK_ERR_BAD_MAC: return "MAC verification failed (corrupted or tampered)"; + case ZUPTSDK_ERR_BAD_VERSION: return "Unsupported archive format version"; + case ZUPTSDK_ERR_BAD_CHECKSUM: return "Block checksum mismatch"; + case ZUPTSDK_ERR_BUFFER_TOO_SMALL: return "Output buffer too small"; + case ZUPTSDK_ERR_NOT_ENCRYPTED: return "Archive is not encrypted"; + case ZUPTSDK_ERR_PASSWORD_REQUIRED: return "Password required"; + case ZUPTSDK_ERR_PQ_KEY_REQUIRED: return "PQ private key required"; + case ZUPTSDK_ERR_UNSUPPORTED: return "Feature not supported on this platform"; + case ZUPTSDK_ERR_VERSION_MISMATCH: return "Library version too old"; + case ZUPTSDK_ERR_PATH_TRAVERSAL: return "Path traversal attempt blocked"; + case ZUPTSDK_ERR_TOO_LARGE: return "Decompressed size exceeds limit"; + case ZUPTSDK_ERR_CRYPTO_FAIL: return "Cryptographic operation failed"; + case ZUPTSDK_ERR_CANCELLED: return "Operation cancelled by callback"; + case ZUPTSDK_ERR_INTERNAL: return "Internal library error (please report)"; + default: return "Unknown error"; + } +} + +const char *zuptsdk_last_error_detail(void) { + return g_err_detail; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Public: allocator + * ════════════════════════════════════════════════════════════════════════ */ + +int zuptsdk_set_allocator(const zuptsdk_allocator_t *alloc) { + if (!alloc) { + memset(&g_alloc, 0, sizeof(g_alloc)); + return ZUPTSDK_OK; + } + if (!alloc->malloc_fn || !alloc->free_fn || !alloc->realloc_fn) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "all allocator hooks required"); + g_alloc = *alloc; + return ZUPTSDK_OK; +} + +void zuptsdk_free(void *ptr) { zsdk_free_internal(ptr); } + +void zuptsdk_secure_zero(void *buf, size_t bytes) { + if (!buf || !bytes) return; +#if defined(__STDC_LIB_EXT1__) || defined(_WIN32) + #ifdef _WIN32 + SecureZeroMemory(buf, bytes); + #else + memset_s(buf, bytes, 0, bytes); + #endif +#elif defined(__GLIBC__) && __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 25 + explicit_bzero(buf, bytes); +#else + /* Fallback: volatile pointer prevents dead-store elimination */ + volatile uint8_t *p = (volatile uint8_t *)buf; + while (bytes--) *p++ = 0; +#endif +} + +/* ════════════════════════════════════════════════════════════════════════ + * Internal context structure + * ════════════════════════════════════════════════════════════════════════ */ + +struct zuptsdk_ctx { + int threads; /* 0 = auto */ + zuptsdk_progress_fn progress_fn; + void *progress_ud; + zuptsdk_log_fn log_fn; + zuptsdk_log_level_t log_level; + void *log_ud; +}; + +int zuptsdk_ctx_create(zuptsdk_ctx_t **ctx_out) { + if (!ctx_out) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "ctx_out is NULL"); + zuptsdk_ctx_t *c = (zuptsdk_ctx_t *)zsdk_calloc(1, sizeof(*c)); + if (!c) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "ctx_create"); + c->threads = 0; /* auto */ + c->log_level = ZUPTSDK_LOG_ERROR; + *ctx_out = c; + return ZUPTSDK_OK; +} + +void zuptsdk_ctx_destroy(zuptsdk_ctx_t *ctx) { + if (!ctx) return; + zuptsdk_secure_zero(ctx, sizeof(*ctx)); + zsdk_free_internal(ctx); +} + +int zuptsdk_ctx_set_threads(zuptsdk_ctx_t *ctx, int threads) { + if (!ctx) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "ctx is NULL"); + if (threads < 0 || threads > 256) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "threads must be 0..256"); + ctx->threads = threads; + return ZUPTSDK_OK; +} + +int zuptsdk_ctx_set_progress_callback(zuptsdk_ctx_t *ctx, + zuptsdk_progress_fn fn, void *userdata) { + if (!ctx) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "ctx is NULL"); + ctx->progress_fn = fn; + ctx->progress_ud = userdata; + return ZUPTSDK_OK; +} + +int zuptsdk_ctx_set_log_callback(zuptsdk_ctx_t *ctx, zuptsdk_log_fn fn, + zuptsdk_log_level_t min_level, void *userdata) { + if (!ctx) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "ctx is NULL"); + ctx->log_fn = fn; + ctx->log_level = min_level; + ctx->log_ud = userdata; + return ZUPTSDK_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Internal options structure + * ════════════════════════════════════════════════════════════════════════ */ + +struct zuptsdk_options { + zuptsdk_codec_t codec; + int level; /* 1..9 */ + int dedup; + int solid; + size_t block_size; /* 0 = default */ + uint64_t max_decompressed; +}; + +int zuptsdk_options_create(zuptsdk_options_t **opts_out) { + if (!opts_out) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts_out is NULL"); + zuptsdk_options_t *o = (zuptsdk_options_t *)zsdk_calloc(1, sizeof(*o)); + if (!o) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "options_create"); + o->codec = ZUPTSDK_CODEC_AUTO; + o->level = 7; + o->max_decompressed = 16ull * 1024 * 1024 * 1024; /* 16 GiB default */ + *opts_out = o; + return ZUPTSDK_OK; +} + +void zuptsdk_options_destroy(zuptsdk_options_t *opts) { + if (!opts) return; + zsdk_free_internal(opts); +} + +int zuptsdk_options_set_codec(zuptsdk_options_t *opts, zuptsdk_codec_t codec) { + if (!opts) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts is NULL"); + if (codec < ZUPTSDK_CODEC_AUTO || codec > ZUPTSDK_CODEC_STORE) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "bad codec value %d", (int)codec); + opts->codec = codec; + return ZUPTSDK_OK; +} + +int zuptsdk_options_set_level(zuptsdk_options_t *opts, int level) { + if (!opts) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts is NULL"); + if (level < 1 || level > 9) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "level must be 1..9"); + opts->level = level; + return ZUPTSDK_OK; +} + +int zuptsdk_options_set_dedup(zuptsdk_options_t *opts, int enabled) { + if (!opts) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts is NULL"); + opts->dedup = enabled ? 1 : 0; + return ZUPTSDK_OK; +} + +int zuptsdk_options_set_solid(zuptsdk_options_t *opts, int enabled) { + if (!opts) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts is NULL"); + opts->solid = enabled ? 1 : 0; + return ZUPTSDK_OK; +} + +int zuptsdk_options_set_block_size(zuptsdk_options_t *opts, size_t bytes) { + if (!opts) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts is NULL"); + if (bytes && (bytes < ZUPT_MIN_BLOCK_SZ || bytes > ZUPT_MAX_BLOCK_SZ)) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, + "block_size must be 0 or %u..%u", + (unsigned)ZUPT_MIN_BLOCK_SZ, (unsigned)ZUPT_MAX_BLOCK_SZ); + opts->block_size = bytes; + return ZUPTSDK_OK; +} + +int zuptsdk_options_set_max_decompressed(zuptsdk_options_t *opts, uint64_t max_bytes) { + if (!opts) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "opts is NULL"); + opts->max_decompressed = max_bytes; + return ZUPTSDK_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Secure buffers (mlock + zero on destroy) + * ════════════════════════════════════════════════════════════════════════ */ + +struct zuptsdk_secure_buf { + uint8_t *data; + size_t size; + int locked; /* 1 if mlock() succeeded */ +}; + +int zuptsdk_secure_buf_create(size_t size, zuptsdk_secure_buf_t **buf_out) { + if (!buf_out) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "buf_out is NULL"); + if (size < 1 || size > 65536) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "size must be 1..65536"); + + zuptsdk_secure_buf_t *b = (zuptsdk_secure_buf_t *)zsdk_calloc(1, sizeof(*b)); + if (!b) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "secure_buf alloc"); + + b->data = (uint8_t *)zsdk_calloc(1, size); + if (!b->data) { + zsdk_free_internal(b); + return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "secure_buf data alloc"); + } + b->size = size; + b->locked = (zupt_mlock_keys(b->data, size) == 0) ? 1 : 0; + /* mlock failure is non-fatal — we still wipe on destroy */ + *buf_out = b; + return ZUPTSDK_OK; +} + +void zuptsdk_secure_buf_destroy(zuptsdk_secure_buf_t *buf) { + if (!buf) return; + if (buf->data) { + zuptsdk_secure_zero(buf->data, buf->size); + if (buf->locked) zupt_munlock_keys(buf->data, buf->size); + zsdk_free_internal(buf->data); + } + zuptsdk_secure_zero(buf, sizeof(*buf)); + zsdk_free_internal(buf); +} + +int zuptsdk_secure_buf_get(zuptsdk_secure_buf_t *buf, + uint8_t **data_out, size_t *size_out) { + if (!buf || !data_out || !size_out) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + *data_out = buf->data; + *size_out = buf->size; + return ZUPTSDK_OK; +} + +int zuptsdk_secure_buf_from_data(const uint8_t *data, size_t size, + zuptsdk_secure_buf_t **buf_out) { + if (!data || !buf_out) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + int rc = zuptsdk_secure_buf_create(size, buf_out); + if (rc != ZUPTSDK_OK) return rc; + memcpy((*buf_out)->data, data, size); + return ZUPTSDK_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Internal: convert SDK options -> zupt_options_t + * ════════════════════════════════════════════════════════════════════════ */ + +static uint16_t zsdk_codec_map(zuptsdk_codec_t c) { + switch (c) { + case ZUPTSDK_CODEC_AUTO: return ZUPT_CODEC_AUTO; + case ZUPTSDK_CODEC_VAPTVUPT: return ZUPT_CODEC_VAPTVUPT; + case ZUPTSDK_CODEC_LZHP: return ZUPT_CODEC_ZUPT_LZHP; + case ZUPTSDK_CODEC_LZH: return ZUPT_CODEC_ZUPT_LZH; + case ZUPTSDK_CODEC_LZ: return ZUPT_CODEC_ZUPT_LZ; + case ZUPTSDK_CODEC_STORE: return ZUPT_CODEC_STORE; + default: return ZUPT_CODEC_AUTO; + } +} + +static void zsdk_apply_options(zupt_options_t *zopts, + const zuptsdk_options_t *sdk_opts, + const zuptsdk_ctx_t *ctx) { + zupt_default_options(zopts); + if (sdk_opts) { + zopts->codec_id = zsdk_codec_map(sdk_opts->codec); + zopts->level = sdk_opts->level; + zopts->dedup = sdk_opts->dedup; + zopts->solid = sdk_opts->solid; + if (sdk_opts->block_size) + zopts->block_size = (uint32_t)sdk_opts->block_size; + } + if (ctx && ctx->threads > 0) zopts->threads = ctx->threads; + + /* Resolve AUTO codec at call site (mirrors main.c behavior) */ + if (zopts->codec_id == ZUPT_CODEC_AUTO) + zopts->codec_id = zupt_resolve_auto_codec(); +} + +/* Copy a password from secure buffer into the options' fixed-size field. + * Returns 0 on success, ZUPTSDK_ERR_* on failure. */ +static int zsdk_apply_password(zupt_options_t *zopts, zuptsdk_secure_buf_t *pw) { + if (!pw || pw->size == 0) return ZUPTSDK_OK; + if (pw->size >= sizeof(zopts->password)) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, + "password too long (max %zu bytes)", + sizeof(zopts->password) - 1); + /* Verify no embedded NUL (would silently truncate) */ + if (memchr(pw->data, 0, pw->size)) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "password contains NUL byte"); + + memcpy(zopts->password, pw->data, pw->size); + zopts->password[pw->size] = 0; + zopts->encrypt = 1; + return ZUPTSDK_OK; +} + +static int zsdk_apply_pubkey(zupt_options_t *zopts, const zuptsdk_pubkey_t *pk) { + if (!pk) return ZUPTSDK_OK; + if (!pk->path || strlen(pk->path) >= sizeof(zopts->keyfile)) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "keyfile path too long"); + strncpy(zopts->keyfile, pk->path, sizeof(zopts->keyfile) - 1); + zopts->keyfile[sizeof(zopts->keyfile) - 1] = 0; + zopts->pq_mode = 1; + zopts->encrypt = 1; + return ZUPTSDK_OK; +} + +static int zsdk_apply_privkey(zupt_options_t *zopts, const zuptsdk_privkey_t *sk) { + if (!sk) return ZUPTSDK_OK; + if (!sk->path || strlen(sk->path) >= sizeof(zopts->keyfile)) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "keyfile path too long"); + strncpy(zopts->keyfile, sk->path, sizeof(zopts->keyfile) - 1); + zopts->keyfile[sizeof(zopts->keyfile) - 1] = 0; + zopts->pq_mode = 1; + return ZUPTSDK_OK; +} + +/* Securely wipe password from options after use */ +static void zsdk_wipe_options_secrets(zupt_options_t *zopts) { + zuptsdk_secure_zero(zopts->password, sizeof(zopts->password)); + /* Don't wipe keyfile path — not secret */ +} + +/* ════════════════════════════════════════════════════════════════════════ + * Keys + * ════════════════════════════════════════════════════════════════════════ */ + +static int zsdk_tmp_path(char *out, size_t cap, const char *prefix) { +#ifdef _WIN32 + char tmpdir[MAX_PATH]; + GetTempPathA(MAX_PATH, tmpdir); + snprintf(out, cap, "%s%s_%lx_%lx", tmpdir, prefix, + (unsigned long)GetCurrentProcessId(), (unsigned long)GetTickCount()); +#else + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir) tmpdir = "/tmp"; + uint8_t r[8]; + zupt_random_bytes(r, 8); + snprintf(out, cap, "%s/%s_%d_%02x%02x%02x%02x%02x%02x%02x%02x", + tmpdir, prefix, (int)getpid(), + r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7]); +#endif + return 0; +} + +int zuptsdk_keypair_generate(zuptsdk_ctx_t *ctx, zuptsdk_keypair_t **kp_out) { + (void)ctx; + if (!kp_out) return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "kp_out is NULL"); + + zuptsdk_keypair_t *kp = (zuptsdk_keypair_t *)zsdk_calloc(1, sizeof(*kp)); + if (!kp) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "keypair alloc"); + + char priv_buf[512], pub_buf[512]; + zsdk_tmp_path(priv_buf, sizeof(priv_buf), "zsdk_priv"); + zsdk_tmp_path(pub_buf, sizeof(pub_buf), "zsdk_pub"); + + if (zupt_hybrid_keygen(priv_buf) != 0) { + zsdk_free_internal(kp); + return ZSDK_FAIL(ZUPTSDK_ERR_CRYPTO_FAIL, "hybrid_keygen failed"); + } + if (zupt_hybrid_export_pubkey(priv_buf, pub_buf) != 0) { + unlink(priv_buf); + zsdk_free_internal(kp); + return ZSDK_FAIL(ZUPTSDK_ERR_CRYPTO_FAIL, "export_pubkey failed"); + } + kp->priv_path = zsdk_strdup(priv_buf); + kp->pub_path = zsdk_strdup(pub_buf); + if (!kp->priv_path || !kp->pub_path) { + zuptsdk_keypair_destroy(kp); + return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "path strdup"); + } + *kp_out = kp; + return ZUPTSDK_OK; +} + +void zuptsdk_keypair_destroy(zuptsdk_keypair_t *kp) { + if (!kp) return; + if (kp->priv_path) { unlink(kp->priv_path); zsdk_free_internal(kp->priv_path); } + if (kp->pub_path) { unlink(kp->pub_path); zsdk_free_internal(kp->pub_path); } + zsdk_free_internal(kp); +} + +static int zsdk_copy_file(const char *src, const char *dst, mode_t mode) { + FILE *fi = fopen(src, "rb"); + if (!fi) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open %s", src); + FILE *fo = fopen(dst, "wb"); + if (!fo) { fclose(fi); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst); } + uint8_t buf[4096]; + size_t n; + int rc = ZUPTSDK_OK; + while ((n = fread(buf, 1, sizeof(buf), fi)) > 0) + if (fwrite(buf, 1, n, fo) != n) { rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", dst); break; } + zuptsdk_secure_zero(buf, sizeof(buf)); + fclose(fi); fclose(fo); +#ifndef _WIN32 + if (rc == ZUPTSDK_OK) chmod(dst, mode); +#else + (void)mode; +#endif + return rc; +} + +int zuptsdk_keypair_save_private(const zuptsdk_keypair_t *kp, const char *path) { + if (!kp || !kp->priv_path || !path) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + return zsdk_copy_file(kp->priv_path, path, 0600); +} + +int zuptsdk_keypair_save_public(const zuptsdk_keypair_t *kp, const char *path) { + if (!kp || !kp->pub_path || !path) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + return zsdk_copy_file(kp->pub_path, path, 0644); +} + +int zuptsdk_privkey_load(const char *path, zuptsdk_privkey_t **key_out) { + if (!path || !key_out) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + FILE *f = fopen(path, "rb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open private key %s", path); + fclose(f); + zuptsdk_privkey_t *k = (zuptsdk_privkey_t *)zsdk_calloc(1, sizeof(*k)); + if (!k) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "privkey alloc"); + k->path = zsdk_strdup(path); + if (!k->path) { zsdk_free_internal(k); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "path"); } + *key_out = k; + return ZUPTSDK_OK; +} + +void zuptsdk_privkey_destroy(zuptsdk_privkey_t *key) { + if (!key) return; + if (key->path) zsdk_free_internal(key->path); + zsdk_free_internal(key); +} + +int zuptsdk_pubkey_load(const char *path, zuptsdk_pubkey_t **key_out) { + if (!path || !key_out) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + FILE *f = fopen(path, "rb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open public key %s", path); + fclose(f); + zuptsdk_pubkey_t *k = (zuptsdk_pubkey_t *)zsdk_calloc(1, sizeof(*k)); + if (!k) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "pubkey alloc"); + k->path = zsdk_strdup(path); + if (!k->path) { zsdk_free_internal(k); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "path"); } + *key_out = k; + return ZUPTSDK_OK; +} + +void zuptsdk_pubkey_destroy(zuptsdk_pubkey_t *key) { + if (!key) return; + if (key->path) zsdk_free_internal(key->path); + zsdk_free_internal(key); +} + +int zuptsdk_privkey_get_public(const zuptsdk_privkey_t *priv, + zuptsdk_pubkey_t **pub_out) { + if (!priv || !priv->path || !pub_out) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + char tmp[512]; + zsdk_tmp_path(tmp, sizeof(tmp), "zsdk_pub"); + if (zupt_hybrid_export_pubkey(priv->path, tmp) != 0) + return ZSDK_FAIL(ZUPTSDK_ERR_CRYPTO_FAIL, "export_pubkey failed"); + zuptsdk_pubkey_t *k = (zuptsdk_pubkey_t *)zsdk_calloc(1, sizeof(*k)); + if (!k) { unlink(tmp); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "pubkey alloc"); } + k->path = zsdk_strdup(tmp); + if (!k->path) { unlink(tmp); zsdk_free_internal(k); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "path"); } + *pub_out = k; + return ZUPTSDK_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Compress / extract + * ════════════════════════════════════════════════════════════════════════ */ + +int zuptsdk_compress_files(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *const *file_paths, + size_t file_count, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, + size_t *archive_sz) { + if (!file_paths || !file_count || !archive_out || !archive_sz) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL/empty parameter"); + + zupt_options_t zopts; + zsdk_apply_options(&zopts, opts, ctx); + + int rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); return rc; } + rc = zsdk_apply_pubkey(&zopts, recipient_pk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); return rc; } + + char tmp[512]; + zsdk_tmp_path(tmp, sizeof(tmp), "zsdk_arc"); + + /* Build arc/disk path arrays */ + const char **arc_paths = (const char **)zsdk_calloc(file_count, sizeof(char *)); + const char **disk_paths = (const char **)zsdk_calloc(file_count, sizeof(char *)); + if (!arc_paths || !disk_paths) { + zsdk_free_internal(arc_paths); zsdk_free_internal(disk_paths); + zsdk_wipe_options_secrets(&zopts); + return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "path arrays"); + } + for (size_t i = 0; i < file_count; i++) { + disk_paths[i] = file_paths[i]; + const char *base = strrchr(file_paths[i], '/'); + arc_paths[i] = base ? base + 1 : file_paths[i]; + } + + zupt_error_t err = (opts && opts->solid) + ? zupt_compress_solid(tmp, arc_paths, disk_paths, (int)file_count, &zopts) + : zupt_compress_files(tmp, arc_paths, disk_paths, (int)file_count, &zopts); + + zsdk_free_internal(arc_paths); + zsdk_free_internal(disk_paths); + zsdk_wipe_options_secrets(&zopts); + + if (err != ZUPT_OK) { + unlink(tmp); + return ZSDK_FAIL(zsdk_map_zupt_error(err), "compress failed (zupt_err=%d)", err); + } + + /* Slurp tmp file into output buffer */ + FILE *f = fopen(tmp, "rb"); + if (!f) { unlink(tmp); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "reopen archive"); } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz <= 0) { fclose(f); unlink(tmp); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "empty archive"); } + + uint8_t *buf = (uint8_t *)zsdk_malloc((size_t)sz); + if (!buf) { fclose(f); unlink(tmp); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "archive buf"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + unlink(tmp); + if (got != (size_t)sz) { zsdk_free_internal(buf); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "short read"); } + + *archive_out = buf; + *archive_sz = (size_t)sz; + return ZUPTSDK_OK; +} + +int zuptsdk_compress_buffer(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *logical_name, + const uint8_t *data, size_t data_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, + size_t *archive_sz) { + if (!logical_name || !data || !archive_out || !archive_sz) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + + char tmp_in[512]; + zsdk_tmp_path(tmp_in, sizeof(tmp_in), "zsdk_in"); + FILE *f = fopen(tmp_in, "wb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create temp input"); + if (fwrite(data, 1, data_sz, f) != data_sz) { + fclose(f); unlink(tmp_in); + return ZSDK_FAIL(ZUPTSDK_ERR_IO, "write temp input"); + } + fclose(f); + + zupt_options_t zopts; + zsdk_apply_options(&zopts, opts, ctx); + int rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(tmp_in); return rc; } + rc = zsdk_apply_pubkey(&zopts, recipient_pk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(tmp_in); return rc; } + + char tmp_arc[512]; + zsdk_tmp_path(tmp_arc, sizeof(tmp_arc), "zsdk_arc"); + + const char *arc_paths[1] = { logical_name }; + const char *disk_paths[1] = { tmp_in }; + zupt_error_t err = zupt_compress_files(tmp_arc, arc_paths, disk_paths, 1, &zopts); + + zsdk_wipe_options_secrets(&zopts); + unlink(tmp_in); + + if (err != ZUPT_OK) { + unlink(tmp_arc); + return ZSDK_FAIL(zsdk_map_zupt_error(err), "compress (zupt_err=%d)", err); + } + + f = fopen(tmp_arc, "rb"); + if (!f) { unlink(tmp_arc); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "reopen archive"); } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = (uint8_t *)zsdk_malloc((size_t)sz); + if (!buf) { fclose(f); unlink(tmp_arc); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "archive buf"); } + size_t got = fread(buf, 1, (size_t)sz, f); + fclose(f); + unlink(tmp_arc); + if (got != (size_t)sz) { zsdk_free_internal(buf); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "short read"); } + + *archive_out = buf; + *archive_sz = (size_t)sz; + return ZUPTSDK_OK; +} + +int zuptsdk_extract_to_dir(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + const char *dest_dir, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk) { + if (!archive || !archive_sz || !dest_dir) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + + char tmp[512]; + zsdk_tmp_path(tmp, sizeof(tmp), "zsdk_arc"); + FILE *f = fopen(tmp, "wb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "write temp archive"); + if (fwrite(archive, 1, archive_sz, f) != archive_sz) { + fclose(f); unlink(tmp); + return ZSDK_FAIL(ZUPTSDK_ERR_IO, "write archive"); + } + fclose(f); + + zupt_options_t zopts; + zsdk_apply_options(&zopts, NULL, ctx); + int rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(tmp); return rc; } + rc = zsdk_apply_privkey(&zopts, recipient_sk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(tmp); return rc; } + + zupt_error_t err = zupt_extract_archive(tmp, dest_dir, &zopts); + zsdk_wipe_options_secrets(&zopts); + unlink(tmp); + if (err != ZUPT_OK) + return ZSDK_FAIL(zsdk_map_zupt_error(err), "extract (zupt_err=%d)", err); + return ZUPTSDK_OK; +} + +int zuptsdk_extract_buffer(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk, + uint8_t **data_out, size_t *data_sz) { + if (!archive || !archive_sz || !data_out || !data_sz) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + + char tmpdir[512]; + zsdk_tmp_path(tmpdir, sizeof(tmpdir), "zsdk_x"); + if (zupt_mkdir(tmpdir) != 0) + return ZSDK_FAIL(ZUPTSDK_ERR_IO, "mkdir %s", tmpdir); + + int rc = zuptsdk_extract_to_dir(ctx, archive, archive_sz, tmpdir, password, recipient_sk); + if (rc != ZUPTSDK_OK) { rmdir(tmpdir); return rc; } + + /* Find the single file in tmpdir */ +#ifdef _WIN32 + /* Simplified: assume one file, use FindFirstFile */ + WIN32_FIND_DATAA fd; + char pat[600]; snprintf(pat, sizeof(pat), "%s\\*", tmpdir); + HANDLE h = FindFirstFileA(pat, &fd); + char fpath[1024] = {0}; + if (h != INVALID_HANDLE_VALUE) { + do { + if (fd.cFileName[0] != '.') { + snprintf(fpath, sizeof(fpath), "%s\\%s", tmpdir, fd.cFileName); + break; + } + } while (FindNextFileA(h, &fd)); + FindClose(h); + } +#else + DIR *d = opendir(tmpdir); + char fpath[1024] = {0}; + if (d) { + struct dirent *e; + while ((e = readdir(d))) { + if (e->d_name[0] == '.') continue; + snprintf(fpath, sizeof(fpath), "%s/%s", tmpdir, e->d_name); + break; + } + closedir(d); + } +#endif + if (!fpath[0]) { + rmdir(tmpdir); + return ZSDK_FAIL(ZUPTSDK_ERR_BAD_ARCHIVE, "no extracted file"); + } + + FILE *f = fopen(fpath, "rb"); + if (!f) { unlink(fpath); rmdir(tmpdir); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open extracted"); } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = (uint8_t *)zsdk_malloc((size_t)sz); + if (!buf) { fclose(f); unlink(fpath); rmdir(tmpdir); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "data buf"); } + if (fread(buf, 1, (size_t)sz, f) != (size_t)sz) { + fclose(f); unlink(fpath); rmdir(tmpdir); zsdk_free_internal(buf); + return ZSDK_FAIL(ZUPTSDK_ERR_IO, "read extracted"); + } + fclose(f); + unlink(fpath); + rmdir(tmpdir); + *data_out = buf; + *data_sz = (size_t)sz; + return ZUPTSDK_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Streaming wrappers (initially via tempfile, swap to true streaming later) + * ════════════════════════════════════════════════════════════════════════ */ + +static int zsdk_drain_to_file(zuptsdk_read_fn input, void *ud, const char *path) { + FILE *f = fopen(path, "wb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", path); + uint8_t buf[65536]; + int64_t n; + while ((n = input(ud, buf, sizeof(buf))) > 0) { + if (fwrite(buf, 1, (size_t)n, f) != (size_t)n) { + fclose(f); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", path); + } + } + zuptsdk_secure_zero(buf, sizeof(buf)); + fclose(f); + if (n < 0) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "read callback returned %lld", (long long)n); + return ZUPTSDK_OK; +} + +static int zsdk_pump_from_file(zuptsdk_write_fn output, void *ud, const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open %s", path); + uint8_t buf[65536]; + size_t n; + int rc = ZUPTSDK_OK; + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { + int64_t w = output(ud, buf, n); + if (w != (int64_t)n) { rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write callback short %lld", (long long)w); break; } + } + zuptsdk_secure_zero(buf, sizeof(buf)); + fclose(f); + return rc; +} + +int zuptsdk_compress_stream(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + zuptsdk_read_fn input, void *input_ud, + const char *input_name, uint64_t input_total, + zuptsdk_write_fn output, void *output_ud, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk) { + (void)input_total; + if (!input || !output || !input_name) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + + char in_tmp[512], arc_tmp[512]; + zsdk_tmp_path(in_tmp, sizeof(in_tmp), "zsdk_in"); + zsdk_tmp_path(arc_tmp, sizeof(arc_tmp), "zsdk_arc"); + + int rc = zsdk_drain_to_file(input, input_ud, in_tmp); + if (rc != ZUPTSDK_OK) { unlink(in_tmp); return rc; } + + zupt_options_t zopts; + zsdk_apply_options(&zopts, opts, ctx); + rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(in_tmp); return rc; } + rc = zsdk_apply_pubkey(&zopts, recipient_pk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(in_tmp); return rc; } + + const char *arc_paths[1] = { input_name }; + const char *disk_paths[1] = { in_tmp }; + zupt_error_t err = zupt_compress_files(arc_tmp, arc_paths, disk_paths, 1, &zopts); + + zsdk_wipe_options_secrets(&zopts); + unlink(in_tmp); + + if (err != ZUPT_OK) { + unlink(arc_tmp); + return ZSDK_FAIL(zsdk_map_zupt_error(err), "compress (zupt_err=%d)", err); + } + + rc = zsdk_pump_from_file(output, output_ud, arc_tmp); + unlink(arc_tmp); + return rc; +} + +int zuptsdk_decompress_stream(zuptsdk_ctx_t *ctx, + zuptsdk_read_fn input, void *input_ud, + zuptsdk_write_fn output, void *output_ud, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk) { + if (!input || !output) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + + char arc_tmp[512]; + zsdk_tmp_path(arc_tmp, sizeof(arc_tmp), "zsdk_arc"); + int rc = zsdk_drain_to_file(input, input_ud, arc_tmp); + if (rc != ZUPTSDK_OK) { unlink(arc_tmp); return rc; } + + /* Read into memory, call buffer extract */ + FILE *f = fopen(arc_tmp, "rb"); + if (!f) { unlink(arc_tmp); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "reopen archive"); } + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *abuf = (uint8_t *)zsdk_malloc((size_t)sz); + if (!abuf) { fclose(f); unlink(arc_tmp); return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "archive buf"); } + size_t got = fread(abuf, 1, (size_t)sz, f); + fclose(f); unlink(arc_tmp); + if (got != (size_t)sz) { zsdk_free_internal(abuf); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "short read"); } + + uint8_t *data = NULL; size_t data_sz = 0; + rc = zuptsdk_extract_buffer(ctx, abuf, (size_t)sz, password, recipient_sk, &data, &data_sz); + zsdk_free_internal(abuf); + if (rc != ZUPTSDK_OK) return rc; + + int64_t w = output(output_ud, data, data_sz); + zuptsdk_secure_zero(data, data_sz); + zsdk_free_internal(data); + if (w != (int64_t)data_sz) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "write callback short"); + return ZUPTSDK_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Verify / info + * ════════════════════════════════════════════════════════════════════════ */ + +int zuptsdk_verify(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk) { + if (!archive || !archive_sz) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + char tmp[512]; + zsdk_tmp_path(tmp, sizeof(tmp), "zsdk_v"); + FILE *f = fopen(tmp, "wb"); + if (!f) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "write temp archive"); + fwrite(archive, 1, archive_sz, f); fclose(f); + + zupt_options_t zopts; + zsdk_apply_options(&zopts, NULL, ctx); + int rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(tmp); return rc; } + rc = zsdk_apply_privkey(&zopts, recipient_sk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); unlink(tmp); return rc; } + + zupt_error_t err = zupt_test_archive(tmp, &zopts); + zsdk_wipe_options_secrets(&zopts); + unlink(tmp); + if (err != ZUPT_OK) + return ZSDK_FAIL(zsdk_map_zupt_error(err), "verify (zupt_err=%d)", err); + return ZUPTSDK_OK; +} + +/* archive_info: minimal v1 — returns header fields parsed from buffer */ +struct zuptsdk_archive_info { + int format_major; + int format_minor; + char *uuid_str; /* heap */ + int64_t created_unix; + uint64_t size; + uint32_t block_count; + int is_encrypted; + int is_pq_hybrid; + int is_solid; + int is_dedup; + int is_disk_image; +}; + +int zuptsdk_archive_info_read(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_archive_info_t **info_out) { + (void)ctx; + if (!archive || !info_out) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + if (archive_sz < 64) return ZSDK_FAIL(ZUPTSDK_ERR_BAD_ARCHIVE, "too small"); + + /* Magic check */ + static const uint8_t magic[6] = { + ZUPT_MAGIC_0, ZUPT_MAGIC_1, ZUPT_MAGIC_2, + ZUPT_MAGIC_3, ZUPT_MAGIC_4, ZUPT_MAGIC_5 }; + if (memcmp(archive, magic, 6) != 0) + return ZSDK_FAIL(ZUPTSDK_ERR_BAD_ARCHIVE, "magic mismatch"); + + zuptsdk_archive_info_t *i = (zuptsdk_archive_info_t *)zsdk_calloc(1, sizeof(*i)); + if (!i) return ZSDK_FAIL(ZUPTSDK_ERR_NO_MEMORY, "info alloc"); + + /* Parse header per format spec: magic[6] | major[1] | minor[1] | flags[4] | + * uuid[16] | created[8] | reserved[20] = 56 bytes */ + i->format_major = archive[6]; + i->format_minor = archive[7]; + uint32_t flags = (uint32_t)archive[8] | ((uint32_t)archive[9] << 8) + | ((uint32_t)archive[10]<<16)| ((uint32_t)archive[11] << 24); + i->is_encrypted = (flags & ZUPT_FLAG_ENCRYPTED) != 0; + i->is_pq_hybrid = (flags & ZUPT_FLAG_PQ_HYBRID) != 0; + i->is_solid = (flags & ZUPT_FLAG_SOLID) != 0; + i->is_dedup = (flags & ZUPT_FLAG_DEDUP) != 0; + i->is_disk_image = 0; /* heuristic: detect later */ + + /* UUID hex string */ + char uuid[37] = {0}; + snprintf(uuid, sizeof(uuid), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + archive[12],archive[13],archive[14],archive[15], + archive[16],archive[17],archive[18],archive[19], + archive[20],archive[21],archive[22],archive[23], + archive[24],archive[25],archive[26],archive[27]); + i->uuid_str = zsdk_strdup(uuid); + + /* Created timestamp: little-endian uint64 at offset 28 */ + uint64_t ts = 0; + for (int k = 0; k < 8; k++) ts |= (uint64_t)archive[28 + k] << (k * 8); + i->created_unix = (int64_t)ts; + i->size = archive_sz; + i->block_count = 0; /* would need full parse to count */ + + *info_out = i; + return ZUPTSDK_OK; +} + +void zuptsdk_archive_info_destroy(zuptsdk_archive_info_t *info) { + if (!info) return; + if (info->uuid_str) zsdk_free_internal(info->uuid_str); + zsdk_free_internal(info); +} + +int zuptsdk_archive_info_format_major(const zuptsdk_archive_info_t *i) { return i ? i->format_major : 0; } +int zuptsdk_archive_info_format_minor(const zuptsdk_archive_info_t *i) { return i ? i->format_minor : 0; } +const char *zuptsdk_archive_info_uuid(const zuptsdk_archive_info_t *i) { return i ? i->uuid_str : ""; } +int64_t zuptsdk_archive_info_created_unix(const zuptsdk_archive_info_t *i) { return i ? i->created_unix : 0; } +uint64_t zuptsdk_archive_info_size(const zuptsdk_archive_info_t *i) { return i ? i->size : 0; } +uint32_t zuptsdk_archive_info_block_count(const zuptsdk_archive_info_t *i) { return i ? i->block_count : 0; } +int zuptsdk_archive_info_is_encrypted(const zuptsdk_archive_info_t *i) { return i ? i->is_encrypted : 0; } +int zuptsdk_archive_info_is_pq_hybrid(const zuptsdk_archive_info_t *i) { return i ? i->is_pq_hybrid : 0; } +int zuptsdk_archive_info_is_solid(const zuptsdk_archive_info_t *i) { return i ? i->is_solid : 0; } +int zuptsdk_archive_info_is_dedup(const zuptsdk_archive_info_t *i) { return i ? i->is_dedup : 0; } +int zuptsdk_archive_info_is_disk_image(const zuptsdk_archive_info_t *i){ return i ? i->is_disk_image : 0; } + +/* ════════════════════════════════════════════════════════════════════════ + * Disk + * ════════════════════════════════════════════════════════════════════════ */ + +int zuptsdk_disk_backup(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *source, const char *output, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk) { + if (!source || !output) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + zupt_options_t zopts; + zsdk_apply_options(&zopts, opts, ctx); + int rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); return rc; } + rc = zsdk_apply_pubkey(&zopts, recipient_pk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); return rc; } + + zupt_error_t err = zupt_disk_backup(output, source, &zopts); + zsdk_wipe_options_secrets(&zopts); + if (err != ZUPT_OK) + return ZSDK_FAIL(zsdk_map_zupt_error(err), "disk_backup (zupt_err=%d)", err); + return ZUPTSDK_OK; +} + +int zuptsdk_disk_restore(zuptsdk_ctx_t *ctx, + const char *archive_path, const char *target, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk) { + if (!archive_path || !target) + return ZSDK_FAIL(ZUPTSDK_ERR_INVALID_ARG, "NULL parameter"); + zupt_options_t zopts; + zsdk_apply_options(&zopts, NULL, ctx); + int rc = zsdk_apply_password(&zopts, password); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); return rc; } + rc = zsdk_apply_privkey(&zopts, recipient_sk); + if (rc != ZUPTSDK_OK) { zsdk_wipe_options_secrets(&zopts); return rc; } + + zupt_error_t err = zupt_disk_restore(archive_path, target, &zopts); + zsdk_wipe_options_secrets(&zopts); + if (err != ZUPT_OK) + return ZSDK_FAIL(zsdk_map_zupt_error(err), "disk_restore (zupt_err=%d)", err); + return ZUPTSDK_OK; +} diff --git a/sdk/tests/test_python.py b/sdk/tests/test_python.py new file mode 100644 index 0000000..97ce1de --- /dev/null +++ b/sdk/tests/test_python.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +zuptsdk Python binding test +SPDX-License-Identifier: AGPL-3.0-or-later +""" +import os +import sys +import tempfile + +# Add path so we can import without installing +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +os.environ.setdefault("ZUPTSDK_LIBRARY", + os.path.join(os.path.dirname(__file__), "..", "..", "build", "libzuptsdk.so.1")) + +import zuptsdk + +PASS = 0 +FAIL = 0 + +def test(name, fn): + global PASS, FAIL + try: + fn() + print(f" {name:<60} PASS") + PASS += 1 + except Exception as e: + print(f" {name:<60} FAIL: {e!r}") + FAIL += 1 + + +print(f"\n zuptsdk Python bindings — version {zuptsdk.__version__}\n") + +DATA = b"Hello world from Python via cffi!\n" * 10 + + +def t_version(): + assert zuptsdk.__version__ == "1.0.0", f"got {zuptsdk.__version__}" + + +def t_roundtrip_plain(): + with zuptsdk.Context() as ctx: + arc = ctx.compress_buffer(DATA, name="hello.txt") + assert isinstance(arc, bytes) + assert len(arc) > 0 + out = ctx.extract_buffer(arc) + assert out == DATA + + +def t_roundtrip_password(): + with zuptsdk.Context() as ctx: + arc = ctx.compress_buffer(DATA, name="secret.txt", + password=b"correct horse battery staple") + out = ctx.extract_buffer(arc, password=b"correct horse battery staple") + assert out == DATA + + +def t_wrong_password(): + with zuptsdk.Context() as ctx: + arc = ctx.compress_buffer(DATA, name="x.txt", password=b"good") + try: + ctx.extract_buffer(arc, password=b"bad") + except zuptsdk.ZuptError: + return + raise AssertionError("wrong password was accepted") + + +def t_secure_buf_password(): + with zuptsdk.Context() as ctx, zuptsdk.SecureBuf(b"locked-pw") as pw: + arc = ctx.compress_buffer(DATA, name="x.txt", password=pw) + out = ctx.extract_buffer(arc, password=pw) + assert out == DATA + + +def t_pq_keypair(): + with zuptsdk.Context() as ctx: + kp = ctx.generate_keypair() + with tempfile.TemporaryDirectory() as td: + kp.save(os.path.join(td, "mykey")) + arc = ctx.compress_buffer(DATA, name="pq.txt", public_key=kp.public) + out = ctx.extract_buffer(arc, private_key=kp.private) + assert out == DATA + + +def t_verify_and_info(): + with zuptsdk.Context() as ctx: + arc = ctx.compress_buffer(DATA, name="v.txt") + assert ctx.verify(arc) is True + info = ctx.info(arc) + assert info.format_major >= 1 + assert info.is_encrypted is False + assert len(info.uuid) == 36, info.uuid + + +def t_corrupted_archive(): + with zuptsdk.Context() as ctx: + # Use enough data that flipping a byte mid-stream lands in the + # compressed payload, not in a zero-padded region. + big = DATA * 100 + arc = bytearray(ctx.compress_buffer(big, name="c.txt")) + # Flip a byte two-thirds of the way through — well into compressed + # data, well before the index/footer. + if len(arc) > 200: + offset = (len(arc) * 2) // 3 + arc[offset] ^= 0xFF + try: + ctx.verify(bytes(arc)) + except zuptsdk.ZuptError: + return + raise AssertionError("corrupted archive accepted") + + +test("version constant", t_version) +test("roundtrip plain", t_roundtrip_plain) +test("roundtrip password (bytes)", t_roundtrip_password) +test("wrong password rejected", t_wrong_password) +test("roundtrip with SecureBuf", t_secure_buf_password) +test("PQ keypair generate + roundtrip", t_pq_keypair) +test("verify and info", t_verify_and_info) +test("corrupted archive rejected", t_corrupted_archive) + +print(f"\n Results: {PASS} passed, {FAIL} failed\n") +sys.exit(0 if FAIL == 0 else 1) diff --git a/sdk/tests/test_sdk_roundtrip.c b/sdk/tests/test_sdk_roundtrip.c new file mode 100644 index 0000000..2176a60 --- /dev/null +++ b/sdk/tests/test_sdk_roundtrip.c @@ -0,0 +1,412 @@ +/* + * libzuptsdk roundtrip test + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Exercises every public SDK function with a byte-exact verification. + * Returns 0 on success; non-zero on any failure. + */ + +#define _DEFAULT_SOURCE 1 +#include +#include +#include +#include +#include + +static int g_pass = 0, g_fail = 0; + +#define TEST(name) do { \ + fprintf(stderr, " %-60s", name); \ + fflush(stderr); \ +} while (0) + +#define PASS() do { \ + fprintf(stderr, "PASS\n"); \ + g_pass++; \ +} while (0) + +#define FAIL(reason) do { \ + fprintf(stderr, "FAIL: %s\n", reason); \ + if (zuptsdk_last_error_detail()[0]) \ + fprintf(stderr, " detail: %s\n", zuptsdk_last_error_detail()); \ + g_fail++; \ +} while (0) + +#define CHECK(rc, msg) do { \ + if ((rc) != ZUPTSDK_OK) { FAIL(msg); return; } \ +} while (0) + +static const uint8_t TEST_DATA[] = + "Post-quantum backup test data for the libzuptsdk roundtrip suite. " + "This payload is repeated to ensure compression actually does something. " + "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. " + "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. " + "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. " + "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. " + "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. " + "End of test data.\n"; + +static void test_version(void) { + TEST("version_string returns non-NULL"); + const char *v = zuptsdk_version_string(); + if (!v || strlen(v) == 0) { FAIL("empty"); return; } + if (strcmp(v, ZUPTSDK_VERSION_STRING) != 0) { FAIL("mismatch"); return; } + PASS(); + + TEST("version_check accepts current version"); + int rc = zuptsdk_version_check(ZUPTSDK_VERSION_MAJOR, + ZUPTSDK_VERSION_MINOR, + ZUPTSDK_VERSION_PATCH); + CHECK(rc, "rejected own version"); + PASS(); + + TEST("version_check rejects future version"); + rc = zuptsdk_version_check(99, 99, 99); + if (rc != ZUPTSDK_ERR_VERSION_MISMATCH) { FAIL("should reject"); return; } + PASS(); +} + +static void test_strerror(void) { + TEST("strerror handles every error code"); + for (int e = 0; e >= -100; e--) { + const char *s = zuptsdk_strerror(e); + if (!s || !*s) { FAIL("empty"); return; } + } + PASS(); +} + +static void test_context(void) { + TEST("ctx_create / ctx_destroy"); + zuptsdk_ctx_t *c = NULL; + CHECK(zuptsdk_ctx_create(&c), "create failed"); + if (!c) { FAIL("ctx is NULL"); return; } + zuptsdk_ctx_destroy(c); + zuptsdk_ctx_destroy(NULL); /* should not crash */ + PASS(); + + TEST("ctx_set_threads validates range"); + CHECK(zuptsdk_ctx_create(&c), "create"); + int rc = zuptsdk_ctx_set_threads(c, 0); /* auto */ + CHECK(rc, "auto"); + rc = zuptsdk_ctx_set_threads(c, 4); /* normal */ + CHECK(rc, "4"); + rc = zuptsdk_ctx_set_threads(c, -1); /* invalid */ + if (rc != ZUPTSDK_ERR_INVALID_ARG) { FAIL("should reject -1"); zuptsdk_ctx_destroy(c); return; } + rc = zuptsdk_ctx_set_threads(c, 999); /* invalid */ + if (rc != ZUPTSDK_ERR_INVALID_ARG) { FAIL("should reject 999"); zuptsdk_ctx_destroy(c); return; } + zuptsdk_ctx_destroy(c); + PASS(); +} + +static void test_options(void) { + TEST("options builder full sequence"); + zuptsdk_options_t *o = NULL; + CHECK(zuptsdk_options_create(&o), "create"); + CHECK(zuptsdk_options_set_codec(o, ZUPTSDK_CODEC_AUTO), "codec auto"); + CHECK(zuptsdk_options_set_codec(o, ZUPTSDK_CODEC_VAPTVUPT), "codec vv"); + CHECK(zuptsdk_options_set_level(o, 7), "level 7"); + CHECK(zuptsdk_options_set_dedup(o, 1), "dedup"); + CHECK(zuptsdk_options_set_solid(o, 0), "solid"); + CHECK(zuptsdk_options_set_max_decompressed(o, 1024 * 1024), "max"); + int rc = zuptsdk_options_set_level(o, 99); + if (rc != ZUPTSDK_ERR_INVALID_ARG) { FAIL("level 99 should fail"); zuptsdk_options_destroy(o); return; } + zuptsdk_options_destroy(o); + PASS(); +} + +static void test_secure_buf(void) { + TEST("secure_buf create/get/destroy"); + zuptsdk_secure_buf_t *b = NULL; + CHECK(zuptsdk_secure_buf_create(64, &b), "create"); + uint8_t *data = NULL; size_t sz = 0; + CHECK(zuptsdk_secure_buf_get(b, &data, &sz), "get"); + if (sz != 64) { FAIL("size wrong"); zuptsdk_secure_buf_destroy(b); return; } + memset(data, 0xAA, 64); + if (data[0] != 0xAA || data[63] != 0xAA) { FAIL("write/read"); zuptsdk_secure_buf_destroy(b); return; } + zuptsdk_secure_buf_destroy(b); + PASS(); + + TEST("secure_buf_from_data copies"); + const uint8_t src[] = "secret password value!"; + CHECK(zuptsdk_secure_buf_from_data(src, sizeof(src) - 1, &b), "from_data"); + CHECK(zuptsdk_secure_buf_get(b, &data, &sz), "get"); + if (sz != sizeof(src) - 1) { FAIL("size"); zuptsdk_secure_buf_destroy(b); return; } + if (memcmp(data, src, sz) != 0) { FAIL("content"); zuptsdk_secure_buf_destroy(b); return; } + zuptsdk_secure_buf_destroy(b); + PASS(); +} + +static int byteexact(const uint8_t *a, size_t na, const uint8_t *b, size_t nb) { + return (na == nb) && (memcmp(a, b, na) == 0); +} + +static void test_compress_buffer_plain(void) { + TEST("compress_buffer + extract_buffer (plain)"); + zuptsdk_ctx_t *ctx = NULL; + CHECK(zuptsdk_ctx_create(&ctx), "ctx"); + + zuptsdk_options_t *opts = NULL; + CHECK(zuptsdk_options_create(&opts), "opts"); + zuptsdk_options_set_codec(opts, ZUPTSDK_CODEC_AUTO); + zuptsdk_options_set_level(opts, 5); + + uint8_t *arc = NULL; size_t arc_sz = 0; + int rc = zuptsdk_compress_buffer(ctx, opts, "test.txt", + TEST_DATA, sizeof(TEST_DATA) - 1, + NULL, NULL, &arc, &arc_sz); + if (rc != ZUPTSDK_OK) { FAIL("compress"); zuptsdk_options_destroy(opts); zuptsdk_ctx_destroy(ctx); return; } + + uint8_t *out = NULL; size_t out_sz = 0; + rc = zuptsdk_extract_buffer(ctx, arc, arc_sz, NULL, NULL, &out, &out_sz); + zuptsdk_free(arc); + + if (rc != ZUPTSDK_OK) { FAIL("extract"); zuptsdk_options_destroy(opts); zuptsdk_ctx_destroy(ctx); return; } + if (!byteexact(out, out_sz, TEST_DATA, sizeof(TEST_DATA) - 1)) { + FAIL("byte mismatch"); + zuptsdk_free(out); + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); + return; + } + zuptsdk_free(out); + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); + PASS(); +} + +static void test_compress_buffer_password(void) { + TEST("compress_buffer + extract_buffer (password)"); + zuptsdk_ctx_t *ctx = NULL; + CHECK(zuptsdk_ctx_create(&ctx), "ctx"); + + zuptsdk_options_t *opts = NULL; + CHECK(zuptsdk_options_create(&opts), "opts"); + + zuptsdk_secure_buf_t *pw = NULL; + const uint8_t pwbuf[] = "Tr0ub4dor&3"; + CHECK(zuptsdk_secure_buf_from_data(pwbuf, sizeof(pwbuf) - 1, &pw), "pw create"); + + uint8_t *arc = NULL; size_t arc_sz = 0; + int rc = zuptsdk_compress_buffer(ctx, opts, "secret.txt", + TEST_DATA, sizeof(TEST_DATA) - 1, + pw, NULL, &arc, &arc_sz); + if (rc != ZUPTSDK_OK) { + FAIL("compress"); + zuptsdk_secure_buf_destroy(pw); + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); + return; + } + + uint8_t *out = NULL; size_t out_sz = 0; + rc = zuptsdk_extract_buffer(ctx, arc, arc_sz, pw, NULL, &out, &out_sz); + zuptsdk_free(arc); + + int ok = (rc == ZUPTSDK_OK) && byteexact(out, out_sz, TEST_DATA, sizeof(TEST_DATA) - 1); + zuptsdk_free(out); + zuptsdk_secure_buf_destroy(pw); + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); + if (!ok) { FAIL("byte mismatch or rc != OK"); return; } + PASS(); +} + +static void test_compress_buffer_wrong_password(void) { + TEST("extract_buffer rejects wrong password"); + zuptsdk_ctx_t *ctx = NULL; + CHECK(zuptsdk_ctx_create(&ctx), "ctx"); + + zuptsdk_options_t *opts = NULL; + CHECK(zuptsdk_options_create(&opts), "opts"); + + zuptsdk_secure_buf_t *pw_good = NULL, *pw_bad = NULL; + zuptsdk_secure_buf_from_data((const uint8_t*)"correct", 7, &pw_good); + zuptsdk_secure_buf_from_data((const uint8_t*)"WRONG-pw", 8, &pw_bad); + + uint8_t *arc = NULL; size_t arc_sz = 0; + int rc = zuptsdk_compress_buffer(ctx, opts, "x.txt", + TEST_DATA, sizeof(TEST_DATA) - 1, + pw_good, NULL, &arc, &arc_sz); + if (rc != ZUPTSDK_OK) { FAIL("compress"); goto cleanup; } + + uint8_t *out = NULL; size_t out_sz = 0; + rc = zuptsdk_extract_buffer(ctx, arc, arc_sz, pw_bad, NULL, &out, &out_sz); + if (rc == ZUPTSDK_OK) { + FAIL("wrong password accepted"); + zuptsdk_free(out); + zuptsdk_free(arc); + goto cleanup; + } + zuptsdk_free(arc); + PASS(); + +cleanup: + zuptsdk_secure_buf_destroy(pw_good); + zuptsdk_secure_buf_destroy(pw_bad); + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); +} + +static void test_keypair_pq(void) { + TEST("keypair_generate + compress_pq + extract_pq"); + zuptsdk_ctx_t *ctx = NULL; + CHECK(zuptsdk_ctx_create(&ctx), "ctx"); + + zuptsdk_keypair_t *kp = NULL; + int rc = zuptsdk_keypair_generate(ctx, &kp); + if (rc != ZUPTSDK_OK) { FAIL("keygen"); zuptsdk_ctx_destroy(ctx); return; } + + /* Save and load to exercise that path too */ + rc = zuptsdk_keypair_save_private(kp, "/tmp/_zsdk_priv.key"); + if (rc != ZUPTSDK_OK) { FAIL("save priv"); goto err; } + rc = zuptsdk_keypair_save_public(kp, "/tmp/_zsdk_pub.key"); + if (rc != ZUPTSDK_OK) { FAIL("save pub"); goto err; } + + zuptsdk_pubkey_t *pub = NULL; + zuptsdk_privkey_t *priv = NULL; + rc = zuptsdk_pubkey_load("/tmp/_zsdk_pub.key", &pub); + if (rc != ZUPTSDK_OK) { FAIL("load pub"); goto err; } + rc = zuptsdk_privkey_load("/tmp/_zsdk_priv.key", &priv); + if (rc != ZUPTSDK_OK) { FAIL("load priv"); zuptsdk_pubkey_destroy(pub); goto err; } + + zuptsdk_options_t *opts = NULL; + zuptsdk_options_create(&opts); + + uint8_t *arc = NULL; size_t arc_sz = 0; + rc = zuptsdk_compress_buffer(ctx, opts, "pq.txt", + TEST_DATA, sizeof(TEST_DATA) - 1, + NULL, pub, &arc, &arc_sz); + if (rc != ZUPTSDK_OK) { + FAIL("compress_pq"); + zuptsdk_pubkey_destroy(pub); + zuptsdk_privkey_destroy(priv); + zuptsdk_options_destroy(opts); + goto err; + } + + uint8_t *out = NULL; size_t out_sz = 0; + rc = zuptsdk_extract_buffer(ctx, arc, arc_sz, NULL, priv, &out, &out_sz); + int ok = (rc == ZUPTSDK_OK) && byteexact(out, out_sz, TEST_DATA, sizeof(TEST_DATA) - 1); + + zuptsdk_free(arc); + zuptsdk_free(out); + zuptsdk_pubkey_destroy(pub); + zuptsdk_privkey_destroy(priv); + zuptsdk_options_destroy(opts); + + unlink("/tmp/_zsdk_priv.key"); + unlink("/tmp/_zsdk_pub.key"); + + if (!ok) { FAIL("byte mismatch or rc != OK"); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); return; } + zuptsdk_keypair_destroy(kp); + zuptsdk_ctx_destroy(ctx); + PASS(); + return; + +err: + zuptsdk_keypair_destroy(kp); + zuptsdk_ctx_destroy(ctx); +} + +static void test_verify_and_info(void) { + TEST("verify and archive_info_read"); + zuptsdk_ctx_t *ctx = NULL; + CHECK(zuptsdk_ctx_create(&ctx), "ctx"); + zuptsdk_options_t *opts = NULL; + zuptsdk_options_create(&opts); + + uint8_t *arc = NULL; size_t arc_sz = 0; + int rc = zuptsdk_compress_buffer(ctx, opts, "v.txt", + TEST_DATA, sizeof(TEST_DATA) - 1, + NULL, NULL, &arc, &arc_sz); + if (rc != ZUPTSDK_OK) { FAIL("compress"); goto cleanup; } + + rc = zuptsdk_verify(ctx, arc, arc_sz, NULL, NULL); + if (rc != ZUPTSDK_OK) { FAIL("verify"); zuptsdk_free(arc); goto cleanup; } + + zuptsdk_archive_info_t *info = NULL; + rc = zuptsdk_archive_info_read(ctx, arc, arc_sz, &info); + if (rc != ZUPTSDK_OK) { FAIL("info read"); zuptsdk_free(arc); goto cleanup; } + + int major = zuptsdk_archive_info_format_major(info); + int enc = zuptsdk_archive_info_is_encrypted(info); + const char *uuid = zuptsdk_archive_info_uuid(info); + + if (major < 1 || enc != 0 || !uuid || strlen(uuid) != 36) { + fprintf(stderr, "[major=%d enc=%d uuid=%s] ", major, enc, uuid ? uuid : "NULL"); + FAIL("info fields wrong"); + zuptsdk_archive_info_destroy(info); + zuptsdk_free(arc); + goto cleanup; + } + + zuptsdk_archive_info_destroy(info); + zuptsdk_free(arc); + PASS(); + +cleanup: + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); +} + +static void test_corrupted_archive(void) { + TEST("verify rejects corrupted archive"); + zuptsdk_ctx_t *ctx = NULL; + CHECK(zuptsdk_ctx_create(&ctx), "ctx"); + zuptsdk_options_t *opts = NULL; + zuptsdk_options_create(&opts); + + /* Build a larger archive so flipping a byte hits compressed data, + * not a zero-padded header field. */ + size_t big_sz = (sizeof(TEST_DATA) - 1) * 100; + uint8_t *big = (uint8_t *)malloc(big_sz); + if (!big) { FAIL("alloc"); goto cleanup; } + for (size_t i = 0; i < 100; i++) + memcpy(big + i * (sizeof(TEST_DATA) - 1), TEST_DATA, sizeof(TEST_DATA) - 1); + + uint8_t *arc = NULL; size_t arc_sz = 0; + int rc = zuptsdk_compress_buffer(ctx, opts, "c.txt", big, big_sz, + NULL, NULL, &arc, &arc_sz); + free(big); + if (rc != ZUPTSDK_OK) { FAIL("compress"); goto cleanup; } + + /* Flip a byte two-thirds through, in the real compressed data region */ + if (arc_sz > 200) arc[(arc_sz * 2) / 3] ^= 0xFF; + + rc = zuptsdk_verify(ctx, arc, arc_sz, NULL, NULL); + if (rc == ZUPTSDK_OK) { + FAIL("verify accepted corrupted archive"); + zuptsdk_free(arc); + goto cleanup; + } + zuptsdk_free(arc); + PASS(); + +cleanup: + zuptsdk_options_destroy(opts); + zuptsdk_ctx_destroy(ctx); +} + +int main(void) { + fprintf(stderr, "\n═══════════════════════════════════════════════════════════════\n"); + fprintf(stderr, " libzuptsdk %s — roundtrip test suite\n", zuptsdk_version_string()); + fprintf(stderr, "═══════════════════════════════════════════════════════════════\n\n"); + + test_version(); + test_strerror(); + test_context(); + test_options(); + test_secure_buf(); + test_compress_buffer_plain(); + test_compress_buffer_password(); + test_compress_buffer_wrong_password(); + test_keypair_pq(); + test_verify_and_info(); + test_corrupted_archive(); + + fprintf(stderr, "\n═══════════════════════════════════════════════════════════════\n"); + fprintf(stderr, " Results: %d passed, %d failed\n", g_pass, g_fail); + fprintf(stderr, "═══════════════════════════════════════════════════════════════\n\n"); + return g_fail == 0 ? 0 : 1; +} diff --git a/sdk/zuptsdk.map b/sdk/zuptsdk.map new file mode 100644 index 0000000..82ed533 --- /dev/null +++ b/sdk/zuptsdk.map @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2026 Cristian Cezar Moisés +# libzuptsdk symbol version script +# All public symbols are namespaced under ZUPTSDK_1.0. +# Anything not listed here is hidden via `local: *;` — guaranteed no leakage +# of internal zupt_*, vv_*, or other symbols from the static libraries we +# link against. +# +# Adding new symbols in v1.x: add a new ZUPTSDK_1.x block that inherits from +# the previous one. NEVER modify or remove symbols within an existing block. +# +# Verification: after build, `nm -D --defined-only libzuptsdk.so | grep ' T '` +# must show ONLY symbols listed below. + +ZUPTSDK_1.0 { + global: + # Version + zuptsdk_version_string; + zuptsdk_version_check; + + # Errors + zuptsdk_strerror; + zuptsdk_last_error_detail; + + # Global config + zuptsdk_set_allocator; + + # Context + zuptsdk_ctx_create; + zuptsdk_ctx_destroy; + zuptsdk_ctx_set_threads; + zuptsdk_ctx_set_progress_callback; + zuptsdk_ctx_set_log_callback; + + # Options + zuptsdk_options_create; + zuptsdk_options_destroy; + zuptsdk_options_set_codec; + zuptsdk_options_set_level; + zuptsdk_options_set_dedup; + zuptsdk_options_set_solid; + zuptsdk_options_set_block_size; + zuptsdk_options_set_max_decompressed; + + # Secure buffers + zuptsdk_secure_buf_create; + zuptsdk_secure_buf_destroy; + zuptsdk_secure_buf_get; + zuptsdk_secure_buf_from_data; + + # Keys + zuptsdk_keypair_generate; + zuptsdk_keypair_destroy; + zuptsdk_keypair_save_private; + zuptsdk_keypair_save_public; + zuptsdk_privkey_load; + zuptsdk_privkey_destroy; + zuptsdk_pubkey_load; + zuptsdk_pubkey_destroy; + zuptsdk_privkey_get_public; + + # Compress / decompress (buffer) + zuptsdk_compress_files; + zuptsdk_compress_buffer; + zuptsdk_extract_to_dir; + zuptsdk_extract_buffer; + + # Compress / decompress (stream) + zuptsdk_compress_stream; + zuptsdk_decompress_stream; + + # Verify / info + zuptsdk_verify; + zuptsdk_archive_info_read; + zuptsdk_archive_info_destroy; + zuptsdk_archive_info_format_major; + zuptsdk_archive_info_format_minor; + zuptsdk_archive_info_uuid; + zuptsdk_archive_info_created_unix; + zuptsdk_archive_info_size; + zuptsdk_archive_info_block_count; + zuptsdk_archive_info_is_encrypted; + zuptsdk_archive_info_is_pq_hybrid; + zuptsdk_archive_info_is_solid; + zuptsdk_archive_info_is_dedup; + zuptsdk_archive_info_is_disk_image; + + # Disk + zuptsdk_disk_backup; + zuptsdk_disk_restore; + + # Misc + zuptsdk_free; + zuptsdk_secure_zero; + + local: + *; +}; diff --git a/src/vaptvupt_api.c b/src/vaptvupt_api.c index 2dce710..abf914b 100644 --- a/src/vaptvupt_api.c +++ b/src/vaptvupt_api.c @@ -1,8 +1,7 @@ /* * VaptVupt — Zupt Integration API Implementation * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright 2026 Cristian. * * ZUPT-COMPAT: thin wrapper over vv_compress/vv_decompress with * backup-optimized defaults. Decode speed prioritized over encode. @@ -15,7 +14,8 @@ int64_t vvz_compress(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, int level) { vv_options_t opts; vv_default_options(&opts); - opts.checksum = 1; /* Always verify integrity for backups */ + opts.checksum = 1; /* frame-level integrity */ + opts.format_v2 = 1; /* 4-7% better binary ratio (v2.33.0+ decoders) */ if (level <= 2) { opts.mode = VV_MODE_ULTRA_FAST; @@ -33,7 +33,9 @@ int64_t vvz_compress(const uint8_t *src, size_t src_len, int64_t vvz_decompress(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap) { - return vv_decompress(src, src_len, dst, dst_cap); + /* Skip XXH64 verification — zupt's HMAC-SHA256 already authenticates */ + return vv_decompress_flags(src, src_len, dst, dst_cap, + VV_DECOMPRESS_SKIP_CHECKSUM); } size_t vvz_compress_bound(size_t src_len) { diff --git a/src/vv_ans.c b/src/vv_ans.c index da26705..fa83005 100644 --- a/src/vv_ans.c +++ b/src/vv_ans.c @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — tANS v2 (sparse header + 4-way interleaved decode) * @@ -19,7 +17,6 @@ #include "vv_ans.h" #include "vv_platform.h" -#include "vv_huffman.h" #include #include @@ -1236,48 +1233,18 @@ static const uint8_t of_extra[VVA_OF_CODES] = { /* Encode match length → (code, extra_value, extra_bits). * Parameterized so both 'S' (ml_base) and 'T' (ml_base_v2) tags - * share one implementation. - * - * SPRINT 56: the original linear-from-top scan iterated up to 36 - * comparisons per call. Profile showed this is called once per - * matched sequence (nseq-many times per compress). Replacing with - * a hybrid lookup: - * 1. Small values (0-18 raw mlen, covering codes 0-16): direct - * lookup table since the first 16 codes are consecutive - * integers. - * 2. Medium-large values: branchless binary search over 36 entries - * = 6 comparisons max vs the previous 36. - * - * Both 'S' (ml_base, min 4) and 'T' (ml_base_v2, min 3) tags share - * this function; the direct-lookup threshold uses ml_base[16]=18 - * which works for both tables since they diverge only at the high - * end. */ + * share one implementation. */ static void ml_encode_with(uint32_t mlen, const uint32_t *base_tab, uint8_t *code, uint32_t *extra, int *nbits) { - /* Fast path: small mlen covers the majority of binary matches. - * ml_base[c] for c=0..15 is consecutive integers: - * v1 ml_base[0..15] = 4,5,...,19 (covers up to 19) - * v2 ml_base[0..15] = 3,4,...,18 (covers up to 18) - * Using base_tab[15] as the upper inclusive bound lets the fast - * path cover code 15 for both variants. */ - if (mlen <= base_tab[15]) { - uint32_t c = (mlen >= base_tab[0]) ? (mlen - base_tab[0]) : 0; - *code = (uint8_t)c; - *extra = 0; /* codes 0-15 all have ml_extra[c] = 0 */ - *nbits = 0; - return; + for (int c = VVA_ML_CODES - 1; c >= 0; c--) { + if (mlen >= base_tab[c]) { + *code = (uint8_t)c; + *extra = mlen - base_tab[c]; + *nbits = ml_extra[c]; + return; + } } - - /* Binary search over codes 16..35 for larger values. */ - int lo = 16, hi = VVA_ML_CODES - 1; - while (lo < hi) { - int mid = (lo + hi + 1) >> 1; - if (mlen >= base_tab[mid]) lo = mid; - else hi = mid - 1; - } - *code = (uint8_t)lo; - *extra = mlen - base_tab[lo]; - *nbits = ml_extra[lo]; + *code = 0; *extra = 0; *nbits = 0; } /* (ml_encode legacy wrapper removed — all callers migrated to * ml_encode_with for explicit table selection.) */ @@ -1320,27 +1287,15 @@ static const uint8_t ll_extra[VVA_LL_CODES] = { }; static void ll_encode(uint32_t litlen, uint8_t *code, uint32_t *extra, int *nbits) { - /* SPRINT 56: same optimization as ml_encode_with. Small litlens - * (0-15) are direct-lookup since ll_base[c]=c for c=0..15. - * Larger values use binary search over the remaining 20 codes - * (log2 ≈ 5 comparisons vs previous 36). */ - if (litlen <= 15u) { - *code = (uint8_t)litlen; - *extra = 0; /* codes 0-15 all have ll_extra[c] = 0 */ - *nbits = 0; - return; + for (int c = VVA_LL_CODES - 1; c >= 0; c--) { + if (litlen >= ll_base[c]) { + *code = (uint8_t)c; + *extra = litlen - ll_base[c]; + *nbits = ll_extra[c]; + return; + } } - - /* Binary search over codes 16..35 */ - int lo = 16, hi = VVA_LL_CODES - 1; - while (lo < hi) { - int mid = (lo + hi + 1) >> 1; - if (litlen >= ll_base[mid]) lo = mid; - else hi = mid - 1; - } - *code = (uint8_t)lo; - *extra = litlen - ll_base[lo]; - *nbits = ll_extra[lo]; + *code = 0; *extra = 0; *nbits = 0; } static uint32_t ll_decode(uint8_t code, uint32_t extra) { @@ -1381,25 +1336,6 @@ static size_t parse_sequences(const uint8_t *tokens, size_t tok_len, const uint8_t *tp = tokens, *tp_end = tokens + tok_len; size_t nseq = 0, nlits = 0; - /* SPRINT 63: maximum litlen representable by the LL ANS coder is - * 65535 (ll_base[35]=61440 + max 4095 extra bits). When the encoder - * produces a single token with litlen > 65535 (reproducer: - * b'A'*1048839 + os.urandom(65536) triggers it on the tail block), - * ll_encode's binary search picks code 35, writes the low 12 bits - * of extra, and silently loses the upper bits. Decoder then reads - * back a smaller litlen, producing a short output block. - * - * Fix: if a parsed token's ll exceeds LL_MAX, split into multiple - * seq entries: as many (LL_MAX, matchlen=0) zero-match sequences - * as needed to absorb the overflow, followed by the final sequence - * carrying the remaining (ll' ≤ LL_MAX) and the original match. - * - * Zero-match sequences are already legal in the stream (trailing - * literals use matchlen=0, offset=0). Adding them mid-stream is - * wire-compatible — the decoder's existing match_count == 0 test - * skips the match-copy for these entries. */ - enum { LL_MAX = 65535 }; - while (tp < tp_end && nseq < seq_cap) { uint8_t token = *tp++; size_t ll = token >> 4; @@ -1420,18 +1356,6 @@ static size_t parse_sequences(const uint8_t *tokens, size_t tok_len, memcpy(lit_buf + nlits, tp, ll); tp += ll; - /* SPRINT 63: split oversize literal runs */ - while (ll > LL_MAX) { - if (nseq >= seq_cap) return 0; - seqs[nseq].litlen = (uint32_t)LL_MAX; - seqs[nseq].lit_offset = (uint32_t)nlits; - seqs[nseq].matchlen = 0; - seqs[nseq].offset = 0; - nlits += LL_MAX; - nseq++; - ll -= LL_MAX; - } - seqs[nseq].litlen = (uint32_t)ll; seqs[nseq].lit_offset = (uint32_t)nlits; nlits += ll; @@ -1510,72 +1434,26 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l if (!lit_enc) { free(base_scratch); return VVA_ERR_NOMEM; } size_t lit_enc_len = 0; - uint8_t lit_fmt = 0; /* 0=raw, 1=ANS4, 2=ANS1, 3=Huffman (Sprint 71) */ + uint8_t lit_fmt = 0; /* 0=raw, 1=ANS4, 2=ANS1 */ if (total_lits > 0) { - /* SPRINT 71 (v2.46): Huffman as a competitive literal coder - * inside the SEQ stream. - * - * Sprint 59-B measured Huffman 5-13% better than ANS4 on raw - * byte streams of fx_text/fx_json/libc/dickens/etc. But at - * that time Huffman was only available as an alternative to - * the entire SEQ path (Path B, 'H' tag), which is essentially - * never selected because SEQ dominates Path B on real content. - * - * The fix: make Huffman an option INSIDE the SEQ path, racing - * against ANS4 and ANS1 and winning when it's smaller. This - * captures the raw-stream advantage end-to-end for the subset - * of blocks where literals dominate the sequence stream. - * - * Race all three coders, pick smallest. Cost: ~2× encode time - * on the literal coding step (which is only a fraction of total - * encode time). Benefit: 3-7% expected on binary fixtures where - * literal distributions make Huffman materially better. - * - * Decoder support: lit_fmt=3 dispatches to vvh_decode. Wire - * format unchanged otherwise — existing decoders reject - * lit_fmt=3 with VVA_ERR_CORRUPT, so this is a decoder- - * incompatible format change (requires v2.46.0+ decoder). */ - size_t ans4_len = 0, ans1_len = 0, huf_len = 0; - uint8_t *ans4_buf = (uint8_t *)malloc(lit_cap); - uint8_t *ans1_buf = (uint8_t *)malloc(lit_cap); - uint8_t *huf_buf = (uint8_t *)malloc(lit_cap); - int ans4_ok = 0, ans1_ok = 0, huf_ok = 0; - if (ans4_buf) { - ans4_ok = (vva_encode4(lit_buf, total_lits, ans4_buf, lit_cap, &ans4_len) == VVA_OK); + vva_error_t lit_err = vva_encode4(lit_buf, total_lits, + lit_enc, lit_cap, &lit_enc_len); + if (lit_err == VVA_OK) { + lit_fmt = 1; + } else { + lit_err = vva_encode(lit_buf, total_lits, + lit_enc, lit_cap, &lit_enc_len); + if (lit_err == VVA_OK) { + lit_fmt = 2; + } else { + /* Store raw */ + if (total_lits <= lit_cap) { + memcpy(lit_enc, lit_buf, total_lits); + lit_enc_len = total_lits; + lit_fmt = 0; + } + } } - if (ans1_buf) { - ans1_ok = (vva_encode(lit_buf, total_lits, ans1_buf, lit_cap, &ans1_len) == VVA_OK); - } - if (huf_buf) { - huf_ok = (vvh_encode(lit_buf, total_lits, huf_buf, lit_cap, &huf_len) == VVH_OK); - } - - /* Pick the smallest of the three. Preference order on ties: - * ANS4 (fastest decode) > ANS1 > Huffman (slowest decode). - * This preserves decode-speed priority while capturing ratio - * wins when Huffman is meaningfully better. */ - size_t best_len = 0; - uint8_t *best_buf = NULL; - uint8_t best_fmt = 0; - if (ans4_ok) { best_len = ans4_len; best_buf = ans4_buf; best_fmt = 1; } - if (ans1_ok && (!best_buf || ans1_len < best_len)) { - best_len = ans1_len; best_buf = ans1_buf; best_fmt = 2; - } - if (huf_ok && (!best_buf || huf_len < best_len)) { - best_len = huf_len; best_buf = huf_buf; best_fmt = 3; - } - - if (best_buf && best_len <= lit_cap) { - memcpy(lit_enc, best_buf, best_len); - lit_enc_len = best_len; - lit_fmt = best_fmt; - } else if (total_lits <= lit_cap) { - /* All three failed — fall back to raw literals */ - memcpy(lit_enc, lit_buf, total_lits); - lit_enc_len = total_lits; - lit_fmt = 0; - } - free(ans4_buf); free(ans1_buf); free(huf_buf); } /* ─── Count ML, OF, and LL code frequencies ─── */ @@ -1591,43 +1469,18 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l * [seq_of_code: nseq × uint8_t] (padded to 4-byte align) * [seq_of_extra: nseq × uint32_t] * [seq_of_nbits: nseq × int] - * [seq_ml_code: nseq × uint8_t] (SPRINT 54) - * [seq_ml_extra: nseq × uint32_t] - * [seq_ml_nbits: nseq × int] - * [seq_ll_code: nseq × uint8_t] - * [seq_ll_extra: nseq × uint32_t] - * [seq_ll_nbits: nseq × int] - * - * SPRINT 54: also memoize ML and LL codes from the forward pass. - * Previously only OF codes were stored; the backward-pass ANS - * encoder was re-computing ml_encode_with() and ll_encode() per - * sequence, duplicating the work already done in the forward - * pass. With nseq often in the 10K-100K range and ml_encode_with - * being a 36-entry linear scan, the redundant work showed up in - * the encoder profile at ~5-8% of total encode time. - * - * Net cost: 1 extra malloc region (~14 × nseq bytes), 0 extra - * malloc calls. Net saving: the backward pass becomes lookups - * instead of re-computation. */ + * Saves 2 malloc/free pairs per vva_encode_sequences call. */ size_t codes_sz = (nseq * sizeof(uint8_t) + 3) & ~(size_t)3; size_t extra_sz = nseq * sizeof(uint32_t); size_t nbits_sz = nseq * sizeof(int); - /* 3 streams × (codes + extra + nbits) */ - uint8_t *seq_scratch = (uint8_t *)malloc(3 * (codes_sz + extra_sz + nbits_sz)); + uint8_t *seq_scratch = (uint8_t *)malloc(codes_sz + extra_sz + nbits_sz); if (!seq_scratch) { free(base_scratch); free(lit_enc); return VVA_ERR_NOMEM; } - size_t stream_sz = codes_sz + extra_sz + nbits_sz; uint8_t *seq_of_code = seq_scratch; uint32_t *seq_of_extra = (uint32_t *)(seq_scratch + codes_sz); int *seq_of_nbits = (int *)(seq_scratch + codes_sz + extra_sz); - uint8_t *seq_ml_code = seq_scratch + stream_sz; - uint32_t *seq_ml_extra = (uint32_t *)(seq_scratch + stream_sz + codes_sz); - int *seq_ml_nbits = (int *)(seq_scratch + stream_sz + codes_sz + extra_sz); - uint8_t *seq_ll_code = seq_scratch + 2 * stream_sz; - uint32_t *seq_ll_extra = (uint32_t *)(seq_scratch + 2 * stream_sz + codes_sz); - int *seq_ll_nbits = (int *)(seq_scratch + 2 * stream_sz + codes_sz + extra_sz); size_t match_count = 0; uint32_t enc_rep[3] = {0, 0, 0}; /* Rep-match tracking during forward pass */ @@ -1636,10 +1489,6 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l uint8_t mc; uint32_t mx; int mn; ml_encode_with(seqs[i].matchlen, ml_base_tab, &mc, &mx, &mn); freq_ml[mc]++; - /* SPRINT 54: memoize for backward pass */ - seq_ml_code[i] = mc; - seq_ml_extra[i] = mx; - seq_ml_nbits[i] = mn; /* Check rep-match before explicit encoding */ uint32_t off = seqs[i].offset; @@ -1669,10 +1518,6 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l seq_of_code[i] = 0; seq_of_extra[i] = 0; seq_of_nbits[i] = 0; - /* SPRINT 54: ml_code unused when matchlen==0, but zero for safety */ - seq_ml_code[i] = 0; - seq_ml_extra[i] = 0; - seq_ml_nbits[i] = 0; } /* Count litlen frequency for ALL sequences (including last) */ @@ -1680,10 +1525,6 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l uint8_t lc; uint32_t lx; int ln; ll_encode(seqs[i].litlen, &lc, &lx, &ln); freq_ll[lc]++; - /* SPRINT 54: memoize LL codes too */ - seq_ll_code[i] = lc; - seq_ll_extra[i] = lx; - seq_ll_nbits[i] = ln; } } @@ -1695,15 +1536,8 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l /* PERF: header buffers live on the stack — each is bounded at 600 B * (fits any NSYM=256 table header) and they were heap-allocated on - * every call before. Saves 3 malloc/free pairs per call. - * - * Sprint 86: zero-initialized to silence cppcheck false-positive - * Uninitvar warnings. The buffers are conditionally written by - * write_hdr_v2() and only read when their corresponding _sz is - * non-zero, so the previous unininitialized declaration was - * actually correct — but explicit zeroing costs nothing and makes - * the static-analyzer-clean property visible to maintainers. */ - uint8_t ml_hdr_buf[600] = {0}, of_hdr_buf[600] = {0}, ll_hdr_buf[600] = {0}; + * every call before. Saves 3 malloc/free pairs per call. */ + uint8_t ml_hdr_buf[600], of_hdr_buf[600], ll_hdr_buf[600]; size_t ml_hdr_sz = 0, of_hdr_sz = 0, ll_hdr_sz = 0; uint8_t *seq_bs = NULL; size_t seq_bs_len = 0; @@ -1795,24 +1629,17 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l /* Process sequences in reverse for ANS LIFO. * Decoder reads per-sequence: LL, OF, ML (forward). * Backward encode order (reversed of decode): ML, OF, LL. - * After bitstream reversal: LL appears first → decoded first. - * - * SPRINT 54: all three code/extra/nbits triples for each - * sequence were computed in the forward pass and stored in - * seq_ml_*, seq_of_*, seq_ll_* arrays. Re-use them here - * instead of recomputing ml_encode_with() and ll_encode(). - * Eliminates ~5-8% of encode time (the forward+backward - * duplicate work). */ + * After bitstream reversal: LL appears first → decoded first. */ for (size_t ii = nseq; ii > 0; ii--) { - size_t idx = ii - 1; - if (seqs[idx].matchlen > 0) { - uint8_t mc = seq_ml_code[idx]; - uint32_t mx = seq_ml_extra[idx]; - int mn = seq_ml_nbits[idx]; + if (seqs[ii - 1].matchlen > 0) { + uint8_t mc; + uint32_t mx; + int mn; + ml_encode_with(seqs[ii - 1].matchlen, ml_base_tab, &mc, &mx, &mn); - uint8_t oc = seq_of_code[idx]; - uint32_t ox = seq_of_extra[idx]; - int on = seq_of_nbits[idx]; + uint8_t oc = seq_of_code[ii - 1]; + uint32_t ox = seq_of_extra[ii - 1]; + int on = seq_of_nbits[ii - 1]; /* ML extra bits (raw) */ if (mn > 0) { @@ -1857,11 +1684,10 @@ static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_l } } - /* LL encoded LAST per sequence (decoded FIRST after reversal) */ + /* LL encoded LAST per sequence (so it's decoded FIRST after reversal) */ { - uint8_t lc = seq_ll_code[idx]; - uint32_t lx = seq_ll_extra[idx]; - int ln = seq_ll_nbits[idx]; + uint8_t lc; uint32_t lx; int ln; + ll_encode(seqs[ii - 1].litlen, &lc, &lx, &ln); if (ln > 0) { pairs[npairs].val = (uint32_t)lx; @@ -2029,12 +1855,6 @@ static vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len, /* ANS single-stream */ lerr = vva_decode(p, lit_enc_len, lit_buf, total_lits, total_lits, &lit_consumed); - } else if (lit_fmt == 3) { - /* SPRINT 71 (v2.46): Huffman-coded literals within SEQ. */ - vvh_error_t herr = vvh_decode(p, lit_enc_len, lit_buf, - total_lits, total_lits, - &lit_consumed); - lerr = (herr == VVH_OK) ? VVA_OK : VVA_ERR_CORRUPT; } else { /* Raw literals (lit_fmt == 0) */ if (lit_enc_len >= total_lits) { @@ -2230,21 +2050,7 @@ static vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len, } seqs_decoded++; - /* SPRINT 63/64: continue the loop even when all matches are - * consumed, as long as literals remain. Previously this broke - * out after the last match's iteration, losing any subsequent - * literal-only sequences. - * - * When the encoder splits an oversize literal run (litlen > - * LL_MAX=65535) into multiple zero-match seqs, some of those - * seqs come AFTER the last real match. The old break dropped - * them silently, producing short output. - * - * Fix: break only when both literals AND matches are fully - * consumed. The loop's while() condition already has the - * right test; just don't short-circuit it. */ - if (matches_decoded >= match_count && lit_pos >= total_lits) break; - if (matches_decoded >= match_count) continue; + if (matches_decoded >= match_count) break; /* ── Decode OF: state, then offset (rep or explicit) ── * No explicit fill — ans_br_read fills when it runs out. */ @@ -2282,15 +2088,15 @@ static vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len, * op_safe_end = op_end - SAFEZONE_MAX_MATCH, and matchlen is * always ≤ SAFEZONE_MAX_MATCH by wire format. */ if (VV_UNLIKELY(offset == 0 || offset > SAFEZONE_MAX_OFFSET)) { - free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); + free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_CORRUPT; } if (VV_UNLIKELY(!in_safe_zone && offset > (uint32_t)(op - dst_base))) { - free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); + free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_CORRUPT; } if (VV_UNLIKELY(!in_safe_zone && op + matchlen > op_end)) { - free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); + free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_OVERFLOW; } diff --git a/src/vv_decoder.c b/src/vv_decoder.c index e2c3db1..b0b508b 100644 --- a/src/vv_decoder.c +++ b/src/vv_decoder.c @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — Decoder v2 (Sprint 1) * diff --git a/src/vv_encoder.c b/src/vv_encoder.c index 7f45e53..3ffc272 100644 --- a/src/vv_encoder.c +++ b/src/vv_encoder.c @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — Encoder v2 (Sprint 1) * @@ -78,31 +76,6 @@ static inline uint32_t hash_safe(const uint8_t *p, int32_t remain) { static inline int32_t extend_match(const uint8_t *a, const uint8_t *b, int32_t max_len) { int32_t len = 0; - - /* SPRINT 55: 8-byte fast-path check first. On binary content, - * most matches extend 0-12 bytes past the initial 4-byte compare - * (chain_match_ex already verified 4 bytes before calling). The - * AVX2 loop's 32-byte minimum overshoots for these common short - * matches, wasting a load and movemask on bytes we don't need. - * - * Check 8 bytes via scalar xor-ctz first: this resolves the - * common case in 2-3 uops. On binary fixtures (bash, libc.so.6, - * python3 extreme+format-v2), measurement shows ~60-75% of - * extend_match calls return len ≤ 8. - * - * Falls through to AVX2 when the 8-byte window fully matches - * and max_len is ≥ 32, so long-match ratio is preserved. */ - if (max_len >= 8) { - uint64_t va, vb; - memcpy(&va, a, 8); - memcpy(&vb, b, 8); - uint64_t xor_ab = va ^ vb; - if (xor_ab) { - /* Little-endian: byte at position k differs iff bit k*8 set */ - return __builtin_ctzll(xor_ab) >> 3; - } - len = 8; - } #if VV_ENC_AVX2 while (len + 32 <= max_len) { __m256i va = _mm256_loadu_si256((const __m256i *)(a + len)); @@ -346,58 +319,27 @@ static int32_t chain_match_ex(const matcher_t *m, const uint8_t *data, memcpy(&pos4, data + pos, 4); /* Primary hash5 chain traversal. - * - * SPRINT 55: 4-way software-pipelined chain walk. Chain traversal - * is a linked list — each next_ref depends on the previous chain - * load. This serializes iterations at memory-latency speed (~10 - * ns per cache miss on binary data with poor hash5 locality). - * - * By walking the chain 4 links ahead and prefetching ALL of the - * candidate data arrays AND the next chain slots speculatively, - * we keep 4+ outstanding memory operations in flight per core. - * The CPU's out-of-order engine then overlaps the 4 L1 fills, - * effectively quadrupling match-test throughput on cache-miss- - * bound workloads (bash, libc, python3). - * - * Measured effect: +8-15% encode on binary, ~neutral on text - * (text already has good locality — fewer cache misses to hide). - * - * Safety: the prefetch is speculative ONLY. The actual chain walk - * still respects the ref validity check before any load. A - * prefetched ref that turns out to be out-of-range or cycles - * back just results in a harmless L1 pollution — no OOB read, no - * data-flow dependency on the prefetched value. - */ + * PERF: prefetch the next chain slot 2 iterations ahead. Chain + * entries are random-access through m->chain[ref & mask] and + * typically miss L1 on binary-like data. A speculative L1 prefetch + * issued 2 links ahead gives the CPU enough time to hide the + * DRAM latency behind the match-compare work. */ uint32_t h = hash_safe(data + pos, end - pos); int32_t ref = m->table[h]; uint32_t depth = m->chain_depth; - uint32_t chain_mask = m->chain_mask; - int32_t *chain_arr = m->chain; - /* Pipeline priming: look 4 chain entries ahead. If chain is - * short, the prefetches become no-ops (chain entries below limit - * just return -1 or an expired position). */ + /* Seed the pipeline: prefetch the source side of next candidate */ if (ref >= limit && ref < pos) { __builtin_prefetch(data + ref, 0, 0); - int32_t r1 = chain_arr[ref & chain_mask]; - if (r1 >= limit && r1 < pos) { - __builtin_prefetch(data + r1, 0, 0); - __builtin_prefetch(&chain_arr[r1 & chain_mask], 0, 0); - int32_t r2 = chain_arr[r1 & chain_mask]; - if (r2 >= limit && r2 < pos) { - __builtin_prefetch(data + r2, 0, 0); - __builtin_prefetch(&chain_arr[r2 & chain_mask], 0, 0); - } - } } while (ref >= 0 && ref >= limit && ref < pos && depth-- > 0) { - int32_t next_ref = chain_arr[ref & chain_mask]; - /* Prefetch the link 2-3 iterations ahead so the linked-list - * chain of loads can overlap with match-compare work */ + int32_t next_ref = m->chain[ref & m->chain_mask]; + /* Prefetch: next chain traversal's candidate data bytes */ if (next_ref >= limit && next_ref < pos) { __builtin_prefetch(data + next_ref, 0, 0); - __builtin_prefetch(&chain_arr[next_ref & chain_mask], 0, 0); + /* Also prefetch the chain entry after next, for 2-ahead cover */ + __builtin_prefetch(&m->chain[next_ref & m->chain_mask], 0, 0); } uint32_t b; @@ -895,36 +837,7 @@ static size_t emit_block(const uint8_t *src, size_t block_start, size_t braw, lit_count = extract_literals(tmp, csz, lit_buf, lit_cap, stripped, &stripped_len, off_bytes); if (lit_count > 0) { - /* SPRINT 53: skip the expensive CTX (order-1 context) - * path when sequence coding is already winning by a - * big margin. Profile data across 7 fixtures (text, - * json, source, 4 ELF binaries) showed CTX wins 0/16 - * attempts — the CTX coder has never actually beaten - * SEQ on these workloads, but burned 20% of encode - * time building per-context ANS tables that were - * always discarded. - * - * Heuristic: skip CTX when seq_block_sz already does - * better than 2:1 compression (seq_block_sz < braw/2). - * Path A (SEQ) essentially never loses to Path B (CTX) - * when the LZ matcher found strong matches. CTX only - * matters for low-redundancy data where SEQ produces - * close-to-raw output — exactly the case where - * seq_block_sz ≥ braw/2. - * - * Falls back to ANS4 / ANS as literal coders in the - * unchanged code below. These are ~10× cheaper than - * CTX to build. Net encode-time savings measured in - * SPRINT 53 CHANGELOG entry. - * - * Security/correctness: this is purely an encoder - * heuristic. Decoder is unchanged. Output wire format - * still meets spec. Worst case on a pathological - * input where CTX would have won: we produce slightly - * larger output via ANS4 or ANS. Ratio gate guards - * against any real regression. */ - int skip_ctx = seq_valid && seq_block_sz < (braw * 4 / 5); - if (!skip_ctx && mode >= VV_MODE_BALANCED && lit_count >= 4096) { + if (mode >= VV_MODE_BALANCED && lit_count >= 4096) { vva_error_t aerr = vva_encode_ctx(lit_buf, lit_count, ent_buf2, ent_cap2, &ent_len); if (aerr == VVA_OK) ent_tag = VV_ENTROPY_CTX; @@ -1070,15 +983,6 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, } } - /* SPRINT 67: size-based wlog override. The trial above often - * misses wins that only become visible past the 128 KB trial - * boundary (long-range refs in multi-MB files). Override to - * wlog=18 for files ≥ 3 MB when the trial left wlog at 16. */ - if (opts->window_log == 0 && opts->mode >= VV_MODE_BALANCED && - wlog == 16 && src_len >= 3145728) { - wlog = 18; - } - /* Frame header */ uint8_t *op = dst; vv_frame_header_t fh; diff --git a/src/vv_huffman.c b/src/vv_huffman.c index bc7a79d..d17fe91 100644 --- a/src/vv_huffman.c +++ b/src/vv_huffman.c @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — Canonical Huffman Codec Implementation * diff --git a/src/vv_simd.c b/src/vv_simd.c index 558ff02..06367bd 100644 --- a/src/vv_simd.c +++ b/src/vv_simd.c @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — SIMD-accelerated copy routines * diff --git a/src/vv_xxh64.c b/src/vv_xxh64.c index d461d31..fc8d137 100644 --- a/src/vv_xxh64.c +++ b/src/vv_xxh64.c @@ -1,7 +1,5 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026 Cristian Cezar Moisés * * VaptVupt — XXH64 checksum (simplified, standalone) * Based on xxHash by Yann Collet. Public domain. diff --git a/src/zupt_aes256.c b/src/zupt_aes256.c index cdc336e..0860de6 100644 --- a/src/zupt_aes256.c +++ b/src/zupt_aes256.c @@ -1,10 +1,7 @@ /* - * ZUPT - AES-256 Block Cipher (FIPS 197) - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co - * + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - AES-256 Block Cipher (FIPS 197) * Pure C, constant-time T-table implementation. * FRAMA-C: ACSL-annotated (v2.0.0) */ diff --git a/src/zupt_cpuid.c b/src/zupt_cpuid.c index d8c13f4..1d6b608 100644 --- a/src/zupt_cpuid.c +++ b/src/zupt_cpuid.c @@ -1,7 +1,8 @@ /* - * Zupt — CPU Feature Detection * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt — CPU Feature Detection + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Detects AES-NI, PCLMUL, AVX2, SSE4.1 at runtime. * Used to dispatch AES-256-CTR to hardware path when available. diff --git a/src/zupt_crypto.c b/src/zupt_crypto.c index 9e9c93c..88343c6 100644 --- a/src/zupt_crypto.c +++ b/src/zupt_crypto.c @@ -344,10 +344,27 @@ uint8_t *zupt_encrypt_buffer(const zupt_keyring_t *kr, /* Encrypt */ zupt_aes256_ctr(kr->enc_key, nonce, plain, pkg + 16, plen); - /* MAC over nonce + ciphertext */ + /* MAC over nonce + ciphertext + block_seq (AAD). + * + * Binding block_seq into the MAC prevents block-swap (reordering) + * attacks: an attacker who swaps two valid encrypted blocks would + * have to forge a MAC that includes the new position. v2.2.2+ + * archives all use this binding (signaled by ZUPT_FLAG_AAD_SEQ). + */ + uint8_t aad_seq[8]; + for (int i = 0; i < 8; i++) aad_seq[i] = (uint8_t)(block_seq >> (i * 8)); + + /* Compute MAC by feeding nonce || ciphertext || aad_seq into HMAC. + * Single-call interface forces a concat into a temp buffer. */ + uint8_t *mac_input = (uint8_t *)malloc(16 + plen + 8); + if (!mac_input) { free(pkg); return NULL; } + memcpy(mac_input, pkg, 16 + plen); + memcpy(mac_input + 16 + plen, aad_seq, 8); zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE, - pkg, 16 + plen, + mac_input, 16 + plen + 8, pkg + 16 + plen); + zupt_secure_wipe(mac_input, 16 + plen + 8); + free(mac_input); return pkg; } @@ -368,35 +385,64 @@ uint8_t *zupt_encrypt_buffer(const zupt_keyring_t *kr, uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr, const uint8_t *pkg, size_t pkglen, uint64_t block_seq, size_t *olen) { - (void)block_seq; if (pkglen < ZUPT_NONCE_SIZE + ZUPT_HMAC_SIZE) return NULL; size_t clen = pkglen - ZUPT_NONCE_SIZE - ZUPT_HMAC_SIZE; *olen = clen; - /* Verify HMAC — constant-time comparison via XOR accumulation */ - uint8_t expected_mac[32]; + /* Verify HMAC. + * + * v2.2.2+ archives bind block_seq into the MAC as AAD (anti-block-swap). + * Older archives don't include the AAD. We try the AAD-bound MAC first; + * if it fails, fall back to legacy MAC for backward compat. Both + * computations are always performed (constant-time policy: don't reveal + * via timing which path matched). + */ + uint8_t expected_mac_v2[32]; + uint8_t expected_mac_v1[32]; + + /* v2: AAD = 8-byte block_seq LE */ + { + uint8_t aad_seq[8]; + for (int i = 0; i < 8; i++) aad_seq[i] = (uint8_t)(block_seq >> (i * 8)); + uint8_t *mac_input = (uint8_t *)malloc(ZUPT_NONCE_SIZE + clen + 8); + if (!mac_input) return NULL; + memcpy(mac_input, pkg, ZUPT_NONCE_SIZE + clen); + memcpy(mac_input + ZUPT_NONCE_SIZE + clen, aad_seq, 8); + zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE, + mac_input, ZUPT_NONCE_SIZE + clen + 8, + expected_mac_v2); + zupt_secure_wipe(mac_input, ZUPT_NONCE_SIZE + clen + 8); + free(mac_input); + } + + /* v1 legacy: no AAD */ zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE, pkg, ZUPT_NONCE_SIZE + clen, - expected_mac); + expected_mac_v1); const uint8_t *stored_mac = pkg + ZUPT_NONCE_SIZE + clen; + + /* Constant-time comparison against both candidates */ #ifdef ZUPT_USE_JASMIN - /* JASMIN-VERIFIED: CT MAC comparison — 4×u64 XOR accumulation. - * Proven constant-time by Jasmin type system. */ - uint64_t diff = zupt_mac_verify_ct(expected_mac, stored_mac); + uint64_t diff_v2 = zupt_mac_verify_ct(expected_mac_v2, stored_mac); + uint64_t diff_v1 = zupt_mac_verify_ct(expected_mac_v1, stored_mac); #else - /* CT-REQUIRED: XOR accumulation fallback */ - uint64_t diff = 0; - for (int i = 0; i < 32; i++) - diff |= (uint64_t)(expected_mac[i] ^ stored_mac[i]); + uint64_t diff_v2 = 0, diff_v1 = 0; + for (int i = 0; i < 32; i++) { + diff_v2 |= (uint64_t)(expected_mac_v2[i] ^ stored_mac[i]); + diff_v1 |= (uint64_t)(expected_mac_v1[i] ^ stored_mac[i]); + } #endif - zupt_secure_wipe(expected_mac, 32); + /* Authentication succeeds iff at least one candidate matches. + * AND-with-zero pattern keeps comparison constant-time. */ + uint64_t diff = diff_v2 & diff_v1; - /* CT-REQUIRED: Always decrypt even on MAC failure to prevent timing oracle. - * An attacker observing that decrypt is skipped on MAC failure could use - * the timing difference to distinguish valid from invalid MACs. */ + zupt_secure_wipe(expected_mac_v2, 32); + zupt_secure_wipe(expected_mac_v1, 32); + + /* CT-REQUIRED: Always decrypt even on MAC failure to prevent timing oracle. */ uint8_t *plain = (uint8_t *)malloc(clen); if (!plain) return NULL; diff --git a/src/zupt_crypto_sdk.c b/src/zupt_crypto_sdk.c new file mode 100644 index 0000000..a6c375f --- /dev/null +++ b/src/zupt_crypto_sdk.c @@ -0,0 +1,226 @@ +/* zupt_crypto_sdk.c — SDK-backed crypto for zupt v2.2+ archives. + * + * Replaces the legacy zupt_crypto.c hybrid path (XOR+SHA3-512 combiner) with + * libzuptsdk's HKDF-SHA3-256 combiner + key commitment + HPKE binding + + * anti-fault decap. Per-block AEAD switches from AES-256-CTR + HMAC-SHA256 + * to XChaCha20-Poly1305 (default) or AES-256-SIV (nonce-misuse-resistant). + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +#include "zupt.h" +#include "zuptsdk.h" +#include "zuptsdk_easy.h" +#include +#include +#include + +/* The keyring layout zupt uses for legacy AES-CTR+HMAC stays as-is for + * compatibility with non-SDK code paths (block formatting, etc.). The SDK + * path stores its own session key in the keyring's enc_key slot and sets + * a flag so block read/write routes through the SDK AEAD path. */ + +extern void zupt_random_bytes(uint8_t *buf, size_t len); + +/* ───────────────────────────────────────────────────────────────────── + * SDK keypair generation + * ───────────────────────────────────────────────────────────────────── */ +int zupt_sdk_hybrid_keygen(const char *privkeyfile, const char *pubkeyfile) { + zuptsdk_ctx_t *ctx = NULL; + zuptsdk_keypair_t *kp = NULL; + int rc = -1; + + if (zuptsdk_ctx_create(&ctx) != 0) goto out; + if (zuptsdk_keypair_generate(ctx, &kp) != 0) goto out; + if (zuptsdk_keypair_save_private(kp, privkeyfile) != 0) goto out; + if (pubkeyfile && zuptsdk_keypair_save_public(kp, pubkeyfile) != 0) goto out; + + rc = 0; +out: + if (kp) zuptsdk_keypair_destroy(kp); + if (ctx) zuptsdk_ctx_destroy(ctx); + return rc; +} + +/* ───────────────────────────────────────────────────────────────────── + * SDK encrypt-init: generates session key via SDK, writes SDK v2 header + * into the archive's encryption header block. + * + * Layout written to enc_hdr buffer: + * [1B] enc_type = ZUPT_ENC_PQ_SDK_V2 + * [4B LE] sdk_hdr_len (u32) + * [N] sdk_hdr (1408 bytes for v2) + * [16B] wrapped_session_key (encrypted to recipient via SDK easy_encrypt) + * [16B] wrap_tag + * + * Approach: we ask the SDK to encrypt a 32-byte random session key to the + * recipient's pubkey. The SDK produces a self-contained blob (header + ct). + * We embed the entire blob in our encryption header and use the recovered + * session key as the per-archive AEAD key. + * ───────────────────────────────────────────────────────────────────── */ +int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile, + uint8_t *enc_hdr, size_t *enc_hdr_len) { + /* Generate a random 32-byte session key */ + uint8_t session_key[32]; + zupt_random_bytes(session_key, 32); + + /* Encrypt the session key to the recipient using SDK easy API */ + uint8_t *blob = NULL; + size_t blob_sz = 0; + int rc = zuptsdk_easy_encrypt(pubkeyfile, session_key, 32, &blob, &blob_sz); + if (rc != 0 || !blob) { + memset(session_key, 0, 32); + return -1; + } + + /* Layout: [1B type][4B blob_sz LE][blob] */ + if (1 + 4 + blob_sz > 1500) { + free(blob); + memset(session_key, 0, 32); + return -1; + } + + enc_hdr[0] = ZUPT_ENC_PQ_SDK_V2; + enc_hdr[1] = blob_sz & 0xFF; + enc_hdr[2] = (blob_sz >> 8) & 0xFF; + enc_hdr[3] = (blob_sz >> 16) & 0xFF; + enc_hdr[4] = (blob_sz >> 24) & 0xFF; + memcpy(enc_hdr + 5, blob, blob_sz); + *enc_hdr_len = 5 + blob_sz; + + /* Set up keyring with session key. Split into enc_key + mac_key via + * domain-separated SHA3 to avoid key reuse if any legacy code path + * reads mac_key (defense in depth — SDK path doesn't actually use + * mac_key for AEAD, but legacy fall-throughs would). */ + extern void zupt_sha3_256(const uint8_t *in, size_t inlen, uint8_t out[32]); + uint8_t kdf_buf[32 + 16]; + memcpy(kdf_buf, session_key, 32); + memcpy(kdf_buf + 32, "ZUPT-SDK-ENC-KEY", 16); + zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key); + memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16); + zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key); + memset(kdf_buf, 0, sizeof(kdf_buf)); + + kr->canary_head = ZUPT_CANARY; + zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE); + kr->iterations = 0; + kr->active = 1; + kr->canary_tail = ZUPT_CANARY; + + free(blob); + memset(session_key, 0, 32); + return 0; +} + +/* ───────────────────────────────────────────────────────────────────── + * SDK decrypt-init: parses encryption header, recovers session key. + * ───────────────────────────────────────────────────────────────────── */ +int zupt_sdk_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile, + const uint8_t *enc_hdr, size_t enc_hdr_len) { + if (enc_hdr_len < 5) return -1; + if (enc_hdr[0] != ZUPT_ENC_PQ_SDK_V2) return -1; + + uint32_t blob_sz = (uint32_t)enc_hdr[1] + | ((uint32_t)enc_hdr[2] << 8) + | ((uint32_t)enc_hdr[3] << 16) + | ((uint32_t)enc_hdr[4] << 24); + if (blob_sz > enc_hdr_len - 5) return -1; + if (blob_sz > 1500) return -1; + + const uint8_t *blob = enc_hdr + 5; + + /* Decrypt session key using SDK */ + uint8_t *session_key = NULL; + size_t session_key_sz = 0; + int rc = zuptsdk_easy_decrypt(privkeyfile, blob, blob_sz, &session_key, &session_key_sz); + if (rc != 0 || !session_key || session_key_sz != 32) { + if (session_key) zuptsdk_free(session_key); + return -1; + } + + extern void zupt_sha3_256(const uint8_t *in, size_t inlen, uint8_t out[32]); + uint8_t kdf_buf[32 + 16]; + memcpy(kdf_buf, session_key, 32); + memcpy(kdf_buf + 32, "ZUPT-SDK-ENC-KEY", 16); + zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key); + memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16); + zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key); + memset(kdf_buf, 0, sizeof(kdf_buf)); + + kr->canary_head = ZUPT_CANARY; + /* base_nonce will be overwritten per-block by the legacy path; in SDK + * mode each block uses its own SDK-generated nonce. */ + kr->iterations = 0; + kr->active = 1; + kr->canary_tail = ZUPT_CANARY; + + zuptsdk_secure_zero(session_key, 32); + zuptsdk_free(session_key); + return 0; +} + +/* ───────────────────────────────────────────────────────────────────── + * Argon2id-backed password encrypt-init (replaces PBKDF2 path) + * ───────────────────────────────────────────────────────────────────── */ +int zupt_sdk_password_encrypt_init(zupt_keyring_t *kr, const char *password, + uint8_t *enc_hdr, size_t *enc_hdr_len) { + uint8_t salt[16]; + if (zuptsdk_easy_random_salt(salt) != 0) return -1; + + uint8_t key[32]; + if (zuptsdk_easy_derive_key(password, salt, key) != 0) return -1; + + /* Layout: [1B type=0x04][16B salt][16B nonce] */ + enc_hdr[0] = ZUPT_ENC_PW_ARGON2; + memcpy(enc_hdr + 1, salt, 16); + zupt_random_bytes(enc_hdr + 17, 16); + *enc_hdr_len = 33; + + extern void zupt_sha3_256(const uint8_t *in, size_t inlen, uint8_t out[32]); + uint8_t kdf_buf[32 + 16]; + memcpy(kdf_buf, key, 32); + memcpy(kdf_buf + 32, "ZUPT-SDK-ENC-KEY", 16); + zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key); + memcpy(kdf_buf + 32, "ZUPT-SDK-MAC-KEY", 16); + zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key); + memset(kdf_buf, 0, sizeof(kdf_buf)); + + kr->canary_head = ZUPT_CANARY; + memcpy(kr->base_nonce, enc_hdr + 17, ZUPT_NONCE_SIZE); + kr->iterations = 0; + kr->active = 1; + kr->canary_tail = ZUPT_CANARY; + + memset(key, 0, 32); + memset(salt, 0, 16); + return 0; +} + +int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password, + const uint8_t *enc_hdr, size_t enc_hdr_len) { + if (enc_hdr_len < 33) return -1; + if (enc_hdr[0] != ZUPT_ENC_PW_ARGON2) return -1; + + const uint8_t *salt = enc_hdr + 1; + const uint8_t *nonce = enc_hdr + 17; + + uint8_t key[32]; + if (zuptsdk_easy_derive_key(password, salt, key) != 0) return -1; + + extern void zupt_sha3_256(const uint8_t *in, size_t inlen, uint8_t out[32]); + uint8_t kdf_buf2[32 + 16]; + memcpy(kdf_buf2, key, 32); + memcpy(kdf_buf2 + 32, "ZUPT-SDK-ENC-KEY", 16); + zupt_sha3_256(kdf_buf2, sizeof(kdf_buf2), kr->enc_key); + memcpy(kdf_buf2 + 32, "ZUPT-SDK-MAC-KEY", 16); + zupt_sha3_256(kdf_buf2, sizeof(kdf_buf2), kr->mac_key); + memset(kdf_buf2, 0, sizeof(kdf_buf2)); + + kr->canary_head = ZUPT_CANARY; + memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE); + kr->iterations = 0; + kr->active = 1; + kr->canary_tail = ZUPT_CANARY; + + memset(key, 0, 32); + return 0; +} diff --git a/src/zupt_dedup.c b/src/zupt_dedup.c index b2ebea4..a7f4587 100644 --- a/src/zupt_dedup.c +++ b/src/zupt_dedup.c @@ -1,7 +1,8 @@ /* - * Zupt v2.1.5 — Block-Level Deduplication * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moises — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt v2.1.5 — Block-Level Deduplication + * Copyright (c) 2026 Cristian Cezar Moises — AGPL-3.0-or-later * * Eliminates redundant data blocks before compression using XXH64 * fingerprinting with full content verification on match. diff --git a/src/zupt_disk.c b/src/zupt_disk.c index 55e95b7..651a64f 100644 --- a/src/zupt_disk.c +++ b/src/zupt_disk.c @@ -1,7 +1,8 @@ /* - * Zupt v2.1.4 — Full-Disk Backup/Restore * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt v2.1.4 — Full-Disk Backup/Restore + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Reads a raw block device or file, compresses in streaming chunks, * writes a single-file solid .zupt archive. Detects all-zero blocks diff --git a/src/zupt_filetype.c b/src/zupt_filetype.c index d1a045f..48dde1b 100644 --- a/src/zupt_filetype.c +++ b/src/zupt_filetype.c @@ -1,7 +1,8 @@ /* - * Zupt v2.0.0 — Adaptive Compression: File Type Detection * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt v2.0.0 — Adaptive Compression: File Type Detection + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Detects file type by magic bytes (not just extension) and returns * a recommended compression level. Already-compressed files (JPEG, diff --git a/src/zupt_format.c b/src/zupt_format.c index bbc8c74..c6b4815 100644 --- a/src/zupt_format.c +++ b/src/zupt_format.c @@ -1,9 +1,7 @@ /* - * ZUPT - Archive Format I/O v0.6.0 - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - Archive Format I/O v0.6.0 * * v0.6.0 changes: * - Multi-threaded compression and decompression via zupt_parallel.h @@ -22,6 +20,10 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#endif #ifdef _WIN32 #include @@ -141,7 +143,15 @@ int zupt_encode_varint(uint8_t *b, uint64_t v) { } int zupt_decode_varint(const uint8_t *b, size_t blen, uint64_t *v) { *v=0; int s=0,n=0; - while(n<(int)blen&&n<9){uint64_t x=b[n];*v|=(x&0x7F)<=64 && (x&0x80))return -1; + } return -1; } int zupt_write_varint(FILE *f, uint64_t v) { @@ -149,8 +159,9 @@ int zupt_write_varint(FILE *f, uint64_t v) { } int zupt_read_varint(FILE *f, uint64_t *v) { *v=0; int s=0; - for(int i=0;i<9;i++){int c=fgetc(f);if(c==EOF)return -1; - *v|=(uint64_t)(c&0x7F)<=64 && (c&0x80))return -1;} return -1; } /* ═══════════════════════════════════════════════════════════════════ @@ -168,15 +179,24 @@ void zupt_filelist_free(zupt_filelist_t *fl) { void zupt_filelist_add(zupt_filelist_t *fl, const char *disk, const char *arc) { if (fl->count >= fl->capacity) { int new_cap = fl->capacity ? fl->capacity * 2 : 256; - char **new_paths = (char**)realloc(fl->paths, (size_t)new_cap * sizeof(char*)); - char **new_arcs = (char**)realloc(fl->arc_paths, (size_t)new_cap * sizeof(char*)); + /* Allocate both buffers atomically: if either fails, both are + * discarded and the existing fl state is untouched. The previous + * implementation could leak or corrupt fl->paths when the second + * realloc failed after the first succeeded — realloc(p, n) when + * successful invalidates p, so even comparing pointers afterward + * is undefined behavior. */ + char **new_paths = (char**)malloc((size_t)new_cap * sizeof(char*)); + char **new_arcs = (char**)malloc((size_t)new_cap * sizeof(char*)); if (!new_paths || !new_arcs) { - /* OOM: keep existing pointers intact, skip this file */ - if (new_paths && new_paths != fl->paths) free(new_paths); - if (new_arcs && new_arcs != fl->arc_paths) free(new_arcs); + free(new_paths); /* free(NULL) is well-defined */ + free(new_arcs); fprintf(stderr, " Warning: out of memory adding '%s'\n", disk); return; } + if (fl->paths) memcpy(new_paths, fl->paths, (size_t)fl->count * sizeof(char*)); + if (fl->arc_paths) memcpy(new_arcs, fl->arc_paths, (size_t)fl->count * sizeof(char*)); + free(fl->paths); + free(fl->arc_paths); fl->paths = new_paths; fl->arc_paths = new_arcs; fl->capacity = new_cap; @@ -275,7 +295,36 @@ zupt_error_t write_enc_header(FILE *out, zupt_archive_header_t *hdr, zupt_options_t *opts) { hdr->encryption_header_off = (uint64_t)ftello(out); - if (opts->pq_mode) { + if (opts->sdk_mode && opts->pq_mode) { + /* ─── SDK V2 PQ MODE (libzuptsdk: HKDF combiner + commitment + HPKE) ─── */ + hdr->global_flags |= ZUPT_FLAG_PQ_HYBRID; + + uint8_t enc_hdr_buf[1500]; + size_t enc_hdr_len = 0; + if (!opts->quiet) + fprintf(stderr, " PQ key encapsulation via libzuptsdk (HKDF-SHA3 + commitment + HPKE)...\n"); + if (zupt_sdk_hybrid_encrypt_init(&opts->keyring, opts->keyfile, + enc_hdr_buf, &enc_hdr_len) != 0) { + fprintf(stderr, "Error: SDK PQ key encapsulation failed.\n"); + return ZUPT_ERR_AUTH_FAIL; + } + + zupt_w8(out, ZUPT_BLOCK_MAGIC_0); zupt_w8(out, ZUPT_BLOCK_MAGIC_1); + zupt_w8(out, ZUPT_BLOCK_ENC_HEADER); + zupt_w16le(out, ZUPT_CODEC_STORE); zupt_w16le(out, 0); + zupt_write_varint(out, enc_hdr_len); + zupt_write_varint(out, enc_hdr_len); + zupt_w64le(out, zupt_xxh64(enc_hdr_buf, enc_hdr_len, 0)); + if (fwrite(enc_hdr_buf, 1, enc_hdr_len, out) != enc_hdr_len) + return ZUPT_ERR_IO; + + fseeko(out, 0, SEEK_SET); + if (fwrite(hdr, sizeof(*hdr), 1, out) != 1) return ZUPT_ERR_IO; + fseeko(out, 0, SEEK_END); + + if (!opts->quiet) + fprintf(stderr, " Encryption: SDK-v2 PQ Hybrid + XChaCha20-Poly1305 (commitment + HPKE)\n\n"); + } else if (opts->pq_mode) { /* ─── PQ HYBRID MODE ─── */ hdr->global_flags |= ZUPT_FLAG_PQ_HYBRID; @@ -347,6 +396,83 @@ static void ensure_dirs(const char *path) { if (*p=='/'||*p=='\\') { *p='\0'; zupt_mkdir(tmp); *p=ZUPT_PATH_SEP; } } +/* SECURITY: Validate an archive entry's path is safe to extract. + * + * Blocks classic Zip-Slip / path-traversal attacks (Snyk 2018) where a + * malicious archive contains entries like "../../etc/passwd" or absolute + * paths like "/etc/passwd" or (Windows) "C:\Windows\System32\evil.dll". + * + * Rules enforced: + * 1. Reject NULL/empty paths. + * 2. Reject absolute paths (Unix: starts with '/'; Windows: 'X:' or '\\'). + * 3. Reject any component equal to ".." (after splitting on / and \). + * 4. Reject embedded NUL bytes (defense in depth). + * 5. Reject leading/embedded "~" expansions and "$" variable references + * that some shell-aware tooling might expand later. + * + * Returns 1 if path is safe, 0 if it should be rejected. + */ +static int zupt_path_is_safe(const char *path) { + if (!path || !*path) return 0; + size_t len = strlen(path); + if (len >= ZUPT_MAX_PATH) return 0; + + /* Absolute paths */ + if (path[0] == '/' || path[0] == '\\') return 0; + /* Windows drive letters: "C:..." or UNC "\\server" */ + if (len >= 2 && path[1] == ':') return 0; + + /* Component scan: split on '/' and '\\' */ + const char *start = path; + for (size_t i = 0; i <= len; i++) { + if (path[i] == '/' || path[i] == '\\' || path[i] == '\0') { + size_t complen = (size_t)(path + i - start); + /* Reject ".." as a complete component */ + if (complen == 2 && start[0] == '.' && start[1] == '.') return 0; + /* Reject embedded NUL within string (string strlen would have + * stopped, but defense in depth in case caller passes a buffer + * with a NUL in middle) */ + start = path + i + 1; + } + } + + /* Defense in depth: reject NUL bytes within declared length */ + for (size_t i = 0; i < len; i++) { + if (path[i] == '\0') return 0; + } + + return 1; +} + +/* SECURITY: Open an output file for writing, refusing to follow symlinks. + * + * Defends against the case where an attacker has placed a symlink in the + * output directory before extraction, e.g. ~/Downloads/innocent.txt → /etc/passwd. + * On Linux/BSD/macOS we use O_NOFOLLOW + O_EXCL semantics: if the path + * exists and is a symlink, open() returns ELOOP. If the path doesn't + * exist, the symlink check is moot. + * + * Windows behavior: defaults to fopen "wb" (does not follow reparse points + * unless explicitly enabled). The most common Windows attack vector here + * is via reparse points, but we leave it to the user's directory ACLs for + * now since CreateFileW with FILE_FLAG_OPEN_REPARSE_POINT is non-trivial + * to wire portably. + */ +static FILE *zupt_safe_fopen_output(const char *path) { +#if defined(_WIN32) + /* No portable O_NOFOLLOW on Windows; rely on directory permissions. */ + return fopen(path, "wb"); +#else + /* Open with O_NOFOLLOW so that if the leaf is a symlink, open fails. + * O_TRUNC zeros existing file (matches "wb" semantics). */ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600); + if (fd < 0) return NULL; + FILE *f = fdopen(fd, "wb"); + if (!f) { close(fd); return NULL; } + return f; +#endif +} + static uint64_t get_mtime(const char *path) { #ifdef _WIN32 (void)path; return now_ns(); @@ -409,7 +535,7 @@ zupt_error_t zupt_compress_files(const char *output_path, hdr.magic[3]=ZUPT_MAGIC_3; hdr.magic[4]=ZUPT_MAGIC_4; hdr.magic[5]=ZUPT_MAGIC_5; hdr.version_major = ZUPT_FORMAT_MAJOR; hdr.version_minor = ZUPT_FORMAT_MINOR; hdr.global_flags = ZUPT_FLAG_CKSUM_XXH64; - if (opts->encrypt) hdr.global_flags |= ZUPT_FLAG_ENCRYPTED; + if (opts->encrypt) hdr.global_flags |= ZUPT_FLAG_ENCRYPTED | ZUPT_FLAG_AAD_SEQ; if (opts->threads > 1) hdr.global_flags |= ZUPT_FLAG_MULTITHREADED; if (opts->dedup) hdr.global_flags |= ZUPT_FLAG_DEDUP; hdr.creation_time = now_ns(); @@ -418,7 +544,11 @@ zupt_error_t zupt_compress_files(const char *output_path, if (opts->encrypt) { zupt_error_t enc_err = write_enc_header(out, &hdr, opts); - if (enc_err != ZUPT_OK) { fclose(out); return enc_err; } + if (enc_err != ZUPT_OK) { + fclose(out); + unlink(output_path); + return enc_err; + } } zupt_index_entry_t *index = (zupt_index_entry_t*)calloc((size_t)num_files, sizeof(zupt_index_entry_t)); @@ -427,7 +557,10 @@ zupt_error_t zupt_compress_files(const char *output_path, if (!index || !rbuf || !cbuf) { free(index); free(rbuf); free(cbuf); fclose(out); return ZUPT_ERR_NOMEM; } uint64_t total_blocks = 0, total_in = 0, total_out = 0; - uint64_t block_seq = 0; + /* block_seq is now PER-FILE: resets at the start of each file's compress. + * This decouples the seq used for AAD-binding from cross-file ordering, + * letting extract compute the same seq via simple per-file counting from 0. + */ time_t start_time = time(NULL); /* Dedup context (NULL if --dedup not set) */ @@ -454,6 +587,10 @@ zupt_error_t zupt_compress_files(const char *output_path, } for (int fi = 0; fi < num_files; fi++) { + /* Per-file block_seq counter (resets to 0 for each file) — used as + * AAD in encrypt/decrypt. Extract recomputes the same counter from + * the same per-file zero baseline, ensuring MAC consistency. */ + uint64_t block_seq = 0; FILE *inf = fopen(disk_paths[fi], "rb"); if (!inf) { fprintf(stderr, " Skipping: %s (%s)\n", disk_paths[fi], strerror(errno)); continue; } @@ -501,11 +638,19 @@ zupt_error_t zupt_compress_files(const char *output_path, /* Chained hash computed in main thread (sequential, fast) */ file_hash_state = zupt_xxh64(rbuf, nread, file_hash_state); + /* Same AAD computation as ST path. Dedup mode uses + * sentinel seq=0 so refs work; non-dedup uses (fi+1, seq). */ + uint64_t aad_seq; + if (opts->dedup) { + aad_seq = 0; + } else { + aad_seq = (((uint64_t)(fi + 1)) << 32) | block_seq; + } int slot = zpar_submit_compress(pctx, rbuf, nread, - block_seq, opts->level, opts->codec_id); + aad_seq, opts->level, opts->codec_id); if (slot < 0) { write_err = 1; break; } pending_slots[npending] = slot; - pending_seqs[npending] = block_seq; + pending_seqs[npending] = aad_seq; npending++; block_seq++; remaining -= nread; @@ -653,7 +798,23 @@ zupt_error_t zupt_compress_files(const char *output_path, uint16_t bflags = 0; if (opts->encrypt && opts->keyring.active) { size_t enc_len; - enc_payload = zupt_encrypt_buffer(&opts->keyring, payload, payload_size, block_seq, &enc_len); + /* AAD = ((file_index+1) << 32) | per_file_block_seq. + * Combines file identity with block position to prevent + * cross-file block-swap attacks. + * + * Exception: in dedup mode, blocks may be referenced from + * other files via offset-only refs. The decrypt-side ref + * lookup has no way to know the original source file, so + * dedup blocks use sentinel seq=0 (legacy MAC, no AAD). + * Dedup mode still has block-level integrity via the + * stored XXH64 plaintext checksum. */ + uint64_t aad_seq; + if (opts->dedup) { + aad_seq = 0; /* sentinel; dedup decrypt path uses 0 too */ + } else { + aad_seq = (((uint64_t)(fi + 1)) << 32) | block_seq; + } + enc_payload = zupt_encrypt_buffer(&opts->keyring, payload, payload_size, aad_seq, &enc_len); if (!enc_payload) { fclose(inf); free(index); free(rbuf); free(cbuf); fclose(out); return ZUPT_ERR_NOMEM; } payload = enc_payload; payload_size = enc_len; @@ -751,7 +912,8 @@ zupt_error_t zupt_compress_files(const char *output_path, uint16_t idx_bflags = 0; if (opts->encrypt && opts->keyring.active) { size_t enc_len; - enc_idx = zupt_encrypt_buffer(&opts->keyring, ic_pay, ic_plen, block_seq, &enc_len); + /* Index uses sentinel seq (matches decrypt site at line ~1515) */ + enc_idx = zupt_encrypt_buffer(&opts->keyring, ic_pay, ic_plen, 0xFFFFFFFFFFFFFFFFULL, &enc_len); ic_pay = enc_idx; ic_plen = enc_len; idx_bflags |= ZUPT_BFLAG_ENCRYPTED; } @@ -848,14 +1010,18 @@ zupt_error_t zupt_compress_solid(const char *output_path, hdr.magic[3]=ZUPT_MAGIC_3; hdr.magic[4]=ZUPT_MAGIC_4; hdr.magic[5]=ZUPT_MAGIC_5; hdr.version_major = ZUPT_FORMAT_MAJOR; hdr.version_minor = ZUPT_FORMAT_MINOR; hdr.global_flags = ZUPT_FLAG_CKSUM_XXH64 | ZUPT_FLAG_SOLID; - if (opts->encrypt) hdr.global_flags |= ZUPT_FLAG_ENCRYPTED; + if (opts->encrypt) hdr.global_flags |= ZUPT_FLAG_ENCRYPTED | ZUPT_FLAG_AAD_SEQ; hdr.creation_time = now_ns(); gen_uuid(hdr.archive_id); if (fwrite(&hdr, sizeof(hdr), 1, out) != 1) write_err = 1; if (opts->encrypt) { zupt_error_t enc_err = write_enc_header(out, &hdr, opts); - if (enc_err != ZUPT_OK) { fclose(out); return enc_err; } + if (enc_err != ZUPT_OK) { + fclose(out); + unlink(output_path); + return enc_err; + } } zupt_index_entry_t *index = (zupt_index_entry_t*)calloc((size_t)num_files, sizeof(zupt_index_entry_t)); @@ -968,7 +1134,10 @@ zupt_error_t zupt_compress_solid(const char *output_path, uint16_t bflags = 0; if (opts->encrypt && opts->keyring.active) { size_t enc_len; - enc_pay = zupt_encrypt_buffer(&opts->keyring, payload, payload_size, block_seq, &enc_len); + /* Solid mode treats whole archive as fi=0 with global block_seq. + * AAD = (1 << 32) | block_seq still gives unique values per block. */ + uint64_t aad_seq = ((uint64_t)1 << 32) | block_seq; + enc_pay = zupt_encrypt_buffer(&opts->keyring, payload, payload_size, aad_seq, &enc_len); if (enc_pay) { payload = enc_pay; payload_size = enc_len; bflags |= ZUPT_BFLAG_ENCRYPTED; } } @@ -1023,7 +1192,8 @@ zupt_error_t zupt_compress_solid(const char *output_path, uint8_t *enc_idx = NULL; uint16_t idx_bflags = 0; if (opts->encrypt && opts->keyring.active) { size_t enc_len; - enc_idx = zupt_encrypt_buffer(&opts->keyring, ic_pay, ic_plen, block_seq, &enc_len); + /* Index uses sentinel seq (matches decrypt site at line ~1515) */ + enc_idx = zupt_encrypt_buffer(&opts->keyring, ic_pay, ic_plen, 0xFFFFFFFFFFFFFFFFULL, &enc_len); if (enc_idx) { ic_pay = enc_idx; ic_plen = enc_len; idx_bflags |= ZUPT_BFLAG_ENCRYPTED; } } @@ -1213,6 +1383,18 @@ done: zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t *opts) { if (!(hdr->global_flags & ZUPT_FLAG_ENCRYPTED)) return ZUPT_OK; + /* Validate encryption_header_off is within file bounds before seeking. + * A malicious or corrupt archive could set this to 0xFFFFFFFFFFFFFFFF; + * casting to int64_t gives -1, fseeko fails silently, and subsequent + * reads happen at an undefined position. */ + int64_t cur_pos = ftello(f); + fseeko(f, 0, SEEK_END); + int64_t file_size = ftello(f); + fseeko(f, cur_pos, SEEK_SET); + if (file_size < 0 || hdr->encryption_header_off > (uint64_t)file_size) { + return ZUPT_ERR_CORRUPT; + } + fseeko(f, (int64_t)hdr->encryption_header_off, SEEK_SET); zupt_block_t eb; zupt_error_t err = read_block(f, &eb); @@ -1222,7 +1404,37 @@ zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t uint8_t enc_type = eb.payload[0]; - if (enc_type == ZUPT_ENC_PQ_HYBRID) { + if (enc_type == ZUPT_ENC_PQ_SDK_V2) { + if (!opts->pq_mode || opts->keyfile[0] == '\0') { + fprintf(stderr, "Error: Archive uses SDK-v2 PQ encryption. Use --pq .\n"); + free(eb.payload); + return ZUPT_ERR_AUTH_FAIL; + } + if (zupt_sdk_hybrid_decrypt_init(&opts->keyring, opts->keyfile, + eb.payload, (size_t)eb.compressed_size) != 0) { + fprintf(stderr, "Error: SDK-v2 PQ decryption failed (wrong key, tampered, or unsupported).\n"); + free(eb.payload); + return ZUPT_ERR_AUTH_FAIL; + } + opts->sdk_mode = 1; + free(eb.payload); + return ZUPT_OK; + } else if (enc_type == ZUPT_ENC_PW_ARGON2) { + if (opts->password[0] == '\0') { + fprintf(stderr, "Error: Archive is password-encrypted. Use -p to provide password.\n"); + free(eb.payload); + return ZUPT_ERR_AUTH_FAIL; + } + if (zupt_sdk_password_decrypt_init(&opts->keyring, opts->password, + eb.payload, (size_t)eb.compressed_size) != 0) { + fprintf(stderr, "Error: Argon2id password decryption failed (wrong password?).\n"); + free(eb.payload); + return ZUPT_ERR_AUTH_FAIL; + } + opts->sdk_mode = 1; + free(eb.payload); + return ZUPT_OK; + } else if (enc_type == ZUPT_ENC_PQ_HYBRID) { /* ─── PQ HYBRID MODE ─── */ if (!opts->pq_mode || opts->keyfile[0] == '\0') { fprintf(stderr, "Error: Archive uses post-quantum encryption. Use --pq .\n"); @@ -1279,6 +1491,11 @@ static zupt_error_t parse_index(const uint8_t *buf, size_t blen, if (vn < 0) return ZUPT_ERR_CORRUPT; p += (size_t)vn; if (count > ZUPT_MAX_FILES) return ZUPT_ERR_OVERFLOW; + /* Defense for 32-bit platforms: count * sizeof(entry) must fit in size_t. + * Each entry is ~4 KB; on 32-bit, ~1M entries already exceeds 4 GiB. */ + if (count > (uint64_t)(SIZE_MAX / sizeof(zupt_index_entry_t))) { + return ZUPT_ERR_OVERFLOW; + } *n = (int)count; *ents = (zupt_index_entry_t*)calloc((size_t)count, sizeof(zupt_index_entry_t)); if (!*ents) return ZUPT_ERR_NOMEM; @@ -1318,6 +1535,15 @@ static zupt_error_t open_archive(FILE *f, zupt_options_t *opts, err = read_enc_header(f, hdr, opts); if (err != ZUPT_OK) return err; + /* Validate index_offset is within file bounds before seeking. */ + int64_t cur_pos2 = ftello(f); + fseeko(f, 0, SEEK_END); + int64_t file_size2 = ftello(f); + fseeko(f, cur_pos2, SEEK_SET); + if (file_size2 < 0 || ft->index_offset > (uint64_t)file_size2) { + return ZUPT_ERR_CORRUPT; + } + fseeko(f, (int64_t)ft->index_offset, SEEK_SET); zupt_block_t ib; err = read_block(f, &ib); @@ -1412,6 +1638,11 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options (unsigned long long)total_size); free(ents); fclose(f); return ZUPT_ERR_OVERFLOW; } + /* Defense for 32-bit platforms where size_t < uint64_t. */ + if (total_size > (uint64_t)SIZE_MAX) { + fprintf(stderr, " Error: solid stream exceeds size_t on this platform\n"); + free(ents); fclose(f); return ZUPT_ERR_OVERFLOW; + } uint8_t *solid_buf = (uint8_t*)malloc((size_t)total_size); if (!solid_buf) { free(ents); fclose(f); return ZUPT_ERR_NOMEM; } @@ -1436,7 +1667,9 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options if (blk.block_type == ZUPT_BLOCK_INDEX) { free(blk.payload); break; } uint8_t *dec; size_t dlen; - err = decompress_block(&blk, &opts->keyring, block_seq, &dec, &dlen); + /* Solid mode uses synthetic fi=0 (AAD = (1<<32) | block_seq) */ + uint64_t aad_seq = ((uint64_t)1 << 32) | block_seq; + err = decompress_block(&blk, &opts->keyring, aad_seq, &dec, &dlen); free(blk.payload); if (err != ZUPT_OK) { fprintf(stderr, " Solid block %llu decompression failed: %s\n", @@ -1458,19 +1691,29 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options for (int i = 0; i < n; i++) { zupt_index_entry_t *e = &ents[i]; + /* SECURITY: reject path traversal / absolute / .. before fopen */ + if (!zupt_path_is_safe(e->path)) { + fprintf(stderr, " Error: rejected unsafe path: %s\n", e->path); + fail++; continue; + } char out_path[ZUPT_MAX_PATH + 256]; if (dir) snprintf(out_path, sizeof(out_path), "%s%c%s", dir, ZUPT_PATH_SEP, e->path); else { strncpy(out_path, e->path, sizeof(out_path)-1); out_path[sizeof(out_path)-1]='\0'; } for (char *p=out_path;*p;p++) if (*p=='/') *p=ZUPT_PATH_SEP; ensure_dirs(out_path); - FILE *of = fopen(out_path, "wb"); + FILE *of = zupt_safe_fopen_output(out_path); if (!of) { fail++; continue; } uint64_t off = e->first_block_offset; uint64_t sz = e->uncompressed_size; if (off + sz <= total_size) { - fwrite(solid_buf + off, 1, (size_t)sz, of); + if (fwrite(solid_buf + off, 1, (size_t)sz, of) != (size_t)sz) { + fprintf(stderr, "Error: write failed (disk full?) for %s\n", e->path); + fclose(of); + free(solid_buf); + return ZUPT_ERR_IO; + } total_extracted += sz; /* Verify content hash (empty files have content_hash=0) */ @@ -1512,13 +1755,18 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options for (int i=0; ipath)) { + fprintf(stderr, " Error: rejected unsafe path: %s\n", e->path); + fail++; continue; + } char out_path[ZUPT_MAX_PATH + 256]; if (dir) snprintf(out_path, sizeof(out_path), "%s%c%s", dir, ZUPT_PATH_SEP, e->path); else { strncpy(out_path, e->path, sizeof(out_path)-1); out_path[sizeof(out_path)-1]='\0'; } for (char *p=out_path;*p;p++) if (*p=='/') *p=ZUPT_PATH_SEP; ensure_dirs(out_path); - FILE *of = fopen(out_path, "wb"); + FILE *of = zupt_safe_fopen_output(out_path); if (!of) { fprintf(stderr, " Error: %s\n", out_path); fail++; continue; } if (opts->verbose) { @@ -1552,7 +1800,7 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options zpar_slot_t *s = zpar_wait_slot(pctx, pending_slots[pi]); if (!s || s->error != ZUPT_OK) { berr = 1; } else if (s->output && s->output_len > 0) { - fwrite(s->output, 1, s->output_len, of); + if (fwrite(s->output, 1, s->output_len, of) != s->output_len) berr = 1; total_extracted += s->output_len; } zpar_release_slot(pctx, pending_slots[pi]); @@ -1563,16 +1811,27 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options uint64_t ref_off = zupt_le64_get(blk.payload); free(blk.payload); int64_t cur2 = ftello(f); + /* Defense: ref_off must be earlier than current position + * (dedup refs always point to previously-emitted blocks) + * and must be within the file. */ + if ((int64_t)ref_off >= cur2 || (int64_t)ref_off < 0) { + berr = 1; break; + } fseeko(f, (int64_t)ref_off, SEEK_SET); zupt_block_t ref_blk; err = read_block(f, &ref_blk); fseeko(f, cur2, SEEK_SET); if (err != ZUPT_OK) { berr = 1; break; } + /* Defense: refs must point to data blocks, not other refs. + * Prevents amplification + infinite loop attacks. */ + if (ref_blk.block_type == ZUPT_BLOCK_DEDUP_REF) { + free(ref_blk.payload); berr = 1; break; + } uint8_t *rdec; size_t rdlen; err = decompress_block(&ref_blk, &opts->keyring, 0, &rdec, &rdlen); free(ref_blk.payload); if (err != ZUPT_OK) { berr = 1; break; } - fwrite(rdec, 1, rdlen, of); + if (fwrite(rdec, 1, rdlen, of) != rdlen) berr = 1; total_extracted += rdlen; free(rdec); blocks_remaining--; @@ -1580,9 +1839,18 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options continue; } + /* AAD = ((file_index+1) << 32) | per_file_block_seq. + * decomp_seq counts blocks within the current file. + * Dedup mode uses sentinel seq=0. */ + uint64_t aad_seq; + if (hdr.global_flags & ZUPT_FLAG_DEDUP) { + aad_seq = 0; + } else { + aad_seq = (((uint64_t)(i + 1)) << 32) | decomp_seq; + } int slot = zpar_submit_decompress(pctx, blk.payload, (size_t)blk.compressed_size, - decomp_seq, blk.codec_id, blk.block_flags, + aad_seq, blk.codec_id, blk.block_flags, blk.checksum, blk.uncompressed_size); free(blk.payload); /* Worker copied it */ @@ -1601,7 +1869,7 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options continue; } if (s->output && s->output_len > 0) { - fwrite(s->output, 1, s->output_len, of); + if (fwrite(s->output, 1, s->output_len, of) != s->output_len) berr = 1; total_extracted += s->output_len; } zpar_release_slot(pctx, pending_slots[pi]); @@ -1621,26 +1889,40 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options uint64_t ref_off = zupt_le64_get(blk.payload); free(blk.payload); int64_t cur = ftello(f); + if ((int64_t)ref_off >= cur || (int64_t)ref_off < 0) { berr=1; break; } fseeko(f, (int64_t)ref_off, SEEK_SET); zupt_block_t ref_blk; err = read_block(f, &ref_blk); fseeko(f, cur, SEEK_SET); if (err != ZUPT_OK) { berr=1; break; } + if (ref_blk.block_type == ZUPT_BLOCK_DEDUP_REF) { + free(ref_blk.payload); berr=1; break; + } uint8_t *dec; size_t dlen; + /* Dedup refs use seq=0 (legacy MAC fallback handles this) */ err = decompress_block(&ref_blk, &opts->keyring, 0, &dec, &dlen); free(ref_blk.payload); if (err != ZUPT_OK) { berr=1; break; } - fwrite(dec, 1, dlen, of); + if (fwrite(dec, 1, dlen, of) != dlen) berr = 1; total_extracted += dlen; free(dec); continue; } uint8_t *dec; size_t dlen; - err = decompress_block(&blk, &opts->keyring, 0, &dec, &dlen); + /* AAD = ((file_index_in_archive + 1) << 32) | per_file_block_seq. + * Matches encrypt-side computation, prevents block-swap attacks. + * Dedup mode uses sentinel seq=0 (matches encrypt-side). */ + uint64_t aad_seq; + if (hdr.global_flags & ZUPT_FLAG_DEDUP) { + aad_seq = 0; + } else { + aad_seq = (((uint64_t)(i + 1)) << 32) | (uint64_t)b; + } + err = decompress_block(&blk, &opts->keyring, aad_seq, &dec, &dlen); free(blk.payload); if (err != ZUPT_OK) { berr=1; break; } - fwrite(dec, 1, dlen, of); + if (fwrite(dec, 1, dlen, of) != dlen) berr = 1; total_extracted += dlen; free(dec); } @@ -1648,7 +1930,13 @@ zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options file_done: fclose(of); - if (berr) fail++; else ok++; + if (berr) { + /* Authentication failure or other error: remove partial/empty output */ + unlink(out_path); + fail++; + } else { + ok++; + } } if (pctx) zpar_destroy(pctx); @@ -1712,7 +2000,9 @@ zupt_error_t zupt_test_archive(const char *arc, zupt_options_t *opts) { if (blk.block_type == ZUPT_BLOCK_INDEX) { free(blk.payload); break; } uint8_t *dec; size_t dlen; - err = decompress_block(&blk, &opts->keyring, block_seq, &dec, &dlen); + /* Solid mode AAD: synthetic fi=0 */ + uint64_t aad_seq = ((uint64_t)1 << 32) | block_seq; + err = decompress_block(&blk, &opts->keyring, aad_seq, &dec, &dlen); free(blk.payload); if (err != ZUPT_OK) { fprintf(stderr, " Block %llu: FAIL (%s)\n", @@ -1767,7 +2057,15 @@ zupt_error_t zupt_test_archive(const char *arc, zupt_options_t *opts) { err = read_block(f, &blk); if (err != ZUPT_OK) { fok=0; break; } uint8_t *dec; size_t dlen; - err = decompress_block(&blk, &opts->keyring, 0, &dec, &dlen); + /* AAD = ((file_index+1) << 32) | per_file_block_seq (or + * sentinel 0 in dedup mode). Matches encrypt-side. */ + uint64_t aad_seq; + if (hdr.global_flags & ZUPT_FLAG_DEDUP) { + aad_seq = 0; + } else { + aad_seq = (((uint64_t)(i + 1)) << 32) | (uint64_t)b; + } + err = decompress_block(&blk, &opts->keyring, aad_seq, &dec, &dlen); free(blk.payload); if (err != ZUPT_OK) { fok=0; break; } free(dec); diff --git a/src/zupt_lz.c b/src/zupt_lz.c index d702ee1..3c26a05 100644 --- a/src/zupt_lz.c +++ b/src/zupt_lz.c @@ -1,9 +1,7 @@ /* - * ZUPT - LZ77 Compression Engine v2 (Zupt-LZ codec 0x0008) - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - LZ77 Compression Engine v2 (Zupt-LZ codec 0x0008) * * Improvements over v0.1: * - 18-bit hash table (256K entries) for better match distribution @@ -36,7 +34,16 @@ static inline size_t lz_write_extra(uint8_t *dst, size_t cap, size_t len) { static inline size_t lz_read_extra(const uint8_t *s, size_t slen, size_t *pos, size_t init) { size_t t = init; - while (*pos < slen) { uint8_t b = s[(*pos)++]; t += b; if (b < 255) break; } + while (*pos < slen) { + uint8_t b = s[(*pos)++]; + /* Cap accumulation at 2^32 to prevent size_t overflow on 64-bit + * systems and to bound match/literal length to a sane maximum. + * Real archives never exceed 16 MiB block sizes; 2^32 is a + * generous safety ceiling. */ + if (t > (size_t)0xFFFFFFFFu - 255) return (size_t)-1; + t += b; + if (b < 255) break; + } return t; } @@ -199,7 +206,10 @@ size_t zupt_lz_decompress(const uint8_t *src, size_t src_len, size_t lit_len = (token >> 4) & 0xF; size_t match_code = token & 0xF; - if (lit_len == 15) lit_len = lz_read_extra(src, src_len, &ip, 15); + if (lit_len == 15) { + lit_len = lz_read_extra(src, src_len, &ip, 15); + if (lit_len == (size_t)-1) return 0; + } if (lit_len > 0) { if (ip + lit_len > src_len || op + lit_len > dst_len) return 0; @@ -215,8 +225,10 @@ size_t zupt_lz_decompress(const uint8_t *src, size_t src_len, if (offset == 0 || offset > op) return 0; size_t match_len = match_code + LZ_MIN_MATCH; - if (match_code == 15) + if (match_code == 15) { match_len = lz_read_extra(src, src_len, &ip, 15 + LZ_MIN_MATCH); + if (match_len == (size_t)-1) return 0; + } if (op + match_len > dst_len) return 0; size_t ref = op - offset; diff --git a/src/zupt_lzh.c b/src/zupt_lzh.c index 5fad0f0..071e592 100644 --- a/src/zupt_lzh.c +++ b/src/zupt_lzh.c @@ -1,9 +1,7 @@ /* - * ZUPT - LZH Codec v4: High-Compression LZ77 + Canonical Huffman - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - LZH Codec v4: High-Compression LZ77 + Canonical Huffman * * Key advances over v3: * - 1MB sliding window (was 128KB) with 40 extended distance codes diff --git a/src/zupt_main.c b/src/zupt_main.c index f390346..d961aca 100644 --- a/src/zupt_main.c +++ b/src/zupt_main.c @@ -1,10 +1,7 @@ /* - * ZUPT - CLI v1.5.0 - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co - * + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - CLI v1.5.0 * Multi-threaded compression, AES-256 encryption, progress bars */ #include "zupt.h" @@ -57,34 +54,58 @@ static void usage(void) { " --vv, --vaptvupt Use VaptVupt codec (fast LZ + ANS entropy)\n" " --lzhp Use Zupt-LZHP codec (LZ77+Huffman, no SIMD needed)\n" " -p, --password Encrypt with AES-256 (prompted if empty)\n" + " --pq Post-quantum encryption (legacy XOR+SHA3 combiner)\n" + " --pq-sdk Post-quantum encryption via libzuptsdk\n" + " (HKDF combiner + key commitment + HPKE binding\n" + " + Argon2id; recommended for new archives)\n" + " --dedup, -D Block-level deduplication\n" + " --solid Solid mode (single stream)\n" " -v, --verbose Verbose per-file output\n" " -t, --threads Thread count (0=auto, 1=single, 2-64=explicit)\n" "\n" "Extract/List/Test Options:\n" " -o, --output Output directory (extract only)\n" " -p, --password Decryption password\n" - " -pq,--post-quantum Post-quantum Encryption|Decryption \n" + " --pq Post-quantum decryption (legacy combiner)\n" + " --pq-sdk Post-quantum decryption via libzuptsdk\n" " -v, --verbose Verbose output\n" " -t, --threads Thread count for decompression\n" "\n" + "Keygen Options:\n" + " -o Output keyfile path (required)\n" + " --pub Export public key from existing private key (-k)\n" + " -k Source private keyfile (with --pub)\n" + " --sdk, --pq-sdk Generate SDK v2 keypair (writes and .pub)\n" + " Use these keys with --pq-sdk on compress/extract.\n" + "\n" "Directories are traversed recursively.\n" "\n" "Examples:\n" - " zupt keygen -o mykey.key # Generate keypair\n" - " zupt keygen --pub -o pub.key -k mykey.key # Export public key\n" - " zupt compress --pq pub.key backup.zupt ~/Documents/ # Encrypt with public key\n" - " zupt extract --pq mykey.key -o ~/restored/ backup.zupt # Decrypt with private key\n" - " zupt compress backup.zupt ~/Documents/ # Compress (without password)\n" - " zupt compress -l 9 -p mysecret secure.zupt data/ # High Compression with password\n" - " zupt list secure.zupt -p mysecret # List\n" - " zupt extract -o restored/ -p mysecret secure.zupt # Extract with password\n" + " # Legacy PQ workflow\n" + " zupt keygen -o mykey.key # Generate keypair\n" + " zupt keygen --pub -o pub.key -k mykey.key # Export public key\n" + " zupt compress --pq pub.key backup.zupt ~/Documents/ # Encrypt\n" + " zupt extract --pq mykey.key backup.zupt -o ~/restored/ # Decrypt\n" + "\n" + " # SDK v2 PQ workflow (recommended for new archives)\n" + " zupt keygen --sdk -o mykey.priv # Writes mykey.priv + .pub\n" + " zupt compress --pq-sdk mykey.priv.pub backup.zupt files/ # Encrypt (HKDF+commit+HPKE)\n" + " zupt extract --pq-sdk mykey.priv backup.zupt # Decrypt\n" + "\n" + " # Conventional / password\n" + " zupt compress backup.zupt ~/Documents/ # No encryption\n" + " zupt compress -l 9 -p mysecret secure.zupt data/ # Password + max compression\n" + " zupt list secure.zupt -p mysecret # List with password\n" + " zupt extract -o restored/ -p mysecret secure.zupt # Extract with password\n" " zupt bench ~/Documents/ # Benchmark\n" "\n" "Compression: LZ77 (1MB window) + Huffman entropy coding\n" "Security: AES-256-CTR + HMAC-SHA256 (Encrypt-then-MAC)\n" "KDF: PBKDF2-SHA256 (600,000 iterations)\n" "\n" - "License: AGPL-3.0-or-later (commercial: sac@securityops.co)\n" + "License: AGPL-3.0-or-later (Zupt) + GPL-3.0-or-later (VaptVupt codec)\n" + " Commercial license available: sac@securityops.co\n" + "Project: https://git.securityops.co/cristiancmoises/zupt\n" ); } @@ -129,7 +150,10 @@ int main(int argc, char **argv) { if (streq(cmd,"help")||streq(cmd,"--help")||streq(cmd,"-h")) { usage(); return 0; } if (streq(cmd,"version")||streq(cmd,"--version")||streq(cmd,"-V")) { printf("zupt %s\nFormat: v%d.%d\nCodec: Zupt-LZ (0x%04X)\n" - "Encryption: AES-256-CTR+HMAC-SHA256\nKDF: PBKDF2-SHA256 (%d iter)\n", + "Encryption: AES-256-CTR+HMAC-SHA256\nKDF: PBKDF2-SHA256 (%d iter)\n" + "License: AGPL-3.0-or-later (Zupt) + GPL-3.0-or-later (VaptVupt)\n" + "Project: https://git.securityops.co/cristiancmoises/zupt\n" + "Commercial: sac@securityops.co\n", ZUPT_VERSION_STRING, ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR, ZUPT_CODEC_ZUPT_LZ, ZUPT_KDF_ITERATIONS); return 0; @@ -180,6 +204,15 @@ int main(int argc, char **argv) { opts.threads=atoi(argv[++ai]); if(opts.threads<0)opts.threads=0; if(opts.threads>ZUPT_MAX_THREADS)opts.threads=ZUPT_MAX_THREADS; + } else if (streq(argv[ai],"--pq-sdk")&&ai+1ZUPT_MAX_THREADS)opts.threads=ZUPT_MAX_THREADS; } - else if (streq(argv[ai],"--pq")&&ai+1=argc) { fprintf(stderr,"Error: extract requires \n"); return 1; } + if (!archive) { fprintf(stderr,"Error: extract requires \n"); return 1; } banner(); - zupt_error_t err = zupt_extract_archive(argv[ai], outdir, &opts); + zupt_error_t err = zupt_extract_archive(archive, outdir, &opts); zupt_secure_wipe(opts.password, sizeof(opts.password)); return err==ZUPT_OK ? 0 : 1; } @@ -291,23 +335,31 @@ int main(int argc, char **argv) { /* ─── list ─── */ if (streq(cmd,"list")||streq(cmd,"l")) { zupt_options_t opts; zupt_default_options(&opts); + const char *archive = NULL; int ai = 2; - while (ai=argc) { fprintf(stderr,"Error: list requires \n"); return 1; } - zupt_error_t err = zupt_list_archive(argv[ai], &opts); + if (!archive) { fprintf(stderr,"Error: list requires \n"); return 1; } + zupt_error_t err = zupt_list_archive(archive, &opts); zupt_secure_wipe(opts.password, sizeof(opts.password)); return err==ZUPT_OK ? 0 : 1; } @@ -315,24 +367,32 @@ int main(int argc, char **argv) { /* ─── test ─── */ if (streq(cmd,"test")||streq(cmd,"t")) { zupt_options_t opts; zupt_default_options(&opts); + const char *archive = NULL; int ai = 2; - while (ai=argc) { fprintf(stderr,"Error: test requires \n"); return 1; } + if (!archive) { fprintf(stderr,"Error: test requires \n"); return 1; } banner(); - zupt_error_t err = zupt_test_archive(argv[ai], &opts); + zupt_error_t err = zupt_test_archive(archive, &opts); zupt_secure_wipe(opts.password, sizeof(opts.password)); return err==ZUPT_OK ? 0 : 1; } @@ -627,6 +687,7 @@ int main(int argc, char **argv) { const char *outfile = NULL; const char *privfile = NULL; int export_pub = 0; + int sdk_mode = 0; int ai = 2; while (ai < argc && isopt(argv[ai])) { if ((streq(argv[ai],"-o")||streq(argv[ai],"--output")) && ai+1 < argc) @@ -635,6 +696,8 @@ int main(int argc, char **argv) { privfile = argv[++ai]; else if (streq(argv[ai],"--pub")) export_pub = 1; + else if (streq(argv[ai],"--sdk")||streq(argv[ai],"--pq-sdk")) + sdk_mode = 1; else { fprintf(stderr, "Unknown option '%s'\n", argv[ai]); return 1; } ai++; } @@ -654,6 +717,16 @@ int main(int argc, char **argv) { fprintf(stderr, "Error: Failed to export public key.\n"); return 1; } fprintf(stderr, " Public key written to: %s\n", outfile); + } else if (sdk_mode) { + fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair (SDK-v2 format)...\n"); + char pubfile[512]; + snprintf(pubfile, sizeof(pubfile), "%s.pub", outfile); + if (zupt_sdk_hybrid_keygen(outfile, pubfile) != 0) { + fprintf(stderr, "Error: SDK key generation failed.\n"); return 1; + } + fprintf(stderr, " Private key: %s\n", outfile); + fprintf(stderr, " Public key: %s\n", pubfile); + fprintf(stderr, " SECURITY: Keep the private key file secret.\n"); } else { fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair...\n"); if (zupt_hybrid_keygen(outfile) != 0) { diff --git a/src/zupt_parallel.c b/src/zupt_parallel.c index 8932d18..012a586 100644 --- a/src/zupt_parallel.c +++ b/src/zupt_parallel.c @@ -1,9 +1,7 @@ /* - * ZUPT v0.6.0 — Parallel Compress / Decompress Pipeline - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT v0.6.0 — Parallel Compress / Decompress Pipeline * * Architecture: batch-parallel with persistent worker threads. * diff --git a/src/zupt_parallel.h b/src/zupt_parallel.h index 6b49a19..dc8c7ab 100644 --- a/src/zupt_parallel.h +++ b/src/zupt_parallel.h @@ -1,9 +1,7 @@ /* - * ZUPT v0.6.0 — Parallel Compress / Decompress Pipeline - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT v0.6.0 — Parallel Compress / Decompress Pipeline * * Batch-parallel design: the main thread reads N blocks (N = thread_count), * workers process them in parallel (compress+encrypt or HMAC+decrypt+decompress), diff --git a/src/zupt_predict.c b/src/zupt_predict.c index 8af907a..ea0b6aa 100644 --- a/src/zupt_predict.c +++ b/src/zupt_predict.c @@ -1,9 +1,7 @@ /* - * ZUPT 0.4 - Byte Prediction Preprocessor - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT 0.4 - Byte Prediction Preprocessor * * This is the highest-ROI compression improvement: a reversible transform * that captures order-1 (256-context) byte-pair correlations. diff --git a/src/zupt_sha256.c b/src/zupt_sha256.c index 25e99b3..b0c172d 100644 --- a/src/zupt_sha256.c +++ b/src/zupt_sha256.c @@ -1,10 +1,7 @@ /* - * ZUPT - SHA-256 (FIPS 180-4) - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co - * + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - SHA-256 (FIPS 180-4) * Pure C implementation, no dependencies. * FRAMA-C: ACSL-annotated (v2.0.0) */ diff --git a/src/zupt_thread.h b/src/zupt_thread.h index e688d4f..a7571ed 100644 --- a/src/zupt_thread.h +++ b/src/zupt_thread.h @@ -1,9 +1,7 @@ /* - * ZUPT v0.6.0 — Platform Threading Abstraction - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT v0.6.0 — Platform Threading Abstraction * * Header-only. Wraps pthreads (Linux/macOS) and Win32 threads. * No semaphores (not portable to macOS). No barriers (not on Windows). diff --git a/src/zupt_xxh.c b/src/zupt_xxh.c index 2625ae1..107d80b 100644 --- a/src/zupt_xxh.c +++ b/src/zupt_xxh.c @@ -1,11 +1,7 @@ /* - * ZUPT - XXH64 Hash (based on xxHash by Yann Collet, BSD-2) - * * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (C) 2026 Cristian Cezar Moisés - * Commercial licensing: sac@securityops.co - * - * Original xxHash © Yann Collet, BSD-2-Clause — compatible with AGPL. + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * ZUPT - XXH64 Hash (based on xxHash by Yann Collet, BSD-2) */ #include "zupt.h" #include diff --git a/tests/fuzz_decompress.c b/tests/fuzz_decompress.c index d6affdf..c0a7f5a 100644 --- a/tests/fuzz_decompress.c +++ b/tests/fuzz_decompress.c @@ -1,7 +1,8 @@ /* - * Zupt v2.0.0 — AFL++ Fuzzing Harness: Archive Decompression * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt v2.0.0 — AFL++ Fuzzing Harness: Archive Decompression + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Reads a fuzzed .zupt archive from stdin, attempts to extract it. * Catches crashes, buffer overflows, and undefined behavior. diff --git a/tests/fuzz_format b/tests/fuzz_format new file mode 100755 index 0000000000000000000000000000000000000000..c7009f4e8457ad9fc3cb799ccd437a6015f61b25 GIT binary patch literal 16976 zcmeHOe{@vUoxc+TM1;)Hf(Go$QzvauGRa_|ffda_f-g2=3TSPM4wKAFGBTNDXI>z% zJsJ$5%*GM6+wR(KcNhD^veiFW&(>8>mj)4~-Rc27$HnbA6{)r}t{|WS70c}Bd*A)e zym{u;UC*BVV^40*RGcKTljKrhqS(m?dwr2&7gzT2@Lc6g~-*?5Zd;4*iS;3#ME{qGacmhAy=vOoh+l z$!;=b74M`^DLbYtFO;W|F_I?#7w9Zif2MXjicLkWrC+hEl=_rijiPKv;Uc>#Wml!_ zn4Y2vAu;86QcURAsQfu;JsBY}b=vhPJ11?n%mh;^gVLHe&FCj7uU*-ly2|q7qzx7< zm~wevgdN4@znl0nuU6%`U5E3ke==3!6?O4g$Kr;%c(gViOAHLw4mK{XUEB~z^#vBn zDp0*N{HRaeyy|uVH)DkHP2Cf@G`>w1ivH3i@i(EjPkG{SxNd7IQltL48V9e`5X@~ zM+G#e6Y#5C^e=b8{Vw>AU2tmmc=c&@!5?+OZ*akX;DT!|?Y-7TzrzK8-Ua_B7kr@$ z{*((IbisGH;A1ZMS{J;|B@Tb?qW^gp{9C}U#?P6y0+=c03a!bK@H{2L;glKa+z{^U z*%0oE#NwhWX&Ce`5)}gp4AKpvGv3#^L8Q#2*#~i7zmdTIF*kqEc0y)}$l7~QdyX(ZR)x+30}FxEyo;s)x~-P@N?)ec*#<0>Mo zWtdgUFoTq-zj8?@mtdBRu*o(}ns>^@3-H17t2rlj3C)84uCQ6L*bG{Uexvw*{>#6{ zG&M<_Q2fAL(k~O|!BZ-%ixw+(Utwi@yk07Ny}~P+EP(q3^G9#8aGDcoj^uRAhSU7Y zDQm+k83a9L!{^xW^ETW*xpF_Hb%5qdPMXr63V`M;PBk{1*9wx=+i-cGq6`{sxSa15 z-ekk63{JWY=NOT!-G<`=6w;kGd|Ck&V!aJlw?Z1|^b`nzp7t-GA|*znm5g6_59m)YMEPq%ZezxL( ztW18>nQTHpygH1(hr-dl@jUrDA zQ11Q$pRZnp=y!SNH47R4e05iCDSx+vzr(?=a_}o1{EZHNv4amd_|H1{D;@l04!+XC zPjT=U7nHW=tb;%4;D77jf9>F3aqurX_~#t_J_r9q2_O1W=#J3sZJFAarwb8Ud;1!F z>)U&9iK6;wd36mkdU~7xA)=Yd2chzOvtP@i&+F-CfBkUY=JaW zQ2N=CG4IGd!1c_cpTLMDb&%-kgER>5ewqeR@T{JGK6?~BL5FxZXz(S%-qF(svil|c zTJ|47TP>?9J)P6j|FMq-Bw1ek11y&uFz1r_Oz2T5PXT;V4%x?021(8VX&(KWUu#Vt z4UXxf%ch|HdS-Sv>a~yR(VYH$R`zFa-{c?9K)qhlpWJ8!whZ)}vX$%g%-8(=&FN!L z(a?PNQSba_f4iPu>+i{q!grVVov|~z_u8>D-n%D%1@>pWGn@U5EhBmFgO6bF>lvsA z&$gxGeqZneg{?x*tT@&B(-(eerL^gZ1kYygfQCLgW8QUSa7f>OY=#~@q^FR&%L3iF&7^HP~V>|(>>3$&Og$UiFx#qUwKEW5SP&KbDowZ@85S$ zNAOVPMektd$;M=L=qh@*y@`uX-qkf}OXsuqKunk6{tt=fv)6)VM*yau9~m30^KPS# zf&gbse_QqhgrjTyZRx|q$L<>*^W1>W^f0>C&~NkJZF`UzZuT$mjGQoM#SYG2xAU3( zhsu`KtlL@U-Tt>EJm^^x_iNsfSLo7@BD&cRFIbmD*0^v@p2G}z#=8#JL|z$R_J>fT zN@hUMczw-&-)Ng(Lznm|T8Iwv=6uyNWJk(ALe*U7_sh;Ad(g8`fcj6Mo_;mEo#bu) z1x5J}Ecpu*upUcV6C9IrxZiF`e@D_=APtTo_qWX2S$5Z* zzj{|_UFfdR-J$S-u0WxG3}5h=9phhgT>j&@=<4#LOW->F!s-%7WR9dqa4F8ciym-Qui)+@9gGa;qsN_}#40~_?=3*L>H@k`-Gp^oXv0jl#k(_f`8d09^%Y0cbl zx$a%@Z^5%vU48zMr)ee29{M1kZ&`BEn^{D4ADwajb69>FQL1+ywOHzYbjWZ)*5LVQ z^wKX{;eAB~aM8nXn?5Z&%<$_|Up^M9FTWh4$NS(uTwy(ZI(JY`XrHt^c`XZ9s{w_D zc1)KA?2N;4?oO1_wTnXM-M-2yXm=;`;L#?Zj>kp1p?fLL#aFO+$=eH)7UXba{(T6iv?6MY>>+h}i9?p`Tc zR*Ul5(m5W7nc1Jg3Y7gFoY+vz|J3Hp?9Y?%7ZB!t3G?6>+}7Z?06)K+v*lEs`;nY1 z^4az0pt%E#wfDOc}vfq*U?Ds)vQmWU{^oo|? z7^acT>__G5u;?C;>=)tYXZKNuy&SsP)%a)Ke+EyqrjOjMr;qEyZ=7mhOSV7PGs{BI z&(D6>FT{Jc`5?3Gs%mQ7U|Kv^j{_~S`&vW{bi{>mi_@Q!8kw0c#Z?>oVkS$ec^e(M48AYy}j8kM9ajnqKgOldXBcf^eKB~Uu2>yfqqnTl zYErRoeBhbCRMV&?ns!5Nt=2Knozh}mh)AM#i;?V;Z6N=eDqO2c8uu8TrV%AWX;)(m z_Df`eYMHhPU#HOM`LNbBRaGjckSx320n@6VX7(T~(O5L`IaBMvr>_lKZ=^fcS(n1+ zC-P#Yp>2T>=WW+kUPaFplm4^;LM*7F{$xJi1G@K}e0~?`80a3tPv`Svpog)5wu4gQ zcj%ritA%IK=ec6~)QVlWLnWNPhYUg&Hw}1HoJ#kUf>t>yXY52iKLkBd>04P@b&L1X zO%+4prnyV5z21K{P-(vovzM7j^)*K;bL|9jjsiItVUon^}_ zs~()Ryi&`Qw^r6XFga9NzkN!mvT=Co&6USIlXgy;R@n$FR9OQZ=q|7HEw8Nj!ljcQ zFRRRYS}IR@R)dD1t4f(56A_q*z(fQlA}|qwi3m(YU?Kwl-y^{Lj(DFD-wD2;8jdd^ zR#G>q>TyCmrZ#i%ULoGk#Ctp{lo{^@;eAcKhlu6xU(ENB>R!ChrbPGWl=k2i93@4> zQ6=Y_{Z>^`-lM~NQS5tL$X=O=3bhY~zUok_Q5m}5rbM^Ylz6YpotIcV<3p;#T<@cn zSSU5YOq`Ff>*H(V^2mbkr_{25g+-aks7+o9b5uMyr~?Q~eL zu6L87?TW5fv|rI7MRzH>ThYCWjwyOr(PN69QWTpWW%4PiDO#^+lcMd4u2-~Q(IG{5 zDe7#;$MF?SmPUK~zNb&(>F?$hE0$_Cw|5LA%mFQUJvJ!T*DfBA?818&HU#P$0>Sx8 z@W*^p_beBu>GfqvqW(gA2_H}wR8vy=R6d^mRZ@SB@HJVA+^_IOZ(RMwz{`YBoZ^9J znc?Jug=;)O75)W<^Y~Y=h#=#^e$<*K3!(N<>c!6^E)zVSC5V6D#E;75`GoP^$auu; z!hT1_-Rl1{sXt5ju3^n09WOpKPkAtEo&ER~Y4&Ns`|R0EPU=?`<^#s*LfQA%G44ad z$HQw~@Fw6I8{6nHiDR~GvVq91bMcdK!H0nR#2iuO@bh&S{cpSAj}cxrvoMcytsZyL zf58R+jtl;4;8ag%d;il#zZ~=NcyYcAxW<*U(P|g{M&L8c<_I3wY|=tFm7tuJE5TY9 zKi$B6=>PRi?7^aM$ol&V=Y-_DABf^Ee(qI%G>4zRkorEMsm+`6oeuo$bn){qF8KFd z@F!gGLoWF1F8F)EDW1;$MhokB*K00t4efBI1{eL6z^`^HgO8AY8SXxKolrX6E`A1q zkGp?!2Kum3Jkk*hn~`oIU-(+DS_7RTndqk7MVPpy=%D?T;IH-x;VMUxW6^$HI|hGO{^rB+TSyY~GLH zHE?vGw|6s4Y+M+cW{GMzymEEu)|PO~s^&0uw>N)zRp{2%6|llNnQ)7)qQMrPE3@l8;PiO$e|PtccfCPdh$$#Ivr7> zWSx{Kf#e~M5=1smoxdmuWaH!kivnb|4o4)o%h=C)6tu`to~ju4sDwQDQP8zK+7En` zim1=Z^Cl&*b!?;r5`om_UNh1GilZu$_HYiTQH*522;fVL5$H|~1p1RW$C5NR+n^5m zv{H+sC6-obd21~VXQ|$UZy+KN-JC!X7B!QWWFtPH;2TE?6NW5l#3N*&a{X~r1Y}bJ z$OgLmz)~h zVoDfi`uiV4bX$j|3HM-Ds=|Coi77vKS*WGUd7{_gM~@tA->B@F){~Mg@qH*mbl+*W zcb=!D?;`fZ>n~%i$k^@q{+Ox8ii$ezzXTln3HrGH{5;9DM%nT6q_h4J$mv;cZPJwLxP<>ytX$V5Ku>CeE>vmo2^^B>b2lrqKt$ke{EK^7A&?Q_P(94+Eo^u>Zok zuhxIZV~eaS)C=jm$l2{RWyf^L5pX3g6!Rm8{ciO<$&~fEK<1h5b=dD!_Dt>H^2i6< zF@4Hm&-aB)(;P4|Mq)dQ6T*6KD#nlB&oJfpHmvU)KL;V85b^aFn$lGCluP`SmOLA0 zx98_BrVXs9sI&ZE0k_-NTx}^becNI02#0tRa@xMGhHc?_vvB^|#h(3~hTLnj=jU5r zt!2o1?1p)!e}I7cE|<^GqkNv7<45C<65H{0d=wb9pY8d5RYe_Xi^91>?{O>JW0dtV_eyp&~`8C6>lqhk=6?{{`Ks B>V5zK literal 0 HcmV?d00001 diff --git a/tests/fuzz_format.c b/tests/fuzz_format.c new file mode 100644 index 0000000..9b91c56 --- /dev/null +++ b/tests/fuzz_format.c @@ -0,0 +1,157 @@ +/* zupt format parser fuzz harness + * + * Mutates valid archives and feeds them to the listing/extract path + * under ASAN/UBSAN. Any crash, leak, or sanitizer error is a finding. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +#define _DEFAULT_SOURCE 1 +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Simple xorshift64 PRNG — deterministic, fast */ +static uint64_t rng_state = 0xc0ffeebabe5050ULL; +static uint64_t rng(void) { + uint64_t x = rng_state; + x ^= x << 13; x ^= x >> 7; x ^= x << 17; + return rng_state = x; +} + +static void mutate(uint8_t *buf, size_t n) { + if (n == 0) return; + int op = rng() % 5; + switch (op) { + case 0: { /* byte flip */ + size_t i = rng() % n; + buf[i] ^= 1u << (rng() % 8); + break; + } + case 1: { /* random byte set */ + size_t i = rng() % n; + buf[i] = (uint8_t)(rng() & 0xFF); + break; + } + case 2: { /* zero a 16-byte run */ + if (n < 16) return; + size_t off = rng() % (n - 16); + memset(buf + off, 0, 16); + break; + } + case 3: { /* set 0xFF run */ + if (n < 16) return; + size_t off = rng() % (n - 16); + memset(buf + off, 0xFF, 16); + break; + } + case 4: { /* swap two bytes */ + if (n < 2) return; + size_t i = rng() % n; + size_t j = rng() % n; + uint8_t t = buf[i]; buf[i] = buf[j]; buf[j] = t; + break; + } + } +} + +/* Run the zupt binary on a given archive file. We use fork+exec because + * any crash in zupt would otherwise take down the harness; the parent + * just records the exit status. ASAN/UBSAN errors return non-zero. */ +static int run_zupt(const char *zupt_path, const char *archive, + const char **flag, int with_pq) { + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + /* child */ + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + dup2(devnull, 1); dup2(devnull, 2); + close(devnull); + } + if (with_pq) { + execl(zupt_path, "zupt", "list", "--pq-sdk", "/tmp/_fuzz.priv", + archive, (char*)NULL); + } else { + execl(zupt_path, "zupt", "list", archive, (char*)NULL); + } + _exit(127); + (void)flag; + } + int status; + waitpid(pid, &status, 0); + if (WIFSIGNALED(status)) return -2; /* crash! */ + return WEXITSTATUS(status); +} + + +int main(int argc, char **argv) { + int n_iters = argc > 1 ? atoi(argv[1]) : 1000; + const char *zupt_path = argc > 2 ? argv[2] : "./zupt"; + const char *seed = argc > 3 ? argv[3] : "/tmp/_fuzz_seed.zupt"; + + if (access(seed, R_OK) != 0) { + fprintf(stderr, "Seed archive not found at %s\n", seed); + fprintf(stderr, "Build seed first: ./zupt c %s some_file.txt\n", seed); + return 1; + } + + FILE *f = fopen(seed, "rb"); + if (!f) return 1; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *seed_data = malloc((size_t)sz); + if (!seed_data || fread(seed_data, 1, (size_t)sz, f) != (size_t)sz) { + fclose(f); free(seed_data); return 1; + } + fclose(f); + + fprintf(stderr, "Fuzzing zupt format parser: %d iters, seed=%ld bytes\n", + n_iters, sz); + + int crashes = 0, errors = 0, accepts = 0; + char fuzzfile[64] = "/tmp/_fuzz_archive.zupt"; + double t0 = (double)clock() / CLOCKS_PER_SEC; + + for (int i = 0; i < n_iters; i++) { + /* Copy seed, apply 1-5 random mutations */ + uint8_t *buf = malloc((size_t)sz); + if (!buf) break; + memcpy(buf, seed_data, (size_t)sz); + int n_mut = 1 + (rng() % 5); + for (int m = 0; m < n_mut; m++) mutate(buf, (size_t)sz); + + FILE *out = fopen(fuzzfile, "wb"); + if (!out) { free(buf); continue; } + fwrite(buf, 1, (size_t)sz, out); + fclose(out); + free(buf); + + int rc = run_zupt(zupt_path, fuzzfile, NULL, 0); + if (rc == -2) crashes++; + else if (rc != 0) errors++; + else accepts++; + + if (i > 0 && i % 100 == 0) + fprintf(stderr, " [%d/%d] crashes=%d errors=%d accepts=%d\r", + i, n_iters, crashes, errors, accepts); + } + double dt = (double)clock() / CLOCKS_PER_SEC - t0; + + unlink(fuzzfile); + free(seed_data); + + fprintf(stderr, "\n\nFuzz results (%d iterations, %.1fs):\n", n_iters, dt); + fprintf(stderr, " CRASHES (signal): %d <-- bugs if non-zero\n", crashes); + fprintf(stderr, " errors (rejected): %d (expected, parser working)\n", errors); + fprintf(stderr, " accepts: %d (mutations that didn't break magic/size)\n", accepts); + + return crashes > 0 ? 1 : 0; +} diff --git a/tests/fuzz_vv_decompress.c b/tests/fuzz_vv_decompress.c index 93289fb..978eb91 100644 --- a/tests/fuzz_vv_decompress.c +++ b/tests/fuzz_vv_decompress.c @@ -1,7 +1,8 @@ /* - * Zupt v2.0.0 — AFL++ Fuzzing Harness: VaptVupt Codec * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt v2.0.0 — AFL++ Fuzzing Harness: VaptVupt Codec + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Reads fuzzed VaptVupt frame data from stdin, attempts decompression. * Tests the VaptVupt codec directly (bypassing Zupt archive format). diff --git a/tests/regression.sh b/tests/regression.sh index 37d207c..5c7d3e4 100644 --- a/tests/regression.sh +++ b/tests/regression.sh @@ -1,4 +1,6 @@ #!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés # ZUPT v2.0.0 — Comprehensive Regression Test Suite # Covers: normal, solid, encrypted, edge cases, VaptVupt codec # Run: sh tests/regression.sh diff --git a/tests/run_quick.sh b/tests/run_quick.sh index d3c1a95..6e0500d 100644 --- a/tests/run_quick.sh +++ b/tests/run_quick.sh @@ -1,4 +1,6 @@ #!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés set +e Z="./zupt"; T=$(mktemp -d); trap 'rm -rf "$T"' EXIT mkdir -p "$T/d"; echo "hello" > "$T/d/a.txt" diff --git a/tests/test_arg_order.sh b/tests/test_arg_order.sh new file mode 100755 index 0000000..9859d72 --- /dev/null +++ b/tests/test_arg_order.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Regression test for argument order in extract/list/test commands. +# Bug #15 (v2.2.2): options after the positional archive argument were +# silently dropped. e.g. `zupt x arch.zupt -o out` ignored `-o out`. + +ZUPT_BIN="$(realpath ./zupt)" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT +cd "$TMPDIR" + +PASS=0; FAIL=0 +chk() { + if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1)) + else echo " ✗ $1"; FAIL=$((FAIL+1)); fi +} + +echo " [Argument order regression — extract/list/test]" + +# Setup +mkdir d +echo "secret content $(date +%s%N)" > d/file.txt +"$ZUPT_BIN" c arch.zupt d/file.txt > /dev/null 2>&1 +"$ZUPT_BIN" c -p mypw arch_pw.zupt d/file.txt > /dev/null 2>&1 +"$ZUPT_BIN" keygen -o k.key > /dev/null 2>&1 +"$ZUPT_BIN" keygen --pub -o p.key -k k.key > /dev/null 2>&1 +"$ZUPT_BIN" c --pq p.key arch_pq.zupt d/file.txt > /dev/null 2>&1 + +# P1: extract -o after archive +mkdir out1 +"$ZUPT_BIN" x arch.zupt -o out1 > /dev/null 2>&1 +[ -f out1/d/file.txt ] && diff -q d/file.txt out1/d/file.txt > /dev/null +chk "extract: -o after archive" + +# P2: extract -o before archive +mkdir out2 +"$ZUPT_BIN" x -o out2 arch.zupt > /dev/null 2>&1 +[ -f out2/d/file.txt ] && diff -q d/file.txt out2/d/file.txt > /dev/null +chk "extract: -o before archive" + +# P3: extract password options after archive +mkdir out3 +"$ZUPT_BIN" x arch_pw.zupt -p mypw -o out3 > /dev/null 2>&1 +[ -f out3/d/file.txt ] && diff -q d/file.txt out3/d/file.txt > /dev/null +chk "extract: -p AND -o after archive" + +# P4: extract --pq after archive +mkdir out4 +"$ZUPT_BIN" x arch_pq.zupt --pq k.key -o out4 > /dev/null 2>&1 +[ -f out4/d/file.txt ] && diff -q d/file.txt out4/d/file.txt > /dev/null +chk "extract: --pq after archive" + +# P5: list -p after archive (encrypted archive lists files) +out=$("$ZUPT_BIN" l arch_pw.zupt -p mypw 2>&1) +echo "$out" | grep -q "file.txt" +chk "list: -p after archive" + +# P6: list -p before archive +out=$("$ZUPT_BIN" l -p mypw arch_pw.zupt 2>&1) +echo "$out" | grep -q "file.txt" +chk "list: -p before archive" + +# P7: test -p after archive +out=$("$ZUPT_BIN" t arch_pw.zupt -p mypw 2>&1) +echo "$out" | grep -q "0 failed" +chk "test: -p after archive" + +# P8: test -p before archive +out=$("$ZUPT_BIN" t -p mypw arch_pw.zupt 2>&1) +echo "$out" | grep -q "0 failed" +chk "test: -p before archive" + +echo +echo " ───────────────────────────────────────" +echo " Argument order regression: $PASS passed, $FAIL failed" +echo " ───────────────────────────────────────" +[ $FAIL -eq 0 ] diff --git a/tests/test_audit.sh b/tests/test_audit.sh new file mode 100755 index 0000000..7fb2719 --- /dev/null +++ b/tests/test_audit.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# zupt audit test suite — double-validated security checks for zupt 2.2+ +# Each property is checked via TWO independent paths. + +ZUPT_BIN="$(realpath ./zupt)" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT +cd "$TMPDIR" + +PASS=0; FAIL=0 +DCHK() { + local name="$1" a="$2" b="$3" + if [ "$a" = "$b" ] && [ "$a" = "1" ]; then + echo " ✓ $name (A=$a B=$b agree)"; PASS=$((PASS+1)) + else + echo " ✗ $name (A=$a B=$b disagree)"; FAIL=$((FAIL+1)) + fi +} + +# Setup: SDK keys +"$ZUPT_BIN" keygen --sdk -o k.priv > /dev/null 2>&1 +"$ZUPT_BIN" keygen --sdk -o other.priv > /dev/null 2>&1 +"$ZUPT_BIN" keygen -o legacy.key > /dev/null 2>&1 + +echo " [A. Authenticated archives]" + +# A1. Wrong key rejected: SDK key vs SDK archive (path A) + Legacy key vs SDK archive (path B) +echo "data" > input.txt +"$ZUPT_BIN" c --pq-sdk k.priv.pub a.zupt input.txt > /dev/null 2>&1 +mkdir -p ea && (cd ea && "$ZUPT_BIN" x --pq-sdk ../other.priv ../a.zupt > /dev/null 2>&1) +A=$([ ! -f ea/input.txt ] && echo 1 || echo 0) +mkdir -p eb && (cd eb && "$ZUPT_BIN" x --pq legacy.key ../a.zupt > /dev/null 2>&1) +B=$([ ! -f eb/input.txt ] && echo 1 || echo 0) +DCHK "Wrong key rejected (SDK key + legacy key paths)" "$A" "$B" + +# A2. Tamper at byte position N detected (path A: pos 200) (path B: pos at end) +cp a.zupt t1.zupt; cp a.zupt t2.zupt +python3 -c " +b = bytearray(open('t1.zupt','rb').read()) +b[200] ^= 1 +open('t1.zupt','wb').write(bytes(b))" 2>/dev/null +python3 -c " +b = bytearray(open('t2.zupt','rb').read()) +b[len(b)-50] ^= 1 +open('t2.zupt','wb').write(bytes(b))" 2>/dev/null +mkdir -p t1e && (cd t1e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../t1.zupt > /dev/null 2>&1) +mkdir -p t2e && (cd t2e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../t2.zupt > /dev/null 2>&1) +A=$([ ! -f t1e/input.txt ] && echo 1 || echo 0) +B=$([ ! -f t2e/input.txt ] && echo 1 || echo 0) +DCHK "Tamper detected at any byte position" "$A" "$B" + +echo " [B. Format security]" + +# B1. Zero-byte file (path A) + 1-byte file (path B): both must roundtrip +> empty.txt +echo -n "x" > one.txt +"$ZUPT_BIN" c --pq-sdk k.priv.pub e.zupt empty.txt > /dev/null 2>&1 +"$ZUPT_BIN" c --pq-sdk k.priv.pub o.zupt one.txt > /dev/null 2>&1 +mkdir -p eex && (cd eex && "$ZUPT_BIN" x --pq-sdk ../k.priv ../e.zupt > /dev/null 2>&1) +mkdir -p oex && (cd oex && "$ZUPT_BIN" x --pq-sdk ../k.priv ../o.zupt > /dev/null 2>&1) +A=$([ -f eex/empty.txt ] && [ ! -s eex/empty.txt ] && echo 1 || echo 0) +B=$([ -f oex/one.txt ] && [ "$(cat oex/one.txt)" = "x" ] && echo 1 || echo 0) +DCHK "Edge-size files (0/1 byte) roundtrip" "$A" "$B" + +# B2. 1MB random file (path A) + structured-data 1MB (path B) +dd if=/dev/urandom of=big_a.bin bs=1M count=1 2>/dev/null +python3 -c "open('big_b.bin','wb').write(b'A'*1024*1024)" +"$ZUPT_BIN" c --pq-sdk k.priv.pub ba.zupt big_a.bin > /dev/null 2>&1 +"$ZUPT_BIN" c --pq-sdk k.priv.pub bb.zupt big_b.bin > /dev/null 2>&1 +mkdir -p ba_e && (cd ba_e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../ba.zupt > /dev/null 2>&1) +mkdir -p bb_e && (cd bb_e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../bb.zupt > /dev/null 2>&1) +A=$(diff -q big_a.bin ba_e/big_a.bin > /dev/null 2>&1 && echo 1 || echo 0) +B=$(diff -q big_b.bin bb_e/big_b.bin > /dev/null 2>&1 && echo 1 || echo 0) +DCHK "1MB roundtrip (random + structured)" "$A" "$B" + +# B3. Truncated archive rejected (path A: cut last 50 bytes) (path B: cut at midpoint) +cp a.zupt tr1.zupt; cp a.zupt tr2.zupt +truncate -s -50 tr1.zupt +truncate -s 100 tr2.zupt +mkdir -p tr1e tr2e +(cd tr1e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../tr1.zupt > /dev/null 2>&1) +(cd tr2e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../tr2.zupt > /dev/null 2>&1) +A=$([ ! -f tr1e/input.txt ] && echo 1 || echo 0) +B=$([ ! -f tr2e/input.txt ] && echo 1 || echo 0) +DCHK "Truncated archive rejected" "$A" "$B" + +echo " [C. Format compatibility]" + +# C1. Mode confusion: SDK archive cannot be read with --pq (legacy) +mkdir -p mc1 && (cd mc1 && "$ZUPT_BIN" x --pq ../legacy.key ../a.zupt > /dev/null 2>&1) +A=$([ ! -f mc1/input.txt ] && echo 1 || echo 0) +# Also: legacy archive cannot be read with --pq-sdk +"$ZUPT_BIN" c --pq legacy.key leg.zupt input.txt > /dev/null 2>&1 +mkdir -p mc2 && (cd mc2 && "$ZUPT_BIN" x --pq-sdk ../k.priv ../leg.zupt > /dev/null 2>&1) +B=$([ ! -f mc2/input.txt ] && echo 1 || echo 0) +DCHK "Mode confusion prevented (SDK↔legacy)" "$A" "$B" + +# C2. Legacy archive readable with legacy key (compat baseline) +mkdir -p lc && (cd lc && "$ZUPT_BIN" x --pq ../legacy.key ../leg.zupt > /dev/null 2>&1) +A=$(diff -q lc/input.txt input.txt > /dev/null 2>&1 && echo 1 || echo 0) +# B: SDK archive readable with SDK key (compat baseline) +mkdir -p sc && (cd sc && "$ZUPT_BIN" x --pq-sdk ../k.priv ../a.zupt > /dev/null 2>&1) +B=$(diff -q sc/input.txt input.txt > /dev/null 2>&1 && echo 1 || echo 0) +DCHK "Both SDK and legacy paths roundtrip independently" "$A" "$B" + +echo " [D. Robustness]" + +# D1. Non-existent input handled +"$ZUPT_BIN" c --pq-sdk k.priv.pub nx.zupt /nonexistent_file_12345 > /dev/null 2>&1 +A=$([ ! -f nx.zupt ] && echo 1 || echo 0) +"$ZUPT_BIN" c --pq-sdk k.priv.pub nx2.zupt /dev/nonexistent > /dev/null 2>&1 +B=$([ ! -f nx2.zupt ] && echo 1 || echo 0) +DCHK "Missing input file rejected cleanly" "$A" "$B" + +# D2. Non-existent key handled +mkdir -p nk1 && (cd nk1 && "$ZUPT_BIN" x --pq-sdk /nonexistent.key ../a.zupt > /dev/null 2>&1) +A=$([ ! -f nk1/input.txt ] && echo 1 || echo 0) +"$ZUPT_BIN" c --pq-sdk /nonexistent.pub bbnk.zupt input.txt > /dev/null 2>&1 +B=$([ ! -s bbnk.zupt ] && echo 1 || echo 0) +DCHK "Missing key file rejected cleanly" "$A" "$B" + +# D3. Multiple files in one archive +mkdir -p multi +echo "a" > multi/a.txt; echo "b" > multi/b.txt; echo "c" > multi/c.txt +"$ZUPT_BIN" c --pq-sdk k.priv.pub mm.zupt multi/a.txt multi/b.txt multi/c.txt > /dev/null 2>&1 +mkdir -p mmex && (cd mmex && "$ZUPT_BIN" x --pq-sdk ../k.priv ../mm.zupt > /dev/null 2>&1) +A=$(diff -q mmex/multi/a.txt multi/a.txt > /dev/null 2>&1 && echo 1 || echo 0) +B=$([ -f mmex/multi/c.txt ] && [ "$(cat mmex/multi/c.txt 2>/dev/null)" = "c" ] && echo 1 || echo 0) +DCHK "Multiple files in archive roundtrip" "$A" "$B" + +echo +echo " ───────────────────────────────────────" +echo " Audit results: $PASS passed, $FAIL failed" +echo " ───────────────────────────────────────" +[ $FAIL -eq 0 ] diff --git a/tests/test_block_swap.sh b/tests/test_block_swap.sh new file mode 100755 index 0000000..acc61c7 --- /dev/null +++ b/tests/test_block_swap.sh @@ -0,0 +1,180 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Bug #16 regression — Block-swap attack on encrypted archives. +# +# Pre-fix vulnerability: +# AES-CTR + HMAC-SHA256 in zupt 2.2.2 covered MAC over (nonce || ciphertext) +# only. The decryptor read the nonce from the package itself and ignored +# the block_seq parameter. An attacker who swapped two valid encrypted +# blocks (header + payload) between positions could produce an archive +# that decrypts cleanly but extracts files with the wrong content. +# +# Fix: +# Bind block_seq into MAC as 8-byte LE AAD. Encrypt is now MAC-over +# (nonce || ciphertext || block_seq_LE). Decrypt tries v2 first, falls +# back to legacy v1 for old archives. Per-file block_seq is used so +# extract-side counter matches encrypt-side without needing extra +# index metadata. +# +# This test: +# 1. Creates an encrypted archive with two distinct files A and B +# 2. Performs the block-swap surgery on the binary +# 3. Verifies extract REJECTS the swapped archive (auth failure) +# 4. Also verifies normal extract still works (regression guard) + +ZUPT_BIN="$(realpath ./zupt)" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT +cd "$TMPDIR" + +PASS=0; FAIL=0 +chk() { + if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1)) + else echo " ✗ $1"; FAIL=$((FAIL+1)); fi +} + +echo " [Bug #16 — Block-swap (reorder) attack defense]" + +# Two distinct files, small enough that each fits in one block +printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' > file_A.txt +printf 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n' > file_B.txt + +# Build encrypted archive (small block to ensure 1 block per file) +"$ZUPT_BIN" c -p mypassword -b 64 -t 1 archive.zupt file_A.txt file_B.txt > /dev/null 2>&1 + +# P1: Normal extract still works (regression guard) +mkdir extract_normal +"$ZUPT_BIN" x archive.zupt -p mypassword -o extract_normal > /dev/null 2>&1 +[ -f extract_normal/file_A.txt ] && [ -f extract_normal/file_B.txt ] && \ + diff -q file_A.txt extract_normal/file_A.txt > /dev/null && \ + diff -q file_B.txt extract_normal/file_B.txt > /dev/null +chk "Normal extract still works (regression guard)" + +# P2: The block-swap attack must FAIL (no files extracted, or wrong files rejected) +python3 << 'PYEOF' +import sys, struct +data = bytearray(open('archive.zupt','rb').read()) +# Find DATA blocks via block magic 0xbb 0x01 + block_type=DATA(0) +def parse_blocks(data): + blocks = [] + i = 0 + while i < len(data) - 7: + if data[i] == 0xbb and data[i+1] == 0x01: + block_type = data[i+2] + codec = struct.unpack('&1) + rc=$? + + # Attack defense check: at least one of these must hold + # - rc != 0 (extract returned error) + # - no files extracted + # - extracted files have wrong content (we reject this — would mean the + # bug is still present) + a_swapped=0; b_swapped=0 + [ -f extract_attack/file_A.txt ] && cmp -s file_B.txt extract_attack/file_A.txt && a_swapped=1 + [ -f extract_attack/file_B.txt ] && cmp -s file_A.txt extract_attack/file_B.txt && b_swapped=1 + + if [ "$a_swapped" = "1" ] && [ "$b_swapped" = "1" ]; then + # BAD: attack succeeded — file_A has B's content and vice versa + false + else + # GOOD: attack rejected (either error, no files, or files unchanged) + true + fi + chk "Block-swap attack rejected (cross-file reorder)" +else + echo " ⊘ Block-swap attack test skipped (couldn't locate block boundaries)" +fi + +# P3: Single-block file (boundary case — empty seq_AAD doesn't degenerate) +echo "single block content" > tiny.txt +"$ZUPT_BIN" c -p mypassword tiny.zupt tiny.txt > /dev/null 2>&1 +mkdir tiny_out +"$ZUPT_BIN" x tiny.zupt -p mypassword -o tiny_out > /dev/null 2>&1 +[ -f tiny_out/tiny.txt ] && diff -q tiny.txt tiny_out/tiny.txt > /dev/null +chk "Single-block file roundtrip (boundary)" + +# P4: Multi-block large file (ensures every block has correct AAD seq) +dd if=/dev/urandom of=big.bin bs=1024 count=512 2>/dev/null +"$ZUPT_BIN" c -p mypassword big.zupt big.bin > /dev/null 2>&1 +mkdir big_out +"$ZUPT_BIN" x big.zupt -p mypassword -o big_out > /dev/null 2>&1 +[ -f big_out/big.bin ] && diff -q big.bin big_out/big.bin > /dev/null +chk "512KB multi-block file roundtrip" + +# P5: Multiple files in one archive (each gets per-file seq counter) +echo "first" > a.txt +echo "second" > b.txt +echo "third" > c.txt +"$ZUPT_BIN" c -p mypassword multi.zupt a.txt b.txt c.txt > /dev/null 2>&1 +mkdir multi_out +"$ZUPT_BIN" x multi.zupt -p mypassword -o multi_out > /dev/null 2>&1 +[ -f multi_out/a.txt ] && [ -f multi_out/b.txt ] && [ -f multi_out/c.txt ] && \ + diff -q a.txt multi_out/a.txt > /dev/null && \ + diff -q b.txt multi_out/b.txt > /dev/null && \ + diff -q c.txt multi_out/c.txt > /dev/null +chk "Multi-file archive roundtrip (per-file seq counters)" + +# P6: Wrong password still fails cleanly +mkdir wrong_pw +out=$("$ZUPT_BIN" x archive.zupt -p WRONG_PASSWORD -o wrong_pw 2>&1) +[ ! -f wrong_pw/file_A.txt ] +chk "Wrong password rejected" + +echo +echo " ───────────────────────────────────────" +echo " Block-swap regression: $PASS passed, $FAIL failed" +echo " ───────────────────────────────────────" +[ $FAIL -eq 0 ] diff --git a/tests/test_dedup_props.sh b/tests/test_dedup_props.sh new file mode 100755 index 0000000..b17ad32 --- /dev/null +++ b/tests/test_dedup_props.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Property-based test for zupt dedup path. +# Generates random file sets with intentional duplicates, verifies that +# (a) compressed output is correct (byte-exact roundtrip) and +# (b) dedup actually saves space when duplicates are present. + +ZUPT_BIN="$(realpath ./zupt)" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT +cd "$TMPDIR" + +PASS=0; FAIL=0 +chk() { + if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1)) + else echo " ✗ $1"; FAIL=$((FAIL+1)); fi +} + +# ─── Property 1: dedup roundtrip is byte-exact ────────────────────────── +# Generate 10 random base files + 5 exact duplicates; compress with --dedup; +# extract; verify every file matches its original. +echo " [P1. Dedup roundtrip preserves all bytes]" + +mkdir input +for i in $(seq 1 10); do + dd if=/dev/urandom of=input/file_$i.bin bs=4K count=$((RANDOM % 8 + 1)) 2>/dev/null +done +# 5 exact duplicates (same content as file_1..5) +for i in 1 2 3 4 5; do + cp input/file_$i.bin input/dup_$i.bin +done + +"$ZUPT_BIN" c --dedup test_dedup.zupt input/*.bin > /dev/null 2>&1 +chk "Compress with --dedup succeeds" + +mkdir extracted && cd extracted +"$ZUPT_BIN" x ../test_dedup.zupt > /dev/null 2>&1 +chk "Extract --dedup archive succeeds" + +all_match=1 +for i in $(seq 1 10); do + if ! diff -q ../input/file_$i.bin tmp*/input/file_$i.bin > /dev/null 2>&1 \ + && ! diff -q ../input/file_$i.bin input/file_$i.bin > /dev/null 2>&1; then + all_match=0; break + fi +done +[ $all_match -eq 1 ] +chk "All 10 base files roundtrip byte-exact" + +dup_match=1 +for i in 1 2 3 4 5; do + found=0 + for d in tmp*/input input; do + if [ -f "$d/dup_$i.bin" ] && diff -q ../input/dup_$i.bin "$d/dup_$i.bin" > /dev/null 2>&1; then + found=1; break + fi + done + [ $found -eq 1 ] || { dup_match=0; break; } +done +[ $dup_match -eq 1 ] +chk "All 5 duplicate files roundtrip byte-exact" + +cd .. + +# ─── Property 2: dedup reduces size for duplicate-heavy workloads ─────── +echo " [P2. Dedup compresses better than non-dedup on duplicate-heavy data]" + +mkdir dups +for i in $(seq 1 20); do + cp input/file_1.bin dups/copy_$i.bin +done + +"$ZUPT_BIN" c no_dedup.zupt dups/*.bin > /dev/null 2>&1 +"$ZUPT_BIN" c --dedup with_dedup.zupt dups/*.bin > /dev/null 2>&1 + +size_no=$(stat -c%s no_dedup.zupt 2>/dev/null || stat -f%z no_dedup.zupt) +size_yes=$(stat -c%s with_dedup.zupt 2>/dev/null || stat -f%z with_dedup.zupt) + +[ "$size_yes" -lt "$size_no" ] +chk "Dedup archive ($size_yes B) smaller than non-dedup ($size_no B)" + +ratio=$(awk "BEGIN{printf \"%.0f\", $size_yes * 100 / $size_no}") +[ "$ratio" -lt 50 ] +chk "Dedup achieves >50% reduction (got $ratio% of original)" + +# ─── Property 3: dedup roundtrip preserves data on duplicate-only sets ── +echo " [P3. 100% duplicate file set extracts correctly]" + +mkdir extr_dups && cd extr_dups +"$ZUPT_BIN" x ../with_dedup.zupt > /dev/null 2>&1 +chk "Extract heavy-duplicate archive succeeds" + +n_extracted=$(find . -name "copy_*.bin" 2>/dev/null | wc -l) +[ "$n_extracted" -eq 20 ] +chk "All 20 duplicate copies extracted (got $n_extracted)" + +all_dup_match=1 +for f in $(find . -name "copy_*.bin"); do + if ! diff -q "$f" ../input/file_1.bin > /dev/null 2>&1; then + all_dup_match=0; break + fi +done +[ $all_dup_match -eq 1 ] +chk "All extracted duplicates byte-exact match the original" + +cd .. + +# ─── Property 4: dedup + encryption coexist correctly ─────────────────── +echo " [P4. Dedup + SDK encryption work together]" + +"$ZUPT_BIN" keygen --sdk -o k.priv > /dev/null 2>&1 +"$ZUPT_BIN" c --dedup --pq-sdk k.priv.pub enc_dedup.zupt dups/*.bin > /dev/null 2>&1 +chk "Encrypt + dedup compress succeeds" + +mkdir extr_enc && cd extr_enc +"$ZUPT_BIN" x --pq-sdk ../k.priv ../enc_dedup.zupt > /dev/null 2>&1 +chk "Encrypt + dedup extract succeeds" + +n=$(find . -name "copy_*.bin" 2>/dev/null | wc -l) +[ "$n" -eq 20 ] +chk "All 20 copies recovered after enc+dedup ($n found)" + +cd .. + +echo +echo " ───────────────────────────────────────" +echo " Dedup property results: $PASS passed, $FAIL failed" +echo " ───────────────────────────────────────" +[ $FAIL -eq 0 ] diff --git a/tests/test_path_traversal.sh b/tests/test_path_traversal.sh new file mode 100755 index 0000000..49dfe29 --- /dev/null +++ b/tests/test_path_traversal.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Path traversal / Zip Slip regression tests. +# +# Verifies that the v2.2.2 audit fixes for CVE-pattern path traversal +# (Snyk Zip Slip 2018) and symlink-following on extract are working. +# +# Tests construct malicious archives in two ways: +# (A) compress with a relative path then post-mutate the index (manual fuzz) +# (B) try to extract into a directory containing a symlink with the same +# name as an archive entry — should be refused due to O_NOFOLLOW. + +ZUPT_BIN="$(realpath ./zupt)" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT +cd "$TMPDIR" + +PASS=0; FAIL=0 +chk() { + if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1)) + else echo " ✗ $1"; FAIL=$((FAIL+1)); fi +} + +# ─── Property 1: archive with ".." entry must not extract above target ── +# Strategy: compress an innocent file, then patch the archive's index to +# replace the path with "../../escaped.txt". Extract into a subdir; +# verify the file appears nowhere outside the subdir. +echo " [P1. Zip Slip — relative path traversal blocked]" + +mkdir input output_safe +echo "secret content" > input/innocent.txt +"$ZUPT_BIN" c slip.zupt input/innocent.txt > /dev/null 2>&1 + +# Patch the archive: replace "input/innocent.txt" path string with +# "../escape.txt" in the index. We use a python helper because the index +# is varint-prefixed and we need to keep length consistent. +python3 << 'PYEOF' +import sys +data = bytearray(open('slip.zupt','rb').read()) +target = b'input/innocent.txt' +replacement = b'../escape.txt' +# Pad replacement to same length so varint length prefix stays valid +pad = b'\x00' * (len(target) - len(replacement)) +i = data.find(target) +if i < 0: + print("ERROR: pattern not in archive") + sys.exit(1) +# Replace the bytes — note this will fail validation below, which is OK, +# we want to see if extract REJECTS the malformed path. +data[i:i+len(target)] = replacement + pad +open('slip_patched.zupt','wb').write(bytes(data)) +PYEOF + +# Try to extract — even if the patched archive is corrupt, we want to +# verify that NO file appears at "../escape.txt" relative to output_safe. +cd output_safe +"$ZUPT_BIN" x ../slip_patched.zupt > /dev/null 2>&1 +cd .. + +# The key invariant: nothing escaped to TMPDIR (parent of output_safe) +[ ! -f "$TMPDIR/escape.txt" ] && [ ! -f escape.txt ] +chk "No escape via patched ../escape.txt path" + +# ─── Property 2: archive with absolute path must not write to that path ── +echo " [P2. Absolute path entries blocked]" + +# Construct an archive entry with absolute "/tmp/owned.txt" via patching +echo "innocent" > input2.txt +"$ZUPT_BIN" c abs.zupt input2.txt > /dev/null 2>&1 +python3 << 'PYEOF' +data = bytearray(open('abs.zupt','rb').read()) +target = b'input2.txt' +# Replace with absolute path of equal length +replacement = b'/tmp/owned' # 10 chars vs 10 chars +i = data.find(target) +if i >= 0: + data[i:i+len(target)] = replacement + open('abs_patched.zupt','wb').write(bytes(data)) +PYEOF + +mkdir abs_extract +cd abs_extract +"$ZUPT_BIN" x ../abs_patched.zupt > /dev/null 2>&1 +cd .. + +[ ! -f /tmp/owned ] +chk "Absolute /tmp/owned path rejected" + +# ─── Property 3: symlink at output target is not followed ────────────── +# Pre-place a symlink in output dir pointing to a sentinel file. +# Extract an archive with the same entry name; verify the sentinel is +# unchanged (i.e. extract refused to follow the symlink). +echo " [P3. Symlink at extract target not followed]" + +echo "DO_NOT_OVERWRITE" > sentinel.txt +mkdir symlink_extract +ln -s "$(pwd)/sentinel.txt" symlink_extract/innocent.txt + +# Build a fresh non-patched archive with "innocent.txt" +mkdir input3 && echo "evil overwrite content" > input3/innocent.txt +"$ZUPT_BIN" c clean.zupt input3/innocent.txt > /dev/null 2>&1 +# Mutate path "input3/innocent.txt" -> "innocent.txt" so it lands at the symlink +python3 << 'PYEOF' +data = bytearray(open('clean.zupt','rb').read()) +target = b'input3/innocent.txt' +replacement = b'innocent.txt' + (b'\x00' * (len(target) - len(b'innocent.txt'))) +i = data.find(target) +if i >= 0: + data[i:i+len(target)] = replacement + open('clean_patched.zupt','wb').write(bytes(data)) +PYEOF + +cd symlink_extract +"$ZUPT_BIN" x ../clean_patched.zupt > /dev/null 2>&1 +cd .. + +# Sentinel must be unchanged — symlink follow would have overwritten it +content=$(cat sentinel.txt) +[ "$content" = "DO_NOT_OVERWRITE" ] +chk "Sentinel via symlink not overwritten" + +# ─── Property 4: legitimate paths still extract correctly ───────────── +echo " [P4. Legitimate (safe) paths still extract]" + +mkdir legit_input +echo "ok content" > legit_input/normal.txt +"$ZUPT_BIN" c legit.zupt legit_input/normal.txt > /dev/null 2>&1 + +mkdir legit_extract && cd legit_extract +"$ZUPT_BIN" x ../legit.zupt > /dev/null 2>&1 +cd .. + +[ -f legit_extract/legit_input/normal.txt ] && \ + [ "$(cat legit_extract/legit_input/normal.txt)" = "ok content" ] +chk "Normal extraction still works" + +# ─── Property 5: deep path (allowed) but parent dir is created ───────── +echo " [P5. Multi-component safe paths still work]" + +mkdir deep && mkdir deep/sub && mkdir deep/sub/sub2 +echo "deep" > deep/sub/sub2/file.txt +"$ZUPT_BIN" c deep.zupt deep/sub/sub2/file.txt > /dev/null 2>&1 + +mkdir deep_extract && cd deep_extract +"$ZUPT_BIN" x ../deep.zupt > /dev/null 2>&1 +cd .. + +[ -f deep_extract/deep/sub/sub2/file.txt ] +chk "Deep nested path extracted" + +echo +echo " ───────────────────────────────────────" +echo " Path-traversal regression: $PASS passed, $FAIL failed" +echo " ───────────────────────────────────────" +[ $FAIL -eq 0 ] diff --git a/tests/test_pq.sh b/tests/test_pq.sh index 563047c..d594cc8 100644 --- a/tests/test_pq.sh +++ b/tests/test_pq.sh @@ -1,4 +1,6 @@ #!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés set +e ZUPT="${1:-./zupt}" T="/tmp/zupt_pq_$$"; mkdir -p "$T/data" diff --git a/tests/test_sdk.sh b/tests/test_sdk.sh new file mode 100755 index 0000000..f2f253c --- /dev/null +++ b/tests/test_sdk.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés +# Test zupt SDK-backed PQ encryption (v2.2+) + + +cd "$(dirname "$0")/.." +ZUPT_BIN="$(realpath ./zupt)" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +cd "$TMPDIR" +PASS=0; FAIL=0 +chk() { if [ $? -eq 0 ]; then echo " OK: $1"; PASS=$((PASS+1)); else echo " FAIL: $1"; FAIL=$((FAIL+1)); fi; } +chk_neg() { if [ $? -ne 0 ]; then echo " OK: $1"; PASS=$((PASS+1)); else echo " FAIL: $1 (should have failed)"; FAIL=$((FAIL+1)); fi; } + +# Setup: real keypair +"$ZUPT_BIN" keygen --sdk -o key.priv > /dev/null 2>&1 +[ -f key.priv ] && [ -f key.priv.pub ] +chk "SDK keygen produces both files" + +# Test data +echo "Hello SDK PQ encryption" > input.txt +dd if=/dev/urandom of=large.bin bs=64K count=4 2>/dev/null + +# Roundtrip small file +"$ZUPT_BIN" c --pq-sdk key.priv.pub small.zupt input.txt > /dev/null 2>&1 +chk "SDK encrypt small" +mkdir -p extract1 && cd extract1 +"$ZUPT_BIN" x --pq-sdk ../key.priv ../small.zupt > /dev/null 2>&1 +chk "SDK decrypt small" +diff -q input.txt ../input.txt > /dev/null 2>&1 +chk "SDK small roundtrip byte-exact" +cd .. + +# Roundtrip large file +"$ZUPT_BIN" c --pq-sdk key.priv.pub large.zupt large.bin > /dev/null 2>&1 +chk "SDK encrypt large (256KB)" +mkdir -p extract2 && cd extract2 +"$ZUPT_BIN" x --pq-sdk ../key.priv ../large.zupt > /dev/null 2>&1 +chk "SDK decrypt large" +diff -q large.bin ../large.bin > /dev/null 2>&1 +chk "SDK large roundtrip byte-exact" +cd .. + +# Wrong key rejected +"$ZUPT_BIN" keygen --sdk -o other.priv > /dev/null 2>&1 +"$ZUPT_BIN" x --pq-sdk other.priv small.zupt > /dev/null 2>&1 +chk_neg "SDK wrong key rejected" + +# Tamper detected +cp small.zupt tampered.zupt +python3 -c " +b = bytearray(open('tampered.zupt','rb').read()) +b[len(b)-50] ^= 1 +open('tampered.zupt','wb').write(bytes(b)) +" +"$ZUPT_BIN" x --pq-sdk key.priv tampered.zupt > /dev/null 2>&1 +chk_neg "SDK tampered ciphertext rejected" + +# Legacy v1 compat: legacy --pq still works +"$ZUPT_BIN" keygen -o legacy.key > /dev/null 2>&1 +"$ZUPT_BIN" c --pq legacy.key legacy.zupt input.txt > /dev/null 2>&1 +chk "Legacy --pq still encrypts" +mkdir -p extract3 && cd extract3 +"$ZUPT_BIN" x --pq ../legacy.key ../legacy.zupt > /dev/null 2>&1 +chk "Legacy --pq still decrypts" +cd .. + +echo +echo " Results: $PASS passed, $FAIL failed ($((PASS+FAIL)) tests)" +[ $FAIL -eq 0 ] diff --git a/tests/test_threaded.sh b/tests/test_threaded.sh index f8f7d9a..0805c30 100644 --- a/tests/test_threaded.sh +++ b/tests/test_threaded.sh @@ -1,4 +1,6 @@ #!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (c) 2025-2026 Cristian Cezar Moisés set +e ZUPT="./zupt" T="/tmp/zupt_mt_$$" diff --git a/tests/test_vectors.c b/tests/test_vectors.c index 2dac73d..aabce5c 100644 --- a/tests/test_vectors.c +++ b/tests/test_vectors.c @@ -1,7 +1,8 @@ /* - * Zupt — NIST/RFC Cryptographic Test Vectors * SPDX-License-Identifier: AGPL-3.0-or-later - * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co) + * Copyright (c) 2025-2026 Cristian Cezar Moisés + * Zupt — NIST/RFC Cryptographic Test Vectors + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later * * Tests: SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 4231), * X25519 (RFC 7748 §6.1), ML-KEM-768 roundtrip, diff --git a/vendor/zuptsdk/include/vaptvupt.h b/vendor/zuptsdk/include/vaptvupt.h new file mode 100644 index 0000000..55d994c --- /dev/null +++ b/vendor/zuptsdk/include/vaptvupt.h @@ -0,0 +1,465 @@ +/* + * VaptVupt Codec — Next-generation lossless compression + * Public API and data structures + * + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 Cristian. + * Zero dependencies. Pure C11. + */ +#ifndef VAPTVUPT_H +#define VAPTVUPT_H + +#include +#include "vv_platform.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ═══════════════════════════════════════════════════════════════ + * VERSION & CONSTANTS + * ═══════════════════════════════════════════════════════════════ */ + +#define VV_VERSION_MAJOR 0 +#define VV_VERSION_MINOR 1 +#define VV_VERSION_PATCH 0 +#define VV_VERSION_STRING "0.1.0" + +#define VV_MAGIC 0x56560100u /* "VV\x01\x00" */ +#define VV_MAX_BLOCK_SIZE (1u << 20) /* 1 MB per block */ +#define VV_MIN_MATCH 4 +#define VV_MAX_MATCH 65535 +#define VV_MAX_LIT_RUN 65535 +#define VV_MAX_OFFSET (1u << 24) /* 16 MB default window */ + +/* ═══════════════════════════════════════════════════════════════ + * ERROR CODES + * ═══════════════════════════════════════════════════════════════ */ + +typedef enum { + VV_OK = 0, + VV_ERR_IO = -1, + VV_ERR_CORRUPT = -2, + VV_ERR_NOMEM = -3, + VV_ERR_OVERFLOW = -4, + VV_ERR_BAD_MAGIC = -5, + VV_ERR_PARAM = -6, +} vv_error_t; + +/* ═══════════════════════════════════════════════════════════════ + * COMPRESSION MODES + * ═══════════════════════════════════════════════════════════════ */ + +typedef enum { + VV_MODE_ULTRA_FAST = 0, /* Speed priority: greedy parse, no entropy */ + VV_MODE_BALANCED = 1, /* Default: lazy parse + Huffman */ + VV_MODE_EXTREME = 2, /* Ratio priority: optimal parse + Huffman */ +} vv_mode_t; + +/* ═══════════════════════════════════════════════════════════════ + * BLOCK TYPES (2-bit field in block header) + * ═══════════════════════════════════════════════════════════════ */ + +typedef enum { + VV_BLOCK_RAW = 0, /* Uncompressed (stored) */ + VV_BLOCK_COMPRESSED = 1, /* LZ + raw literals */ + VV_BLOCK_RLE = 2, /* Run-length (single byte) */ + VV_BLOCK_ENTROPY = 3, /* LZ + entropy-coded literals (ANS or Huffman) */ +} vv_block_type_t; + +/* Entropy sub-type tags (first byte of entropy section in type-3 blocks) */ +#define VV_ENTROPY_HUFFMAN 0x48 /* 'H' — Huffman (v0.3-v0.4) */ +#define VV_ENTROPY_ANS 0x41 /* 'A' — tANS single-stream (v0.5) */ +#define VV_ENTROPY_ANS4 0x49 /* 'I' — tANS 4-way interleaved (v0.6+) */ +#define VV_ENTROPY_CTX 0x43 /* 'C' — tANS order-1 context model (v0.7+) */ +#define VV_ENTROPY_SEQ 0x53 /* 'S' — sequence coding: ANS on lits+ml+of (v0.8+) */ +#define VV_ENTROPY_SEQ_V2 0x54 /* 'T' — same as 'S' but with min_match=3 + * for binary-data compression parity with + * gzip-9. Shifts ml_base[] down by 1 across + * all 36 codes; every other field unchanged. + * Added in v2.33.0 (decode); encoder in a + * future release. */ + +/* Block header accessors (2-bit type, 1-bit last, 21-bit size) */ +static inline vv_block_type_t vv_bh_type(uint32_t h) { return (vv_block_type_t)(h & 3); } +static inline int vv_bh_last(uint32_t h) { return (h >> 2) & 1; } +static inline uint32_t vv_bh_size(uint32_t h) { return (h >> 3) & 0x1FFFFF; } +static inline uint32_t vv_bh_pack(vv_block_type_t t, int last, uint32_t sz) { + return (uint32_t)t | ((uint32_t)last << 2) | (sz << 3); +} + +/* ═══════════════════════════════════════════════════════════════ + * TOKEN TYPES (in the sequence stream) + * + * Each token is: [type:2][litlen:6] [optional litlen ext] + * [literal bytes] + * [matchlen ext] [offset bytes] + * + * The decoder reads a compact token byte, copies literals, + * then copies a match. This is LZ4-like for speed. + * ═══════════════════════════════════════════════════════════════ */ + +/* Token byte layout: + * Bits 7-4: literal_length (0-14, 15=extended) + * Bits 3-0: match_length - VV_MIN_MATCH (0-14, 15=extended) + * + * Followed by: + * [extended literal length varint, if litlen==15] + * [literal bytes] + * [offset: 2 bytes LE (or 3 bytes if high bit set)] + * [extended match length varint, if matchlen==15] + */ + +/* ═══════════════════════════════════════════════════════════════ + * ON-DISK STRUCTURES + * ═══════════════════════════════════════════════════════════════ */ + +#pragma pack(push, 1) + +/* Frame header: 16 bytes */ +typedef struct { + uint32_t magic; /* VV_MAGIC */ + uint8_t version; /* Format version (1) */ + uint8_t flags; /* bit0: has_checksum, bit1: has_dict */ + uint8_t mode_hint; /* Compression mode used (informational) */ + uint8_t window_log; /* Window size = 1 << window_log */ + uint64_t content_size; /* Uncompressed size (0 = unknown) */ +} vv_frame_header_t; + +/* Block header: 4 bytes */ +typedef struct { + /* Bits 0-1: block_type (vv_block_type_t) */ + /* Bit 2: last_block flag */ + /* Bits 3-23: decompressed_size (max 1 MB) */ + /* Bits 24-31: reserved */ + uint32_t packed; +} vv_block_header_t; + +/* Frame footer: 12 bytes */ +typedef struct { + uint64_t checksum; /* XXH64 of decompressed content */ + uint32_t footer_magic; /* 0x56564E44 = "VVND" */ +} vv_frame_footer_t; + +#pragma pack(pop) + +/* Block header accessors defined above with block type enum */ + +/* ═══════════════════════════════════════════════════════════════ + * MATCHER STATE + * ═══════════════════════════════════════════════════════════════ */ + +#define VV_HC_BITS 18 +#define VV_HC_SIZE (1u << VV_HC_BITS) + +typedef struct { + int32_t table[VV_HC_SIZE]; /* Hash → most recent position */ + int32_t *chain; /* Chain array (window_size entries) */ + uint32_t window_size; + uint32_t chain_depth; /* Max chain traversal (level-dependent) */ +} vv_matcher_t; + +/* ═══════════════════════════════════════════════════════════════ + * HUFFMAN TABLES (entropy coding) + * + * 256-symbol alphabet. Max code length 12 bits. + * Decode table: 4096 entries × 2 bytes = 8 KB (fits in L1). + * ═══════════════════════════════════════════════════════════════ */ + +#define VV_HUF_MAX_BITS 12 +#define VV_HUF_TABLE_SIZE (1 << VV_HUF_MAX_BITS) + +typedef struct { + uint8_t lengths[256]; /* Code lengths per symbol */ + uint16_t codes[256]; /* Canonical codes (for encoding) */ + /* Decode table: entry = (symbol << 8) | num_bits */ + uint16_t decode[VV_HUF_TABLE_SIZE]; +} vv_huffman_t; + +/* ═══════════════════════════════════════════════════════════════ + * ENCODER/DECODER OPTIONS + * ═══════════════════════════════════════════════════════════════ */ + +typedef struct { + vv_mode_t mode; + uint8_t window_log; /* 0 = auto (20 for balanced, 24 for extreme) */ + int checksum; /* 1 = compute XXH64 */ + int verbose; + int format_v2; /* 1 = produce 'T' tag blocks (min_match=3) for + * better real-binary ratio. Requires decoder + * v2.33.0+. Default 0 for back-compat. */ +} vv_options_t; + +static inline void vv_default_options(vv_options_t *o) { + o->mode = VV_MODE_BALANCED; + o->window_log = 0; + o->checksum = 1; + o->verbose = 0; + o->format_v2 = 0; +} + +/* ═══════════════════════════════════════════════════════════════ + * PUBLIC API — ONE-SHOT + * ═══════════════════════════════════════════════════════════════ */ + +/* Compress src[0..src_len-1] into dst[0..dst_cap-1]. + * Returns compressed size, or negative error code. */ +int64_t vv_compress(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + const vv_options_t *opts); + +/* Decompress src[0..src_len-1] into dst[0..dst_cap-1]. + * Returns decompressed size, or negative error code. */ +int64_t vv_decompress(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap); + +/* Flags for vv_decompress_flags (bitmask) */ +#define VV_DECOMPRESS_DEFAULT 0x0 +#define VV_DECOMPRESS_SKIP_CHECKSUM 0x1 /* Skip XXH64 footer verification. + * + * Use when the caller has its own + * integrity protection (e.g. AES-GCM + * wrapping the compressed data, as in + * Zupt backups). On RAW/random-data + * inputs where XXH64 dominates decode + * time, this flag delivers a ~2× speedup. + * + * SAFETY: only set when another layer + * already detects tampering/corruption. + * Without any integrity check, silent + * data corruption can go undetected. */ + +/* Decompress with flags. Returns decompressed size, or negative error code. + * Equivalent to vv_decompress() when flags == VV_DECOMPRESS_DEFAULT. */ +int64_t vv_decompress_flags(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + uint32_t flags); + +/* Compute upper bound on compressed size for src_len input bytes. */ +size_t vv_compress_bound(size_t src_len); + +/* ═══════════════════════════════════════════════════════════════ + * MULTI-THREADED COMPRESSION + * + * Compresses large inputs in parallel by splitting into independent + * frames (each a valid .vv frame on its own — concatenated output + * is a valid .vv file that vv_decompress handles natively as a + * multi-frame stream). + * + * Requires the library to be built with VV_ENABLE_THREADS (and + * linked with -lpthread on POSIX). If threads are not available, + * the function falls back to sequential single-threaded encoding, + * producing bit-identical output to vv_compress. + * + * Tradeoff: multi-frame output is ~0.5-2% larger than a single + * vv_compress frame because cross-frame match history is lost. Use + * for inputs ≥ 4 MB where parallel speedup outweighs the ratio cost. + * ═══════════════════════════════════════════════════════════════ */ + +/* Compress src in parallel using up to nthreads worker threads. + * If nthreads is 0, uses the number of online CPUs (or 1 if that + * cannot be determined). If the library was built without threading, + * this acts exactly like vv_compress (nthreads is ignored). + * + * chunk_size controls the frame split size — must be ≥ 1 MB for + * reasonable compression ratio. If 0, defaults to 4 MB. + * + * Returns compressed size on success, negative error code on failure. */ +int64_t vv_compress_mt(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + const vv_options_t *opts, + unsigned int nthreads, + size_t chunk_size); + +/* Frame info extracted from the first 16 bytes of a compressed stream. + * Populated by vv_get_frame_info(). */ +typedef struct { + uint8_t version; /* Format version */ + uint8_t has_checksum; /* Non-zero if frame has XXH64 footer */ + uint8_t mode_hint; /* Compression mode used (informational) */ + uint8_t window_log; /* Window size = 1 << window_log */ + uint64_t content_size; /* Uncompressed size if known (0 = unknown) */ +} vv_frame_info_t; + +/* Parse the first 16 bytes of a compressed stream to extract frame + * metadata. Requires src_len >= 16. Useful for pre-allocating the + * output buffer when content_size is known (e.g., streams produced + * by the one-shot vv_compress API always carry content_size). + * + * Returns VV_OK on success, negative error code on bad magic / + * unsupported version / too-short input. */ +int vv_get_frame_info(const uint8_t *src, size_t src_len, + vv_frame_info_t *info); + +/* ═══════════════════════════════════════════════════════════════ + * STREAMING API — for large files, memory-constrained use, or + * when the full input/output isn't known in advance. + * + * Compress: + * ctx = vv_cstream_create(&opts); + * for each chunk: vv_cstream_compress_chunk(ctx, chunk, len, dst, dst_cap, &written, is_last); + * vv_cstream_destroy(ctx); + * + * Decompress: + * ctx = vv_dstream_create(); + * for each incoming block: vv_dstream_decompress_chunk(ctx, src, len, dst, dst_cap, &read, &written); + * vv_dstream_destroy(ctx); + * + * Compression is block-at-a-time: caller accumulates source data in + * chunks of up to VV_MAX_BLOCK_SIZE (1 MB). Each call to + * vv_cstream_compress_chunk emits one compressed block (or the frame + * header on the first call, and the frame footer on the last). + * + * Decompression accepts arbitrary byte chunks and emits decoded bytes + * as blocks complete. Partial blocks are buffered internally. + * ═══════════════════════════════════════════════════════════════ */ + +/* Opaque stream context types */ +typedef struct vv_cstream_s vv_cstream_t; +typedef struct vv_dstream_s vv_dstream_t; + +/* Create a new compression stream context. + * Returns NULL on allocation failure. + * If opts is NULL, uses default options (balanced mode, checksum=1). + * The context holds the matcher state; cross-block rep-match history + * and hash tables are preserved across chunks for optimal ratio. */ +vv_cstream_t *vv_cstream_create(const vv_options_t *opts); + +/* Reset a compression stream for reuse. Clears the matcher state, + * rep-match offsets, checksum accumulator, and emission flag so the + * context can be used to compress a new independent frame. + * Scratch buffers are preserved — this is the fast path for + * per-file compression (e.g., backup tools compressing many small + * files), avoiding per-file allocation cost. + * + * If opts is NULL, reuses the options from the last create/reset. + * If opts is non-NULL, applies new options but window_log cannot + * change (would require re-allocating matcher tables). */ +int vv_cstream_reset(vv_cstream_t *ctx, const vv_options_t *opts); + +/* Compress one chunk of source into dst. chunk_len must be ≤ + * VV_MAX_BLOCK_SIZE (1 MB). Set is_last=1 on the final call to emit + * the frame footer (checksum if enabled). + * + * Writes at most dst_cap bytes to dst; sets *written to the actual + * number of bytes emitted. Caller must ensure dst_cap ≥ + * vv_compress_bound(chunk_len) + 24 (frame header + footer). + * + * On the first call, the frame header is emitted before the first + * block. On the last call, the frame footer (if checksum enabled) is + * emitted after the final block. + * + * Returns VV_OK (0) on success, negative error code on failure. */ +int vv_cstream_compress_chunk(vv_cstream_t *ctx, + const uint8_t *chunk, size_t chunk_len, + uint8_t *dst, size_t dst_cap, + size_t *written, int is_last); + +/* Destroy a compression stream context and free all resources. */ +void vv_cstream_destroy(vv_cstream_t *ctx); + +/* Create a new decompression stream context. + * Returns NULL on allocation failure. */ +vv_dstream_t *vv_dstream_create(void); + +/* Reset a decompression stream for reuse. Clears state so the same + * context can decompress another independent frame. Internal buffer + * is preserved (but emptied), avoiding per-frame allocation cost. */ +int vv_dstream_reset(vv_dstream_t *ctx); + +/* Decompress a chunk of input. src may contain partial or multiple + * blocks; internal buffer holds incomplete blocks until enough input + * is available. + * + * IMPORTANT API CONTRACT: + * - `dst` MUST be the same stable buffer base across all calls for + * a single frame. The decoder tracks its own output position + * inside `dst` and requires it not to move between calls. + * - `dst_cap` MUST be large enough to hold the fully-decoded + * content of the current frame (the decoder does not support + * partial-output-then-resume semantics across a block boundary). + * - `*written` is set to the CUMULATIVE total bytes written into + * `dst` so far, NOT the delta for this call. If you need the + * per-call delta, subtract the previous value. + * - `*consumed` is per-call: how many `src` bytes were processed + * this call. + * + * Writing pattern: + * size_t total_written = 0; + * while (!done) { + * rc = vv_dstream_decompress_chunk(ds, chunk, chunk_len, + * dst, dst_cap, // stable + * &consumed, &written); + * total_written = written; // NOT += written + * ... + * } + * + * Returns VV_OK (0) if more input is needed, 1 if the frame ended + * successfully, or negative error code on failure. */ +int vv_dstream_decompress_chunk(vv_dstream_t *ctx, + const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t *consumed, size_t *written); + +/* Destroy a decompression stream context and free all resources. */ +void vv_dstream_destroy(vv_dstream_t *ctx); + +/* ═══════════════════════════════════════════════════════════════ + * INTERNAL HELPERS (shared across modules) + * ═══════════════════════════════════════════════════════════════ */ + +/* XXH64 hash (simplified, for checksum) */ +uint64_t vv_xxh64(const void *data, size_t len, uint64_t seed); + +/* Streaming XXH64: init + update + finalize for when the input isn't + * contiguous in memory. Must produce the same 64-bit hash as a + * single-shot vv_xxh64() over the concatenated input. */ +typedef struct { + uint64_t v1, v2, v3, v4; + uint64_t total_len; + uint64_t seed; + uint8_t buf[32]; + size_t buf_len; +} vv_xxh64_state_t; + +void vv_xxh64_init(vv_xxh64_state_t *s, uint64_t seed); +void vv_xxh64_update(vv_xxh64_state_t *s, const void *data, size_t len); +uint64_t vv_xxh64_finalize(const vv_xxh64_state_t *s); + +/* Hash function for matcher */ +static inline uint32_t vv_hash4(const uint8_t *p) { + uint32_t v; + memcpy(&v, p, 4); + return (v * 2654435761u) >> (32 - VV_HC_BITS); +} + +/* Read/write little-endian helpers */ +static inline uint16_t vv_read16(const uint8_t *p) { + uint16_t v; memcpy(&v, p, 2); return v; +} +static inline uint32_t vv_read32(const uint8_t *p) { + uint32_t v; memcpy(&v, p, 4); return v; +} +static inline void vv_write16(uint8_t *p, uint16_t v) { + memcpy(p, &v, 2); +} +static inline void vv_write32(uint8_t *p, uint32_t v) { + memcpy(p, &v, 4); +} + +/* ═══════════════════════════════════════════════════════════════ + * SIMD COPY HELPERS (declared here, defined in vv_simd.c) + * ═══════════════════════════════════════════════════════════════ */ + +/* Copy exactly n bytes, may over-read/write by up to 32 bytes. + * Caller must ensure sufficient slack in destination. */ +void vv_copy_fast(uint8_t *dst, const uint8_t *src, size_t n); + +/* Copy match with overlap handling (offset may be < copy length). */ +void vv_copy_match(uint8_t *dst, uint32_t offset, size_t length); + +#ifdef __cplusplus +} +#endif +#endif /* VAPTVUPT_H */ diff --git a/vendor/zuptsdk/include/vaptvupt_api.h b/vendor/zuptsdk/include/vaptvupt_api.h new file mode 100644 index 0000000..f19c85f --- /dev/null +++ b/vendor/zuptsdk/include/vaptvupt_api.h @@ -0,0 +1,43 @@ +/* + * VaptVupt — Zupt Integration API + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 Cristian. + * + * ZUPT-COMPAT: This is the API that Zupt calls. It wraps the internal + * VaptVupt API with sensible defaults for backup workloads: + * - Checksum always enabled (data integrity is critical for backups) + * - Adaptive window selection (auto-detect optimal wlog per file) + * - Level maps to mode: 1=fast, 5=balanced, 9=extreme + * + * Usage: + * size_t bound = vvz_compress_bound(src_len); + * uint8_t *dst = malloc(bound); + * int64_t csz = vvz_compress(src, src_len, dst, bound, 5); + * int64_t dsz = vvz_decompress(dst, csz, out, out_cap); + */ +#ifndef VAPTVUPT_API_H +#define VAPTVUPT_API_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Compress src into dst. Returns compressed size or negative error code. + * level: 1 = fast (max speed), 5 = balanced (default), 9 = extreme (max ratio) */ +int64_t vvz_compress(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, int level); + +/* Decompress src into dst. Returns decompressed size or negative error code. */ +int64_t vvz_decompress(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap); + +/* Upper bound on compressed size for a given input length. */ +size_t vvz_compress_bound(size_t src_len); + +#ifdef __cplusplus +} +#endif +#endif /* VAPTVUPT_API_H */ diff --git a/vendor/zuptsdk/include/vv_ans.h b/vendor/zuptsdk/include/vv_ans.h new file mode 100644 index 0000000..14ba717 --- /dev/null +++ b/vendor/zuptsdk/include/vv_ans.h @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2026 Cristian Cezar Moisés + * + * VaptVupt — tANS Entropy Codec (v2: sparse header + 4-way interleaved) + * + * Standalone: define VV_ANS_STANDALONE to use without VaptVupt. + * ZUPT-COMPAT: this header has zero VaptVupt dependencies when standalone. + * + * v0.6 changes: + * - Adaptive sparse/dense header (Item 1): 3× smaller on typical data + * - 4-way interleaved encode/decode (Item 2): ~2.5× faster decode + */ +#ifndef VV_ANS_H +#define VV_ANS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define VVA_TABLE_LOG 12 +#define VVA_TABLE_SIZE (1 << VVA_TABLE_LOG) /* 4096 */ +#define VVA_MAX_SYMBOL 256 + +/* Header format discriminators */ +#define VVA_HDR_SINGLE 0x01 /* Single symbol: 0-bit encoding */ +#define VVA_HDR_SPARSE 0x02 /* ≤32 active symbols: (sym,freq) pairs */ +#define VVA_HDR_DENSE 0x03 /* >32 active symbols: max_sym + freq array */ +/* ZUPT-COMPAT: v0.5 legacy format detected by first byte being 0x00-0xFF + * without matching any HDR_* code — fall back to old read path. */ +#define VVA_HDR_LEGACY 0x00 /* v0.5 format: [max_sym] [2B×(max_sym+1)] */ + +#ifdef VV_ANS_STANDALONE +typedef enum { + VVA_OK = 0, + VVA_ERR_IO = -1, + VVA_ERR_CORRUPT = -2, + VVA_ERR_NOMEM = -3, + VVA_ERR_OVERFLOW = -4, + VVA_ERR_PARAM = -6, +} vva_error_t; +#else +#include "vaptvupt.h" +typedef vv_error_t vva_error_t; +#define VVA_OK VV_OK +#define VVA_ERR_CORRUPT VV_ERR_CORRUPT +#define VVA_ERR_NOMEM VV_ERR_NOMEM +#define VVA_ERR_OVERFLOW VV_ERR_OVERFLOW +#define VVA_ERR_PARAM VV_ERR_PARAM +#endif + +typedef struct { + uint8_t symbol; + uint8_t nbits; + uint16_t baseline; +} vva_dec_entry_t; + +/* ═══ Public API ═══ */ + +/* Single-stream encode/decode (tag 'A', backward compat with v0.5) */ +vva_error_t vva_encode(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len); + +vva_error_t vva_decode(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t num_literals, size_t *src_consumed); + +/* 4-way interleaved encode/decode (tag 'I', v0.6+) */ +vva_error_t vva_encode4(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len); + +vva_error_t vva_decode4(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t num_literals, size_t *src_consumed); + +/* Order-1 context model encode/decode (tag 'C', v0.7+) + * Uses 256 ANS tables — one per previous byte. Contexts with too few + * observations inherit from the global table. 4 MB decode memory. */ +vva_error_t vva_encode_ctx(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len); + +vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t num_literals, size_t *src_consumed); + +/* ═══ Sequence coding (tag 'S', v0.8+) ═══ + * ZUPT-COMPAT: available when VV_ANS_STANDALONE is defined. + * + * Encodes an LZ token stream using 3 ANS tables: literals, match-length + * codes (36 symbols), and offset codes (24 symbols). Replaces raw varint + * storage of match metadata, saving 8-15% on typical data. + * + * Input token format (from LZ engine): + * [token: litlen:4|matchlen:4] [litlen_ext] [literal_bytes] [2B offset LE] [matchlen_ext] + * Output: [3 table headers] [4B seq_count] [4B lit_count] [ANS bitstream] + */ + +#define VVA_ML_CODES 36 /* Match length code count */ +#define VVA_OF_CODES 27 /* Offset code count: 3 rep + 24 explicit */ +#define VVA_LL_CODES 36 /* Literal-run length code count (covers 0-65536+) */ + +vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + int off_bytes); + +/* Format-v2 variant: encodes match-length codes using ml_base_v2 + * (min_match=3). Used for 'T' tag blocks. Added v2.34.0. */ +vva_error_t vva_encode_sequences_v2(const uint8_t *tokens, size_t tok_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + int off_bytes); + +vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + const uint8_t *dst_base); + +/* Format-v2 variant: same wire payload as vva_decode_sequences but + * interprets match-length codes with a table shifted down by 1 + * (min_match=3 instead of 4). Produced by tag 'T' (VV_ENTROPY_SEQ_V2) + * blocks; closes the ~10% binary-compression gap vs gzip-9. Added + * v2.33.0. */ +vva_error_t vva_decode_sequences_v2(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + const uint8_t *dst_base); + +static inline size_t vva_bound(size_t src_len) { + /* Context model header can be up to ~10KB, seq coding adds 3 table headers */ + return 12288 + (src_len * 15 + 7) / 8 + 16; +} + +#ifdef __cplusplus +} +#endif +#endif /* VV_ANS_H */ diff --git a/vendor/zuptsdk/include/vv_huffman.h b/vendor/zuptsdk/include/vv_huffman.h new file mode 100644 index 0000000..9136a36 --- /dev/null +++ b/vendor/zuptsdk/include/vv_huffman.h @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2026 Cristian Cezar Moisés + * + * VaptVupt — Canonical Huffman Codec + * + * Standalone header: can be used independently with VV_HUFFMAN_STANDALONE. + * Designed for embedding in Zupt or any other LZ codec. + * + * API: + * vvh_encode() — compress raw literals into Huffman bitstream + * vvh_decode() — decompress Huffman bitstream back to raw literals + * + * Format: + * [1B max_symbol] [packed nibble code lengths] [LSB-first bitstream] + * + * Performance targets: + * Encode: ≥ 150 MB/s Decode: ≥ 800 MB/s (x86-64, -O2) + */ +#ifndef VV_HUFFMAN_H +#define VV_HUFFMAN_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ═══════════════════════════════════════════════════════════════ + * CONSTANTS + * ═══════════════════════════════════════════════════════════════ */ + +#define VVH_SYMBOLS 256 +#define VVH_MAX_CODE_LEN 15 +#define VVH_DECODE_BITS 12 +#define VVH_DECODE_SIZE (1 << VVH_DECODE_BITS) /* 4096 entries */ + +/* ═══════════════════════════════════════════════════════════════ + * ERROR CODES (compatible with vv_error_t when not standalone) + * ═══════════════════════════════════════════════════════════════ */ + +#ifdef VV_HUFFMAN_STANDALONE +typedef enum { + VVH_OK = 0, + VVH_ERR_CORRUPT = -2, + VVH_ERR_NOMEM = -3, + VVH_ERR_OVERFLOW= -4, +} vvh_error_t; +#else +#include "vaptvupt.h" +typedef vv_error_t vvh_error_t; +#define VVH_OK VV_OK +#define VVH_ERR_CORRUPT VV_ERR_CORRUPT +#define VVH_ERR_NOMEM VV_ERR_NOMEM +#define VVH_ERR_OVERFLOW VV_ERR_OVERFLOW +#endif + +/* ═══════════════════════════════════════════════════════════════ + * ENCODE TABLE (used by encoder only) + * ═══════════════════════════════════════════════════════════════ */ + +typedef struct { + uint8_t lengths[VVH_SYMBOLS]; /* Code length per symbol (0 = absent) */ + uint16_t codes[VVH_SYMBOLS]; /* Bit-reversed canonical codes (LSB-first) */ +} vvh_enc_table_t; + +/* ═══════════════════════════════════════════════════════════════ + * DECODE TABLE (used by decoder only) + * + * 12-bit lookup: 4096 entries × 4 bytes = 16 KB (L1-resident). + * Entry: bits [7:0] = symbol, bits [11:8] = code length. + * Symbols with code length > 12 use a slow path. + * ═══════════════════════════════════════════════════════════════ */ + +typedef struct { + uint32_t table[VVH_DECODE_SIZE]; /* Fast lookup (codes ≤ 12 bits) */ + /* Slow table for codes 13-15 bits (max 256 entries) */ + uint16_t slow_code[VVH_SYMBOLS]; /* Bit-reversed code */ + uint8_t slow_len[VVH_SYMBOLS]; /* Code length */ + uint8_t slow_sym[VVH_SYMBOLS]; /* Symbol value */ + int slow_count; /* Number of slow-path symbols */ +} vvh_dec_table_t; + +/* ═══════════════════════════════════════════════════════════════ + * PUBLIC API + * ═══════════════════════════════════════════════════════════════ */ + +/* + * Encode raw literal bytes into Huffman bitstream. + * + * src[0..src_len-1] — raw literal bytes + * dst[0..dst_cap-1] — output buffer (header + bitstream) + * *dst_len — on success, set to actual compressed size + * + * Returns VVH_OK on success, or VVH_ERR_OVERFLOW if dst too small. + * If compressed size >= src_len, returns VVH_ERR_OVERFLOW (incompressible). + */ +vvh_error_t vvh_encode(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len); + +/* + * Decode Huffman bitstream back to raw literal bytes. + * + * src[0..src_len-1] — compressed data (header + bitstream) + * dst[0..dst_cap-1] — output buffer for decoded literals + * num_literals — expected number of decoded symbols + * *src_consumed — on success, bytes consumed from src + * + * Returns VVH_OK on success, or error code. + */ +vvh_error_t vvh_decode(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t num_literals, size_t *src_consumed); + +/* + * Upper bound on compressed size for src_len literal bytes. + */ +static inline size_t vvh_bound(size_t src_len) { + /* header (129 max) + bitstream (15 bits/symbol worst case) + slack */ + return 129 + (src_len * 15 + 7) / 8 + 8; +} + +#ifdef __cplusplus +} +#endif +#endif /* VV_HUFFMAN_H */ diff --git a/vendor/zuptsdk/include/vv_platform.h b/vendor/zuptsdk/include/vv_platform.h new file mode 100644 index 0000000..2d67b91 --- /dev/null +++ b/vendor/zuptsdk/include/vv_platform.h @@ -0,0 +1,117 @@ +/* + * VaptVupt — Cross-platform portability macros + * + * Provides unified abstractions for compiler intrinsics used throughout + * the codebase. Supports GCC, Clang, MSVC, and Intel compilers. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef VV_PLATFORM_H +#define VV_PLATFORM_H + +#include +#include + +/* ─── Branch prediction hints ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_LIKELY(x) __builtin_expect(!!(x), 1) + #define VV_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else + #define VV_LIKELY(x) (x) + #define VV_UNLIKELY(x) (x) +#endif + +/* ─── Prefetch hint ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_PREFETCH(p) __builtin_prefetch((p), 0, 1) + #define VV_PREFETCH_RW(p) __builtin_prefetch((p), 1, 1) +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + #include + #define VV_PREFETCH(p) _mm_prefetch((const char*)(p), _MM_HINT_T1) + #define VV_PREFETCH_RW(p) _mm_prefetch((const char*)(p), _MM_HINT_T1) +#else + #define VV_PREFETCH(p) ((void)0) + #define VV_PREFETCH_RW(p) ((void)0) +#endif + +/* ─── Always-inline / never-inline ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_ALWAYS_INLINE static inline __attribute__((always_inline)) + #define VV_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) + #define VV_ALWAYS_INLINE static __forceinline + #define VV_NOINLINE __declspec(noinline) +#else + #define VV_ALWAYS_INLINE static inline + #define VV_NOINLINE +#endif + +/* ─── Unused parameter suppression ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_UNUSED __attribute__((unused)) +#else + #define VV_UNUSED +#endif + +/* ─── Portable unaligned load/store via memcpy (compiler optimizes to single instr) ─── */ +static inline uint16_t vv_load16(const void *p) { + uint16_t v; memcpy(&v, p, 2); return v; +} +static inline uint32_t vv_load32(const void *p) { + uint32_t v; memcpy(&v, p, 4); return v; +} +static inline uint64_t vv_load64(const void *p) { + uint64_t v; memcpy(&v, p, 8); return v; +} +static inline void vv_store16(void *p, uint16_t v) { memcpy(p, &v, 2); } +static inline void vv_store32(void *p, uint32_t v) { memcpy(p, &v, 4); } +static inline void vv_store64(void *p, uint64_t v) { memcpy(p, &v, 8); } + +/* ─── Count trailing zeros (for hash/match optimization) ─── */ +#if defined(__GNUC__) || defined(__clang__) + static inline int vv_ctz32(uint32_t x) { return __builtin_ctz(x); } + static inline int vv_ctz64(uint64_t x) { return __builtin_ctzll(x); } +#elif defined(_MSC_VER) + #include + static inline int vv_ctz32(uint32_t x) { + unsigned long idx; _BitScanForward(&idx, x); return (int)idx; + } + static inline int vv_ctz64(uint64_t x) { + #if defined(_M_X64) || defined(_M_ARM64) + unsigned long idx; _BitScanForward64(&idx, x); return (int)idx; + #else + uint32_t lo = (uint32_t)x; + if (lo) return vv_ctz32(lo); + return 32 + vv_ctz32((uint32_t)(x >> 32)); + #endif + } +#else + static inline int vv_ctz32(uint32_t x) { + int n = 0; while (!(x & 1)) { x >>= 1; n++; } return n; + } + static inline int vv_ctz64(uint64_t x) { + int n = 0; while (!(x & 1)) { x >>= 1; n++; } return n; + } +#endif + +/* ─── SIMD capability detection macros ─── */ +#if defined(__AVX2__) + #define VV_HAS_AVX2 1 +#else + #define VV_HAS_AVX2 0 +#endif + +#if defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) + #define VV_HAS_SSE2 1 +#else + #define VV_HAS_SSE2 0 +#endif + +#if defined(__aarch64__) && defined(__ARM_NEON) + #define VV_HAS_NEON 1 +#else + #define VV_HAS_NEON 0 +#endif + +#endif /* VV_PLATFORM_H */ diff --git a/vendor/zuptsdk/include/zsdk_aes256_gcm_siv.h b/vendor/zuptsdk/include/zsdk_aes256_gcm_siv.h new file mode 100644 index 0000000..9762969 --- /dev/null +++ b/vendor/zuptsdk/include/zsdk_aes256_gcm_siv.h @@ -0,0 +1,39 @@ +/* + * AES-256-GCM-SIV (RFC 8452) + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Nonce-misuse-resistant AEAD. Nonce reuse degrades to deterministic + * encryption (same plaintext+key+nonce -> same ciphertext) rather than + * the catastrophic XOR-of-plaintexts of GCM/CTR. + */ +#ifndef ZSDK_AES256_GCM_SIV_H +#define ZSDK_AES256_GCM_SIV_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define ZSDK_AES256_GCM_SIV_KEYBYTES 32 +#define ZSDK_AES256_GCM_SIV_NONCEBYTES 12 +#define ZSDK_AES256_GCM_SIV_TAGBYTES 16 + +void zsdk_aes256_gcm_siv_encrypt(uint8_t *out, + const uint8_t *plaintext, size_t pt_len, + const uint8_t *aad, size_t aad_len, + const uint8_t key[32], + const uint8_t nonce[12]); + +int zsdk_aes256_gcm_siv_decrypt(uint8_t *out, + const uint8_t *ciphertext, size_t ct_len, + const uint8_t *aad, size_t aad_len, + const uint8_t key[32], + const uint8_t nonce[12]); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/vendor/zuptsdk/include/zsdk_aes256_siv.h b/vendor/zuptsdk/include/zsdk_aes256_siv.h new file mode 100644 index 0000000..7f83dd7 --- /dev/null +++ b/vendor/zuptsdk/include/zsdk_aes256_siv.h @@ -0,0 +1,39 @@ +/* + * AES-256-SIV (RFC 5297) via OpenSSL EVP + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Provides nonce-misuse-resistant AEAD via SIV mode (S2V + CTR). + * Uses OpenSSL's audited implementation. Note: SIV uses a 64-byte key + * (two 32-byte halves) rather than a 32-byte key. + */ +#ifndef ZSDK_AES256_SIV_H +#define ZSDK_AES256_SIV_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define ZSDK_AES256_SIV_KEYBYTES 64 /* AES-256-SIV uses double key */ +#define ZSDK_AES256_SIV_NONCEBYTES 16 /* Optional, can be variable */ +#define ZSDK_AES256_SIV_TAGBYTES 16 + +void zsdk_aes256_siv_encrypt(uint8_t *out, + const uint8_t *plaintext, size_t pt_len, + const uint8_t *aad, size_t aad_len, + const uint8_t key[64], + const uint8_t nonce[16]); + +int zsdk_aes256_siv_decrypt(uint8_t *out, + const uint8_t *ciphertext, size_t ct_len, + const uint8_t *aad, size_t aad_len, + const uint8_t key[64], + const uint8_t nonce[16]); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/vendor/zuptsdk/include/zsdk_argon2id.h b/vendor/zuptsdk/include/zsdk_argon2id.h new file mode 100644 index 0000000..29923b3 --- /dev/null +++ b/vendor/zuptsdk/include/zsdk_argon2id.h @@ -0,0 +1,39 @@ +/* + * Argon2id (RFC 9106) + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Memory-hard password hashing function. Reference implementation + * (single-lane focus), verified against RFC 9106 §5 test vectors. + */ +#ifndef ZSDK_ARGON2ID_H +#define ZSDK_ARGON2ID_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* Returns 0 on success, -1 on parameter validation failure or alloc fail. + * + * passwd, passwd_len the password (any length) + * salt, salt_len random salt (>= 8 bytes recommended; >= 16 standard) + * memory_kib memory cost in KiB (>= 8 * lanes; we require >= 19456) + * iterations time cost (>= 1; we require >= 2) + * lanes parallelism (>= 1, <= 4 here) + * out, out_len output buffer (>= 4 bytes; typically 32) + */ +int zsdk_argon2id(const uint8_t *passwd, size_t passwd_len, + const uint8_t *salt, size_t salt_len, + uint32_t memory_kib, + uint32_t iterations, + uint32_t lanes, + uint8_t *out, size_t out_len); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/vendor/zuptsdk/include/zsdk_blake2b.h b/vendor/zuptsdk/include/zsdk_blake2b.h new file mode 100644 index 0000000..dcc83b3 --- /dev/null +++ b/vendor/zuptsdk/include/zsdk_blake2b.h @@ -0,0 +1,46 @@ +/* + * BLAKE2b (RFC 7693) + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +#ifndef ZSDK_BLAKE2B_H +#define ZSDK_BLAKE2B_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define ZSDK_BLAKE2B_BLOCKBYTES 128 +#define ZSDK_BLAKE2B_OUTBYTES 64 + +typedef struct { + uint64_t h[8]; + uint64_t t[2]; + uint64_t f[2]; + uint8_t buf[ZSDK_BLAKE2B_BLOCKBYTES]; + size_t buflen; + size_t outlen; +} zsdk_blake2b_state; + +int zsdk_blake2b_init(zsdk_blake2b_state *s, size_t outlen); +int zsdk_blake2b_init_key(zsdk_blake2b_state *s, size_t outlen, + const void *key, size_t keylen); +int zsdk_blake2b_update(zsdk_blake2b_state *s, const void *in, size_t inlen); +int zsdk_blake2b_final(zsdk_blake2b_state *s, void *out, size_t outlen); + +/* One-shot. */ +int zsdk_blake2b(void *out, size_t outlen, + const void *in, size_t inlen, + const void *key, size_t keylen); + +/* Argon2's "long hash" H' producing arbitrary length output. */ +int zsdk_blake2b_long(uint8_t *out, size_t outlen, + const uint8_t *in, size_t inlen); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/vendor/zuptsdk/include/zsdk_hkdf.h b/vendor/zuptsdk/include/zsdk_hkdf.h new file mode 100644 index 0000000..2a72d67 --- /dev/null +++ b/vendor/zuptsdk/include/zsdk_hkdf.h @@ -0,0 +1,41 @@ +/* + * HKDF-SHA3-256 (RFC 5869, with SHA3-256 as the hash) + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * SHA3-256 is preferred over SHA-256 here because Keccak's sponge + * construction has stronger structural properties (no length-extension, + * indifferentiable from a random oracle in the standard model). + */ +#ifndef ZSDK_HKDF_H +#define ZSDK_HKDF_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define ZSDK_HKDF_HASHLEN 32 /* SHA3-256 output size */ + +/* HKDF-Extract: PRK = HMAC-SHA3-256(salt, IKM) */ +void zsdk_hkdf_extract(uint8_t prk[32], + const uint8_t *salt, size_t salt_len, + const uint8_t *ikm, size_t ikm_len); + +/* HKDF-Expand: produces `out_len` bytes (out_len <= 255 * 32). */ +int zsdk_hkdf_expand(uint8_t *out, size_t out_len, + const uint8_t prk[32], + const uint8_t *info, size_t info_len); + +/* Convenience: extract+expand in one call. */ +int zsdk_hkdf(uint8_t *out, size_t out_len, + const uint8_t *salt, size_t salt_len, + const uint8_t *ikm, size_t ikm_len, + const uint8_t *info, size_t info_len); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/vendor/zuptsdk/include/zsdk_xchacha20_poly1305.h b/vendor/zuptsdk/include/zsdk_xchacha20_poly1305.h new file mode 100644 index 0000000..c1d7628 --- /dev/null +++ b/vendor/zuptsdk/include/zsdk_xchacha20_poly1305.h @@ -0,0 +1,57 @@ +/* + * XChaCha20-Poly1305 AEAD + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Implements: + * - ChaCha20 (RFC 8439) + * - HChaCha20 (draft-irtf-cfrg-xchacha-03 §2.2) + * - XChaCha20 (draft-irtf-cfrg-xchacha-03 §2.3) + * - Poly1305 (RFC 8439 §2.5) + * - XChaCha20-Poly1305 AEAD (draft-irtf-cfrg-xchacha-03 §2.4) + * + * Constant-time implementation: no secret-dependent branches or memory + * accesses. Verified against RFC 8439 test vectors and Wycheproof corpus. + */ + +#ifndef ZUPTSDK_XCHACHA20_POLY1305_H +#define ZUPTSDK_XCHACHA20_POLY1305_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ZSDK_XCHACHA20_POLY1305_KEYBYTES 32 +#define ZSDK_XCHACHA20_POLY1305_NONCEBYTES 24 +#define ZSDK_XCHACHA20_POLY1305_TAGBYTES 16 + +/* Encrypt: ciphertext_len = plaintext_len; tag is 16 bytes appended. + * out buffer size must be >= plaintext_len + 16. */ +void zsdk_xchacha20_poly1305_encrypt( + uint8_t *out, /* [out] ciphertext || tag */ + const uint8_t *plaintext, + size_t plaintext_len, + const uint8_t *aad, + size_t aad_len, + const uint8_t key[32], + const uint8_t nonce[24]); + +/* Decrypt: returns 0 on success, -1 on tag mismatch (out untouched). + * out buffer size must be >= ciphertext_len - 16. */ +int zsdk_xchacha20_poly1305_decrypt( + uint8_t *out, /* [out] plaintext */ + const uint8_t *ciphertext, /* ciphertext || tag */ + size_t ciphertext_len, /* includes 16-byte tag */ + const uint8_t *aad, + size_t aad_len, + const uint8_t key[32], + const uint8_t nonce[24]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/vendor/zuptsdk/include/zupt.h b/vendor/zuptsdk/include/zupt.h new file mode 100644 index 0000000..c598916 --- /dev/null +++ b/vendor/zuptsdk/include/zupt.h @@ -0,0 +1,401 @@ +/* + * Zupt — Backup-oriented compression with AES-256 encryption + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +#ifndef ZUPT_H +#define ZUPT_H + +/* Feature test macros — must precede all system includes. + * _DEFAULT_SOURCE gives us lstat() on glibc without -D_GNU_SOURCE. */ +#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) + #define _DEFAULT_SOURCE 1 +#endif + +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #define ZUPT_PATH_SEP '\\' + #define zupt_mkdir(p) _mkdir(p) +#else + #include + #include + #include + #include + #define ZUPT_PATH_SEP '/' + #define zupt_mkdir(p) mkdir(p, 0755) +#endif + +#define ZUPT_VERSION_STRING "2.2.2" +#define ZUPT_FORMAT_MAJOR 1 +#define ZUPT_FORMAT_MINOR 4 + +#define ZUPT_MAGIC_0 0x5A +#define ZUPT_MAGIC_1 0x55 +#define ZUPT_MAGIC_2 0x50 +#define ZUPT_MAGIC_3 0x54 +#define ZUPT_MAGIC_4 0x1A +#define ZUPT_MAGIC_5 0x00 +#define ZUPT_BLOCK_MAGIC_0 0xBB +#define ZUPT_BLOCK_MAGIC_1 0x01 + +#define ZUPT_MAX_PATH 4096 +#define ZUPT_MAX_FILES 2000000 +#define ZUPT_DEFAULT_BLOCK_SZ (4 * 1024 * 1024) +#define ZUPT_MIN_BLOCK_SZ (64 * 1024) +#define ZUPT_MAX_BLOCK_SZ (256 * 1024 * 1024) + +/* Global flags */ +#define ZUPT_FLAG_ENCRYPTED (1u << 0) +#define ZUPT_FLAG_CKSUM_XXH64 (0u << 5) +#define ZUPT_FLAG_SOLID (1u << 1) +#define ZUPT_FLAG_MULTITHREADED (1u << 2) /* Informational: archive was produced with MT */ +#define ZUPT_FLAG_PQ_HYBRID (1u << 3) /* Post-quantum hybrid encryption */ +#define ZUPT_FLAG_FORMAT_STABLE (1u << 4) /* v1.0: format frozen */ +#define ZUPT_FLAG_DEDUP (1u << 7) /* Block-level deduplication enabled */ +#define ZUPT_FLAG_AAD_SEQ (1u << 8) /* MAC binds block_seq as AAD (anti-reorder) */ + +/* Encryption types (stored in encryption header block) */ +#define ZUPT_ENC_PBKDF2 0x01 /* Password-based: PBKDF2 → AES-256-CTR + HMAC */ +#define ZUPT_ENC_PQ_HYBRID 0x02 /* ML-KEM-768 + X25519 hybrid KEM (legacy XOR+SHA3) */ +#define ZUPT_ENC_PQ_SDK_V2 0x03 /* libzuptsdk v2 header: HKDF combiner + commitment + HPKE binding */ +#define ZUPT_ENC_PW_ARGON2 0x04 /* Password-based via libzuptsdk: Argon2id + XChaCha20-Poly1305 */ + +/* Block types */ +#define ZUPT_BLOCK_DATA 0x00 +#define ZUPT_BLOCK_INDEX 0x02 +#define ZUPT_BLOCK_ENC_HEADER 0x03 +#define ZUPT_BLOCK_DEDUP_REF 0x04 /* Dedup reference: payload = 8B offset of original block */ + +/* Block flags */ +#define ZUPT_BFLAG_ENCRYPTED (1u << 0) + +/* Codec IDs */ +#define ZUPT_CODEC_STORE 0x0000 +#define ZUPT_CODEC_ZUPT_LZ 0x0008 +#define ZUPT_CODEC_ZUPT_LZH 0x0009 /* LZ77 + Huffman */ +#define ZUPT_CODEC_ZUPT_LZHP 0x000A /* LZ77 + Huffman + Byte Prediction (default) */ +#define ZUPT_CODEC_VAPTVUPT 0x0010 /* VAPTVUPT: VaptVupt LZ + ANS entropy codec */ +#define ZUPT_CODEC_AUTO 0xFFFF /* Auto-detect: VaptVupt if AVX2, else LZHP */ + +/* Crypto */ +#define ZUPT_SALT_SIZE 32 +#define ZUPT_NONCE_SIZE 16 +#define ZUPT_HMAC_SIZE 32 +#define ZUPT_AES_KEY_SIZE 32 +#define ZUPT_KDF_ITERATIONS 600000 + +typedef enum { + ZUPT_OK = 0, ZUPT_ERR_IO = -1, ZUPT_ERR_CORRUPT = -2, + ZUPT_ERR_BAD_MAGIC = -3, ZUPT_ERR_BAD_VERSION = -4, + ZUPT_ERR_BAD_CHECKSUM = -5, ZUPT_ERR_NOMEM = -6, + ZUPT_ERR_OVERFLOW = -7, ZUPT_ERR_INVALID = -8, + ZUPT_ERR_NOT_FOUND = -9, ZUPT_ERR_UNSUPPORTED = -10, + ZUPT_ERR_AUTH_FAIL = -11, +} zupt_error_t; + +/* ─── On-disk (packed LE) ─── */ +#pragma pack(push, 1) +typedef struct { + uint8_t magic[6]; + uint8_t version_major, version_minor; + uint32_t global_flags; + uint64_t creation_time; + uint8_t archive_id[16]; + uint64_t encryption_header_off; + uint64_t comment_offset; + uint8_t reserved[12]; +} zupt_archive_header_t; /* 64 bytes */ + +typedef struct { + uint64_t index_offset; + uint64_t total_blocks; + uint64_t archive_checksum; + uint8_t footer_magic[4]; /* "ZEND" */ + uint32_t footer_version; +} zupt_footer_t; /* 32 bytes */ +#pragma pack(pop) + +/* ─── In-memory ─── */ +typedef struct { + char path[ZUPT_MAX_PATH]; + uint64_t uncompressed_size, compressed_size; + uint64_t modification_time, content_hash; + uint64_t first_block_offset; + uint32_t block_count, attributes; +} zupt_index_entry_t; + +typedef struct { + uint8_t block_type; uint16_t codec_id, block_flags; + uint64_t uncompressed_size, compressed_size, checksum; + uint8_t *payload; +} zupt_block_t; + +/* Buffer canary for keyring overflow detection */ +#define ZUPT_CANARY 0xDEADCAFEBABEFACEULL + +typedef struct { + uint64_t canary_head; /* Must equal ZUPT_CANARY */ + uint8_t enc_key[ZUPT_AES_KEY_SIZE]; + uint8_t mac_key[ZUPT_HMAC_SIZE]; + uint8_t salt[ZUPT_SALT_SIZE]; + uint8_t base_nonce[ZUPT_NONCE_SIZE]; + uint32_t iterations; + int active; + uint64_t canary_tail; /* Must equal ZUPT_CANARY */ +} zupt_keyring_t; + +/* Check keyring canaries — abort on buffer overflow */ +static inline void zupt_keyring_init(zupt_keyring_t *kr) { + volatile uint8_t *p = (volatile uint8_t *)kr; + for (size_t i = 0; i < sizeof(*kr); i++) p[i] = 0; + kr->canary_head = ZUPT_CANARY; + kr->canary_tail = ZUPT_CANARY; +} +static inline void zupt_keyring_check(const zupt_keyring_t *kr) { + if (kr->canary_head != ZUPT_CANARY || kr->canary_tail != ZUPT_CANARY) { + fprintf(stderr, "FATAL: keyring buffer overflow detected (canary corrupted)\n"); + /* Use exit(127) instead of abort() to avoid needing */ + _exit(127); + } +} + +typedef struct { + char **paths, **arc_paths; + int count, capacity; +} zupt_filelist_t; + +typedef struct { + int level; uint32_t block_size; uint16_t codec_id; + int verbose, encrypt, quiet, solid, threads; + int pq_mode; /* 1 = post-quantum hybrid KEM mode */ + int sdk_mode; /* 1 = use libzuptsdk-backed v3 crypto (HKDF combiner + commitment + HPKE) */ + int dedup; /* 1 = block-level deduplication enabled */ + char password[256]; + char keyfile[ZUPT_MAX_PATH]; /* Path to .zupt-key file */ + zupt_keyring_t keyring; +} zupt_options_t; + +/* ═══════════════════════════════════════════════════════════════════ + * PORTABLE LITTLE-ENDIAN SERIALIZATION + * + * All multi-byte fields in the on-disk format are stored as LE. + * These helpers ensure correct behaviour on both LE and BE hosts. + * ═══════════════════════════════════════════════════════════════════ */ + +static inline void zupt_le16_put(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); +} +static inline void zupt_le32_put(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); + p[2] = (uint8_t)((v >> 16) & 0xFF); + p[3] = (uint8_t)((v >> 24) & 0xFF); +} +static inline void zupt_le64_put(uint8_t *p, uint64_t v) { + for (int i = 0; i < 8; i++) { p[i] = (uint8_t)(v & 0xFF); v >>= 8; } +} +static inline uint16_t zupt_le16_get(const uint8_t *p) { + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} +static inline uint32_t zupt_le32_get(const uint8_t *p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} +static inline uint64_t zupt_le64_get(const uint8_t *p) { + uint64_t v = 0; + for (int i = 7; i >= 0; i--) v = (v << 8) | p[i]; + return v; +} + +/* ═══════════════════════════════════════════════════════════════════ + * SECURE MEMORY WIPE (resists dead-store elimination by compilers) + * ═══════════════════════════════════════════════════════════════════ */ + +/* FRAMA-C: Secure memory wipe — resists dead-store elimination */ +/*@ requires \valid((uint8_t *)ptr + (0..len-1)); + @ assigns ((uint8_t *)ptr)[0..len-1]; + @ ensures \forall integer i; 0 <= i < len ==> ((uint8_t *)ptr)[i] == 0; +*/ +static inline void zupt_secure_wipe(void *ptr, size_t len) { +#if defined(_WIN32) + SecureZeroMemory(ptr, len); +#elif (defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 25))) + extern void explicit_bzero(void *, size_t); + explicit_bzero(ptr, len); +#elif defined(__FreeBSD__) || defined(__OpenBSD__) + extern void explicit_bzero(void *, size_t); + explicit_bzero(ptr, len); +#else + volatile uint8_t *vp = (volatile uint8_t *)ptr; + for (size_t i = 0; i < len; i++) vp[i] = 0; +#endif +} + +/* ═══════════════════════════════════════════════════════════════════ + * REGULAR-FILE CHECK (skip symlinks, devices, FIFOs, sockets) + * ═══════════════════════════════════════════════════════════════════ */ + +static inline int zupt_is_regular_file(const char *path) { +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path); + if (attr == INVALID_FILE_ATTRIBUTES) return 0; + return !(attr & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_DEVICE | + FILE_ATTRIBUTE_REPARSE_POINT)); +#else + struct stat st; + if (lstat(path, &st) != 0) return 0; + return S_ISREG(st.st_mode); +#endif +} + +/* ─── Solid-mode compression ─── */ +zupt_error_t zupt_compress_solid(const char *out, const char **arc, const char **disk, int n, zupt_options_t *opts); + +/* ─── SHA-256 ─── */ +typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; } zupt_sha256_ctx; +void zupt_sha256_init(zupt_sha256_ctx *c); +void zupt_sha256_update(zupt_sha256_ctx *c, const uint8_t *d, size_t n); +void zupt_sha256_final(zupt_sha256_ctx *c, uint8_t h[32]); +void zupt_sha256(const uint8_t *d, size_t n, uint8_t h[32]); + +/* ─── AES-256 ─── */ +typedef struct { uint32_t rk[60]; } zupt_aes256_ctx; +void zupt_aes256_init(zupt_aes256_ctx *c, const uint8_t key[32]); +void zupt_aes256_encrypt_block(const zupt_aes256_ctx *c, const uint8_t in[16], uint8_t out[16]); + +/* ─── Crypto ops ─── */ +void zupt_hmac_sha256(const uint8_t *key, size_t klen, const uint8_t *data, size_t dlen, uint8_t mac[32]); +void zupt_pbkdf2_sha256(const uint8_t *pw, size_t pwlen, const uint8_t *salt, size_t slen, uint32_t iter, uint8_t *out, size_t olen); +void zupt_aes256_ctr(const uint8_t key[32], const uint8_t nonce[16], const uint8_t *in, uint8_t *out, size_t len); +void zupt_derive_keys(zupt_keyring_t *kr, const char *pw, const uint8_t salt[32], const uint8_t nonce[16], uint32_t iter); +uint8_t *zupt_encrypt_buffer(const zupt_keyring_t *kr, const uint8_t *plain, size_t plen, uint64_t seq, size_t *olen); +uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr, const uint8_t *pkg, size_t pkglen, uint64_t seq, size_t *olen); +void zupt_random_bytes(uint8_t *buf, size_t len); + +/* ─── Memory locking for key material ─── */ +int zupt_mlock_keys(void *ptr, size_t len); +void zupt_munlock_keys(void *ptr, size_t len); + +/* ─── Adaptive compression: file type detection ─── */ +/* Returns: -1=store (incompressible), 0=default, 5=medium, 9=max */ +int zupt_detect_filetype(const uint8_t *header, size_t header_len); + +/* ─── XXH64 ─── */ +uint64_t zupt_xxh64(const void *data, size_t len, uint64_t seed); + +/* ─── LZ ─── */ +size_t zupt_lz_compress(const uint8_t *src, size_t slen, uint8_t *dst, size_t dcap, int level); +size_t zupt_lz_decompress(const uint8_t *src, size_t slen, uint8_t *dst, size_t dlen); +size_t zupt_lz_bound(size_t slen); + +/* ─── LZH (LZ77 + Huffman) ─── */ +size_t zupt_lzh_compress(const uint8_t *src, size_t slen, uint8_t *dst, size_t dcap, int level); +size_t zupt_lzh_decompress(const uint8_t *src, size_t slen, uint8_t *dst, size_t dlen); +size_t zupt_lzh_bound(size_t slen); + +/* ─── Byte Prediction (order-1 context transform) ─── */ +void zupt_predict_build(const uint8_t *data, size_t len, uint8_t prediction[256]); +void zupt_predict_encode(const uint8_t *in, uint8_t *out, size_t len, const uint8_t pred[256]); +void zupt_predict_decode(const uint8_t *in, uint8_t *out, size_t len, const uint8_t pred[256]); +float zupt_predict_benefit(const uint8_t *data, size_t len); + +/* ─── Format I/O ─── */ +int zupt_write_varint(FILE *f, uint64_t v); +int zupt_read_varint(FILE *f, uint64_t *v); +int zupt_encode_varint(uint8_t *b, uint64_t v); +int zupt_decode_varint(const uint8_t *b, size_t blen, uint64_t *v); + +void zupt_filelist_init(zupt_filelist_t *fl); +void zupt_filelist_free(zupt_filelist_t *fl); +void zupt_filelist_add(zupt_filelist_t *fl, const char *disk_path, const char *arc_path); +void zupt_collect_files(zupt_filelist_t *fl, const char *path, const char *base); + +zupt_error_t zupt_compress_files(const char *out, const char **arc, const char **disk, int n, zupt_options_t *opts); +zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options_t *opts); +zupt_error_t zupt_list_archive(const char *arc, zupt_options_t *opts); +zupt_error_t zupt_test_archive(const char *arc, zupt_options_t *opts); + +/* ─── Hybrid PQ KEM (ML-KEM-768 + X25519) ─── */ +int zupt_hybrid_keygen(const char *keyfile); +int zupt_hybrid_export_pubkey(const char *privfile, const char *pubfile); +int zupt_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile, + uint8_t *enc_hdr, size_t *enc_hdr_len); +int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile, + const uint8_t *enc_hdr, size_t enc_hdr_len); + +/* ─── SDK-backed crypto (zupt v2.2+, libzuptsdk under the hood) ─── */ +int zupt_sdk_hybrid_keygen(const char *privkeyfile, const char *pubkeyfile); +int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile, + uint8_t *enc_hdr, size_t *enc_hdr_len); +int zupt_sdk_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile, + const uint8_t *enc_hdr, size_t enc_hdr_len); +int zupt_sdk_password_encrypt_init(zupt_keyring_t *kr, const char *password, + uint8_t *enc_hdr, size_t *enc_hdr_len); +int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password, + const uint8_t *enc_hdr, size_t enc_hdr_len); + +const char *zupt_strerror(zupt_error_t e); +const char *zupt_codec_name(uint16_t id); +void zupt_default_options(zupt_options_t *o); +void zupt_format_size(uint64_t bytes, char *buf, size_t cap); + +/* Resolve ZUPT_CODEC_AUTO to a concrete codec based on hardware. + * On x86_64 with AVX2: VaptVupt (fast ANS+SIMD decode). + * On all other arches: Zupt-LZHP (no SIMD dependency). + * Decompression of ALL codecs works on ALL architectures. */ +uint16_t zupt_resolve_auto_codec(void); + +/* ─── Full-Disk Backup/Restore ─── */ +#define ZUPT_FLAG_DISK_IMAGE (1u << 6) /* Archive contains a raw disk/partition image */ + +/* Compress a raw block device or file as a disk image. + * Reads source in block_size chunks, detects zero/sparse regions, + * compresses non-zero blocks. Supports encryption + PQ. */ +zupt_error_t zupt_disk_backup(const char *output_path, const char *source_path, + zupt_options_t *opts); + +/* Restore a disk image archive to a block device or file. + * Writes blocks sequentially, restoring sparse regions as zeros. */ +zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path, + zupt_options_t *opts); + +/* ─── Internal Block I/O (used by format + disk modules) ─── */ +zupt_error_t read_block(FILE *f, zupt_block_t *b); +zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t *opts); +zupt_error_t decompress_block(const zupt_block_t *b, const zupt_keyring_t *kr, + uint64_t block_seq, uint8_t **out, size_t *olen); +zupt_error_t write_enc_header(FILE *out, zupt_archive_header_t *hdr, + zupt_options_t *opts); +int zupt_w8(FILE *f, uint8_t v); +int zupt_w16le(FILE *f, uint16_t v); +int zupt_w64le(FILE *f, uint64_t v); + +/* ─── Block-Level Deduplication ─── */ +#define ZUPT_DEDUP_MAX_ENTRIES (2 * 1024 * 1024) /* 2M entries, ~48MB RAM */ + +typedef struct zupt_dedup_ctx zupt_dedup_ctx_t; + +zupt_dedup_ctx_t *zupt_dedup_init(void); +void zupt_dedup_free(zupt_dedup_ctx_t *ctx); +int zupt_dedup_lookup(zupt_dedup_ctx_t *ctx, uint64_t fingerprint, + uint64_t *ref_offset, uint32_t *ref_size); +int zupt_dedup_insert(zupt_dedup_ctx_t *ctx, uint64_t fingerprint, + uint64_t block_offset, uint32_t block_size); +void zupt_dedup_record_hit(zupt_dedup_ctx_t *ctx, uint64_t saved_bytes); +void zupt_dedup_record_block(zupt_dedup_ctx_t *ctx); +void zupt_dedup_stats(const zupt_dedup_ctx_t *ctx, + uint64_t *blocks_seen, uint64_t *blocks_deduped, + uint64_t *bytes_saved); +int zupt_dedup_write_ref(FILE *out, uint64_t ref_offset, + uint32_t orig_size, uint64_t orig_checksum); + +/* ─── Archive Info (read-only metadata inspection) ─── */ +zupt_error_t zupt_archive_info(const char *path); + +#endif /* ZUPT_H */ diff --git a/vendor/zuptsdk/include/zupt_acsl.h b/vendor/zuptsdk/include/zupt_acsl.h new file mode 100644 index 0000000..812498c --- /dev/null +++ b/vendor/zuptsdk/include/zupt_acsl.h @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright (c) 2026 Cristian Cezar Moisés + * + * Zupt — ACSL Custom Predicates for Frama-C/WP + * + * Usage: frama-c -wp -wp-rte -wp-model Typed+Cast + * -cpp-extra-args="-Iinclude -Isrc" src/zupt_crypto.c + */ +#ifndef ZUPT_ACSL_H +#define ZUPT_ACSL_H + +#ifdef __FRAMAC__ +#include + +/*@ predicate ValidBuffer{L}(uint8_t *p, size_t n) = + @ \valid_read(p + (0..n-1)) && + @ \initialized(p + (0..n-1)); + @ + @ predicate ValidWriteBuffer{L}(uint8_t *p, size_t n) = + @ \valid(p + (0..n-1)); + @ + @ predicate Separated2(uint8_t *a, size_t an, + @ uint8_t *b, size_t bn) = + @ \separated(a + (0..an-1), b + (0..bn-1)); + @ + @ predicate KeyWiped{L}(uint8_t *k, size_t n) = + @ \forall integer i; 0 <= i < n ==> \at(k[i],L) == 0; + @ + @ predicate ValidKey{L}(uint8_t *k, size_t n) = + @ ValidBuffer{L}(k, n) && n == 32; + @ + @ predicate ConstantTimeCompare{L}(uint8_t *a, uint8_t *b, + @ size_t n) = + @ \forall integer i; 0 <= i < n ==> + @ \initialized(\at(a+i,L)) && \initialized(\at(b+i,L)); + @ + @ predicate MACValid{L}(uint8_t *mac) = + @ ValidBuffer{L}(mac, 32); +*/ +#endif /* __FRAMAC__ */ + +#endif /* ZUPT_ACSL_H */ diff --git a/vendor/zuptsdk/include/zupt_cpuid.h b/vendor/zuptsdk/include/zupt_cpuid.h new file mode 100644 index 0000000..6297b2c --- /dev/null +++ b/vendor/zuptsdk/include/zupt_cpuid.h @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright (c) 2026 Cristian Cezar Moisés + * + * Zupt — CPU Feature Detection + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later + */ +#ifndef ZUPT_CPUID_H +#define ZUPT_CPUID_H + +#include + +typedef struct { + int has_aesni; /* CPUID.01H:ECX[25] — AES-NI instructions */ + int has_avx; /* AVX (VEX-encoded SSE) — requires CPUID + OS XSAVE */ + int has_pclmul; /* CPUID.01H:ECX[1] — CLMUL (carry-less multiply) */ + int has_avx2; /* CPUID.07H:EBX[5] — AVX2 (256-bit SIMD) */ + int has_sse41; /* CPUID.01H:ECX[19] — SSE4.1 */ +} zupt_cpu_features_t; + +/*@ assigns f->has_aesni, f->has_avx, f->has_pclmul, f->has_avx2, f->has_sse41; + @ ensures f->has_aesni == 0 || f->has_aesni == 1; + @ ensures f->has_avx == 0 || f->has_avx == 1; + @ ensures f->has_pclmul == 0 || f->has_pclmul == 1; + @ ensures f->has_avx2 == 0 || f->has_avx2 == 1; + @ ensures f->has_sse41 == 0 || f->has_sse41 == 1; +*/ +void zupt_detect_cpu(zupt_cpu_features_t *f); + +/* Global instance — set once at program start */ +extern zupt_cpu_features_t zupt_cpu; + +#endif /* ZUPT_CPUID_H */ diff --git a/vendor/zuptsdk/include/zupt_jasmin.h b/vendor/zuptsdk/include/zupt_jasmin.h new file mode 100644 index 0000000..e6bbc5b --- /dev/null +++ b/vendor/zuptsdk/include/zupt_jasmin.h @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright (c) 2026 Cristian Cezar Moisés + * + * Zupt — Jasmin Verified Crypto Declarations + * Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later + * + * Extern declarations for Jasmin-compiled assembly functions. + * These replace C fallbacks when built with -DZUPT_USE_JASMIN. + * + * Calling convention: System V AMD64 ABI. + * Pointer args passed in RDI, RSI, RDX, RCX, R8, R9. + * + * v2.0.0: All 4 Jasmin functions wired and active. + */ +#ifndef ZUPT_JASMIN_H +#define ZUPT_JASMIN_H + +#ifdef ZUPT_USE_JASMIN +#include + +/* JASMIN-VERIFIED: CT MAC comparison (4×u64 XOR accumulation). + * Returns 0 if all 32 bytes match, nonzero if any differ. + * Replaces XOR loop in zupt_decrypt_buffer(). */ +extern uint64_t zupt_mac_verify_ct(const void *expected, const void *actual); + +/* JASMIN-VERIFIED: CT conditional select (4×u64 masked select). + * if cond==0: copies a→out. if cond!=0: copies b→out. + * Replaces cmov in zupt_mlkem768_decaps(). */ +extern void zupt_ct_select_32(void *out, const void *a, + const void *b, uint64_t cond); + +/* JASMIN-VERIFIED: CT conditional swap (4×u64 masked XOR swap). + * if cond==0: no-op. if cond==1: swaps a↔b in place. + * Replaces fe_cswap in zupt_x25519.c. + * NOTE: Requires 4×u64 field element layout (donna64). */ +extern void zupt_fe_cswap(void *a, void *b, uint64_t cond); + +/* JASMIN-VERIFIED: AES-256 single-block encrypt via AES-NI. + * out = AES-256-ECB(key, ctr) XOR in. + * FIX v2.0.0: Stack offset bug resolved — round keys at correct + * 16-byte aligned offsets. Requires AES-NI (checked via CPUID). + * + * Args (System V ABI): + * out_ptr (RDI): destination for 16-byte result + * in_blk (RSI): pointer to 16-byte plaintext block + * key (RDX): pointer to 32-byte AES-256 key (two u128) + * ctr_blk (RCX): pointer to 16-byte counter block + */ +extern void zupt_aes256_blk(void *out, const void *in, + const void *key, const void *ctr); + +/* JASMIN-VERIFIED: AES-256-CTR 4-block pipeline via AES-NI. + * Processes nblocks×16 bytes with 4-way interleaving. + * Counter is updated in-place (big-endian increment in bytes [8..15]). + * Requires AES-NI. Falls back to zupt_aes256_blk for remaining 1-3 blocks. + * + * Args: out(RDI), in(RSI), key(RDX), ctr(RCX), nblocks(R8) + */ +extern void zupt_aes256_ctr4(void *out, const void *in, + const void *key, void *ctr, + uint64_t nblocks); + +#endif /* ZUPT_USE_JASMIN */ +#endif /* ZUPT_JASMIN_H */ diff --git a/vendor/zuptsdk/include/zupt_keccak.h b/vendor/zuptsdk/include/zupt_keccak.h new file mode 100644 index 0000000..56ba0e9 --- /dev/null +++ b/vendor/zuptsdk/include/zupt_keccak.h @@ -0,0 +1,49 @@ +/* + * Zupt — Backup-oriented compression with AES-256 encryption + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Keccak-f[1600] sponge: SHA3-256, SHA3-512, SHAKE-128, SHAKE-256 + * Required by ML-KEM-768 (FIPS 203). + * Pure C11, zero dependencies, no dynamic allocation. + */ +#ifndef ZUPT_KECCAK_H +#define ZUPT_KECCAK_H + +#include +#include + +/* Sponge state: 25 × 64-bit lanes = 200 bytes */ +typedef struct { + uint64_t st[25]; + uint8_t buf[200]; /* absorption buffer */ + size_t rate; /* rate in bytes */ + size_t pt; /* position in buf */ + uint8_t dsuf; /* domain suffix: 0x06 for SHA3, 0x1F for SHAKE */ +} zupt_keccak_ctx; + +/* SHA3-256: 32-byte output */ +void zupt_sha3_256(const uint8_t *data, size_t len, uint8_t out[32]); + +/* SHA3-512: 64-byte output */ +void zupt_sha3_512(const uint8_t *data, size_t len, uint8_t out[64]); + +/* SHAKE-128: extendable output */ +void zupt_shake128(const uint8_t *data, size_t dlen, uint8_t *out, size_t olen); + +/* SHAKE-256: extendable output */ +void zupt_shake256(const uint8_t *data, size_t dlen, uint8_t *out, size_t olen); + +/* Incremental SHAKE-128 for ML-KEM sampling */ +void zupt_shake128_init(zupt_keccak_ctx *ctx); +void zupt_shake128_absorb(zupt_keccak_ctx *ctx, const uint8_t *data, size_t len); +void zupt_shake128_finalize(zupt_keccak_ctx *ctx); +void zupt_shake128_squeeze(zupt_keccak_ctx *ctx, uint8_t *out, size_t len); + +/* Incremental SHAKE-256 */ +void zupt_shake256_init(zupt_keccak_ctx *ctx); +void zupt_shake256_absorb(zupt_keccak_ctx *ctx, const uint8_t *data, size_t len); +void zupt_shake256_finalize(zupt_keccak_ctx *ctx); +void zupt_shake256_squeeze(zupt_keccak_ctx *ctx, uint8_t *out, size_t len); + +#endif diff --git a/vendor/zuptsdk/include/zupt_mlkem.h b/vendor/zuptsdk/include/zupt_mlkem.h new file mode 100644 index 0000000..d928916 --- /dev/null +++ b/vendor/zuptsdk/include/zupt_mlkem.h @@ -0,0 +1,65 @@ +/* + * Zupt — Backup-oriented compression with AES-256 encryption + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * ML-KEM-768 (FIPS 203, formerly CRYSTALS-Kyber). + * Post-quantum key encapsulation mechanism. + * + * Parameters (ML-KEM-768): + * k = 3, η₁ = 2, η₂ = 2, d_u = 10, d_v = 4 + * Public key: 1184 bytes + * Secret key: 2400 bytes + * Ciphertext: 1088 bytes + * Shared secret: 32 bytes + * + * SECURITY NOTE: This implementation must undergo independent review + * before deployment in high-assurance contexts. It targets correctness + * against NIST test vectors and constant-time operation. + */ +#ifndef ZUPT_MLKEM_H +#define ZUPT_MLKEM_H + +#include + +#define MLKEM_K 3 +#define MLKEM_N 256 +#define MLKEM_Q 3329 +#define MLKEM_ETA1 2 +#define MLKEM_ETA2 2 +#define MLKEM_DU 10 +#define MLKEM_DV 4 + +#define MLKEM_PUBLICKEYBYTES 1184 +#define MLKEM_SECRETKEYBYTES 2400 +#define MLKEM_CIPHERTEXTBYTES 1088 +#define MLKEM_SSBYTES 32 + +/* KeyGen: generate public/secret keypair. + * pk: output public key (1184 bytes) + * sk: output secret key (2400 bytes) + * Returns 0 on success. */ +int zupt_mlkem768_keygen(uint8_t pk[MLKEM_PUBLICKEYBYTES], + uint8_t sk[MLKEM_SECRETKEYBYTES]); + +/* Encapsulate: produce ciphertext and shared secret from public key. + * ct: output ciphertext (1088 bytes) + * ss: output shared secret (32 bytes) + * pk: input public key (1184 bytes) + * Returns 0 on success. */ +int zupt_mlkem768_encaps(uint8_t ct[MLKEM_CIPHERTEXTBYTES], + uint8_t ss[MLKEM_SSBYTES], + const uint8_t pk[MLKEM_PUBLICKEYBYTES]); + +/* Decapsulate: recover shared secret from ciphertext and secret key. + * ss: output shared secret (32 bytes) + * ct: input ciphertext (1088 bytes) + * sk: input secret key (2400 bytes) + * Returns 0 on success. + * CT-REQUIRED: Implicit rejection — invalid ciphertext produces a + * pseudorandom shared secret (no distinguishable failure). */ +int zupt_mlkem768_decaps(uint8_t ss[MLKEM_SSBYTES], + const uint8_t ct[MLKEM_CIPHERTEXTBYTES], + const uint8_t sk[MLKEM_SECRETKEYBYTES]); + +#endif diff --git a/vendor/zuptsdk/include/zupt_x25519.h b/vendor/zuptsdk/include/zupt_x25519.h new file mode 100644 index 0000000..ea62a95 --- /dev/null +++ b/vendor/zuptsdk/include/zupt_x25519.h @@ -0,0 +1,22 @@ +/* + * Zupt — Backup-oriented compression with AES-256 encryption + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * X25519 Diffie-Hellman key agreement (RFC 7748). + * Montgomery ladder — constant-time by construction. + */ +#ifndef ZUPT_X25519_H +#define ZUPT_X25519_H + +#include + +/* X25519(scalar, point) → result. All inputs/outputs are 32 bytes. + * CT-REQUIRED: Montgomery ladder is inherently constant-time. */ +void zupt_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32]); + +/* X25519 with the standard basepoint (9). + * Used for keygen: public = X25519(private, basepoint). */ +void zupt_x25519_base(uint8_t out[32], const uint8_t scalar[32]); + +#endif diff --git a/vendor/zuptsdk/include/zuptsdk.h b/vendor/zuptsdk/include/zuptsdk.h new file mode 100644 index 0000000..f30ea6c --- /dev/null +++ b/vendor/zuptsdk/include/zuptsdk.h @@ -0,0 +1,605 @@ +/* + * libzuptsdk — Public C ABI for the Zupt backup compression library + * + * Copyright (c) 2026 Cristian Cezar Moisés + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Repository: https://git.securityops.co/cristiancmoises/zupt + * Website: https://zupt.securityops.co + * Contact: zupt@riseup.net + * + * -------------------------------------------------------------------------- + * STABILITY GUARANTEE + * -------------------------------------------------------------------------- + * Every symbol declared in this header is part of the stable v1.0 ABI and + * is gated behind the linker version tag ZUPTSDK_1.0. New symbols may be + * added in minor versions (1.1, 1.2, ...) under new tags (ZUPTSDK_1.1, ...). + * Existing symbols will never change signature within v1.x. Breaking + * changes require a major version bump (libzuptsdk.so.2). + * + * No symbol prefixed with anything other than `zuptsdk_` or `ZUPTSDK_` is + * part of this ABI. Do not link against internal `zupt_*` symbols even if + * they appear in the static archive — they will disappear without notice. + * + * -------------------------------------------------------------------------- + * THREAD SAFETY + * -------------------------------------------------------------------------- + * Every function that takes a `zuptsdk_ctx_t *` operates only on that + * context's state and on caller-provided buffers. Concurrent calls on + * DISTINCT contexts are safe (MT-Safe). Concurrent calls on the SAME + * context are NOT safe (MT-Unsafe-Same-Context) unless explicitly + * documented otherwise. + * + * -------------------------------------------------------------------------- + * MEMORY OWNERSHIP + * -------------------------------------------------------------------------- + * Every function documents ownership using these conventions in the param + * comments: + * [in] caller owns, library reads only + * [out] caller owns, library writes + * [in,out] caller owns, library reads and writes + * [transfers] ownership moves caller -> library (or library -> caller) + * [borrowed] pointer valid only for the duration of the call + * + * Any function that returns a heap-allocated value via an output pointer + * documents the corresponding zuptsdk_*_destroy() or zuptsdk_free() call + * the caller must invoke. Calling free() on libc-allocated memory from a + * different allocator is undefined; always use the documented destroyer. + * + * -------------------------------------------------------------------------- + * ERROR HANDLING + * -------------------------------------------------------------------------- + * Functions return `int` where 0 == ZUPTSDK_OK and negative values are + * `zuptsdk_error_t` codes. Use zuptsdk_strerror() for a static description + * and zuptsdk_last_error_detail(ctx) for a thread-local detailed message + * including filename, line number, and underlying errno where applicable. + * + * The library never calls abort(), exit(), or _exit(). It never writes to + * stdout or stderr unless the caller explicitly enables logging via + * zuptsdk_ctx_set_log_callback(). + * + * -------------------------------------------------------------------------- + * SECURE MEMORY + * -------------------------------------------------------------------------- + * Inputs and outputs containing secret material (passwords, raw keys, + * decrypted plaintext keys) MUST be passed via `zuptsdk_secure_buffer_t` + * to ensure mlock()-backed storage and explicit_bzero() on destroy. + * Passing such material via plain `const uint8_t *` is allowed for + * convenience but the library cannot guarantee zeroization of caller + * memory in that case. + */ + +#ifndef ZUPTSDK_H +#define ZUPTSDK_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ════════════════════════════════════════════════════════════════════════ + * VERSION + * ════════════════════════════════════════════════════════════════════════ */ + +#define ZUPTSDK_VERSION_MAJOR 1 +#define ZUPTSDK_VERSION_MINOR 0 +#define ZUPTSDK_VERSION_PATCH 0 +#define ZUPTSDK_VERSION_STRING "1.0.0" + +/* Compile-time version check helper (negative if header older than required) */ +#define ZUPTSDK_VERSION_AT_LEAST(maj, min, pat) \ + ((ZUPTSDK_VERSION_MAJOR > (maj)) || \ + (ZUPTSDK_VERSION_MAJOR == (maj) && ZUPTSDK_VERSION_MINOR > (min)) || \ + (ZUPTSDK_VERSION_MAJOR == (maj) && ZUPTSDK_VERSION_MINOR == (min) && \ + ZUPTSDK_VERSION_PATCH >= (pat))) + +/** + * Return the runtime version string of the linked library, e.g. "1.0.0". + * The returned pointer is to static storage and must NOT be freed. + * + * Use this with the compile-time ZUPTSDK_VERSION_STRING to detect mismatch + * between header and library at runtime. + */ +const char *zuptsdk_version_string(void); + +/** + * Verify that the linked library is at least the requested version. + * Returns 0 if compatible, ZUPTSDK_ERR_VERSION_MISMATCH otherwise. + * Call this once at startup before any other zuptsdk_* function. + */ +int zuptsdk_version_check(int major, int minor, int patch); + +/* ════════════════════════════════════════════════════════════════════════ + * ERRORS + * ════════════════════════════════════════════════════════════════════════ */ + +typedef enum { + ZUPTSDK_OK = 0, + ZUPTSDK_ERR_INVALID_ARG = -1, /* NULL pointer, bad size, bad enum value */ + ZUPTSDK_ERR_NO_MEMORY = -2, /* malloc/calloc/realloc returned NULL */ + ZUPTSDK_ERR_IO = -3, /* read/write error; see errno detail */ + ZUPTSDK_ERR_BAD_ARCHIVE = -4, /* Magic mismatch or truncated header */ + ZUPTSDK_ERR_BAD_PASSWORD = -5, /* MAC verification failed */ + ZUPTSDK_ERR_BAD_KEY = -6, /* PQ key file malformed or wrong type */ + ZUPTSDK_ERR_BAD_MAC = -7, /* HMAC mismatch — archive corrupted or tampered */ + ZUPTSDK_ERR_BAD_VERSION = -8, /* Archive format version not supported */ + ZUPTSDK_ERR_BAD_CHECKSUM = -9, /* Block checksum mismatch */ + ZUPTSDK_ERR_BUFFER_TOO_SMALL = -10, /* Output buffer insufficient */ + ZUPTSDK_ERR_NOT_ENCRYPTED = -11, /* Tried to decrypt unencrypted archive */ + ZUPTSDK_ERR_PASSWORD_REQUIRED = -12, /* Archive needs password but none supplied */ + ZUPTSDK_ERR_PQ_KEY_REQUIRED = -13, /* Archive needs PQ key but none supplied */ + ZUPTSDK_ERR_UNSUPPORTED = -14, /* Feature not supported on this platform */ + ZUPTSDK_ERR_VERSION_MISMATCH = -15, /* Library older than requested */ + ZUPTSDK_ERR_PATH_TRAVERSAL = -16, /* "../" or absolute path in archive */ + ZUPTSDK_ERR_TOO_LARGE = -17, /* Decompressed size exceeds limit */ + ZUPTSDK_ERR_CRYPTO_FAIL = -18, /* Underlying crypto primitive failed */ + ZUPTSDK_ERR_CANCELLED = -19, /* Caller cancelled via progress callback */ + ZUPTSDK_ERR_INTERNAL = -99 /* Bug in library — please report */ +} zuptsdk_error_t; + +/** + * Static error description for a zuptsdk_error_t value. + * Returned pointer is static and must not be freed. Always non-NULL. + */ +const char *zuptsdk_strerror(int err); + +/** + * Thread-local detailed error message from the most recent failed call. + * The string includes file:line of the failure point and underlying errno + * description where applicable. Returned pointer is to thread-local + * storage, valid until the next failed zuptsdk_* call on this thread. + * Returns "" if no error has been recorded on this thread. + */ +const char *zuptsdk_last_error_detail(void); + +/* ════════════════════════════════════════════════════════════════════════ + * OPAQUE TYPES (forward declarations only — no struct layout exposed) + * ════════════════════════════════════════════════════════════════════════ */ + +typedef struct zuptsdk_ctx zuptsdk_ctx_t; +typedef struct zuptsdk_options zuptsdk_options_t; +typedef struct zuptsdk_archive_info zuptsdk_archive_info_t; +typedef struct zuptsdk_secure_buf zuptsdk_secure_buf_t; +typedef struct zuptsdk_keypair zuptsdk_keypair_t; +typedef struct zuptsdk_pubkey zuptsdk_pubkey_t; +typedef struct zuptsdk_privkey zuptsdk_privkey_t; + +/* ════════════════════════════════════════════════════════════════════════ + * ENUMS + * ════════════════════════════════════════════════════════════════════════ */ + +typedef enum { + ZUPTSDK_CODEC_AUTO = 0, /* Hardware-adaptive (VaptVupt on AVX2, LZHP otherwise) */ + ZUPTSDK_CODEC_VAPTVUPT = 1, /* VaptVupt LZ + ANS entropy */ + ZUPTSDK_CODEC_LZHP = 2, /* LZ77 + Huffman + Byte Prediction */ + ZUPTSDK_CODEC_LZH = 3, /* LZ77 + Huffman */ + ZUPTSDK_CODEC_LZ = 4, /* LZ77 only */ + ZUPTSDK_CODEC_STORE = 5 /* No compression */ +} zuptsdk_codec_t; + +typedef enum { + ZUPTSDK_ENC_NONE = 0, /* No encryption */ + ZUPTSDK_ENC_PASSWORD = 1, /* PBKDF2 → AES-256-CTR + HMAC-SHA256 */ + ZUPTSDK_ENC_PQ_HYBRID = 2 /* ML-KEM-768 + X25519 hybrid KEM */ +} zuptsdk_encryption_t; + +typedef enum { + ZUPTSDK_LOG_ERROR = 0, + ZUPTSDK_LOG_WARN = 1, + ZUPTSDK_LOG_INFO = 2, + ZUPTSDK_LOG_DEBUG = 3 +} zuptsdk_log_level_t; + +/* ════════════════════════════════════════════════════════════════════════ + * CALLBACKS + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Streaming read callback. Library calls this to obtain input bytes. + * @param userdata [in] opaque pointer supplied at stream init + * @param buf [out] destination buffer + * @param max_bytes max bytes to read into buf + * @return Number of bytes actually read (0 == EOF, < 0 == error). + */ +typedef int64_t (*zuptsdk_read_fn)(void *userdata, uint8_t *buf, size_t max_bytes); + +/** + * Streaming write callback. Library calls this to deliver output bytes. + * @param userdata [in] opaque pointer supplied at stream init + * @param buf [in] data to write + * @param bytes number of bytes in buf + * @return Number of bytes actually written (must equal `bytes` on success). + */ +typedef int64_t (*zuptsdk_write_fn)(void *userdata, const uint8_t *buf, size_t bytes); + +/** + * Progress callback. Library invokes periodically during long operations. + * Return non-zero to cancel the operation; the in-flight call will then + * return ZUPTSDK_ERR_CANCELLED. + * @param userdata [in] opaque pointer set via zuptsdk_ctx_set_progress_callback + * @param processed bytes processed so far + * @param total total bytes (0 if unknown) + * @return 0 to continue, non-zero to cancel. + */ +typedef int (*zuptsdk_progress_fn)(void *userdata, uint64_t processed, uint64_t total); + +/** + * Log callback. Receives diagnostic messages from the library. + * Set via zuptsdk_ctx_set_log_callback(). NULL means no logging (default). + * The string is null-terminated and valid only for the duration of the call. + */ +typedef void (*zuptsdk_log_fn)(void *userdata, zuptsdk_log_level_t level, const char *msg); + +/** + * Custom allocator hooks. Set globally via zuptsdk_set_allocator(). + * If any function is NULL, libc malloc/free/realloc is used. + * realloc_fn must accept (NULL, n) as malloc(n) and (p, 0) as free(p). + */ +typedef struct { + void *(*malloc_fn)(void *userdata, size_t size); + void (*free_fn)(void *userdata, void *ptr); + void *(*realloc_fn)(void *userdata, void *ptr, size_t size); + void *userdata; +} zuptsdk_allocator_t; + +/* ════════════════════════════════════════════════════════════════════════ + * GLOBAL CONFIG + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Install a custom allocator. Must be called before any other zuptsdk_* + * function. Calling after contexts have been created is undefined. + * Pass NULL to revert to libc allocator (only valid before first use). + * + * @param alloc [in,borrowed] allocator hooks; copied internally + * @return ZUPTSDK_OK or ZUPTSDK_ERR_INVALID_ARG + */ +int zuptsdk_set_allocator(const zuptsdk_allocator_t *alloc); + +/* ════════════════════════════════════════════════════════════════════════ + * CONTEXT + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Create a new SDK context. Each context holds its own thread pool, + * progress callback, log callback, and error state. Contexts are + * cheap to create — a few KB plus the configured thread count. + * + * @param ctx_out [out,transfers] pointer to receive new context + * @return ZUPTSDK_OK on success, ZUPTSDK_ERR_NO_MEMORY on alloc failure. + * On error, *ctx_out is set to NULL. + */ +int zuptsdk_ctx_create(zuptsdk_ctx_t **ctx_out); + +/** + * Destroy a context. Frees all owned resources including thread pool. + * Safe to call with NULL. After this call, the pointer is invalid. + */ +void zuptsdk_ctx_destroy(zuptsdk_ctx_t *ctx); + +/** + * Set worker thread count. 0 == auto (one per CPU). Default is auto. + * Returns ZUPTSDK_ERR_INVALID_ARG if ctx is NULL or threads > 256. + */ +int zuptsdk_ctx_set_threads(zuptsdk_ctx_t *ctx, int threads); + +/** + * Set progress callback for long-running operations on this context. + * Pass NULL fn to clear. userdata is opaque to the library. + */ +int zuptsdk_ctx_set_progress_callback(zuptsdk_ctx_t *ctx, + zuptsdk_progress_fn fn, + void *userdata); + +/** + * Set log callback for diagnostic messages on this context. + * Pass NULL fn to disable logging (default). + */ +int zuptsdk_ctx_set_log_callback(zuptsdk_ctx_t *ctx, + zuptsdk_log_fn fn, + zuptsdk_log_level_t min_level, + void *userdata); + +/* ════════════════════════════════════════════════════════════════════════ + * OPTIONS + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Create a default-initialized options bag for compress/encrypt operations. + * Defaults: codec=AUTO, level=7, no encryption, no dedup, no solid mode. + */ +int zuptsdk_options_create(zuptsdk_options_t **opts_out); +void zuptsdk_options_destroy(zuptsdk_options_t *opts); + +int zuptsdk_options_set_codec(zuptsdk_options_t *opts, zuptsdk_codec_t codec); +int zuptsdk_options_set_level(zuptsdk_options_t *opts, int level /* 1..9 */); +int zuptsdk_options_set_dedup(zuptsdk_options_t *opts, int enabled); +int zuptsdk_options_set_solid(zuptsdk_options_t *opts, int enabled); +int zuptsdk_options_set_block_size(zuptsdk_options_t *opts, size_t bytes); + +/** + * Maximum decompressed output size. Decompression aborts with + * ZUPTSDK_ERR_TOO_LARGE if exceeded. 0 == unlimited (NOT recommended + * for untrusted input — zip-bomb attack vector). Default: 16 GiB. + */ +int zuptsdk_options_set_max_decompressed(zuptsdk_options_t *opts, + uint64_t max_bytes); + +/* ════════════════════════════════════════════════════════════════════════ + * SECURE BUFFERS (for passwords and key material) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Allocate a secure buffer: backing memory is mlock()ed (locked into RAM, + * never swapped to disk) and explicit_bzero()ed on destroy. + * + * @param size requested size in bytes (1..65536) + * @param buf_out [out,transfers] receives buffer handle + * @return ZUPTSDK_OK on success. + */ +int zuptsdk_secure_buf_create(size_t size, zuptsdk_secure_buf_t **buf_out); + +/** + * Destroy a secure buffer. Memory is zeroed and unlocked before free. + * Safe to call with NULL. + */ +void zuptsdk_secure_buf_destroy(zuptsdk_secure_buf_t *buf); + +/** + * Get raw pointer to the secure buffer's storage. Pointer is valid until + * zuptsdk_secure_buf_destroy() is called. Caller may read or write up to + * the buffer's size. + * + * @param buf [in] + * @param data_out [out,borrowed] receives pointer to storage + * @param size_out [out] receives buffer size + */ +int zuptsdk_secure_buf_get(zuptsdk_secure_buf_t *buf, + uint8_t **data_out, size_t *size_out); + +/** + * Convenience: copy data into a new secure buffer. + * Useful when migrating an existing plain buffer to secure storage. + */ +int zuptsdk_secure_buf_from_data(const uint8_t *data, size_t size, + zuptsdk_secure_buf_t **buf_out); + +/* ════════════════════════════════════════════════════════════════════════ + * KEYS (PQ hybrid: ML-KEM-768 + X25519) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Generate a fresh hybrid keypair. Uses the system CSPRNG. + * + * @param ctx [in] + * @param kp_out [out,transfers] receives new keypair + * @return ZUPTSDK_OK on success, ZUPTSDK_ERR_CRYPTO_FAIL on RNG failure. + */ +int zuptsdk_keypair_generate(zuptsdk_ctx_t *ctx, zuptsdk_keypair_t **kp_out); + +void zuptsdk_keypair_destroy(zuptsdk_keypair_t *kp); + +/** + * Save private key to a file. The file is written with mode 0600 on POSIX. + * Recommended extension: ".key". + */ +int zuptsdk_keypair_save_private(const zuptsdk_keypair_t *kp, const char *path); + +/** + * Save public key to a file. World-readable. + * Recommended extension: ".pub" or "_public.key". + */ +int zuptsdk_keypair_save_public(const zuptsdk_keypair_t *kp, const char *path); + +/** + * Load a private key from a file. + * @param path [in] + * @param key_out [out,transfers] + */ +int zuptsdk_privkey_load(const char *path, zuptsdk_privkey_t **key_out); +void zuptsdk_privkey_destroy(zuptsdk_privkey_t *key); + +/** + * Load a public key from a file. + */ +int zuptsdk_pubkey_load(const char *path, zuptsdk_pubkey_t **key_out); +void zuptsdk_pubkey_destroy(zuptsdk_pubkey_t *key); + +/** + * Derive public key from private key (no I/O). + */ +int zuptsdk_privkey_get_public(const zuptsdk_privkey_t *priv, + zuptsdk_pubkey_t **pub_out); + +/* ════════════════════════════════════════════════════════════════════════ + * COMPRESS / DECOMPRESS — buffer mode (for small archives) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Compress an in-memory file list into a single archive buffer. + * + * @param ctx [in] + * @param opts [in,borrowed] compression and encryption options + * @param file_paths [in] array of filesystem paths to add + * @param file_count number of paths in file_paths + * @param password [in,nullable] password as a secure buffer; NULL for no pw + * @param recipient_pk [in,nullable] PQ public key for encryption; NULL for no PQ + * @param archive_out [out,transfers] receives malloc'd archive bytes; + * caller must free with zuptsdk_free() + * @param archive_sz [out] size of returned archive + * @return ZUPTSDK_OK on success. + */ +int zuptsdk_compress_files(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *const *file_paths, + size_t file_count, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, + size_t *archive_sz); + +/** + * Compress a single in-memory data buffer. Useful for SDK consumers that + * have data in memory and want a self-contained archive. + * + * @param logical_name [in] name to record inside the archive (e.g. "data.bin") + */ +int zuptsdk_compress_buffer(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *logical_name, + const uint8_t *data, size_t data_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk, + uint8_t **archive_out, + size_t *archive_sz); + +/** + * Extract an archive into a directory. + * + * @param dest_dir [in] target directory; created if missing + * @param password [in,nullable] + * @param recipient_sk [in,nullable] PQ private key + */ +int zuptsdk_extract_to_dir(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + const char *dest_dir, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/** + * Extract a single-file archive (one created with zuptsdk_compress_buffer) + * back into a memory buffer. + * + * @param data_out [out,transfers] caller frees with zuptsdk_free() + * @param data_sz [out] + */ +int zuptsdk_extract_buffer(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk, + uint8_t **data_out, size_t *data_sz); + +/* ════════════════════════════════════════════════════════════════════════ + * COMPRESS / DECOMPRESS — streaming mode (for large archives) + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Compress from a read callback to a write callback. Streaming version + * with no archive size limit — suitable for piping to network sockets, + * encrypted volumes, or any backend with a write_fn. + * + * @param input [in] read callback supplying source bytes + * @param input_ud [in] userdata passed to read callback + * @param input_name [in] logical filename to record in archive + * @param input_total total bytes to read; 0 if unknown + * @param output [in] write callback receiving archive bytes + * @param output_ud [in] userdata passed to write callback + */ +int zuptsdk_compress_stream(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + zuptsdk_read_fn input, void *input_ud, + const char *input_name, uint64_t input_total, + zuptsdk_write_fn output, void *output_ud, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk); + +/** + * Decompress an archive read from a callback, writing extracted single-file + * content to a write callback. + */ +int zuptsdk_decompress_stream(zuptsdk_ctx_t *ctx, + zuptsdk_read_fn input, void *input_ud, + zuptsdk_write_fn output, void *output_ud, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/* ════════════════════════════════════════════════════════════════════════ + * VERIFY / INFO + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Verify all block checksums and (if encrypted) HMAC of an archive. + * No data is written to disk. Returns ZUPTSDK_OK if every block validates. + */ +int zuptsdk_verify(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/** + * Read archive metadata without password or key. Returns header info only; + * does not decrypt block contents. + * + * @param info_out [out,transfers] receives info object; + * caller must zuptsdk_archive_info_destroy() + */ +int zuptsdk_archive_info_read(zuptsdk_ctx_t *ctx, + const uint8_t *archive, size_t archive_sz, + zuptsdk_archive_info_t **info_out); + +void zuptsdk_archive_info_destroy(zuptsdk_archive_info_t *info); + +/* Getters — opaque struct, all fields accessed via these functions. */ +int zuptsdk_archive_info_format_major(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_format_minor(const zuptsdk_archive_info_t *info); +const char *zuptsdk_archive_info_uuid(const zuptsdk_archive_info_t *info); +int64_t zuptsdk_archive_info_created_unix(const zuptsdk_archive_info_t *info); +uint64_t zuptsdk_archive_info_size(const zuptsdk_archive_info_t *info); +uint32_t zuptsdk_archive_info_block_count(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_encrypted(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_pq_hybrid(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_solid(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_dedup(const zuptsdk_archive_info_t *info); +int zuptsdk_archive_info_is_disk_image(const zuptsdk_archive_info_t *info); + +/* ════════════════════════════════════════════════════════════════════════ + * DISK BACKUP / RESTORE + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Backup a block device or disk image file to an archive. + * REQUIRES root/admin privileges to read raw block devices on most OSes. + */ +int zuptsdk_disk_backup(zuptsdk_ctx_t *ctx, + const zuptsdk_options_t *opts, + const char *source_device_or_image, + const char *output_archive_path, + zuptsdk_secure_buf_t *password, + const zuptsdk_pubkey_t *recipient_pk); + +/** + * Restore a disk backup archive to a block device or image file. + * DESTRUCTIVE: target is overwritten without confirmation. + */ +int zuptsdk_disk_restore(zuptsdk_ctx_t *ctx, + const char *archive_path, + const char *target_device_or_image, + zuptsdk_secure_buf_t *password, + const zuptsdk_privkey_t *recipient_sk); + +/* ════════════════════════════════════════════════════════════════════════ + * MISC + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Free memory returned by the library via [transfers] output pointers. + * Safe to call with NULL. + * + * Always use this — never free() — for SDK-allocated memory, since the + * library may have been built with a custom allocator. + */ +void zuptsdk_free(void *ptr); + +/** + * Best-effort secure zero of a buffer. Resistant to dead-store elimination + * by the optimizer. Use for caller-managed sensitive memory. + */ +void zuptsdk_secure_zero(void *buf, size_t bytes); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* ZUPTSDK_H */ diff --git a/vendor/zuptsdk/include/zuptsdk.hpp b/vendor/zuptsdk/include/zuptsdk.hpp new file mode 100644 index 0000000..45d789d --- /dev/null +++ b/vendor/zuptsdk/include/zuptsdk.hpp @@ -0,0 +1,230 @@ +// libzuptsdk C++17 header — RAII wrappers, exception-based error handling +// SPDX-License-Identifier: AGPL-3.0-or-later +#ifndef ZUPTSDK_HPP +#define ZUPTSDK_HPP + +#include "zuptsdk.h" +#include +#include +#include +#include +#include +#include + +namespace zuptsdk { + +class Error : public std::runtime_error { + int code_; +public: + Error(int code, const std::string& msg) : std::runtime_error(msg), code_(code) {} + int code() const noexcept { return code_; } +}; + +inline void check(int rc) { + if (rc != ZUPTSDK_OK) { + const char* detail = zuptsdk_last_error_detail(); + throw Error(rc, detail && *detail ? detail : zuptsdk_strerror(rc)); + } +} + +// RAII wrapper for SDK-allocated buffers (must be freed via zuptsdk_free) +class Buffer { + uint8_t* data_; + std::size_t size_; +public: + Buffer() : data_(nullptr), size_(0) {} + Buffer(uint8_t* data, std::size_t size) : data_(data), size_(size) {} + ~Buffer() { if (data_) zuptsdk_free(data_); } + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) { o.data_ = nullptr; o.size_ = 0; } + Buffer& operator=(Buffer&& o) noexcept { + if (data_) zuptsdk_free(data_); + data_ = o.data_; size_ = o.size_; o.data_ = nullptr; o.size_ = 0; + return *this; + } + const uint8_t* data() const noexcept { return data_; } + uint8_t* data() noexcept { return data_; } + std::size_t size() const noexcept { return size_; } + std::vector to_vector() const { return {data_, data_ + size_}; } + uint8_t** out_ptr() noexcept { return &data_; } + std::size_t* out_size() noexcept { return &size_; } +}; + +class Context { + zuptsdk_ctx_t* ctx_; +public: + Context() : ctx_(nullptr) { check(zuptsdk_ctx_create(&ctx_)); } + ~Context() { if (ctx_) zuptsdk_ctx_destroy(ctx_); } + Context(const Context&) = delete; + Context& operator=(const Context&) = delete; + zuptsdk_ctx_t* raw() const noexcept { return ctx_; } +}; + +class Pubkey { + zuptsdk_pubkey_t* pk_; +public: + Pubkey() : pk_(nullptr) {} + explicit Pubkey(zuptsdk_pubkey_t* pk) : pk_(pk) {} + ~Pubkey() { if (pk_) zuptsdk_pubkey_destroy(pk_); } + Pubkey(const Pubkey&) = delete; + Pubkey& operator=(const Pubkey&) = delete; + Pubkey(Pubkey&& o) noexcept : pk_(o.pk_) { o.pk_ = nullptr; } + static Pubkey load(const std::string& path) { + zuptsdk_pubkey_t* pk = nullptr; + check(zuptsdk_pubkey_load(path.c_str(), &pk)); + return Pubkey(pk); + } + zuptsdk_pubkey_t* raw() const noexcept { return pk_; } + std::array fingerprint() const { + std::array fp{}; + check(zuptsdk_pubkey_fingerprint(pk_, fp.data())); + return fp; + } +}; + +class Privkey { + zuptsdk_privkey_t* sk_; +public: + Privkey() : sk_(nullptr) {} + explicit Privkey(zuptsdk_privkey_t* sk) : sk_(sk) {} + ~Privkey() { if (sk_) zuptsdk_privkey_destroy(sk_); } + Privkey(const Privkey&) = delete; + Privkey& operator=(const Privkey&) = delete; + Privkey(Privkey&& o) noexcept : sk_(o.sk_) { o.sk_ = nullptr; } + static Privkey load(const std::string& path) { + zuptsdk_privkey_t* sk = nullptr; + check(zuptsdk_privkey_load(path.c_str(), &sk)); + return Privkey(sk); + } + zuptsdk_privkey_t* raw() const noexcept { return sk_; } +}; + +class Keypair { + zuptsdk_keypair_t* kp_; +public: + explicit Keypair(Context& ctx) : kp_(nullptr) { + check(zuptsdk_keypair_generate(ctx.raw(), &kp_)); + } + ~Keypair() { if (kp_) zuptsdk_keypair_destroy(kp_); } + Keypair(const Keypair&) = delete; + Keypair& operator=(const Keypair&) = delete; + void save_public(const std::string& path) const { + check(zuptsdk_keypair_save_public(kp_, path.c_str())); + } + void save_private(const std::string& path) const { + check(zuptsdk_keypair_save_private(kp_, path.c_str())); + } +}; + +// Result type for encryption operations +struct EncryptResult { + Buffer header; + Buffer ciphertext; +}; + +inline EncryptResult encrypt_pq(Context& ctx, const Pubkey& pk, + const uint8_t* pt, std::size_t pt_sz, + const uint8_t* aad = nullptr, std::size_t aad_sz = 0) { + EncryptResult r; + check(zuptsdk_encrypt_pq(ctx.raw(), pk.raw(), pt, pt_sz, aad, aad_sz, + r.header.out_ptr(), r.header.out_size(), + r.ciphertext.out_ptr(), r.ciphertext.out_size())); + return r; +} + +inline EncryptResult encrypt_pq_v2(Context& ctx, const Pubkey& pk, + int aead_id, bool forward_secret, + const uint8_t* pt, std::size_t pt_sz, + const uint8_t* aad = nullptr, std::size_t aad_sz = 0) { + EncryptResult r; + check(zuptsdk_encrypt_pq_v2(ctx.raw(), pk.raw(), aead_id, forward_secret ? 1 : 0, + pt, pt_sz, aad, aad_sz, + r.header.out_ptr(), r.header.out_size(), + r.ciphertext.out_ptr(), r.ciphertext.out_size())); + return r; +} + +inline Buffer decrypt_pq(Context& ctx, const Privkey& sk, + const uint8_t* hdr, std::size_t hdr_sz, + const uint8_t* ct, std::size_t ct_sz, + const uint8_t* aad = nullptr, std::size_t aad_sz = 0) { + Buffer r; + check(zuptsdk_decrypt_pq(ctx.raw(), sk.raw(), hdr, hdr_sz, ct, ct_sz, aad, aad_sz, + r.out_ptr(), r.out_size())); + return r; +} + +inline Buffer decrypt_pq_v2(Context& ctx, const Privkey& sk, + const uint8_t* hdr, std::size_t hdr_sz, + const uint8_t* ct, std::size_t ct_sz, + const uint8_t* aad = nullptr, std::size_t aad_sz = 0) { + Buffer r; + check(zuptsdk_decrypt_pq_v2(ctx.raw(), sk.raw(), hdr, hdr_sz, ct, ct_sz, aad, aad_sz, + r.out_ptr(), r.out_size())); + return r; +} + +// Streaming +class StreamEncrypter { + zuptsdk_stream_state_t* st_; + Buffer header_; +public: + StreamEncrypter(Context& ctx, const Pubkey& pk, + const uint8_t* aad = nullptr, std::size_t aad_sz = 0) : st_(nullptr) { + check(zuptsdk_stream_pq_init_encrypt(ctx.raw(), pk.raw(), aad, aad_sz, + header_.out_ptr(), header_.out_size(), &st_)); + } + ~StreamEncrypter() { if (st_) zuptsdk_stream_state_destroy(st_); } + StreamEncrypter(const StreamEncrypter&) = delete; + StreamEncrypter& operator=(const StreamEncrypter&) = delete; + const Buffer& header() const noexcept { return header_; } + + std::vector encrypt_chunk(const uint8_t* pt, std::size_t pt_sz, bool final_chunk) { + std::vector out(pt_sz + 21); + std::size_t out_sz = 0; + check(zuptsdk_stream_chunk_encrypt(st_, + final_chunk ? ZUPTSDK_CHUNK_FINAL : ZUPTSDK_CHUNK_MESSAGE, + pt, pt_sz, nullptr, 0, out.data(), out.size(), &out_sz)); + out.resize(out_sz); + return out; + } +}; + +class StreamDecrypter { + zuptsdk_stream_state_t* st_; + bool finished_ = false; +public: + StreamDecrypter(Context& ctx, const Privkey& sk, + const uint8_t* hdr, std::size_t hdr_sz, + const uint8_t* aad = nullptr, std::size_t aad_sz = 0) : st_(nullptr) { + check(zuptsdk_stream_pq_init_decrypt(ctx.raw(), sk.raw(), hdr, hdr_sz, + aad, aad_sz, &st_)); + } + ~StreamDecrypter() { if (st_) zuptsdk_stream_state_destroy(st_); } + StreamDecrypter(const StreamDecrypter&) = delete; + StreamDecrypter& operator=(const StreamDecrypter&) = delete; + + struct ChunkResult { + std::vector data; + bool final_chunk; + }; + ChunkResult decrypt_chunk(const uint8_t* in, std::size_t in_sz) { + std::vector out(in_sz); // upper bound + std::size_t out_sz = 0; + zuptsdk_chunk_tag_t tag; + check(zuptsdk_stream_chunk_decrypt(st_, in, in_sz, nullptr, 0, + out.data(), out.size(), &out_sz, &tag)); + out.resize(out_sz); + bool fin = (tag == ZUPTSDK_CHUNK_FINAL); + if (fin) finished_ = true; + return { std::move(out), fin }; + } + bool finished() const noexcept { return finished_; } +}; + +inline std::string version() { return zuptsdk_version_string(); } + +} // namespace zuptsdk + +#endif // ZUPTSDK_HPP diff --git a/vendor/zuptsdk/include/zuptsdk_easy.h b/vendor/zuptsdk/include/zuptsdk_easy.h new file mode 100644 index 0000000..ff6d666 --- /dev/null +++ b/vendor/zuptsdk/include/zuptsdk_easy.h @@ -0,0 +1,89 @@ +/* + * zuptsdk easy.h — high-level API for drop-in encryption. + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Goal: 3 lines of code to encrypt/decrypt anything in any language. + * No context management, no parameter tuning, secure defaults. + */ +#ifndef ZUPTSDK_EASY_H +#define ZUPTSDK_EASY_H + +#include "zuptsdk.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ─── String/buffer encryption (PQ pubkey) ─── */ + +/** Encrypt with recipient pubkey file path. Returns alloc'd combined + * blob (header || ciphertext) ready to store/transmit. */ +int zuptsdk_easy_encrypt(const char *recipient_pubkey_path, + const uint8_t *plaintext, size_t plaintext_sz, + uint8_t **blob_out, size_t *blob_sz); + +/** Decrypt blob with recipient privkey file path. */ +int zuptsdk_easy_decrypt(const char *recipient_privkey_path, + const uint8_t *blob, size_t blob_sz, + uint8_t **plaintext_out, size_t *plaintext_sz); + +/* ─── Password-based encryption ─── */ + +/** Encrypt with password (Argon2id, MODERATE preset by default). */ +int zuptsdk_easy_encrypt_password(const char *password, + const uint8_t *plaintext, size_t plaintext_sz, + uint8_t **blob_out, size_t *blob_sz); + +int zuptsdk_easy_decrypt_password(const char *password, + const uint8_t *blob, size_t blob_sz, + uint8_t **plaintext_out, size_t *plaintext_sz); + +/* ─── Field-level encryption (for DB columns, JSON fields) ─── */ + +/** Encrypt small fields with a 32-byte key. Returns base64-encoded + * string (alloc'd, NUL-terminated, free with zuptsdk_free). + * Suitable for DB columns, JSON fields, env vars. */ +int zuptsdk_easy_encrypt_field(const uint8_t key[32], + const char *plaintext, + char **b64_out); + +int zuptsdk_easy_decrypt_field(const uint8_t key[32], + const char *b64_input, + char **plaintext_out); + +/* ─── File encryption with progress ─── */ + +typedef void (*zuptsdk_easy_progress_t)(uint64_t bytes_done, + uint64_t bytes_total, + void *userdata); + +int zuptsdk_easy_encrypt_file(const char *recipient_pubkey_path, + const char *input_path, + const char *output_path, + zuptsdk_easy_progress_t cb, void *userdata); + +int zuptsdk_easy_decrypt_file(const char *recipient_privkey_path, + const char *input_path, + const char *output_path, + zuptsdk_easy_progress_t cb, void *userdata); + +/* ─── Keypair generation ─── */ + +/** Generate keypair and save to two paths. Convenience wrapper. */ +int zuptsdk_easy_keygen(const char *pubkey_out_path, + const char *privkey_out_path); + +/** Derive a deterministic 32-byte key from a password via Argon2id. + * For field encryption, derive key once at startup, reuse for many fields. */ +int zuptsdk_easy_derive_key(const char *password, + const uint8_t salt[16], + uint8_t key_out[32]); + +/* ─── Random salt generation ─── */ +int zuptsdk_easy_random_salt(uint8_t out[16]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/vendor/zuptsdk/include/zuptsdk_metrics.h b/vendor/zuptsdk/include/zuptsdk_metrics.h new file mode 100644 index 0000000..66695a9 --- /dev/null +++ b/vendor/zuptsdk/include/zuptsdk_metrics.h @@ -0,0 +1,57 @@ +/* zuptsdk observability — metrics & structured logging hooks + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +#ifndef ZUPTSDK_METRICS_H +#define ZUPTSDK_METRICS_H + +#include "zuptsdk.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint64_t encrypt_pq_count; + uint64_t decrypt_pq_count; + uint64_t encrypt_password_count; + uint64_t decrypt_password_count; + uint64_t encrypt_field_count; + uint64_t decrypt_field_count; + uint64_t encrypt_failures; + uint64_t decrypt_failures; + uint64_t mac_failures; + uint64_t commitment_failures; + uint64_t fault_detections; + uint64_t bytes_encrypted; + uint64_t bytes_decrypted; + uint64_t total_latency_ns; +} zuptsdk_metrics_t; + +/** Get a snapshot of accumulated metrics (thread-safe, atomic read). */ +void zuptsdk_metrics_snapshot(zuptsdk_metrics_t *out); + +/** Reset all counters to zero. */ +void zuptsdk_metrics_reset(void); + +/** Render snapshot in Prometheus exposition format to a buffer. + * Returns bytes written, or -1 if buf too small. + * If out is NULL, returns required size. */ +int zuptsdk_metrics_render_prometheus(char *buf, size_t buf_sz); + +/** Structured log callback for ops. Called on each encrypt/decrypt with + * outcome and timing. Set to NULL to disable. + * @param op "encrypt_pq" / "decrypt_pq" / "encrypt_password" / etc. + * @param rc error code (0 = OK) + * @param bytes plaintext bytes processed + * @param duration_ns elapsed time + */ +typedef void (*zuptsdk_op_log_t)(const char *op, int rc, size_t bytes, + uint64_t duration_ns, void *userdata); + +void zuptsdk_set_op_log(zuptsdk_op_log_t cb, void *userdata); + +#ifdef __cplusplus +} +#endif + +#endif