zupt/src/zupt_crypto.c
Cristian Cezar Moisés 06c877ec86 feat: add Jasmin assembly integration for crypto acceleration
- Integrated `zupt_mac_verify_ct` in `zupt_decrypt_buffer()` to replace C XOR loop for HMAC-SHA256
- Integrated `zupt_ct_select_32` in `zupt_mlkem768_decaps()` to replace C `cmov()` for FO transformation
- Added `include/zupt_jasmin.h` with extern declarations and ABI docs
- Added `#ifdef ZUPT_USE_JASMIN` guards with clean C fallbacks in `zupt_crypto.c` and `zupt_mlkem.c`
- Makefile now auto-detects `jasmin/*.s`, assembles and links with `-DZUPT_USE_JASMIN`

Closes #3
2026-03-28 23:03:04 -03:00

536 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Zupt — Backup-oriented compression with AES-256 encryption
* Copyright (c) 2026 Cristian Cezar Moisés
* SPDX-License-Identifier: MIT
*
* Cryptographic operations:
* - HMAC-SHA256, PBKDF2, AES-256-CTR, Encrypt-then-MAC (v0.2+)
* - Hybrid PQ KEM: ML-KEM-768 + X25519 (v0.7.0)
*/
#define _GNU_SOURCE
#include "zupt.h"
#include "zupt_jasmin.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ═══════════════════════════════════════════════════════════════════
* RANDOM BYTES (OS-native CSPRNG — NO FALLBACK)
*
* If the OS CSPRNG is unavailable, this aborts. Using rand() would
* make salt/nonce predictable and destroy all security guarantees.
* ═══════════════════════════════════════════════════════════════════ */
void zupt_random_bytes(uint8_t *buf, size_t len) {
#ifdef _WIN32
/* Windows: RtlGenRandom (SystemFunction036) */
HMODULE lib = LoadLibraryA("advapi32.dll");
if (lib) {
typedef BOOLEAN(WINAPI *RtlGenRandomFunc)(PVOID, ULONG);
RtlGenRandomFunc fn = (RtlGenRandomFunc)(void(*)(void))GetProcAddress(lib, "SystemFunction036");
if (fn && fn(buf, (ULONG)len)) { FreeLibrary(lib); return; }
FreeLibrary(lib);
}
fprintf(stderr, "FATAL: Windows CSPRNG (RtlGenRandom) unavailable.\n");
exit(1);
#else
/* Linux/macOS/BSD: try getrandom(2) first, then /dev/urandom */
#if defined(__linux__) && defined(SYS_getrandom)
#include <sys/syscall.h>
ssize_t r = syscall(SYS_getrandom, buf, len, 0);
if (r == (ssize_t)len) return;
#endif
FILE *f = fopen("/dev/urandom", "rb");
if (f) {
size_t r = fread(buf, 1, len, f);
fclose(f);
if (r == len) return;
}
fprintf(stderr, "FATAL: /dev/urandom unavailable. Cannot generate secure random bytes.\n");
exit(1);
#endif
}
/* ═══════════════════════════════════════════════════════════════════
* HMAC-SHA256 (RFC 2104)
* ═══════════════════════════════════════════════════════════════════ */
void zupt_hmac_sha256(const uint8_t *key, size_t klen,
const uint8_t *data, size_t dlen,
uint8_t mac[32]) {
uint8_t k_pad[64];
uint8_t k_hash[32];
/* If key > 64 bytes, hash it first */
if (klen > 64) {
zupt_sha256(key, klen, k_hash);
key = k_hash; klen = 32;
}
/* ipad = key XOR 0x36 */
memset(k_pad, 0x36, 64);
for (size_t i = 0; i < klen; i++) k_pad[i] ^= key[i];
/* inner = SHA256(ipad || data) */
zupt_sha256_ctx ctx;
zupt_sha256_init(&ctx);
zupt_sha256_update(&ctx, k_pad, 64);
zupt_sha256_update(&ctx, data, dlen);
uint8_t inner[32];
zupt_sha256_final(&ctx, inner);
/* opad = key XOR 0x5c */
memset(k_pad, 0x5c, 64);
for (size_t i = 0; i < klen; i++) k_pad[i] ^= key[i];
/* mac = SHA256(opad || inner) */
zupt_sha256_init(&ctx);
zupt_sha256_update(&ctx, k_pad, 64);
zupt_sha256_update(&ctx, inner, 32);
zupt_sha256_final(&ctx, mac);
/* Wipe sensitive data */
zupt_secure_wipe(k_pad, 64);
zupt_secure_wipe(inner, 32);
zupt_secure_wipe(k_hash, 32);
}
/* ═══════════════════════════════════════════════════════════════════
* PBKDF2-HMAC-SHA256 (RFC 8018)
* ═══════════════════════════════════════════════════════════════════ */
void zupt_pbkdf2_sha256(const uint8_t *pw, size_t pwlen,
const uint8_t *salt, size_t slen,
uint32_t iterations,
uint8_t *output, size_t olen) {
/* Clamp salt length to fit in the stack buffer.
* ZUPT always passes ZUPT_SALT_SIZE (32) so this is a safety net. */
size_t effective_slen = slen;
if (effective_slen > 252) effective_slen = 252;
uint32_t block_num = 1;
size_t pos = 0;
while (pos < olen) {
/* U_1 = HMAC(pw, salt || INT_32_BE(block_num)) */
uint8_t salt_block[256];
memcpy(salt_block, salt, effective_slen);
salt_block[effective_slen+0] = (uint8_t)(block_num >> 24);
salt_block[effective_slen+1] = (uint8_t)(block_num >> 16);
salt_block[effective_slen+2] = (uint8_t)(block_num >> 8);
salt_block[effective_slen+3] = (uint8_t)(block_num);
uint8_t u[32], t[32];
zupt_hmac_sha256(pw, pwlen, salt_block, effective_slen + 4, u);
memcpy(t, u, 32);
/* U_2 .. U_c: XOR chain */
for (uint32_t i = 1; i < iterations; i++) {
zupt_hmac_sha256(pw, pwlen, u, 32, u);
for (int j = 0; j < 32; j++) t[j] ^= u[j];
}
/* Copy to output */
size_t chunk = olen - pos;
if (chunk > 32) chunk = 32;
memcpy(output + pos, t, chunk);
pos += chunk;
block_num++;
/* Wipe per-block intermediates */
zupt_secure_wipe(u, 32);
zupt_secure_wipe(t, 32);
zupt_secure_wipe(salt_block, sizeof(salt_block));
}
}
/* ═══════════════════════════════════════════════════════════════════
* AES-256-CTR MODE
* ═══════════════════════════════════════════════════════════════════ */
void zupt_aes256_ctr(const uint8_t key[32], const uint8_t nonce[16],
const uint8_t *in, uint8_t *out, size_t len) {
zupt_aes256_ctx ctx;
zupt_aes256_init(&ctx, key);
uint8_t counter[16], keystream[16];
memcpy(counter, nonce, 16);
size_t pos = 0;
while (pos < len) {
zupt_aes256_encrypt_block(&ctx, counter, keystream);
size_t chunk = len - pos;
if (chunk > 16) chunk = 16;
for (size_t i = 0; i < chunk; i++)
out[pos + i] = in[pos + i] ^ keystream[i];
pos += chunk;
/* Increment counter (big-endian, last 8 bytes) */
for (int i = 15; i >= 8; i--) {
if (++counter[i] != 0) break;
}
}
zupt_secure_wipe(&ctx, sizeof(ctx));
zupt_secure_wipe(keystream, 16);
}
/* ═══════════════════════════════════════════════════════════════════
* KEY DERIVATION
* ═══════════════════════════════════════════════════════════════════ */
void zupt_derive_keys(zupt_keyring_t *kr, const char *pw,
const uint8_t salt[32], const uint8_t nonce[16],
uint32_t iterations) {
memcpy(kr->salt, salt, ZUPT_SALT_SIZE);
memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE);
kr->iterations = iterations;
kr->active = 1;
/* Derive 64 bytes: 32 enc_key + 32 mac_key */
uint8_t material[64];
zupt_pbkdf2_sha256((const uint8_t *)pw, strlen(pw),
salt, ZUPT_SALT_SIZE,
iterations, material, 64);
memcpy(kr->enc_key, material, 32);
memcpy(kr->mac_key, material + 32, 32);
zupt_secure_wipe(material, 64);
}
/* ═══════════════════════════════════════════════════════════════════
* ENCRYPT-THEN-MAC
*
* Output format: [16-byte per-block nonce] [ciphertext] [32-byte HMAC]
* The HMAC covers the nonce and ciphertext.
* Per-block nonce = base_nonce XOR (block_seq as LE 8 bytes in low half)
* ═══════════════════════════════════════════════════════════════════ */
uint8_t *zupt_encrypt_buffer(const zupt_keyring_t *kr,
const uint8_t *plain, size_t plen,
uint64_t block_seq, size_t *olen) {
*olen = ZUPT_NONCE_SIZE + plen + ZUPT_HMAC_SIZE;
uint8_t *pkg = (uint8_t *)malloc(*olen);
if (!pkg) return NULL;
/* Derive per-block nonce */
uint8_t nonce[16];
memcpy(nonce, kr->base_nonce, 16);
for (int i = 0; i < 8; i++)
nonce[i] ^= (uint8_t)(block_seq >> (i * 8));
/* Store nonce */
memcpy(pkg, nonce, 16);
/* Encrypt */
zupt_aes256_ctr(kr->enc_key, nonce, plain, pkg + 16, plen);
/* MAC over nonce + ciphertext */
zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE,
pkg, 16 + plen,
pkg + 16 + plen);
return pkg;
}
uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr,
const uint8_t *pkg, size_t pkglen,
uint64_t block_seq, size_t *olen) {
(void)block_seq;
if (pkglen < ZUPT_NONCE_SIZE + ZUPT_HMAC_SIZE) return NULL;
size_t clen = pkglen - ZUPT_NONCE_SIZE - ZUPT_HMAC_SIZE;
*olen = clen;
/* Verify HMAC — constant-time comparison via XOR accumulation */
uint8_t expected_mac[32];
zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE,
pkg, ZUPT_NONCE_SIZE + clen,
expected_mac);
const uint8_t *stored_mac = pkg + ZUPT_NONCE_SIZE + clen;
#ifdef ZUPT_USE_JASMIN
/* JASMIN-VERIFIED: CT MAC comparison — 4×u64 XOR accumulation.
* Proven constant-time by Jasmin type system. */
uint64_t diff = zupt_mac_verify_ct(expected_mac, stored_mac);
#else
/* CT-REQUIRED: XOR accumulation fallback */
uint64_t diff = 0;
for (int i = 0; i < 32; i++)
diff |= (uint64_t)(expected_mac[i] ^ stored_mac[i]);
#endif
zupt_secure_wipe(expected_mac, 32);
if (diff != 0) return NULL; /* Authentication failed */
/* Decrypt */
uint8_t *plain = (uint8_t *)malloc(clen);
if (!plain) return NULL;
const uint8_t *nonce = pkg;
zupt_aes256_ctr(kr->enc_key, nonce, pkg + 16, plain, clen);
return plain;
}
/* ═══════════════════════════════════════════════════════════════════
* HYBRID POST-QUANTUM KEM: ML-KEM-768 + X25519 (v0.7.0)
*
* Security model: Secure if EITHER ML-KEM-768 OR X25519 is secure.
* Same approach as Signal (PQXDH), iMessage (PQ3), OpenSSH 9.0+.
*
* Key file format (.zupt-key):
* [4B] magic "ZKEY"
* [1B] version 0x01
* [1B] flags: bit0=has_private
* [2B] reserved
* [1184B] ml_kem_pk
* [32B] x25519_pk
* [2400B] ml_kem_sk (only if has_private)
* [32B] x25519_sk (only if has_private)
* [8B] xxh64 checksum of all above
* ═══════════════════════════════════════════════════════════════════ */
#include "zupt_mlkem.h"
#include "zupt_x25519.h"
#include "zupt_keccak.h"
#define ZKEY_MAGIC "ZKEY"
#define ZKEY_VERSION 0x01
#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 */
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];
/* Generate ML-KEM-768 keypair */
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) return -1;
/* 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;
buf[6] = buf[7] = 0; /* reserved */
memcpy(buf + 8, ml_pk, 1184);
memcpy(buf + 8 + 1184, x_pk, 32);
memcpy(buf + 8 + 1184 + 32, ml_sk, 2400);
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_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;
}
int zupt_hybrid_export_pubkey(const char *privfile, const char *pubfile) {
FILE *f = fopen(privfile, "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;
}
uint8_t pk_data[1184 + 32];
if (fread(pk_data, 1, 1216, f) != 1216) { fclose(f); return -1; }
fclose(f);
/* 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;
}
/* 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);
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);
return 0;
}
/*
* HYBRID ENCRYPT INIT: Encapsulate with ML-KEM + X25519, derive archive keys.
*
* enc_hdr output (1121 bytes):
* [1B] enc_type = 0x02 (PQ-Hybrid)
* [1088B] ml_kem_ciphertext
* [32B] ephemeral_x25519_pubkey
*
* Key derivation:
* hybrid_ikm = ml_kem_ss XOR x25519_ss
* archive_key[64] = SHA-256(hybrid_ikm ‖ ml_kem_ct ‖ ephemeral_pk ‖ "ZUPT-HYBRID-v1")
* enc_key = archive_key[0:32], mac_key = archive_key[32:64]
*/
int zupt_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
uint8_t *enc_hdr, size_t *enc_hdr_len) {
uint8_t ml_pk[1184], x_pk[32];
if (read_pubkey(pubkeyfile, ml_pk, x_pk) != 0) return -1;
/* ML-KEM-768 encapsulation */
uint8_t ml_ct[1088], ml_ss[32];
if (zupt_mlkem768_encaps(ml_ct, ml_ss, ml_pk) != 0) return -1;
/* X25519 ECDH */
uint8_t eph_sk[32], eph_pk[32], x_ss[32];
zupt_random_bytes(eph_sk, 32);
zupt_x25519_base(eph_pk, eph_sk);
zupt_x25519(x_ss, eph_sk, x_pk);
/* Hybrid shared secret: XOR then hash with transcript */
uint8_t hybrid_ikm[32];
for (int i = 0; i < 32; i++) hybrid_ikm[i] = ml_ss[i] ^ x_ss[i];
/* archive_key = SHA-256(hybrid_ikm ‖ ml_ct ‖ eph_pk ‖ "ZUPT-HYBRID-v1") */
/* We need 64 bytes, so use SHA3-512 instead of SHA-256 */
uint8_t kdf_input[32 + 1088 + 32 + 15];
memcpy(kdf_input, hybrid_ikm, 32);
memcpy(kdf_input + 32, ml_ct, 1088);
memcpy(kdf_input + 32 + 1088, eph_pk, 32);
memcpy(kdf_input + 32 + 1088 + 32, "ZUPT-HYBRID-v1", 15);
uint8_t archive_key[64];
zupt_sha3_512(kdf_input, sizeof(kdf_input), archive_key);
/* Set up keyring */
memcpy(kr->enc_key, archive_key, 32);
memcpy(kr->mac_key, archive_key + 32, 32);
zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE);
kr->iterations = 0;
kr->active = 1;
/* Build encryption header: enc_type(1) + ml_ct(1088) + eph_pk(32) + base_nonce(16) */
enc_hdr[0] = ZUPT_ENC_PQ_HYBRID;
memcpy(enc_hdr + 1, ml_ct, 1088);
memcpy(enc_hdr + 1 + 1088, eph_pk, 32);
memcpy(enc_hdr + 1 + 1088 + 32, kr->base_nonce, 16);
*enc_hdr_len = 1 + 1088 + 32 + 16; /* 1137 bytes */
/* Wipe all intermediates */
zupt_secure_wipe(ml_ss, 32);
zupt_secure_wipe(x_ss, 32);
zupt_secure_wipe(eph_sk, 32);
zupt_secure_wipe(hybrid_ikm, 32);
zupt_secure_wipe(kdf_input, sizeof(kdf_input));
zupt_secure_wipe(archive_key, 64);
return 0;
}
/*
* HYBRID DECRYPT INIT: Decapsulate with ML-KEM + X25519, derive archive keys.
*/
int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *enc_hdr, size_t enc_hdr_len) {
if (enc_hdr_len < 1 + 1088 + 32 + 16) return -1; /* enc_type + ct + eph_pk + nonce */
if (enc_hdr[0] != ZUPT_ENC_PQ_HYBRID) return -1;
const uint8_t *ml_ct = enc_hdr + 1;
const uint8_t *eph_pk = enc_hdr + 1 + 1088;
const uint8_t *nonce = enc_hdr + 1 + 1088 + 32;
uint8_t ml_pk[1184], x_pk[32], ml_sk[2400], x_sk[32];
if (read_privkey(privkeyfile, ml_pk, x_pk, ml_sk, x_sk) != 0) return -1;
/* ML-KEM-768 decapsulation */
uint8_t ml_ss[32];
zupt_mlkem768_decaps(ml_ss, ml_ct, ml_sk);
/* X25519 ECDH with ephemeral pubkey */
uint8_t x_ss[32];
zupt_x25519(x_ss, x_sk, eph_pk);
/* Same key derivation as encrypt */
uint8_t hybrid_ikm[32];
for (int i = 0; i < 32; i++) hybrid_ikm[i] = ml_ss[i] ^ x_ss[i];
uint8_t kdf_input[32 + 1088 + 32 + 15];
memcpy(kdf_input, hybrid_ikm, 32);
memcpy(kdf_input + 32, ml_ct, 1088);
memcpy(kdf_input + 32 + 1088, eph_pk, 32);
memcpy(kdf_input + 32 + 1088 + 32, "ZUPT-HYBRID-v1", 15);
uint8_t archive_key[64];
zupt_sha3_512(kdf_input, sizeof(kdf_input), archive_key);
memcpy(kr->enc_key, archive_key, 32);
memcpy(kr->mac_key, archive_key + 32, 32);
memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE); /* Read from enc_hdr, NOT random */
kr->iterations = 0;
kr->active = 1;
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
zupt_secure_wipe(x_sk, 32);
zupt_secure_wipe(ml_ss, 32);
zupt_secure_wipe(x_ss, 32);
zupt_secure_wipe(hybrid_ikm, 32);
zupt_secure_wipe(kdf_input, sizeof(kdf_input));
zupt_secure_wipe(archive_key, 64);
return 0;
}