This commit is contained in:
Cristian Cezar Moisés 2026-05-01 09:58:47 -03:00
commit e5f5d32aab
124 changed files with 11892 additions and 2461 deletions

56
sdk/LICENSE Normal file
View file

@ -0,0 +1,56 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2026 Cristian Cezar Moisés <zupt@riseup.net>
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.

141
sdk/Makefile.sdk Normal file
View file

@ -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/

183
sdk/README.md Normal file
View file

@ -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.h>
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

View file

@ -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 <base_path>.key (private) and <base_path>.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"<ArchiveInfo v{self.format_major}.{self.format_minor} "
f"uuid={self.uuid} encrypted={self.is_encrypted} "
f"pq={self.is_pq_hybrid} size={self.size}>")
# ─────────────────────────────────────────────────────────────────────
# 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")

80
sdk/doc/example.c Normal file
View file

@ -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 <zuptsdk.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
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;
}

605
sdk/include/zuptsdk.h Normal file
View file

@ -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 <stddef.h>
#include <stdint.h>
#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 */

1149
sdk/src/zuptsdk.c Normal file

File diff suppressed because it is too large Load diff

122
sdk/tests/test_python.py Normal file
View file

@ -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)

View file

@ -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 <zuptsdk.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;
}

98
sdk/zuptsdk.map Normal file
View file

@ -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:
*;
};