release: restore ZUPT and harden source-only 5.2.2
This commit is contained in:
parent
74e393ba3e
commit
ff99770bd0
205 changed files with 19627 additions and 13215 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* VaptVupt — Zupt Integration API Implementation
|
||||
* VaptVupt — ZUPT Integration API Implementation
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
*
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
* backup-optimized defaults for VaptVupt 2.65.0.
|
||||
*
|
||||
* Defaults applied here (per ZUPT_INTEGRATION.md, Sprint 122):
|
||||
* - opts.checksum = 0 (Zupt's HMAC-SHA256 / AES-GCM-SIV outer
|
||||
* - opts.checksum = 0 (ZUPT's HMAC-SHA256 / AES-GCM-SIV outer
|
||||
* already authenticates the compressed
|
||||
* bytes; XXH64 footer is redundant work
|
||||
* and saves ~10% encode time)
|
||||
|
|
@ -38,7 +38,7 @@ 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 = 0; /* outer Zupt MAC authenticates compressed bytes */
|
||||
opts.checksum = 0; /* outer ZUPT MAC authenticates compressed bytes */
|
||||
opts.compat_v246_5_decoder = 0; /* allow 4-stream Huffman literal coding */
|
||||
|
||||
if (level <= 2) {
|
||||
|
|
|
|||
|
|
@ -1617,7 +1617,7 @@ static void est_huff_lengths(const uint32_t freq[NSYM], uint8_t len[NSYM]) {
|
|||
/* Depth of each node = depth(parent) + 1; parents always have
|
||||
* higher ids, so one reverse pass suffices. */
|
||||
uint8_t depth[2 * NSYM];
|
||||
depth[nn - 1] = 0;
|
||||
memset(depth, 0, sizeof(depth));
|
||||
for (int i = nn - 2; i >= 0; i--)
|
||||
depth[i] = (uint8_t)(depth[parent[i]] + 1);
|
||||
for (int i = 0; i < n; i++)
|
||||
|
|
|
|||
14
src/vv_bcj.c
14
src/vv_bcj.c
|
|
@ -13,13 +13,13 @@
|
|||
* then compresses well. The inverse runs on decode, before the bytes are
|
||||
* handed back to the caller.
|
||||
*
|
||||
* This is a clean-room reimplementation of the well-known x86 branch-
|
||||
* converter algorithm (the same transform used by 7-Zip/xz and described
|
||||
* in the LZMA SDK). The algorithm is exactly reversible on ARBITRARY input
|
||||
* — it is a bijection, so applying the filter to non-x86 data and then
|
||||
* inverting it reproduces the input byte-for-byte. That property is
|
||||
* fuzz-verified in tests; do not "optimize" the masking logic without
|
||||
* re-checking inverse(forward(x)) == x on random and adversarial inputs.
|
||||
* The x86 transform is adapted from Igor Pavlov's public-domain LZMA SDK
|
||||
* Bra86.c state machine. The exact SDK revision used by the original
|
||||
* integration was not retained; see THIRD-PARTY-NOTICES.md. The algorithm is
|
||||
* exactly reversible on arbitrary input: applying the filter and then its
|
||||
* inverse reproduces the input byte-for-byte. Deterministic randomized and
|
||||
* adversarial regressions exercise inverse(forward(x)) == x; do not "optimize"
|
||||
* the masking logic without rerunning them.
|
||||
*
|
||||
* The buffer is transformed in place. `encoding` is non-zero for the
|
||||
* forward (compress-side) transform, zero for the inverse (decode-side).
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ decode_block_tokens_impl(
|
|||
* Exit boundary: max is 1 token + 14 lits + 3 offset + 6 match_ext = 24.
|
||||
* Plus match_copy_32 may over-copy 32 bytes past the real end, so
|
||||
* op needs at least 64 bytes of margin. */
|
||||
#if VV_INLINE_AVX2
|
||||
const uint8_t *const ip_safe = (ip_len > 48) ? (ip_end - 48) : ip;
|
||||
uint8_t *const op_safe = (dst_cap > 72) ? (op_end - 72) : op;
|
||||
|
||||
|
|
@ -162,7 +163,6 @@ decode_block_tokens_impl(
|
|||
* rejected. */
|
||||
const uint32_t max_valid_off = (off_bytes == 2) ? 0xFFFF : 0xFFFFFF;
|
||||
|
||||
#if VV_INLINE_AVX2
|
||||
/* PERF: two-phase fast path.
|
||||
* Phase 1 (warmup): op hasn't advanced far enough to make any offset
|
||||
* automatically valid. Do full offset validation per sequence.
|
||||
|
|
|
|||
|
|
@ -901,8 +901,6 @@ typedef struct { uint32_t off; int32_t len; } opt_cand_t;
|
|||
* captures most of the available win at zero added complexity, so this
|
||||
* sprint ships it and defers the two-pass design until the window-size
|
||||
* lever has been measured (matters more for nci-class fixtures). */
|
||||
static inline int32_t opt_lit_price(void) { return 8; }
|
||||
|
||||
/* SPRINT 129: per-byte literal prices from the block's byte histogram.
|
||||
* The flat-8 model (Sprint 44) was chosen as the best single constant,
|
||||
* but the real literal coder delivers ~4-6 bits/byte on text and 7-8
|
||||
|
|
@ -1127,6 +1125,10 @@ static int opt_collect(const matcher_t *m, const uint8_t *data,
|
|||
static size_t compress_block_optimal(const uint8_t *src, size_t start_pos,
|
||||
size_t block_len, uint8_t *dst,
|
||||
size_t dst_cap, matcher_t *m, int min_match) {
|
||||
if (min_match < 1 || block_len > (size_t)INT32_MAX ||
|
||||
start_pos > (size_t)INT32_MAX - block_len)
|
||||
return 0;
|
||||
|
||||
uint8_t *op = dst;
|
||||
int32_t base = (int32_t)start_pos;
|
||||
int32_t end = (int32_t)(start_pos + block_len);
|
||||
|
|
@ -1249,8 +1251,8 @@ static size_t compress_block_optimal(const uint8_t *src, size_t start_pos,
|
|||
* bounds worst-case work on repetitive data: instead of
|
||||
* O(match_len) work per interior position, we jump over
|
||||
* the whole match. */
|
||||
int32_t use = best_len;
|
||||
if (i + use > N) use = N - i;
|
||||
int32_t remaining_len = N - i;
|
||||
int32_t use = best_len > remaining_len ? remaining_len : best_len;
|
||||
int32_t np = price[i] + opt_match_price(prep[i], best_off, use, of_bits);
|
||||
int32_t j = i + use;
|
||||
if (np < price[j]) {
|
||||
|
|
@ -1270,11 +1272,14 @@ static size_t compress_block_optimal(const uint8_t *src, size_t start_pos,
|
|||
}
|
||||
for (int c = 0; c < nc; c++) {
|
||||
int32_t mlen = cands[c].len; uint32_t moff = cands[c].off;
|
||||
if (i + mlen > N) mlen = N - i;
|
||||
int32_t remaining_len = N - i;
|
||||
if (mlen > remaining_len) mlen = remaining_len;
|
||||
if (mlen < min_match) continue;
|
||||
for (int32_t L = mlen; L >= min_match; L--) {
|
||||
if (L <= 0 || L > remaining_len) continue;
|
||||
int32_t np = price[i] + opt_match_price(prep[i], moff, L, of_bits);
|
||||
int32_t j = i + L;
|
||||
size_t j = (size_t)i + (size_t)L;
|
||||
if (j > (size_t)N) continue;
|
||||
if (np < price[j]) {
|
||||
price[j] = np; plen[j] = L; poff[j] = moff;
|
||||
opt_rep_push(prep[j], prep[i], moff);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@ static void copy_match_scalar(uint8_t *dst, uint32_t offset, size_t length) {
|
|||
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
|
||||
#ifdef __AVX2__
|
||||
#include <cpuid.h>
|
||||
#include <immintrin.h>
|
||||
|
||||
static int vv_has_avx2(void) {
|
||||
unsigned int eax, ebx, ecx, edx;
|
||||
|
|
@ -91,9 +93,6 @@ static int vv_has_avx2(void) {
|
|||
return (ebx & (1 << 5)) != 0; /* AVX2 bit */
|
||||
}
|
||||
|
||||
#ifdef __AVX2__
|
||||
#include <immintrin.h>
|
||||
|
||||
static void copy_fast_avx2(uint8_t *dst, const uint8_t *src, size_t n) {
|
||||
while (n >= 32) {
|
||||
__m256i v = _mm256_loadu_si256((const __m256i *)src);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later AND BSD-2-Clause
|
||||
* Copyright (c) 2012-2021 Yann Collet
|
||||
*
|
||||
* VaptVupt — XXH64 checksum (simplified, standalone)
|
||||
* Based on xxHash by Yann Collet. Public domain.
|
||||
* Based on xxHash by Yann Collet. See LICENSE-BSD-2-Clause.
|
||||
*/
|
||||
|
||||
#include "vaptvupt.h"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* ZUPT - AES-256 Block Cipher (FIPS 197)
|
||||
* Pure C, constant-time T-table implementation.
|
||||
* Pure C, portable table-based implementation.
|
||||
* FRAMA-C: ACSL-annotated (v2.0.0)
|
||||
*/
|
||||
#include "zupt.h"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* Zupt — CPU Feature Detection
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Zupt — Backup-oriented compression with AES-256 encryption
|
||||
* ZUPT — Backup-oriented compression with AES-256 encryption
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
|
|
@ -13,31 +13,36 @@
|
|||
#include "zupt.h"
|
||||
#include "zupt_acsl.h"
|
||||
#include "zupt_jasmin.h"
|
||||
#include "zupt_cpuid.h" /* JASMIN-VERIFIED: AES-NI dispatch */
|
||||
#include "zupt_cpuid.h" /* CPU dispatch for the optional Jasmin AES-NI path */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#if defined(__linux__)
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* CONSTANT-TIME EQUALITY (single audited primitive)
|
||||
*
|
||||
* Returns 1 if the two buffers are equal, 0 otherwise, in time that
|
||||
* depends only on `n` — never on the contents or on where the first
|
||||
* mismatch occurs. This is the one place the MAC-tag comparison is
|
||||
* implemented; the three former inline byte-OR loops (the v1.6 strict
|
||||
* Returns 1 if the two buffers are equal and 0 otherwise. Its source-level
|
||||
* control flow and memory-access pattern are intended to depend only on `n`,
|
||||
* not the contents or mismatch position. This is the one place the MAC-tag
|
||||
* comparison is implemented; the three former inline byte-OR loops (the v1.6 strict
|
||||
* decrypt path, the v1.4/v1.5 legacy v2 candidate, and the F-08 archive-
|
||||
* integrity-trailer check) now all call here, so the property is audited
|
||||
* and timing-tested in exactly one location (see tests/test_ct_timing).
|
||||
*
|
||||
* A timing leak in a MAC comparison is a forgery oracle: if "wrong on
|
||||
* byte 0" returned faster than "wrong on byte 31", an attacker could
|
||||
* recover a valid tag byte-by-byte. The accumulator is therefore folded
|
||||
* with OR (no early exit) and read through a volatile sink so the
|
||||
* compiler cannot reintroduce a short-circuit or branch.
|
||||
* recover a valid tag byte-by-byte. The source therefore uses volatile byte
|
||||
* loads and an OR accumulator with no explicit early exit. Exact generated
|
||||
* code remains compiler- and platform-dependent and is covered by a
|
||||
* dudect-style regression when its positive control is conclusive.
|
||||
*
|
||||
* CT-REQUIRED: no secret-dependent branch or memory access. */
|
||||
int zupt_ct_memeq(const void *a, const void *b, size_t n) {
|
||||
|
|
@ -79,7 +84,7 @@ void zupt_random_bytes(uint8_t *buf, size_t len) {
|
|||
if (r == (ssize_t)len) return;
|
||||
#endif
|
||||
#endif
|
||||
FILE *f = fopen("/dev/urandom", "rb");
|
||||
FILE *f = zupt_fopen_path("/dev/urandom", "rb");
|
||||
if (f) {
|
||||
size_t nread = fread(buf, 1, len, f);
|
||||
fclose(f);
|
||||
|
|
@ -242,8 +247,8 @@ void zupt_aes256_ctr(const uint8_t key[32], const uint8_t nonce[16],
|
|||
memcpy(counter, nonce, 16);
|
||||
|
||||
#ifdef ZUPT_USE_JASMIN
|
||||
/* JASMIN-VERIFIED: AES-NI path — constant-time, no T-table leakage.
|
||||
* The Jasmin-generated assembly uses VEX-encoded instructions (vaesenc,
|
||||
/* OPTIONAL ASSEMBLY PATH: AES-NI implementation uses no table lookups.
|
||||
* The checked-in assembly uses VEX-encoded instructions (vaesenc,
|
||||
* vmovdqu, vpxor, etc.) which require BOTH AES-NI AND AVX support.
|
||||
* Checking only has_aesni would SIGILL on CPUs with AES-NI but no AVX,
|
||||
* or where the OS hasn't enabled XSAVE for YMM state. */
|
||||
|
|
@ -571,6 +576,288 @@ uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr,
|
|||
return zupt_decrypt_buffer_aad(kr, pkg, pkglen, block_seq, NULL, 0, olen);
|
||||
}
|
||||
|
||||
/* Write a key file without ever opening an existing directory entry. Private
|
||||
* material is created mode 0600 on POSIX independently of the caller's umask.
|
||||
* Windows uses CREATE_NEW and, for private material, a protected DACL granting
|
||||
* access only to the current token's user SID. A failed write, flush, or close
|
||||
* leaves the exclusively created incomplete or durability-uncertain file in
|
||||
* place for the user to review and remove. This deliberately avoids a
|
||||
* pathname-based cleanup after close:
|
||||
* another process with write access to the parent directory could otherwise
|
||||
* replace the entry and trick cleanup into deleting an unrelated file.
|
||||
*
|
||||
* This is intentionally shared with the optional pq-box module. Public-key
|
||||
* output is exclusive too: besides avoiding symlink truncation, that prevents
|
||||
* `keygen --pub -o private.key -k private.key` from destroying the only copy of
|
||||
* a private key. */
|
||||
int zupt_keyfile_write_new(const char *path, const uint8_t *data, size_t length,
|
||||
int private_material) {
|
||||
if (!path || path[0] == '\0' || (!data && length != 0) ||
|
||||
(private_material != 0 && private_material != 1)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (length > (size_t)MAXDWORD) {
|
||||
errno = EFBIG;
|
||||
return -1;
|
||||
}
|
||||
wchar_t *wide_path = zupt_win_utf8_to_wide_alloc(path);
|
||||
if (!wide_path) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SECURITY_ATTRIBUTES attributes;
|
||||
SECURITY_DESCRIPTOR descriptor;
|
||||
SECURITY_ATTRIBUTES *attributes_ptr = NULL;
|
||||
HMODULE advapi = NULL;
|
||||
HANDLE token = NULL;
|
||||
TOKEN_USER *token_user = NULL;
|
||||
ACL *acl = NULL;
|
||||
|
||||
if (private_material) {
|
||||
typedef BOOL (WINAPI *open_process_token_fn)(HANDLE, DWORD, PHANDLE);
|
||||
typedef BOOL (WINAPI *get_token_information_fn)(
|
||||
HANDLE, TOKEN_INFORMATION_CLASS, LPVOID, DWORD, PDWORD);
|
||||
typedef DWORD (WINAPI *get_length_sid_fn)(PSID);
|
||||
typedef BOOL (WINAPI *initialize_acl_fn)(PACL, DWORD, DWORD);
|
||||
typedef BOOL (WINAPI *add_access_allowed_ace_fn)(
|
||||
PACL, DWORD, DWORD, PSID);
|
||||
typedef BOOL (WINAPI *initialize_security_descriptor_fn)(
|
||||
PSECURITY_DESCRIPTOR, DWORD);
|
||||
typedef BOOL (WINAPI *set_security_descriptor_dacl_fn)(
|
||||
PSECURITY_DESCRIPTOR, BOOL, PACL, BOOL);
|
||||
typedef BOOL (WINAPI *set_security_descriptor_control_fn)(
|
||||
PSECURITY_DESCRIPTOR, SECURITY_DESCRIPTOR_CONTROL,
|
||||
SECURITY_DESCRIPTOR_CONTROL);
|
||||
|
||||
advapi = LoadLibraryW(L"advapi32.dll");
|
||||
if (!advapi) goto windows_security_error;
|
||||
open_process_token_fn open_process_token =
|
||||
(open_process_token_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "OpenProcessToken");
|
||||
get_token_information_fn get_token_information =
|
||||
(get_token_information_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "GetTokenInformation");
|
||||
get_length_sid_fn get_length_sid =
|
||||
(get_length_sid_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "GetLengthSid");
|
||||
initialize_acl_fn initialize_acl =
|
||||
(initialize_acl_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "InitializeAcl");
|
||||
add_access_allowed_ace_fn add_access_allowed_ace =
|
||||
(add_access_allowed_ace_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "AddAccessAllowedAce");
|
||||
initialize_security_descriptor_fn initialize_security_descriptor =
|
||||
(initialize_security_descriptor_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "InitializeSecurityDescriptor");
|
||||
set_security_descriptor_dacl_fn set_security_descriptor_dacl =
|
||||
(set_security_descriptor_dacl_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "SetSecurityDescriptorDacl");
|
||||
set_security_descriptor_control_fn set_security_descriptor_control =
|
||||
(set_security_descriptor_control_fn)(void (*)(void))
|
||||
GetProcAddress(advapi, "SetSecurityDescriptorControl");
|
||||
if (!open_process_token || !get_token_information || !get_length_sid ||
|
||||
!initialize_acl || !add_access_allowed_ace ||
|
||||
!initialize_security_descriptor || !set_security_descriptor_dacl ||
|
||||
!set_security_descriptor_control)
|
||||
goto windows_security_error;
|
||||
|
||||
if (!open_process_token(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
goto windows_security_error;
|
||||
DWORD token_size = 0;
|
||||
(void)get_token_information(token, TokenUser, NULL, 0, &token_size);
|
||||
if (token_size == 0) goto windows_security_error;
|
||||
token_user = (TOKEN_USER *)malloc(token_size);
|
||||
if (!token_user) {
|
||||
errno = ENOMEM;
|
||||
goto windows_security_cleanup;
|
||||
}
|
||||
if (!get_token_information(token, TokenUser, token_user, token_size,
|
||||
&token_size))
|
||||
goto windows_security_error;
|
||||
|
||||
DWORD sid_size = get_length_sid(token_user->User.Sid);
|
||||
if (sid_size == 0 ||
|
||||
sid_size > MAXDWORD - (DWORD)sizeof(ACL) -
|
||||
(DWORD)sizeof(ACCESS_ALLOWED_ACE))
|
||||
goto windows_security_error;
|
||||
DWORD acl_size = (DWORD)sizeof(ACL) +
|
||||
(DWORD)sizeof(ACCESS_ALLOWED_ACE) -
|
||||
(DWORD)sizeof(DWORD) + sid_size;
|
||||
acl = (ACL *)malloc(acl_size);
|
||||
if (!acl) {
|
||||
errno = ENOMEM;
|
||||
goto windows_security_cleanup;
|
||||
}
|
||||
if (!initialize_acl(acl, acl_size, ACL_REVISION) ||
|
||||
!add_access_allowed_ace(acl, ACL_REVISION, GENERIC_ALL,
|
||||
token_user->User.Sid) ||
|
||||
!initialize_security_descriptor(&descriptor,
|
||||
SECURITY_DESCRIPTOR_REVISION) ||
|
||||
!set_security_descriptor_dacl(&descriptor, TRUE, acl, FALSE) ||
|
||||
!set_security_descriptor_control(&descriptor, SE_DACL_PROTECTED,
|
||||
SE_DACL_PROTECTED))
|
||||
goto windows_security_error;
|
||||
|
||||
attributes.nLength = sizeof(attributes);
|
||||
attributes.lpSecurityDescriptor = &descriptor;
|
||||
attributes.bInheritHandle = FALSE;
|
||||
attributes_ptr = &attributes;
|
||||
}
|
||||
|
||||
HANDLE handle = CreateFileW(
|
||||
wide_path, GENERIC_WRITE | DELETE, 0, attributes_ptr, CREATE_NEW,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT |
|
||||
FILE_FLAG_WRITE_THROUGH,
|
||||
NULL);
|
||||
{
|
||||
DWORD create_error = handle == INVALID_HANDLE_VALUE ? GetLastError() : 0;
|
||||
free(acl);
|
||||
free(token_user);
|
||||
if (token) CloseHandle(token);
|
||||
if (advapi) FreeLibrary(advapi);
|
||||
acl = NULL;
|
||||
token_user = NULL;
|
||||
token = NULL;
|
||||
advapi = NULL;
|
||||
if (handle == INVALID_HANDLE_VALUE) {
|
||||
free(wide_path);
|
||||
errno = (create_error == ERROR_FILE_EXISTS ||
|
||||
create_error == ERROR_ALREADY_EXISTS)
|
||||
? EEXIST : EACCES;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
DWORD written = 0;
|
||||
int failed = !WriteFile(handle, data, (DWORD)length, &written, NULL) ||
|
||||
written != (DWORD)length || !FlushFileBuffers(handle);
|
||||
if (!CloseHandle(handle)) failed = 1;
|
||||
if (failed) {
|
||||
free(wide_path);
|
||||
errno = EIO;
|
||||
return -1;
|
||||
}
|
||||
free(wide_path);
|
||||
return 0;
|
||||
|
||||
windows_security_error:
|
||||
errno = EACCES;
|
||||
windows_security_cleanup:
|
||||
free(acl);
|
||||
free(token_user);
|
||||
if (token) CloseHandle(token);
|
||||
if (advapi) FreeLibrary(advapi);
|
||||
free(wide_path);
|
||||
return -1;
|
||||
#else
|
||||
int flags = O_WRONLY | O_CREAT | O_EXCL;
|
||||
#ifdef O_NOFOLLOW
|
||||
flags |= O_NOFOLLOW;
|
||||
#endif
|
||||
#ifdef O_CLOEXEC
|
||||
flags |= O_CLOEXEC;
|
||||
#endif
|
||||
int descriptor = open(path, flags, private_material ? 0600 : 0666);
|
||||
if (descriptor < 0) return -1;
|
||||
|
||||
int failed = 0;
|
||||
int saved_errno = 0;
|
||||
if (private_material && fchmod(descriptor, 0600) != 0) {
|
||||
failed = 1;
|
||||
saved_errno = errno;
|
||||
}
|
||||
#ifndef O_CLOEXEC
|
||||
if (!failed) {
|
||||
int descriptor_flags = fcntl(descriptor, F_GETFD);
|
||||
if (descriptor_flags < 0 ||
|
||||
fcntl(descriptor, F_SETFD, descriptor_flags | FD_CLOEXEC) != 0) {
|
||||
failed = 1;
|
||||
saved_errno = errno;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
size_t offset = 0;
|
||||
while (!failed && offset < length) {
|
||||
ssize_t amount = write(descriptor, data + offset, length - offset);
|
||||
if (amount < 0 && errno == EINTR) continue;
|
||||
if (amount <= 0) {
|
||||
failed = 1;
|
||||
saved_errno = amount < 0 ? errno : EIO;
|
||||
break;
|
||||
}
|
||||
offset += (size_t)amount;
|
||||
}
|
||||
if (!failed && fsync(descriptor) != 0) {
|
||||
failed = 1;
|
||||
saved_errno = errno;
|
||||
}
|
||||
if (close(descriptor) != 0 && !failed) {
|
||||
failed = 1;
|
||||
saved_errno = errno;
|
||||
}
|
||||
if (failed) {
|
||||
errno = saved_errno ? saved_errno : EIO;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Load and structurally validate one native key blob before exposing any key
|
||||
* bytes to a caller. The historical writers have always serialized the XXH64
|
||||
* trailer with zupt_le64_put(), so readers deliberately interpret it as
|
||||
* little-endian on every host. This newly enforces the existing format rather
|
||||
* than changing it; valid v1 files remain byte-for-byte compatible. */
|
||||
static int load_native_key_blob(const char *path, const char magic[4],
|
||||
uint8_t version, uint8_t private_flag,
|
||||
size_t public_file_size,
|
||||
size_t private_file_size,
|
||||
int require_private,
|
||||
uint8_t *buffer, size_t buffer_capacity,
|
||||
size_t *file_size) {
|
||||
if (!path || !buffer || !file_size || public_file_size < 16 ||
|
||||
private_file_size <= public_file_size ||
|
||||
private_file_size > buffer_capacity) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
*file_size = 0;
|
||||
FILE *stream = zupt_fopen_path(path, "rb");
|
||||
if (!stream) return -1;
|
||||
|
||||
size_t length = fread(buffer, 1, private_file_size, stream);
|
||||
int trailing = fgetc(stream);
|
||||
int failed = ferror(stream) != 0;
|
||||
if (fclose(stream) != 0) failed = 1;
|
||||
|
||||
int has_private = length >= 6 && buffer[5] == private_flag;
|
||||
size_t expected_size = has_private ? private_file_size : public_file_size;
|
||||
if (failed || trailing != EOF ||
|
||||
(length != public_file_size && length != private_file_size) ||
|
||||
memcmp(buffer, magic, 4) != 0 || buffer[4] != version ||
|
||||
(buffer[5] != 0 && buffer[5] != private_flag) ||
|
||||
buffer[6] != 0 || buffer[7] != 0 ||
|
||||
length != expected_size || (require_private && !has_private)) {
|
||||
zupt_secure_wipe(buffer, buffer_capacity);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t stored_checksum = zupt_le64_get(buffer + length - 8);
|
||||
uint64_t computed_checksum = zupt_xxh64(buffer, length - 8, 0);
|
||||
if (stored_checksum != computed_checksum) {
|
||||
zupt_secure_wipe(buffer, buffer_capacity);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
*file_size = length;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* HYBRID POST-QUANTUM KEM: ML-KEM-768 + X25519 (v0.7.0)
|
||||
*
|
||||
|
|
@ -598,26 +885,25 @@ uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr,
|
|||
#define ZKEY_FLAG_PRIVATE 0x01
|
||||
#define ZKEY_PUB_SIZE (8 + 1184 + 32) /* header + ml_kem_pk + x25519_pk */
|
||||
#define ZKEY_PRIV_SIZE (8 + 1184 + 32 + 2400 + 32) /* + ml_kem_sk + x25519_sk */
|
||||
#define ZKEY_CHECKSUM_SIZE 8
|
||||
#define ZKEY_PUB_FILE_SIZE (ZKEY_PUB_SIZE + ZKEY_CHECKSUM_SIZE)
|
||||
#define ZKEY_PRIV_FILE_SIZE (ZKEY_PRIV_SIZE + ZKEY_CHECKSUM_SIZE)
|
||||
|
||||
int zupt_hybrid_keygen(const char *keyfile) {
|
||||
uint8_t ml_pk[MLKEM_PUBLICKEYBYTES], ml_sk[MLKEM_SECRETKEYBYTES];
|
||||
uint8_t x_sk[32], x_pk[32];
|
||||
uint8_t ml_pk[MLKEM_PUBLICKEYBYTES] = {0};
|
||||
uint8_t ml_sk[MLKEM_SECRETKEYBYTES] = {0};
|
||||
uint8_t x_sk[32] = {0}, x_pk[32] = {0};
|
||||
uint8_t buf[ZKEY_PRIV_FILE_SIZE] = {0};
|
||||
const size_t total = ZKEY_PRIV_SIZE;
|
||||
int result = -1;
|
||||
|
||||
/* Generate ML-KEM-768 keypair */
|
||||
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) return -1;
|
||||
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) goto out;
|
||||
|
||||
/* Generate X25519 keypair */
|
||||
zupt_random_bytes(x_sk, 32);
|
||||
zupt_x25519_base(x_pk, x_sk);
|
||||
|
||||
/* Write private key file */
|
||||
FILE *f = fopen(keyfile, "wb");
|
||||
if (!f) return -1;
|
||||
|
||||
size_t total = ZKEY_PRIV_SIZE;
|
||||
uint8_t *buf = (uint8_t *)calloc(total + 8, 1); /* +8 for checksum */
|
||||
if (!buf) { fclose(f); return -1; }
|
||||
|
||||
memcpy(buf, ZKEY_MAGIC, 4);
|
||||
buf[4] = ZKEY_VERSION;
|
||||
buf[5] = ZKEY_FLAG_PRIVATE;
|
||||
|
|
@ -628,85 +914,77 @@ int zupt_hybrid_keygen(const char *keyfile) {
|
|||
memcpy(buf + 8 + 1184 + 32 + 2400, x_sk, 32);
|
||||
|
||||
/* Checksum */
|
||||
uint64_t ck = zupt_xxh64(buf, total, 0);
|
||||
zupt_le64_put(buf + total, ck);
|
||||
|
||||
size_t written = fwrite(buf, 1, total + 8, f);
|
||||
fclose(f);
|
||||
zupt_le64_put(buf + total, zupt_xxh64(buf, total, 0));
|
||||
|
||||
result = zupt_keyfile_write_new(keyfile, buf, sizeof(buf), 1);
|
||||
out:
|
||||
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
|
||||
zupt_secure_wipe(x_sk, 32);
|
||||
zupt_secure_wipe(buf, total + 8);
|
||||
free(buf);
|
||||
|
||||
return (written == total + 8) ? 0 : -1;
|
||||
zupt_secure_wipe(x_sk, sizeof(x_sk));
|
||||
zupt_secure_wipe(buf, sizeof(buf));
|
||||
return result;
|
||||
}
|
||||
|
||||
int zupt_hybrid_export_pubkey(const char *privfile, const char *pubfile) {
|
||||
FILE *f = fopen(privfile, "rb");
|
||||
if (!f) return -1;
|
||||
uint8_t private_blob[ZKEY_PRIV_FILE_SIZE] = {0};
|
||||
uint8_t public_blob[ZKEY_PUB_FILE_SIZE] = {0};
|
||||
size_t private_size = 0;
|
||||
const size_t total = ZKEY_PUB_SIZE;
|
||||
int result = -1;
|
||||
if (load_native_key_blob(privfile, ZKEY_MAGIC, ZKEY_VERSION,
|
||||
ZKEY_FLAG_PRIVATE, ZKEY_PUB_FILE_SIZE,
|
||||
ZKEY_PRIV_FILE_SIZE, 1, private_blob,
|
||||
sizeof(private_blob), &private_size) != 0)
|
||||
goto out;
|
||||
|
||||
uint8_t hdr[8];
|
||||
if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, ZKEY_MAGIC, 4) != 0 ||
|
||||
!(hdr[5] & ZKEY_FLAG_PRIVATE)) {
|
||||
fclose(f); return -1;
|
||||
}
|
||||
memcpy(public_blob, ZKEY_MAGIC, 4);
|
||||
public_blob[4] = ZKEY_VERSION;
|
||||
public_blob[5] = 0; /* no private key */
|
||||
public_blob[6] = public_blob[7] = 0;
|
||||
memcpy(public_blob + 8, private_blob + 8, 1184 + 32);
|
||||
|
||||
uint8_t pk_data[1184 + 32];
|
||||
if (fread(pk_data, 1, 1216, f) != 1216) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
zupt_le64_put(public_blob + total,
|
||||
zupt_xxh64(public_blob, total, 0));
|
||||
|
||||
/* Write public key file */
|
||||
FILE *out = fopen(pubfile, "wb");
|
||||
if (!out) return -1;
|
||||
|
||||
size_t total = ZKEY_PUB_SIZE;
|
||||
uint8_t buf[ZKEY_PUB_SIZE + 8];
|
||||
memcpy(buf, ZKEY_MAGIC, 4);
|
||||
buf[4] = ZKEY_VERSION;
|
||||
buf[5] = 0; /* no private key */
|
||||
buf[6] = buf[7] = 0;
|
||||
memcpy(buf + 8, pk_data, 1216);
|
||||
|
||||
uint64_t ck = zupt_xxh64(buf, total, 0);
|
||||
zupt_le64_put(buf + total, ck);
|
||||
|
||||
size_t written = fwrite(buf, 1, total + 8, out);
|
||||
fclose(out);
|
||||
return (written == total + 8) ? 0 : -1;
|
||||
result = zupt_keyfile_write_new(pubfile, public_blob,
|
||||
sizeof(public_blob), 0);
|
||||
out:
|
||||
zupt_secure_wipe(private_blob, sizeof(private_blob));
|
||||
zupt_secure_wipe(public_blob, sizeof(public_blob));
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Read public key from a .zupt-key file (works for both pub and priv files) */
|
||||
static int read_pubkey(const char *path, uint8_t ml_pk[1184], uint8_t x_pk[32]) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
|
||||
uint8_t hdr[8];
|
||||
if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, ZKEY_MAGIC, 4) != 0) {
|
||||
fclose(f); return -1;
|
||||
}
|
||||
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||
if (fread(x_pk, 1, 32, f) != 32) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
uint8_t blob[ZKEY_PRIV_FILE_SIZE] = {0};
|
||||
size_t file_size = 0;
|
||||
/* Accept a structurally valid private file here for compatibility: older
|
||||
* releases explicitly allowed encryption directly with either ZKEY role. */
|
||||
if (load_native_key_blob(path, ZKEY_MAGIC, ZKEY_VERSION,
|
||||
ZKEY_FLAG_PRIVATE, ZKEY_PUB_FILE_SIZE,
|
||||
ZKEY_PRIV_FILE_SIZE, 0, blob, sizeof(blob),
|
||||
&file_size) != 0)
|
||||
return -1;
|
||||
memcpy(ml_pk, blob + 8, 1184);
|
||||
memcpy(x_pk, blob + 8 + 1184, 32);
|
||||
zupt_secure_wipe(blob, sizeof(blob));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Read private key from a .zupt-key file */
|
||||
static int read_privkey(const char *path, uint8_t ml_pk[1184], uint8_t x_pk[32],
|
||||
uint8_t ml_sk[2400], uint8_t x_sk[32]) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
|
||||
uint8_t hdr[8];
|
||||
if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, ZKEY_MAGIC, 4) != 0 ||
|
||||
!(hdr[5] & ZKEY_FLAG_PRIVATE)) {
|
||||
fclose(f); return -1;
|
||||
}
|
||||
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||
if (fread(x_pk, 1, 32, f) != 32) { fclose(f); return -1; }
|
||||
if (fread(ml_sk, 1, 2400, f) != 2400) { fclose(f); return -1; }
|
||||
if (fread(x_sk, 1, 32, f) != 32) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
uint8_t blob[ZKEY_PRIV_FILE_SIZE] = {0};
|
||||
size_t file_size = 0;
|
||||
if (load_native_key_blob(path, ZKEY_MAGIC, ZKEY_VERSION,
|
||||
ZKEY_FLAG_PRIVATE, ZKEY_PUB_FILE_SIZE,
|
||||
ZKEY_PRIV_FILE_SIZE, 1, blob, sizeof(blob),
|
||||
&file_size) != 0)
|
||||
return -1;
|
||||
memcpy(ml_pk, blob + 8, 1184);
|
||||
memcpy(x_pk, blob + 8 + 1184, 32);
|
||||
memcpy(ml_sk, blob + 8 + 1184 + 32, 2400);
|
||||
memcpy(x_sk, blob + 8 + 1184 + 32 + 2400, 32);
|
||||
zupt_secure_wipe(blob, sizeof(blob));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -912,18 +1190,18 @@ int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
|
|||
#define ZPQK_HDR 8
|
||||
#define ZPQK_PUB_SIZE (ZPQK_HDR + 1184)
|
||||
#define ZPQK_PRIV_SIZE (ZPQK_HDR + 1184 + 2400)
|
||||
#define ZPQK_CHECKSUM_SIZE 8
|
||||
#define ZPQK_PUB_FILE_SIZE (ZPQK_PUB_SIZE + ZPQK_CHECKSUM_SIZE)
|
||||
#define ZPQK_PRIV_FILE_SIZE (ZPQK_PRIV_SIZE + ZPQK_CHECKSUM_SIZE)
|
||||
#define ZUPT_PQ_ONLY_LABEL "ZUPT-PQ-ONLY-v1" /* 15 bytes */
|
||||
|
||||
int zupt_pq_keygen(const char *keyfile) {
|
||||
uint8_t ml_pk[MLKEM_PUBLICKEYBYTES], ml_sk[MLKEM_SECRETKEYBYTES];
|
||||
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) return -1;
|
||||
|
||||
FILE *f = fopen(keyfile, "wb");
|
||||
if (!f) { zupt_secure_wipe(ml_sk, sizeof(ml_sk)); return -1; }
|
||||
|
||||
size_t total = ZPQK_PRIV_SIZE;
|
||||
uint8_t *buf = (uint8_t *)calloc(total + 8, 1);
|
||||
if (!buf) { fclose(f); zupt_secure_wipe(ml_sk, sizeof(ml_sk)); return -1; }
|
||||
uint8_t ml_pk[MLKEM_PUBLICKEYBYTES] = {0};
|
||||
uint8_t ml_sk[MLKEM_SECRETKEYBYTES] = {0};
|
||||
uint8_t buf[ZPQK_PRIV_FILE_SIZE] = {0};
|
||||
const size_t total = ZPQK_PRIV_SIZE;
|
||||
int result = -1;
|
||||
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) goto out;
|
||||
|
||||
memcpy(buf, ZPQK_MAGIC, 4);
|
||||
buf[4] = ZPQK_VERSION;
|
||||
|
|
@ -932,65 +1210,68 @@ int zupt_pq_keygen(const char *keyfile) {
|
|||
memcpy(buf + ZPQK_HDR, ml_pk, 1184);
|
||||
memcpy(buf + ZPQK_HDR + 1184, ml_sk, 2400);
|
||||
|
||||
uint64_t ck = zupt_xxh64(buf, total, 0);
|
||||
zupt_le64_put(buf + total, ck);
|
||||
|
||||
size_t written = fwrite(buf, 1, total + 8, f);
|
||||
if (fclose(f) != 0) written = 0;
|
||||
zupt_le64_put(buf + total, zupt_xxh64(buf, total, 0));
|
||||
|
||||
result = zupt_keyfile_write_new(keyfile, buf, sizeof(buf), 1);
|
||||
out:
|
||||
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
|
||||
zupt_secure_wipe(buf, total + 8);
|
||||
free(buf);
|
||||
return (written == total + 8) ? 0 : -1;
|
||||
zupt_secure_wipe(buf, sizeof(buf));
|
||||
return result;
|
||||
}
|
||||
|
||||
int zupt_pq_export_pubkey(const char *privfile, const char *pubfile) {
|
||||
FILE *f = fopen(privfile, "rb");
|
||||
if (!f) return -1;
|
||||
uint8_t hdr[ZPQK_HDR];
|
||||
if (fread(hdr, 1, ZPQK_HDR, f) != ZPQK_HDR || memcmp(hdr, ZPQK_MAGIC, 4) != 0 ||
|
||||
!(hdr[5] & ZPQK_FLAG_PRIVATE)) { fclose(f); return -1; }
|
||||
uint8_t ml_pk[1184];
|
||||
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
uint8_t private_blob[ZPQK_PRIV_FILE_SIZE] = {0};
|
||||
uint8_t public_blob[ZPQK_PUB_FILE_SIZE] = {0};
|
||||
size_t private_size = 0;
|
||||
const size_t total = ZPQK_PUB_SIZE;
|
||||
int result = -1;
|
||||
if (load_native_key_blob(privfile, ZPQK_MAGIC, ZPQK_VERSION,
|
||||
ZPQK_FLAG_PRIVATE, ZPQK_PUB_FILE_SIZE,
|
||||
ZPQK_PRIV_FILE_SIZE, 1, private_blob,
|
||||
sizeof(private_blob), &private_size) != 0)
|
||||
goto out;
|
||||
|
||||
FILE *out = fopen(pubfile, "wb");
|
||||
if (!out) return -1;
|
||||
size_t total = ZPQK_PUB_SIZE;
|
||||
uint8_t buf[ZPQK_PUB_SIZE + 8];
|
||||
memcpy(buf, ZPQK_MAGIC, 4);
|
||||
buf[4] = ZPQK_VERSION;
|
||||
buf[5] = 0;
|
||||
buf[6] = buf[7] = 0;
|
||||
memcpy(buf + ZPQK_HDR, ml_pk, 1184);
|
||||
uint64_t ck = zupt_xxh64(buf, total, 0);
|
||||
zupt_le64_put(buf + total, ck);
|
||||
size_t written = fwrite(buf, 1, total + 8, out);
|
||||
if (fclose(out) != 0) written = 0;
|
||||
return (written == total + 8) ? 0 : -1;
|
||||
memcpy(public_blob, ZPQK_MAGIC, 4);
|
||||
public_blob[4] = ZPQK_VERSION;
|
||||
public_blob[5] = 0;
|
||||
public_blob[6] = public_blob[7] = 0;
|
||||
memcpy(public_blob + ZPQK_HDR, private_blob + ZPQK_HDR, 1184);
|
||||
zupt_le64_put(public_blob + total,
|
||||
zupt_xxh64(public_blob, total, 0));
|
||||
result = zupt_keyfile_write_new(pubfile, public_blob,
|
||||
sizeof(public_blob), 0);
|
||||
out:
|
||||
zupt_secure_wipe(private_blob, sizeof(private_blob));
|
||||
zupt_secure_wipe(public_blob, sizeof(public_blob));
|
||||
return result;
|
||||
}
|
||||
|
||||
static int read_pq_pubkey(const char *path, uint8_t ml_pk[1184]) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
uint8_t hdr[ZPQK_HDR];
|
||||
if (fread(hdr, 1, ZPQK_HDR, f) != ZPQK_HDR || memcmp(hdr, ZPQK_MAGIC, 4) != 0) {
|
||||
fclose(f); return -1;
|
||||
}
|
||||
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
uint8_t blob[ZPQK_PRIV_FILE_SIZE] = {0};
|
||||
size_t file_size = 0;
|
||||
/* Preserve the historical convenience of encrypting with a valid private
|
||||
* ZPQK file while still validating its private role, size, and checksum. */
|
||||
if (load_native_key_blob(path, ZPQK_MAGIC, ZPQK_VERSION,
|
||||
ZPQK_FLAG_PRIVATE, ZPQK_PUB_FILE_SIZE,
|
||||
ZPQK_PRIV_FILE_SIZE, 0, blob, sizeof(blob),
|
||||
&file_size) != 0)
|
||||
return -1;
|
||||
memcpy(ml_pk, blob + ZPQK_HDR, 1184);
|
||||
zupt_secure_wipe(blob, sizeof(blob));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_pq_privkey(const char *path, uint8_t ml_pk[1184], uint8_t ml_sk[2400]) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
uint8_t hdr[ZPQK_HDR];
|
||||
if (fread(hdr, 1, ZPQK_HDR, f) != ZPQK_HDR || memcmp(hdr, ZPQK_MAGIC, 4) != 0 ||
|
||||
!(hdr[5] & ZPQK_FLAG_PRIVATE)) { fclose(f); return -1; }
|
||||
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||
if (fread(ml_sk, 1, 2400, f) != 2400) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
uint8_t blob[ZPQK_PRIV_FILE_SIZE] = {0};
|
||||
size_t file_size = 0;
|
||||
if (load_native_key_blob(path, ZPQK_MAGIC, ZPQK_VERSION,
|
||||
ZPQK_FLAG_PRIVATE, ZPQK_PUB_FILE_SIZE,
|
||||
ZPQK_PRIV_FILE_SIZE, 1, blob, sizeof(blob),
|
||||
&file_size) != 0)
|
||||
return -1;
|
||||
memcpy(ml_pk, blob + ZPQK_HDR, 1184);
|
||||
memcpy(ml_sk, blob + ZPQK_HDR + 1184, 2400);
|
||||
zupt_secure_wipe(blob, sizeof(blob));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
* Copyright (c) 2026 Cristian Cezar Moisés
|
||||
*
|
||||
* zupt_crypto_pqbox.c — ZUPT_ENC_PQ_BOX_V1 (0x05): hybrid PQ sealed-box
|
||||
* recipient encryption backed by vendored libpqvaptvupt (v0.6.0).
|
||||
* recipient encryption backed by the optional system libpqvaptvupt.
|
||||
*
|
||||
* Why a third PQ mode:
|
||||
* - legacy --pq (0x02) combines the ML-KEM and X25519 shared secrets
|
||||
* with XOR+SHA3 — functional, but not the modern recommendation;
|
||||
* - --pq-sdk (0x03) is libzuptsdk's v2 envelope (kept for back-compat);
|
||||
* - --pq-sdk (0x03) is libvuptsdk's v2 envelope (kept for back-compat);
|
||||
* - --pq-box (0x05) uses libpqvaptvupt's sealed box, which combines the
|
||||
* two KEM secrets through HKDF-SHA256 Extract/Expand with a
|
||||
* domain-separating info string ("pqvv-seal-v1") — the construction
|
||||
|
|
@ -47,19 +47,23 @@
|
|||
|
||||
static int pqbox_write_keyfile(const char *path, char role,
|
||||
const uint8_t *key, size_t klen) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite(PQBOX_MAGIC, 1, PQBOX_MAGIC_LEN, f) == PQBOX_MAGIC_LEN
|
||||
&& fputc(role, f) != EOF
|
||||
&& fwrite(key, 1, klen, f) == klen;
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
if (klen > SIZE_MAX - PQBOX_HDR_LEN) return -1;
|
||||
size_t length = PQBOX_HDR_LEN + klen;
|
||||
uint8_t *blob = (uint8_t *)malloc(length);
|
||||
if (!blob) return -1;
|
||||
memcpy(blob, PQBOX_MAGIC, PQBOX_MAGIC_LEN);
|
||||
blob[PQBOX_MAGIC_LEN] = (uint8_t)role;
|
||||
memcpy(blob + PQBOX_HDR_LEN, key, klen);
|
||||
int result = zupt_keyfile_write_new(path, blob, length, role == 'S');
|
||||
if (role == 'S') zupt_secure_wipe(blob, length);
|
||||
free(blob);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Reads and validates a key file. Returns 0 and fills `key` on success. */
|
||||
static int pqbox_read_keyfile(const char *path, char role,
|
||||
uint8_t *key, size_t klen) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
FILE *f = zupt_fopen_path(path, "rb");
|
||||
if (!f) return -1;
|
||||
uint8_t hdr[PQBOX_HDR_LEN];
|
||||
int ok = fread(hdr, 1, PQBOX_HDR_LEN, f) == PQBOX_HDR_LEN
|
||||
|
|
@ -67,14 +71,18 @@ static int pqbox_read_keyfile(const char *path, char role,
|
|||
&& hdr[PQBOX_MAGIC_LEN] == (uint8_t)role
|
||||
&& fread(key, 1, klen, f) == klen
|
||||
&& fgetc(f) == EOF; /* exact size — no trailing bytes */
|
||||
fclose(f);
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
if (!ok && role == 'S') zupt_secure_wipe(key, klen);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
int zupt_pqbox_keygen(const char *privkeyfile, const char *pubkeyfile) {
|
||||
uint8_t pk[PQVV_PUBLICKEYBYTES];
|
||||
uint8_t sk[PQVV_SECRETKEYBYTES];
|
||||
if (pqvv_keygen(pk, sk) != PQVV_OK) return -1;
|
||||
uint8_t pk[PQVV_PUBLICKEYBYTES] = {0};
|
||||
uint8_t sk[PQVV_SECRETKEYBYTES] = {0};
|
||||
if (pqvv_keygen(pk, sk) != PQVV_OK) {
|
||||
zupt_secure_wipe(sk, sizeof(sk));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
if (pqbox_write_keyfile(privkeyfile, 'S', sk, sizeof(sk)) != 0) rc = -1;
|
||||
|
|
@ -146,7 +154,7 @@ int zupt_pqbox_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
|
|||
if (sealed_len != PQBOX_SEALED_SESSION || payload_len < 5 + (size_t)sealed_len)
|
||||
return -1;
|
||||
|
||||
uint8_t sk[PQVV_SECRETKEYBYTES];
|
||||
uint8_t sk[PQVV_SECRETKEYBYTES] = {0};
|
||||
if (pqbox_read_keyfile(privkeyfile, 'S', sk, sizeof(sk)) != 0) {
|
||||
fprintf(stderr, "Error: '%s' is not a pq-box SECRET key file.\n", privkeyfile);
|
||||
return -1;
|
||||
|
|
@ -184,16 +192,16 @@ int zupt_pqbox_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
|
|||
|
||||
#else /* !ZUPT_WITH_PQBOX */
|
||||
|
||||
/* Source-only build (no vendored libpqvaptvupt binary). The --pq-box sealed-box
|
||||
* mode is unavailable; use native --pq (ML-KEM-768 + X25519) instead, or rebuild
|
||||
* with `make WITH_SDK=1` (requires the vendored libpqvaptvupt). */
|
||||
/* Baseline build without the optional system libpqvaptvupt. The --pq-box
|
||||
* sealed-box mode is unavailable; use native --pq (ML-KEM-768 + X25519)
|
||||
* instead, or rebuild with WITH_PQBOX=1 and the system development package. */
|
||||
#include <stdio.h>
|
||||
|
||||
static int pqbox_unavailable(const char *what) {
|
||||
fprintf(stderr,
|
||||
"Error: this build has no libpqvaptvupt support, so %s is unavailable.\n"
|
||||
" Use native --pq (ML-KEM-768 + X25519) instead, or rebuild with "
|
||||
"'make WITH_SDK=1'.\n", what);
|
||||
"'make WITH_PQBOX=1' and the system development package.\n", what);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* 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 +
|
||||
* libvuptsdk'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).
|
||||
*
|
||||
|
|
@ -210,7 +210,7 @@ int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password,
|
|||
|
||||
/* v3.4.0 self-describing KDF profile. Absent (33-byte header) means
|
||||
* the implicit legacy profile; present (>=34 bytes) names it
|
||||
* explicitly. Both currently map to the same libzuptsdk MODERATE
|
||||
* explicitly. Both currently map to the same libvuptsdk MODERATE
|
||||
* Argon2id derivation, so the key is identical and old archives keep
|
||||
* decrypting. An unrecognised profile is refused rather than guessed
|
||||
* — better a clear failure than a wrong key derivation. */
|
||||
|
|
@ -249,20 +249,20 @@ int zupt_sdk_password_decrypt_init(zupt_keyring_t *kr, const char *password,
|
|||
|
||||
#else /* !ZUPT_WITH_SDK */
|
||||
|
||||
/* Source-only build (no vendored libzuptsdk binary). The SDK-backed modes
|
||||
/* Baseline build without the optional system libvuptsdk. The SDK-backed modes
|
||||
* — --pq-sdk and the Argon2id default password KDF — are unavailable. These
|
||||
* stubs let the project build and link from source with no prebuilt library;
|
||||
* callers fall back to native crypto (PBKDF2-SHA256 password KDF, native
|
||||
* ML-KEM-768 + X25519 via --pq) or report the requested mode as unsupported.
|
||||
* Rebuild with `make WITH_SDK=1` (requires the vendored libzuptsdk) to enable. */
|
||||
* Rebuild with WITH_SDK=1 and the system development package to enable. */
|
||||
#include <stdio.h>
|
||||
|
||||
static int sdk_unavailable(const char *what) {
|
||||
fprintf(stderr,
|
||||
"Error: this build has no libzuptsdk support, so %s is unavailable.\n"
|
||||
"Error: this build has no libvuptsdk support, so %s is unavailable.\n"
|
||||
" Use native crypto instead (password mode uses PBKDF2-SHA256; "
|
||||
"--pq uses ML-KEM-768 + X25519),\n"
|
||||
" or rebuild with 'make WITH_SDK=1'.\n", what);
|
||||
" or rebuild with 'make WITH_SDK=1' and the system development package.\n", what);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
|
|||
173
src/zupt_dedup.c
173
src/zupt_dedup.c
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* Zupt v2.1.5 — Block-Level Deduplication
|
||||
* 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.
|
||||
* fingerprinting with an independent SHA-256/128 verification on match.
|
||||
*
|
||||
* Architecture:
|
||||
* Source → XXH64 fingerprint → Hash table lookup → Match?
|
||||
|
|
@ -16,13 +16,14 @@
|
|||
* capped at ZUPT_DEDUP_MAX_ENTRIES (2M entries = ~48MB RAM).
|
||||
*
|
||||
* Security:
|
||||
* - XXH64 is not collision-resistant, so we verify full content
|
||||
* on hash match before emitting a reference.
|
||||
* - XXH64 is not collision-resistant, so a reference also requires an
|
||||
* independent 128-bit prefix of SHA-256 to match.
|
||||
* - Hash table memory is securely wiped on free.
|
||||
* - Dedup operates on plaintext before encryption.
|
||||
* - References are intra-archive offsets only.
|
||||
*/
|
||||
#include "zupt.h"
|
||||
#include "zupt_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -31,8 +32,10 @@
|
|||
typedef struct {
|
||||
uint64_t fingerprint; /* XXH64 of the block content */
|
||||
uint64_t block_offset; /* File offset where the block was written */
|
||||
uint8_t digest[ZUPT_DEDUP_DIGEST_SIZE]; /* independent SHA-256 prefix */
|
||||
uint32_t block_size; /* Uncompressed size of the block */
|
||||
uint32_t occupied; /* 0 = empty, 1 = occupied */
|
||||
uint64_t aad_seq; /* Logical position used to authenticate DATA */
|
||||
} zupt_dedup_entry_t;
|
||||
|
||||
/* Dedup context */
|
||||
|
|
@ -75,23 +78,25 @@ void zupt_dedup_free(zupt_dedup_ctx_t *ctx) {
|
|||
* Look up a block in the dedup index.
|
||||
* Returns 1 if a match is found (sets *ref_offset), 0 if not found.
|
||||
*
|
||||
* The caller must verify content equality before trusting the match
|
||||
* (XXH64 is fast but not collision-resistant). The content verification
|
||||
* is done by the caller who has access to the archive FILE* to seek
|
||||
* and re-read the original block.
|
||||
* XXH64 selects the probe chain; the independent SHA-256 prefix must also
|
||||
* match before the stored offset is returned.
|
||||
*/
|
||||
int zupt_dedup_lookup(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
||||
uint64_t *ref_offset, uint32_t *ref_size) {
|
||||
if (!ctx || !ctx->table) return 0;
|
||||
int zupt_dedup_lookup_secure(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
||||
const uint8_t digest[ZUPT_DEDUP_DIGEST_SIZE],
|
||||
uint64_t *ref_offset, uint32_t *ref_size,
|
||||
uint64_t *ref_aad_seq) {
|
||||
if (!ctx || !ctx->table || !digest) return 0;
|
||||
|
||||
uint32_t idx = (uint32_t)(fingerprint % ctx->capacity);
|
||||
for (uint32_t i = 0; i < 64; i++) { /* Max 64 probes */
|
||||
uint32_t slot = (idx + i) % ctx->capacity;
|
||||
zupt_dedup_entry_t *e = &ctx->table[slot];
|
||||
if (!e->occupied) return 0; /* Empty slot = not found */
|
||||
if (e->fingerprint == fingerprint) {
|
||||
if (e->fingerprint == fingerprint &&
|
||||
memcmp(e->digest, digest, ZUPT_DEDUP_DIGEST_SIZE) == 0) {
|
||||
if (ref_offset) *ref_offset = e->block_offset;
|
||||
if (ref_size) *ref_size = e->block_size;
|
||||
if (ref_aad_seq) *ref_aad_seq = e->aad_seq;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -102,9 +107,11 @@ int zupt_dedup_lookup(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
|||
* Insert a block into the dedup index.
|
||||
* Returns 1 on success, 0 if table is full.
|
||||
*/
|
||||
int zupt_dedup_insert(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
||||
uint64_t block_offset, uint32_t block_size) {
|
||||
if (!ctx || !ctx->table) return 0;
|
||||
int zupt_dedup_insert_secure(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
||||
const uint8_t digest[ZUPT_DEDUP_DIGEST_SIZE],
|
||||
uint64_t block_offset, uint32_t block_size,
|
||||
uint64_t block_aad_seq) {
|
||||
if (!ctx || !ctx->table || !digest) return 0;
|
||||
if (ctx->count >= ctx->capacity * 3 / 4) return 0; /* 75% load factor limit */
|
||||
|
||||
uint32_t idx = (uint32_t)(fingerprint % ctx->capacity);
|
||||
|
|
@ -114,7 +121,9 @@ int zupt_dedup_insert(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
|||
if (!e->occupied) {
|
||||
e->fingerprint = fingerprint;
|
||||
e->block_offset = block_offset;
|
||||
memcpy(e->digest, digest, ZUPT_DEDUP_DIGEST_SIZE);
|
||||
e->block_size = block_size;
|
||||
e->aad_seq = block_aad_seq;
|
||||
e->occupied = 1;
|
||||
ctx->count++;
|
||||
return 1;
|
||||
|
|
@ -123,6 +132,22 @@ int zupt_dedup_insert(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
|||
return 0; /* Probe limit */
|
||||
}
|
||||
|
||||
/* Preserve the published 5.2.1 symbols and signatures. First-party archive
|
||||
* writers use the secure variants above with an independent digest. */
|
||||
int zupt_dedup_lookup(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
||||
uint64_t *ref_offset, uint32_t *ref_size) {
|
||||
static const uint8_t legacy_digest[ZUPT_DEDUP_DIGEST_SIZE] = {0};
|
||||
return zupt_dedup_lookup_secure(ctx, fingerprint, legacy_digest,
|
||||
ref_offset, ref_size, NULL);
|
||||
}
|
||||
|
||||
int zupt_dedup_insert(zupt_dedup_ctx_t *ctx, uint64_t fingerprint,
|
||||
uint64_t block_offset, uint32_t block_size) {
|
||||
static const uint8_t legacy_digest[ZUPT_DEDUP_DIGEST_SIZE] = {0};
|
||||
return zupt_dedup_insert_secure(ctx, fingerprint, legacy_digest,
|
||||
block_offset, block_size, 0);
|
||||
}
|
||||
|
||||
void zupt_dedup_record_hit(zupt_dedup_ctx_t *ctx, uint64_t saved_bytes) {
|
||||
if (!ctx) return;
|
||||
ctx->blocks_deduped++;
|
||||
|
|
@ -176,3 +201,121 @@ int zupt_dedup_write_ref(FILE *out, uint64_t ref_offset,
|
|||
if (fwrite(payload, 1, 8, out) != 8) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* New encrypted archives authenticate the otherwise mutable reference offset.
|
||||
* The logical size/checksum remain in the frame preface and are included in
|
||||
* v1.6 preface AAD. The encrypted payload binds both the intra-archive offset
|
||||
* and the logical AAD sequence used by the referenced DATA frame; the
|
||||
* reference frame itself uses its own logical position as AAD. */
|
||||
int zupt_dedup_write_ref_secure(FILE *out, uint64_t ref_offset,
|
||||
uint32_t orig_size, uint64_t orig_checksum,
|
||||
uint64_t current_aad_seq,
|
||||
uint64_t referenced_aad_seq,
|
||||
const zupt_keyring_t *keyring) {
|
||||
uint8_t reference[16];
|
||||
zupt_le64_put(reference, ref_offset);
|
||||
zupt_le64_put(reference + 8, referenced_aad_seq);
|
||||
const uint8_t *payload = reference;
|
||||
size_t payload_size = keyring && keyring->active ? sizeof(reference) : 8u;
|
||||
uint16_t block_flags = 0;
|
||||
uint8_t *encrypted = NULL;
|
||||
|
||||
if (keyring && keyring->active) {
|
||||
size_t encrypted_size = 0;
|
||||
if (keyring->use_preface_aad) {
|
||||
uint8_t preface[ZUPT_PREFACE_AAD_LEN];
|
||||
uint64_t predicted_size = 16u + sizeof(reference) + 32u;
|
||||
zupt_serialize_preface_aad_scalars(
|
||||
ZUPT_BLOCK_DEDUP_REF, ZUPT_CODEC_STORE,
|
||||
ZUPT_BFLAG_ENCRYPTED, orig_size, predicted_size,
|
||||
orig_checksum, preface);
|
||||
encrypted = zupt_encrypt_buffer_aad(
|
||||
keyring, reference, sizeof(reference), current_aad_seq,
|
||||
preface, sizeof(preface), &encrypted_size);
|
||||
zupt_secure_wipe(preface, sizeof(preface));
|
||||
} else {
|
||||
encrypted = zupt_encrypt_buffer(keyring, reference,
|
||||
sizeof(reference), current_aad_seq,
|
||||
&encrypted_size);
|
||||
}
|
||||
if (!encrypted) return -1;
|
||||
payload = encrypted;
|
||||
payload_size = encrypted_size;
|
||||
block_flags = ZUPT_BFLAG_ENCRYPTED;
|
||||
}
|
||||
|
||||
zupt_w8(out, ZUPT_BLOCK_MAGIC_0);
|
||||
zupt_w8(out, ZUPT_BLOCK_MAGIC_1);
|
||||
zupt_w8(out, ZUPT_BLOCK_DEDUP_REF);
|
||||
zupt_w16le(out, ZUPT_CODEC_STORE);
|
||||
zupt_w16le(out, block_flags);
|
||||
zupt_write_varint(out, (uint64_t)orig_size);
|
||||
zupt_write_varint(out, payload_size);
|
||||
zupt_w64le(out, orig_checksum);
|
||||
int result = fwrite(payload, 1, payload_size, out) == payload_size &&
|
||||
!ferror(out) ? 0 : -1;
|
||||
free(encrypted);
|
||||
return result;
|
||||
}
|
||||
|
||||
zupt_error_t zupt_dedup_read_ref(const zupt_block_t *block,
|
||||
const zupt_keyring_t *keyring,
|
||||
int require_authentication,
|
||||
uint64_t current_aad_seq,
|
||||
uint64_t *ref_offset,
|
||||
uint64_t *referenced_aad_seq) {
|
||||
if (!block || !ref_offset || !referenced_aad_seq ||
|
||||
block->block_type != ZUPT_BLOCK_DEDUP_REF ||
|
||||
block->codec_id != ZUPT_CODEC_STORE || !block->payload)
|
||||
return ZUPT_ERR_CORRUPT;
|
||||
|
||||
const uint8_t *payload = block->payload;
|
||||
size_t payload_size = (size_t)block->compressed_size;
|
||||
uint8_t *plain = NULL;
|
||||
|
||||
if (require_authentication) {
|
||||
if (!(block->block_flags & ZUPT_BFLAG_ENCRYPTED) ||
|
||||
!keyring || !keyring->active)
|
||||
return ZUPT_ERR_AUTH_FAIL;
|
||||
size_t plain_size = 0;
|
||||
if (keyring->use_preface_aad) {
|
||||
uint8_t preface[ZUPT_PREFACE_AAD_LEN];
|
||||
zupt_serialize_preface_aad_scalars(
|
||||
block->block_type, block->codec_id, block->block_flags,
|
||||
block->uncompressed_size, block->compressed_size,
|
||||
block->checksum, preface);
|
||||
plain = zupt_decrypt_buffer_aad(
|
||||
keyring, payload, payload_size, current_aad_seq,
|
||||
preface, sizeof(preface), &plain_size);
|
||||
zupt_secure_wipe(preface, sizeof(preface));
|
||||
} else {
|
||||
plain = zupt_decrypt_buffer(keyring, payload, payload_size,
|
||||
current_aad_seq,
|
||||
&plain_size);
|
||||
}
|
||||
if (!plain) return ZUPT_ERR_AUTH_FAIL;
|
||||
if (plain_size != 16) {
|
||||
zupt_secure_wipe(plain, plain_size);
|
||||
free(plain);
|
||||
return ZUPT_ERR_CORRUPT;
|
||||
}
|
||||
payload = plain;
|
||||
payload_size = plain_size;
|
||||
} else if (block->block_flags != 0 || payload_size != 8) {
|
||||
return ZUPT_ERR_CORRUPT;
|
||||
}
|
||||
|
||||
if ((!require_authentication && payload_size != 8) ||
|
||||
(require_authentication && payload_size != 16)) {
|
||||
free(plain);
|
||||
return ZUPT_ERR_CORRUPT;
|
||||
}
|
||||
*ref_offset = zupt_le64_get(payload);
|
||||
*referenced_aad_seq = require_authentication
|
||||
? zupt_le64_get(payload + 8) : 0;
|
||||
if (plain) {
|
||||
zupt_secure_wipe(plain, payload_size);
|
||||
free(plain);
|
||||
}
|
||||
return ZUPT_OK;
|
||||
}
|
||||
|
|
|
|||
1179
src/zupt_disk.c
1179
src/zupt_disk.c
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* Zupt v2.0.0 — Adaptive Compression: File Type Detection
|
||||
* 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
|
||||
|
|
|
|||
3037
src/zupt_format.c
3037
src/zupt_format.c
File diff suppressed because it is too large
Load diff
57
src/zupt_internal.h
Normal file
57
src/zupt_internal.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
#ifndef ZUPT_INTERNAL_H
|
||||
#define ZUPT_INTERNAL_H
|
||||
|
||||
#include "zupt.h"
|
||||
|
||||
/* Keep the published 5.2.1 option layout intact. The high bit is private to
|
||||
* the CLI/read path; ordinary nonzero verbose values retain their behavior. */
|
||||
#define ZUPT_INTERNAL_ALLOW_LEGACY_NO_AIT 0x40000000
|
||||
|
||||
static inline void zupt_internal_set_verbose(zupt_options_t *options) {
|
||||
options->verbose |= 1;
|
||||
}
|
||||
|
||||
static inline int zupt_internal_verbose(const zupt_options_t *options) {
|
||||
return options &&
|
||||
(options->verbose & ~ZUPT_INTERNAL_ALLOW_LEGACY_NO_AIT) != 0;
|
||||
}
|
||||
|
||||
static inline void zupt_internal_allow_legacy_no_ait(
|
||||
zupt_options_t *options) {
|
||||
options->verbose |= ZUPT_INTERNAL_ALLOW_LEGACY_NO_AIT;
|
||||
}
|
||||
|
||||
static inline int zupt_internal_legacy_no_ait_allowed(
|
||||
const zupt_options_t *options) {
|
||||
if (!options) return 0;
|
||||
int value = options->verbose;
|
||||
return (value & ZUPT_INTERNAL_ALLOW_LEGACY_NO_AIT) != 0 &&
|
||||
(value & ~(ZUPT_INTERNAL_ALLOW_LEGACY_NO_AIT | 1)) == 0;
|
||||
}
|
||||
|
||||
/* A negative encoded capacity records an incomplete collection without
|
||||
* enlarging the published zupt_filelist_t structure. */
|
||||
static inline int zupt_internal_filelist_failed(
|
||||
const zupt_filelist_t *filelist) {
|
||||
return filelist && filelist->capacity < 0;
|
||||
}
|
||||
|
||||
static inline void zupt_internal_filelist_mark_failed(
|
||||
zupt_filelist_t *filelist) {
|
||||
if (filelist && filelist->capacity >= 0)
|
||||
filelist->capacity = -filelist->capacity - 1;
|
||||
}
|
||||
|
||||
int zupt_dedup_lookup_secure(
|
||||
zupt_dedup_ctx_t *context, uint64_t fingerprint,
|
||||
const uint8_t digest[ZUPT_DEDUP_DIGEST_SIZE],
|
||||
uint64_t *reference_offset, uint32_t *reference_size,
|
||||
uint64_t *reference_aad_sequence);
|
||||
int zupt_dedup_insert_secure(
|
||||
zupt_dedup_ctx_t *context, uint64_t fingerprint,
|
||||
const uint8_t digest[ZUPT_DEDUP_DIGEST_SIZE],
|
||||
uint64_t block_offset, uint32_t block_size,
|
||||
uint64_t block_aad_sequence);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Zupt — Backup-oriented compression with AES-256 encryption
|
||||
* ZUPT — Backup-oriented compression with AES-256 encryption
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* ZUPT - LZ77 Compression Engine v2 (Zupt-LZ codec 0x0008)
|
||||
* ZUPT - LZ77 Compression Engine v2 (ZUPT-LZ codec 0x0008)
|
||||
*
|
||||
* Improvements over v0.1:
|
||||
* - 18-bit hash table (256K entries) for better match distribution
|
||||
|
|
|
|||
|
|
@ -216,15 +216,16 @@ static void huff_build(const uint32_t *freq, int ns, hcode_t *codes) {
|
|||
|
||||
int ni=0;
|
||||
while(hn>1){
|
||||
hnode_t a=hp[0];hp[0]=hp[--hn];if(hn>0)h_down(hp,hn,0);
|
||||
hnode_t a=hp[0];hp[0]=hp[--hn];h_down(hp,hn,0);
|
||||
hnode_t b=hp[0];hp[0]=hp[--hn];if(hn>0)h_down(hp,hn,0);
|
||||
L[ni]=a.s; R[ni]=b.s;
|
||||
hnode_t in; in.f=a.f+b.f; in.s=-(ni+1); ni++;
|
||||
hp[hn]=in; h_up(hp,hn); hn++;
|
||||
}
|
||||
|
||||
uint8_t *dp=(uint8_t*)calloc(ns,1);
|
||||
if(dp && hn==1) tree_depths(hp[0].s,0,L,R,dp,ns);
|
||||
uint8_t *dp=(uint8_t*)calloc((size_t)ns, 1);
|
||||
if (!dp) { free(hp); free(L); free(R); return; }
|
||||
if(hn==1) tree_depths(hp[0].s,0,L,R,dp,ns);
|
||||
|
||||
/* Enforce max code length using Kraft-sum based redistribution.
|
||||
*
|
||||
|
|
@ -375,7 +376,7 @@ static size_t cl_encode(const uint8_t *lens, int count, uint8_t *out, size_t oca
|
|||
out[op++] = (uint8_t)(r - 11);
|
||||
i += r; run -= r;
|
||||
} else if (run >= 3) {
|
||||
int r = run > 10 ? 10 : run;
|
||||
int r = run;
|
||||
if (op + 2 > ocap) return 0;
|
||||
out[op++] = 17;
|
||||
out[op++] = (uint8_t)(r - 3);
|
||||
|
|
@ -670,6 +671,8 @@ size_t zupt_lzh_compress(const uint8_t *src, size_t slen,
|
|||
|
||||
/* Compress code lengths with RLE */
|
||||
uint8_t ll_lens[LZH_MAX_LITLEN], d_lens[LZH_MAX_DIST];
|
||||
memset(ll_lens, 0, sizeof(ll_lens));
|
||||
memset(d_lens, 0, sizeof(d_lens));
|
||||
for (int i = 0; i < ll_cnt; i++) ll_lens[i] = ll_codes[i].len;
|
||||
for (int i = 0; i < d_cnt; i++) d_lens[i] = d_codes[i].len;
|
||||
|
||||
|
|
@ -741,7 +744,6 @@ size_t zupt_lzh_decompress(const uint8_t *src, size_t slen,
|
|||
int rle_on = (flags & 0x01);
|
||||
uint32_t rle_orig = 0;
|
||||
if (rle_on) {
|
||||
if (ip + 4 > slen) return 0;
|
||||
memcpy(&rle_orig, src + ip, 4); ip += 4;
|
||||
}
|
||||
|
||||
|
|
|
|||
962
src/zupt_main.c
962
src/zupt_main.c
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,13 @@
|
|||
/*
|
||||
* Zupt — Backup-oriented compression with AES-256 encryption
|
||||
* ZUPT — Backup-oriented compression with AES-256 encryption
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later AND CC0-1.0
|
||||
*
|
||||
* Portions are adapted from the pq-crystals/kyber reference implementation,
|
||||
* offered upstream under CC0-1.0 or Apache-2.0. ZUPT uses the CC0-1.0
|
||||
* option for those portions; see THIRD-PARTY-NOTICES.md. The exact upstream
|
||||
* revision used for the original adaptation was not retained, so none is
|
||||
* asserted here.
|
||||
*
|
||||
* ML-KEM-768 (FIPS 203, formerly CRYSTALS-Kyber).
|
||||
* Pure C11, zero dependencies. Uses zupt_keccak.h for SHA3/SHAKE.
|
||||
|
|
@ -592,11 +598,13 @@ int zupt_mlkem768_decaps(uint8_t ss[32], const uint8_t ct[1088],
|
|||
uint8_t ct_prime[1088];
|
||||
kpke_encrypt(ct_prime, pk, m_prime, kr + 32);
|
||||
|
||||
/* CT-REQUIRED: Compare ct and ct' via the single audited constant-time
|
||||
* primitive (the same one used for MAC-tag verification; timing-tested
|
||||
* by tests/test_ct_timing). A timing leak here would be a KEM
|
||||
/* CT-REQUIRED: Compare ct and ct' via the single audited
|
||||
* constant-time-intended primitive (the same one used for MAC-tag
|
||||
* verification; regression-measured by tests/test_ct_timing when its
|
||||
* control is conclusive). A timing leak here would be a KEM
|
||||
* decapsulation oracle — distinguishing valid from invalid ciphertexts
|
||||
* breaks IND-CCA2 — so this comparison must be constant-time over all
|
||||
* breaks IND-CCA2 — so the implementation requires content-independent
|
||||
* behavior over all
|
||||
* 1088 ciphertext bytes. zupt_ct_memeq returns 1 if the buffers are
|
||||
* equal (ct matches → success), 0 otherwise. */
|
||||
int ct_equal = zupt_ct_memeq(ct, ct_prime, 1088);
|
||||
|
|
@ -621,7 +629,7 @@ int zupt_mlkem768_decaps(uint8_t ss[32], const uint8_t ct[1088],
|
|||
* ct_equal == 0 (ct differs): use ss_reject (implicit rejection) → fail = 1. */
|
||||
uint8_t fail = (uint8_t)(1 - ct_equal);
|
||||
#ifdef ZUPT_USE_JASMIN
|
||||
/* JASMIN-VERIFIED: CT select — proven by Jasmin type system.
|
||||
/* JASMIN PATH: compiled masked select; no retained formal proof is claimed.
|
||||
* fail=0 → ss_success, fail=1 → ss_reject */
|
||||
zupt_ct_select_32(ss, ss_success, ss_reject, (uint64_t)fail);
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Zupt — Memory Locking for Key Material
|
||||
* ZUPT — Memory Locking for Key Material
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
|
|
|
|||
|
|
@ -9,23 +9,21 @@
|
|||
* zupt_sha256.c — bit-identical output — but 3-8x faster on CPUs that
|
||||
* implement the extensions (Intel Goldmont+/Ice Lake+, AMD Zen+).
|
||||
*
|
||||
* Security note: SHA-NI is constant-time by construction. It performs
|
||||
* no data-dependent memory accesses or branches, so it has a strictly
|
||||
* stronger side-channel posture than any table- or branch-based
|
||||
* software SHA-256. Since Zupt's authentication is HMAC-SHA256 over
|
||||
* attacker-influenced ciphertext, a constant-time compression function
|
||||
* is the right default wherever the hardware provides it.
|
||||
* Security note: this fixed-round SHA-NI path is designed without intended
|
||||
* data-dependent memory access or branches. Exact generated-code behavior is
|
||||
* compiler-, CPU-, and platform-dependent; this is not a formal constant-time
|
||||
* claim. Avoiding table lookups is nevertheless useful for HMAC-SHA256 over
|
||||
* attacker-influenced ciphertext.
|
||||
*
|
||||
* Dispatch: sha256_transform() in zupt_sha256.c calls
|
||||
* zupt_sha256_transform_shani() when zupt_cpu.has_shani is set. On
|
||||
* non-x86_64 targets this file compiles to nothing (the symbol is
|
||||
* never referenced because has_shani is always 0).
|
||||
*
|
||||
* Reference: Intel SHA Extensions whitepaper (Gulley, Gopal, Yap,
|
||||
* Feghali, Guilford, Wolrich, 2013) and the public-domain intrinsic
|
||||
* reference by Jeffrey Walton. This implementation was written against
|
||||
* the FIPS 180-4 spec and validated bit-exact against the scalar path
|
||||
* and the NIST FIPS 180-4 test vectors on both paths.
|
||||
* Adapted from Jeffrey Walton's public-domain SHA-Intrinsics x86 reference,
|
||||
* itself based on Intel and miTLS material; see THIRD-PARTY-NOTICES.md. The
|
||||
* resulting implementation is validated bit-exact against the scalar path and
|
||||
* the NIST FIPS 180-4 test vectors on both paths.
|
||||
*/
|
||||
|
||||
#include "zupt.h"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
/*
|
||||
* Zupt — Backup-oriented compression with AES-256 encryption
|
||||
* ZUPT — Backup-oriented compression with AES-256 encryption
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later AND BSD-3-Clause
|
||||
*
|
||||
* Portions are adapted from curve25519-donna by Google Inc. and Adam Langley.
|
||||
* This distribution conservatively retains the upstream repository's
|
||||
* BSD-3-Clause terms; see THIRD-PARTY-NOTICES.md. The exact upstream revision
|
||||
* used for the original adaptation was not retained, so none is asserted.
|
||||
*
|
||||
* X25519 Diffie-Hellman (RFC 7748) over Curve25519.
|
||||
* Field: GF(2^255-19), represented as 4 × 64-bit limbs (donna64 layout).
|
||||
* Montgomery ladder: constant-time by construction (no secret-dependent branches).
|
||||
* Field: GF(2^255-19), represented as 5 x 51-bit limbs following
|
||||
* curve25519-donna's 64-bit implementation approach.
|
||||
* Fixed-iteration Montgomery ladder with no intended secret-dependent branch
|
||||
* or table access; exact compiled timing remains platform-dependent.
|
||||
*
|
||||
* CT-REQUIRED: Every operation in this file must be constant-time.
|
||||
* No branches on secret data. No secret-dependent memory access.
|
||||
*
|
||||
* v2.0.0: Rewritten from 5×51-bit to 4×64-bit limb representation
|
||||
* to match Jasmin zupt_fe_cswap (4×u64 masked XOR swap).
|
||||
*
|
||||
* Representation: f = f[0] + f[1]*2^64 + f[2]*2^128 + f[3]*2^192
|
||||
* where limbs can temporarily exceed 2^64 during intermediate calculations.
|
||||
* Representation: f = f[0] + f[1]*2^51 + f[2]*2^102 + f[3]*2^153
|
||||
* + f[4]*2^204. Limbs may temporarily exceed 51 bits during arithmetic;
|
||||
* fe_reduce() brings the result back to canonical form mod 2^255-19.
|
||||
*/
|
||||
#include "zupt_x25519.h"
|
||||
|
|
@ -23,30 +27,12 @@
|
|||
#include <string.h>
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* FIELD ARITHMETIC: GF(2^255 - 19), 4 × 64-bit limbs
|
||||
* FIELD ARITHMETIC: GF(2^255 - 19), 5 x 51-bit limbs
|
||||
*
|
||||
* We use the 5×51-bit schoolbook approach internally for multiplication
|
||||
* (to avoid requiring __int128 for 128×128 products) but store/swap
|
||||
* in 4×64-bit layout to match Jasmin.
|
||||
*
|
||||
* Actually: we keep 5×51-bit for mul/sq (needs 64×64→128 products)
|
||||
* and convert to/from 4×64-bit at the boundary (frombytes/tobytes/cswap).
|
||||
*
|
||||
* CORRECTION: To truly match Jasmin's 4×u64 layout for fe_cswap,
|
||||
* the field elements in memory MUST be 4×u64. We use 5×51-bit
|
||||
* internally in registers only, and store back as 4×u64 after each
|
||||
* operation. This is the donna64 approach used by libsodium.
|
||||
*
|
||||
* SIMPLER APPROACH: Keep everything as 5×51-bit (the proven working
|
||||
* implementation) and just adapt fe_cswap to operate on 5 limbs
|
||||
* with the Jasmin function swapping the first 4 u64 values plus
|
||||
* a C swap of the 5th.
|
||||
*
|
||||
* SIMPLEST CORRECT APPROACH (chosen): Keep the proven 5×51-bit
|
||||
* arithmetic but store field elements as 5×u64 (40 bytes). The
|
||||
* Jasmin fe_cswap swaps 4×u64 (32 bytes). We call it for the first
|
||||
* 4 limbs and handle the 5th limb in C. This is minimal change,
|
||||
* the arithmetic is identical, and the CT property is preserved.
|
||||
* The optional Jasmin swap operates on the first four stored uint64_t limbs;
|
||||
* the fifth limb uses the same masked-XOR pattern in C. The default build uses
|
||||
* the C loop for all five limbs. No retained formal-verification artifact is
|
||||
* claimed for either path.
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
typedef uint64_t fe[5]; /* Field element: 5 limbs, each < 2^52 */
|
||||
|
|
@ -115,12 +101,12 @@ static void fe_tobytes(uint8_t s[32], const fe h) {
|
|||
}
|
||||
|
||||
/* CT-REQUIRED: conditional swap — no branches on secret bit.
|
||||
* JASMIN-VERIFIED: First 4 limbs swapped by Jasmin when available;
|
||||
* JASMIN PATH: first 4 limbs swapped by compiled Jasmin code when available;
|
||||
* 5th limb swapped in C (same constant-time XOR pattern). */
|
||||
static void fe_cswap(fe a, fe b, uint64_t flag) {
|
||||
uint64_t mask = -(uint64_t)(flag & 1);
|
||||
#ifdef ZUPT_USE_JASMIN
|
||||
/* JASMIN-VERIFIED: CT swap of first 32 bytes (4×u64).
|
||||
/* JASMIN PATH: masked swap of first 32 bytes (4×u64).
|
||||
* The Jasmin function operates on 4 consecutive u64 values. */
|
||||
zupt_fe_cswap(a, b, flag & 1);
|
||||
/* 5th limb: C fallback (same CT pattern) */
|
||||
|
|
@ -248,13 +234,13 @@ static void fe_inv(fe h, const fe f) {
|
|||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* X25519 MONTGOMERY LADDER
|
||||
* CT-REQUIRED: No secret-dependent branches. The ladder is constant-time
|
||||
* by construction: every iteration performs the same operations, with
|
||||
* cswap selecting which point to operate on.
|
||||
* CT-REQUIRED: no intended secret-dependent branches or memory access. Every
|
||||
* iteration follows the same source-level operation sequence, with cswap
|
||||
* selecting which point to operate on; this is not a compiled timing proof.
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* FRAMA-C: X25519 Diffie-Hellman key agreement (RFC 7748)
|
||||
* CT-REQUIRED: Montgomery ladder — constant-time by construction */
|
||||
* CT-REQUIRED: fixed-iteration, constant-time-intended Montgomery ladder */
|
||||
/*@ requires \valid(out + (0..31));
|
||||
@ requires \valid_read(scalar + (0..31));
|
||||
@ requires \valid_read(point + (0..31));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later AND BSD-2-Clause
|
||||
* Copyright (c) 2012-2021 Yann Collet
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* ZUPT - XXH64 Hash (based on xxHash by Yann Collet, BSD-2)
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue