v2.2.2
This commit is contained in:
parent
f3e39fb8e6
commit
e5f5d32aab
124 changed files with 11892 additions and 2461 deletions
BIN
sdk/bindings/python/__pycache__/zuptsdk.cpython-312.pyc
Normal file
BIN
sdk/bindings/python/__pycache__/zuptsdk.cpython-312.pyc
Normal file
Binary file not shown.
468
sdk/bindings/python/zuptsdk.py
Normal file
468
sdk/bindings/python/zuptsdk.py
Normal 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")
|
||||
Loading…
Reference in a new issue