Skip to content

Instantly share code, notes, and snippets.

@lastforkbender
Created January 18, 2026 22:30
Show Gist options
  • Select an option

  • Save lastforkbender/a8d221042f442381ee01feded4a4af9d to your computer and use it in GitHub Desktop.

Select an option

Save lastforkbender/a8d221042f442381ee01feded4a4af9d to your computer and use it in GitHub Desktop.
panama sv2026 spectrum net error chained encryption
# panama_sv2026.py
import os
import time
import secrets
import numpy as np
import multiprocessing as mp
from numba import njit, prange
from typing import List, Tuple
from dataclasses import dataclass
from scipy.spatial.distance import cdist
from cryptography.hazmat.primitives import hashes, hmac
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
try:
import bchlib
except Exception as e:
raise RuntimeError("bchlib required") from e
SPECTRUM_LEN = 256
BLOCKS = 8
BLOCK_SIZE = SPECTRUM_LEN // BLOCKS
CODEBOOK_PER_BLOCK = 16
PACKETS = 4096
NOISE_STD = 0.08
BCH_POLY = 137
BCH_T = 5
DERIVED_KEY_LEN = 32
HMAC_KEY_LEN = 32
NUM_WORKERS = max(1, mp.cpu_count() - 1)
BATCH_SIZE = 64
EXPT_SEED = 2026
rng_expt = np.random.default_rng(EXPT_SEED)
def secure_random_bytes(n: int) -> bytes:
return secrets.token_bytes(n)
def hkdf(ikm: bytes, salt: bytes, info: bytes, length: int = DERIVED_KEY_LEN) -> bytes:
hk = HKDF(algorithm=hashes.SHA256(), length=length, salt=salt, info=info)
return hk.derive(ikm)
def hmac_sign(key: bytes, data: bytes) -> bytes:
h = hmac.HMAC(key, hashes.SHA256())
h.update(data)
return h.finalize()
def hmac_verify(key: bytes, data: bytes, tag: bytes) -> bool:
h = hmac.HMAC(key, hashes.SHA256())
h.update(data)
try:
h.verify(tag)
return True
except Exception:
return False
def make_bch(payload_len: int):
bch = bchlib.BCH(BCH_POLY, BCH_T)
dummy = bytes([0] * payload_len)
_ = bch.encode(dummy)
return bch
def generate_spectrum(spectrum_len=SPECTRUM_LEN, peaks=6, rng=None):
if rng is None:
rng = np.random.default_rng()
x = np.linspace(0, 1, spectrum_len)
spec = np.zeros_like(x)
for _ in range(peaks):
c = rng.random()
w = 0.01 + 0.06 * rng.random()
a = 0.4 + 0.6 * rng.random()
spec += a * np.exp(-0.5 * ((x - c) / w) ** 2)
spec += 0.03 * rng.random(spectrum_len)
maxv = np.max(spec)
if maxv > 0:
spec /= maxv
return spec.astype(np.float32)
def kmeans_pp_init(X, k, rng):
n = X.shape[0]
centers = np.empty((k, X.shape[1]), dtype=X.dtype)
first = rng.integers(0, n)
centers[0] = X[first]
dist_sq = cdist(X, centers[0:1], metric='euclidean').ravel() ** 2
for i in range(1, k):
probs = dist_sq / np.sum(dist_sq)
idx = rng.choice(n, p=probs)
centers[i] = X[idx]
d = cdist(X, centers[i:i+1], metric='euclidean').ravel() ** 2
dist_sq = np.minimum(dist_sq, d)
return centers
def build_codebooks(samples: np.ndarray, blocks=BLOCKS, k=CODEBOOK_PER_BLOCK, iters=10):
n, dim = samples.shape
if dim % blocks != 0:
raise ValueError("dimension must be divisible by blocks")
block_len = dim // blocks
codebooks = []
rng = rng_expt
for b in range(blocks):
start = b * block_len
end = start + block_len
bs = samples[:, start:end]
centroids = kmeans_pp_init(bs, k, rng)
for _ in range(iters):
d = cdist(bs, centroids, metric='euclidean')
labels = np.argmin(d, axis=1)
for j in range(k):
mem = bs[labels == j]
if len(mem) > 0:
centroids[j] = mem.mean(axis=0)
else:
centroids[j] = bs[rng.integers(0, n)]
codebooks.append(centroids.astype(np.float32))
return codebooks
def prepare_flat_codebooks(codebooks: List[np.ndarray]) -> Tuple[np.ndarray, np.ndarray]:
blocks = len(codebooks)
k = codebooks[0].shape[0]
block_len = codebooks[0].shape[1]
flat = np.empty((blocks, k, block_len), dtype=np.float32)
for i, cb in enumerate(codebooks):
flat[i, :cb.shape[0], :] = cb
return flat
@njit(parallel=True, fastmath=True)
def batched_quantize(vectors: np.ndarray, flat_centroids: np.ndarray) -> np.ndarray:
B, D = vectors.shape
blocks, k, block_len = flat_centroids.shape
out = np.empty((B, blocks), dtype=np.uint8)
for bi in prange(B):
vec = vectors[bi]
for blk in range(blocks):
start = blk * block_len
best_j = 0
best_d = 0.0
for j in range(k):
d = 0.0
for t in range(block_len):
diff = vec[start + t] - flat_centroids[blk, j, t]
d += diff * diff
if j == 0 or d < best_d:
best_d = d
best_j = j
out[bi, blk] = best_j
return out
def pack_indices(indices: np.ndarray) -> bytes:
return indices.astype(np.uint8).tobytes()
def make_helper_data(indices: np.ndarray, bch, hmac_key: bytes) -> Tuple[bytes, bytes]:
payload = pack_indices(indices)
ecc = bch.encode(payload)
tag = hmac_sign(hmac_key, payload + ecc)
return ecc, tag
def reconcile(indices_noisy: np.ndarray, ecc: bytes, tag: bytes, bch, hmac_key: bytes) -> Tuple[np.ndarray, bool]:
payload_noisy = pack_indices(indices_noisy)
if not hmac_verify(hmac_key, payload_noisy + ecc, tag):
return indices_noisy, False
try:
pkt = bytearray(payload_noisy) + bytearray(ecc)
decoded, status = bch.decode(pkt)
if status >= 0:
corrected = np.frombuffer(bytes(decoded), dtype=np.uint8)[:indices_noisy.size].copy()
return corrected, True
else:
return indices_noisy, False
except Exception:
return indices_noisy, False
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@dataclass
class HelperBlob:
version: int
flags: int
salt: bytes
iv: bytes
payload: bytes
ecc: bytes
mac: bytes
def to_bytes(self) -> bytes:
parts = []
parts.append(bytes([self.version]))
parts.append(bytes([self.flags]))
parts.append(len(self.salt).to_bytes(1, 'big'))
parts.append(self.salt)
parts.append(len(self.iv).to_bytes(1, 'big'))
parts.append(self.iv)
parts.append(len(self.payload).to_bytes(2, 'big'))
parts.append(self.payload)
parts.append(len(self.ecc).to_bytes(2, 'big'))
parts.append(self.ecc)
parts.append(len(self.mac).to_bytes(2, 'big'))
parts.append(self.mac)
return b"".join(parts)
@staticmethod
def from_bytes(b: bytes):
i = 0
version = b[i]; i += 1
flags = b[i]; i += 1
ls = b[i]; i += 1
salt = b[i:i+ls]; i += ls
li = b[i]; i += 1
iv = b[i:i+li]; i += li
lp = int.from_bytes(b[i:i+2], 'big'); i += 2
payload = b[i:i+lp]; i += lp
le = int.from_bytes(b[i:i+2], 'big'); i += 2
ecc = b[i:i+le]; i += le
lm = int.from_bytes(b[i:i+2], 'big'); i += 2
mac = b[i:i+lm]; i += lm
return HelperBlob(version=version, flags=flags, salt=salt, iv=iv, payload=payload, ecc=ecc, mac=mac)
class SecureSketch:
def __init__(self, bch, mac_key: bytes, encrypt_key: bytes = None, hkdf_salt_len: int = 16, derive_len: int = DERIVED_KEY_LEN, version: int = 1):
if mac_key is None or len(mac_key) < 16:
raise ValueError("mac_key must be provided and greater or equal to 16 bytes")
self.bch = bch
self.mac_key = mac_key
self.encrypt_key = encrypt_key
self.hkdf_salt_len = hkdf_salt_len
self.derive_len = derive_len
self.version = version
def _encrypt(self, plaintext: bytes, aad: bytes = b'') -> Tuple[bytes, bytes]:
if self.encrypt_key is None:
return plaintext, b''
iv = secure_random_bytes(12)
algorithm = algorithms.AES(self.encrypt_key)
cipher = Cipher(algorithm, modes.GCM(iv))
enc = cipher.encryptor()
if aad:
enc.authenticate_additional_data(aad)
ct = enc.update(plaintext) + enc.finalize()
tag = enc.tag
return iv + ct, tag
def _decrypt(self, iv_ct: bytes, tag: bytes, aad: bytes = b'') -> bytes:
if self.encrypt_key is None:
return iv_ct
iv = iv_ct[:12]
ct = iv_ct[12:]
algorithm = algorithms.AES(self.encrypt_key)
cipher = Cipher(algorithm, modes.GCM(iv, tag))
dec = cipher.decryptor()
if aad:
dec.authenticate_additional_data(aad)
return dec.update(ct) + dec.finalize()
def enroll(self, indices: np.ndarray, info: bytes = b'') -> bytes:
if indices.ndim != 1:
raise ValueError("enroll must expect a single index vector")
payload = pack_indices(indices)
salt = secure_random_bytes(self.hkdf_salt_len)
flags = 0
ecc = self.bch.encode(payload)
aad = b"SSv1" + salt
iv_ct, tag_enc = self._encrypt(payload + ecc, aad=aad)
if self.encrypt_key is not None:
flags |= 0x1
stored_payload = iv_ct
stored_ecc = tag_enc
mac_input = stored_payload + stored_ecc + salt + bytes([flags, self.version])
else:
stored_payload = payload
stored_ecc = ecc
mac_input = stored_payload + stored_ecc + salt + bytes([flags, self.version])
mac = hmac_sign(self.mac_key, mac_input)
blob = HelperBlob(version=self.version, flags=flags, salt=salt, iv=b'' if self.encrypt_key is None else iv_ct[:12], payload=stored_payload, ecc=stored_ecc, mac=mac)
return blob.to_bytes()
def verify(self, indices_noisy: np.ndarray, helper_blob_bytes: bytes, info: bytes = b'') -> Tuple[bool, bytes]:
blob = HelperBlob.from_bytes(helper_blob_bytes)
flags = blob.flags
mac_input = blob.payload + blob.ecc + blob.salt + bytes([flags, blob.version])
if not hmac_verify(self.mac_key, mac_input, blob.mac):
return False, b''
if flags & 0x1:
if self.encrypt_key is None:
return False, b''
aad = b"SSv1" + blob.salt
try:
pt = self._decrypt(blob.payload, blob.ecc, aad=aad)
except Exception:
return False, b''
payload_len = indices_noisy.size
payload = pt[:payload_len]
ecc = pt[payload_len:]
else:
payload = blob.payload
ecc = blob.ecc
try:
pkt = bytearray(payload) + bytearray(ecc)
decoded, status = self.bch.decode(pkt)
if status < 0:
return False, b''
ref_indices = np.frombuffer(bytes(decoded), dtype=np.uint8)[:indices_noisy.size].copy()
packed = pack_indices(ref_indices)
salt = blob.salt
prk = hkdf(packed, salt, info, length=self.derive_len)
return True, prk
except Exception:
return False, b''
def enroll_blob_from_indices(self, indices: np.ndarray) -> bytes:
return self.enroll(indices)
def verify_blob_with_noisy(self, indices_noisy: np.ndarray, helper_blob_bytes: bytes) -> Tuple[bool, bytes]:
return self.verify(indices_noisy, helper_blob_bytes)
@dataclass
class PacketResult:
pkt_id: int
success: bool
success_blocks: int
total_blocks: int
GLOBAL = {}
def worker_init(state):
GLOBAL['flat_centroids'] = state['flat_centroids']
GLOBAL['bch'] = state['bch']
GLOBAL['hmac_key'] = state['hmac_key']
GLOBAL['session_salt'] = state['session_salt']
encrypt_key = state.get('encrypt_key', None)
GLOBAL['secsketch'] = SecureSketch(state['bch'], state['hmac_key'], encrypt_key=encrypt_key)
def worker_task_batch(args):
tasks = args
n = len(tasks)
rngs = [np.random.default_rng(seed) for (_, seed) in tasks]
bases = np.empty((n, SPECTRUM_LEN), dtype=np.float32)
noised = np.empty((n, SPECTRUM_LEN), dtype=np.float32)
for i, ((pkt_id, seed), rng) in enumerate(zip(tasks, rngs)):
base = generate_spectrum(rng=rng)
noisy = base + NOISE_STD * rng.standard_normal(base.shape).astype(np.float32)
noisy = np.clip(noisy, 0.0, 1.0)
bases[i] = base
noised[i] = noisy
flat = GLOBAL['flat_centroids']
idx_ref = batched_quantize(bases, flat)
idx_noise = batched_quantize(noised, flat)
results = []
bch = GLOBAL['bch']
hmac_key = GLOBAL['hmac_key']
secsketch = GLOBAL.get('secsketch', None)
for i, (pkt_id, _) in enumerate(tasks):
helper_blob = secsketch.enroll_blob_from_indices(idx_ref[i])
ok, derived_key = secsketch.verify_blob_with_noisy(idx_noise[i], helper_blob)
success_blocks = 0
if ok:
blob = HelperBlob.from_bytes(helper_blob)
if blob.flags & 0x1:
aad = b"SSv1" + blob.salt
pt = secsketch._decrypt(blob.payload, blob.ecc, aad=aad)
payload_len = idx_noise[i].size
payload = pt[:payload_len]
ecc = pt[payload_len:]
else:
payload = blob.payload
ref_indices = np.frombuffer(payload, dtype=np.uint8)[:idx_noise[i].size].copy()
success_blocks = int(np.sum(ref_indices == idx_noise[i]))
else:
success_blocks = int(np.sum(idx_ref[i] == idx_noise[i]))
results.append(PacketResult(pkt_id=pkt_id, success=ok, success_blocks=success_blocks, total_blocks=idx_ref.shape[1]))
return results
def run_throughput(packets=PACKETS, batch_size=BATCH_SIZE, workers=NUM_WORKERS, use_encryption=False):
samples = np.stack([generate_spectrum(rng=rng_expt) for _ in range(2000)], axis=0)
codebooks = build_codebooks(samples, blocks=BLOCKS, k=CODEBOOK_PER_BLOCK, iters=12)
flat_centroids = prepare_flat_codebooks(codebooks)
bch = make_bch(BLOCKS)
hmac_key = secure_random_bytes(HMAC_KEY_LEN)
session_salt = secure_random_bytes(16)
encrypt_key = secure_random_bytes(32) if use_encryption else None
state = {'flat_centroids': flat_centroids, 'bch': bch, 'hmac_key': hmac_key, 'session_salt': session_salt, 'encrypt_key': encrypt_key}
tasks = [(i, EXPT_SEED + i * 31) for i in range(packets)]
batched = [tasks[i:i+batch_size] for i in range(0, len(tasks), batch_size)]
ctx = mp.get_context('spawn')
t0 = time.time()
results_all = []
with ctx.Pool(processes=workers, initializer=worker_init, initargs=(state,)) as pool:
for batch in batched:
res = pool.apply(worker_task_batch, (batch,))
for r in res:
results_all.append(r)
dt = time.time() - t0
avg_blocks = sum(r.success_blocks for r in results_all) / len(results_all)
full_success = sum(1 for r in results_all if r.success and r.success_blocks == BLOCKS) / len(results_all)
print(f"Packets={len(results_all)} Time={dt:.2f}s Throughput={len(results_all)/dt:.1f} pkt/s AvgBlocks={avg_blocks:.2f} FullSuccess={full_success:.3f}")
def frr_far_sweep(noise_levels=[0.02,0.05,0.08,0.12], trials=512, use_encryption=False):
samples = np.stack([generate_spectrum(rng=rng_expt) for _ in range(3000)], axis=0)
codebooks = build_codebooks(samples, blocks=BLOCKS, k=CODEBOOK_PER_BLOCK, iters=12)
flat_centroids = prepare_flat_codebooks(codebooks)
bch = make_bch(BLOCKS)
hmac_key = secure_random_bytes(HMAC_KEY_LEN)
encrypt_key = secure_random_bytes(32) if use_encryption else None
secsketch = SecureSketch(bch, hmac_key, encrypt_key=encrypt_key)
def quant_and_recon_pair(base, noise_std, rng):
noisy = base + noise_std * rng.standard_normal(base.shape).astype(np.float32)
noisy = np.clip(noisy, 0.0, 1.0)
idx_ref = batched_quantize(base.reshape(1, -1), flat_centroids)[0]
idx_noisy = batched_quantize(noisy.reshape(1, -1), flat_centroids)[0]
helper = secsketch.enroll_blob_from_indices(idx_ref)
ok, key = secsketch.verify_blob_with_noisy(idx_noisy, helper)
return ok, int(np.sum(idx_ref == idx_noisy))
rng = np.random.default_rng(EXPT_SEED)
for ns in noise_levels:
succ = 0
full = 0
for _ in range(trials):
base = generate_spectrum(rng=rng)
ok, sb = quant_and_recon_pair(base, ns, rng)
if ok and sb == BLOCKS:
succ += 1
if sb == BLOCKS:
full += 1
frr = 1.0 - (succ / trials)
false_accepts = 0
for _ in range(trials):
ref = generate_spectrum(rng=rng)
other = generate_spectrum(rng=rng)
idx_ref = batched_quantize(ref.reshape(1, -1), flat_centroids)[0]
helper = secsketch.enroll_blob_from_indices(idx_ref)
idx_other = batched_quantize(other.reshape(1, -1), flat_centroids)[0]
ok, key = secsketch.verify_blob_with_noisy(idx_other, helper)
if ok and np.sum(idx_other == idx_ref) == BLOCKS:
false_accepts += 1
far = false_accepts / trials
print(f"noise={ns:.3f} FRR={frr:.3f} FAR={far:.6f}")
@lastforkbender
Copy link
Author

lastforkbender commented Jan 19, 2026

psv2026C; includes the AES slice thru error-chain char graphing yet specific BCH builds not shown

CC = gcc
CFLAGS = -O2 -march=native -std=c11 -pthread -Wall -Wextra
LDFLAGS = -lsodium
TARGET = packet_sim
SRCS = packet_sim.c
all: $(TARGET)
$(TARGET): $(SRCS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
	rm -f $(TARGET)
#define _GNU_SOURCE
#include <sodium.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <inttypes.h>
#include <time.h>
#include <stdatomic.h>

#define MAX_HOPS 8
#define TABLE_ROWS 16384
#define VECTOR_R 4
#define WORDS_PER_VEC VECTOR_R
#define WORD_BYTES 16
#define TABLE_ROW_BYTES (WORDS_PER_VEC * WORD_BYTES)
#define CODEBOOK_SYMBOL_BITS 12
#define CODEBOOK_SIZE (1u << CODEBOOK_SYMBOL_BITS)
#define ENCODED_BLOCK_BYTES 32
#define SKETCH_BYTES 16
#define AES_KEY_BYTES 32
#define AEAD_NONCE_BYTES 12
#define AEAD_TAG_BYTES crypto_aead_chacha20poly1305_IETF_ABYTES
#define SEQNUM_BYTES 8
#define FLOWID_BYTES 4
#define HOPIDX_BYTES 4
#define FLAGS_BYTES 1
#define ERROR_BLOCK_BYTES ENCODED_BLOCK_BYTES
#define SKETCH_FIELD_BYTES SKETCH_BYTES
#define PAYLOAD_MAX 256
#define LAYER_OVERHEAD (SEQNUM_BYTES+FLOWID_BYTES+HOPIDX_BYTES+FLAGS_BYTES+AEAD_NONCE_BYTES+ERROR_BLOCK_BYTES+SKETCH_FIELD_BYTES+AEAD_TAG_BYTES)
#define MAX_PACKET_BYTES (PAYLOAD_MAX + LAYER_OVERHEAD*MAX_HOPS + 128)
#define N_WORKERS 4
#define QUEUE_SIZE 1024

typedef struct {
    uint8_t *data;
    size_t len;
} buffer_t;

static uint8_t *TABLE = NULL;
static uint8_t Vchars[MAX_HOPS];
static uint8_t link_keys[MAX_HOPS+1][AES_KEY_BYTES];

static void codebook_encode(uint16_t symbol, uint8_t out[ENCODED_BLOCK_BYTES]) {
    uint8_t buf[2];
    buf[0] = symbol & 0xff;
    buf[1] = (symbol >> 8) & 0xff;
    crypto_generichash(out, ENCODED_BLOCK_BYTES, buf, sizeof(buf), (const unsigned char *)"CODEBOOK", 8);
}

static void sketch_generate(const uint8_t prf_key[32], uint16_t symbol, const uint8_t nonce[SKETCH_FIELD_BYTES], uint8_t W[SKETCH_BYTES]) {
    uint8_t m[2 + SKETCH_FIELD_BYTES];
    m[0] = symbol & 0xff;
    m[1] = (symbol >> 8) & 0xff;
    memcpy(m+2, nonce, SKETCH_FIELD_BYTES);
    crypto_generichash(W, SKETCH_BYTES, m, sizeof(m), prf_key, 32);
}
static int sketch_recover(const uint8_t prf_key[32], uint16_t *out_symbol, const uint8_t nonce[SKETCH_FIELD_BYTES], const uint8_t W[SKETCH_BYTES]) {
    uint8_t candidateW[SKETCH_BYTES];
    for (uint32_t s=0; s < CODEBOOK_SIZE; ++s) {
        uint8_t m[2 + SKETCH_FIELD_BYTES];
        m[0] = s & 0xff;
        m[1] = (s >> 8) & 0xff;
        memcpy(m+2, nonce, SKETCH_FIELD_BYTES);
        crypto_generichash(candidateW, SKETCH_BYTES, m, sizeof(m), prf_key, 32);
        if (sodium_memcmp(candidateW, W, SKETCH_BYTES) == 0) {
            *out_symbol = (uint16_t)s;
            return 0;
        }
    }
    return -1;
}

static void hkdf_expand(const uint8_t *prk, size_t prk_len, const uint8_t *info, size_t info_len, uint8_t *out, size_t outlen) {
    crypto_generichash_state st;
    uint8_t t[crypto_generichash_BYTES];
    crypto_generichash_init(&st, NULL, 0, crypto_generichash_BYTES);
    if (info && info_len) crypto_generichash_update(&st, info, info_len);
    crypto_generichash_update(&st, prk, prk_len);
    crypto_generichash_final(&st, t, crypto_generichash_BYTES);
    size_t copied = 0;
    uint8_t ctr = 1;
    while (copied < outlen) {
        crypto_generichash_state st2;
        crypto_generichash_init(&st2, NULL, 0, crypto_generichash_BYTES);
        crypto_generichash_update(&st2, t, crypto_generichash_BYTES);
        crypto_generichash_update(&st2, &ctr, 1);
        if (info && info_len) crypto_generichash_update(&st2, info, info_len);
        uint8_t block[crypto_generichash_BYTES];
        crypto_generichash_final(&st2, block, crypto_generichash_BYTES);
        size_t take = (outlen - copied) < crypto_generichash_BYTES ? (outlen - copied) : crypto_generichash_BYTES;
        memcpy(out + copied, block, take);
        copied += take;
        ctr++;
    }
    sodium_memzero(t, sizeof t);
}

static void table_vector_read_fold(uint32_t idx, uint8_t out32[32]) {
    uint32_t row = idx & (TABLE_ROWS - 1);
    uint8_t *rowptr = TABLE + ((size_t)row * TABLE_ROW_BYTES);
    uint8_t tmp[32];
    memset(tmp, 0, sizeof tmp);
    for (size_t i=0;i<WORDS_PER_VEC;i++) {
        uint8_t *w = rowptr + i*WORD_BYTES;
        for (size_t j=0;j<16;j++) tmp[j] ^= w[j];
        uint8_t rot = (i*3) & 0xf;
        for (size_t j=0;j<16;j++) {
            uint8_t val = w[j];
            size_t dst = (j + rot) & 0xf;
            tmp[16 + dst] ^= val;
        }
    }
    memcpy(out32, tmp, 32);
    sodium_memzero(tmp, sizeof tmp);
}

static void derive_splice_raw(const uint8_t SK_end[32], uint64_t seqnum, uint32_t flowid, uint8_t vchar, uint8_t out32[32]) {
    uint8_t info[8+4+1];
    memcpy(info, &seqnum, 8);
    memcpy(info+8, &flowid, 4);
    info[12] = vchar;
    uint8_t prk[32];
    crypto_generichash(prk, 32, SK_end, 32, (const unsigned char *)"splice", 6);
    uint8_t idxhash[32];
    crypto_generichash(idxhash, 32, prk, 32, info, sizeof info);
    uint32_t idx = ((uint32_t)idxhash[0] << 24) | ((uint32_t)idxhash[1] << 16) | ((uint32_t)idxhash[2] << 8) | idxhash[3];
    idx = idx & (TABLE_ROWS - 1);
    table_vector_read_fold(idx, out32);
    for (int i=0;i<32;i++) out32[i] ^= prk[i];
    sodium_memzero(prk, sizeof prk);
    sodium_memzero(idxhash, sizeof idxhash);
}

static void derive_aes_key_from_raw(const uint8_t raw32[32], uint32_t hopidx, uint32_t flowid, uint8_t outkey[32]) {
    uint8_t info[8];
    memcpy(info, &hopidx, 4);
    memcpy(info+4, &flowid, 4);
    hkdf_expand(raw32, 32, info, sizeof info, outkey, 32);
}

static void derive_nonce(uint64_t seqnum, uint32_t hopidx, uint8_t nonce[AEAD_NONCE_BYTES]) {
    uint8_t inp[12];
    memcpy(inp, &seqnum, 8);
    memcpy(inp+8, &hopidx, 4);
    uint8_t out[crypto_generichash_BYTES];
    crypto_generichash(out, crypto_generichash_BYTES, inp, sizeof inp, (const unsigned char *)"nonce", 5);
    memcpy(nonce, out, AEAD_NONCE_BYTES);
    sodium_memzero(out, sizeof out);
}

static int aead_encrypt(const uint8_t key[32], const uint8_t *ad, size_t adlen, const uint8_t *pt, size_t ptlen,
                        const uint8_t nonce[AEAD_NONCE_BYTES], uint8_t *ct, size_t *ctlen) {
    unsigned long long clen = 0;
    if (crypto_aead_chacha20poly1305_ietf_encrypt(ct, &clen, pt, ptlen, ad, adlen, NULL, nonce, key) != 0) return -1;
    *ctlen = (size_t)clen;
    return 0;
}
static int aead_decrypt(const uint8_t key[32], const uint8_t *ad, size_t adlen, const uint8_t *ct, size_t ctlen,
                        const uint8_t nonce[AEAD_NONCE_BYTES], uint8_t *pt, size_t *ptlen) {
    unsigned long long plen = 0;
    if (crypto_aead_chacha20poly1305_ietf_decrypt(pt, &plen, NULL, ct, ctlen, ad, adlen, nonce, key) != 0) return -1;
    *ptlen = (size_t)plen;
    return 0;
}

typedef struct {
    uint64_t seqnum;
    uint32_t flowid;
    uint8_t flags;
    uint32_t hopidx;
    uint8_t nonce[AEAD_NONCE_BYTES];
    uint8_t error_block[ERROR_BLOCK_bytes ? ERROR_BLOCK_BYTES : ENCODED_BLOCK_BYTES];
    uint8_t sketch[SKETCH_FIELD_BYTES];
    uint8_t payload[PAYLOAD_MAX];
    size_t payload_len;
} layer_plain_t;

static int build_outermost_packet(uint8_t *outbuf, size_t *outlen, const uint8_t SK_end[32], uint64_t seqnum,
                                  uint32_t flowid, const uint8_t *app_payload, size_t app_payload_len,
                                  int n_hops) {
    uint8_t inner[MAX_PACKET_BYTES];
    size_t inner_len = app_payload_len;
    if (inner_len > PAYLOAD_MAX) return -1;
    memcpy(inner, app_payload, inner_len);
    for (int hop = n_hops - 1; hop >= 0; --hop) {
        uint8_t raw[32];
        derive_splice_raw(SK_end, seqnum, flowid, Vchars[hop % MAX_HOPS], raw);
        uint8_t aeskey[32];
        derive_aes_key_from_raw(raw, (uint32_t)hop, flowid, aeskey);
        uint8_t plain[LAYER_OVERHEAD + inner_len + 16];
        size_t off = 0;
        memcpy(plain + off, &seqnum, SEQNUM_BYTES); off += SEQNUM_BYTES;
        memcpy(plain + off, &flowid, FLOWID_BYTES); off += FLOWID_BYTES;
        uint8_t flags = 0;
        memcpy(plain + off, &flags, FLAGS_BYTES); off += FLAGS_BYTES;
        uint32_t hopidx = (uint32_t)hop;
        memcpy(plain + off, &hopidx, HOPIDX_BYTES); off += HOPIDX_BYTES;
        uint8_t nonce[AEAD_NONCE_BYTES];
        derive_nonce(seqnum, hopidx, nonce);
        memcpy(plain + off, nonce, AEAD_NONCE_BYTES); off += AEAD_NONCE_BYTES;
        uint8_t encblk[ENCODED_BLOCK_BYTES];
        codebook_encode(0, encblk);
        uint8_t mask[ENCODED_BLOCK_BYTES];
        crypto_generichash(mask, ENCODED_BLOCK_BYTES, (const unsigned char *)&hopidx, sizeof hopidx, link_keys[hop], AES_KEY_BYTES);
        for (size_t b=0;b<ENCODED_BLOCK_BYTES;b++) plain[off + b] = encblk[b] ^ mask[b];
        off += ENCODED_BLOCK_BYTES;
        uint8_t sketch[SKETCH_FIELD_BYTES];
        memset(sketch, 0, SKETCH_FIELD_BYTES);
        memcpy(plain + off, sketch, SKETCH_FIELD_BYTES); off += SKETCH_FIELD_BYTES;
        memcpy(plain + off, inner, inner_len); off += inner_len;
        uint8_t ad[SEQNUM_BYTES+FLOWID_BYTES+HOPIDX_BYTES];
        memcpy(ad, &seqnum, SEQNUM_BYTES);
        memcpy(ad+SEQNUM_BYTES, &flowid, FLOWID_BYTES);
        memcpy(ad+SEQNUM_BYTES+FLOWID_BYTES, &hopidx, HOPIDX_BYTES);
        uint8_t ct[MAX_PACKET_BYTES];
        size_t ctlen = 0;
        if (aead_encrypt(aeskey, ad, sizeof ad, plain, off, nonce, ct, &ctlen) != 0) {
            sodium_memzero(raw, sizeof raw);
            sodium_memzero(aeskey, sizeof aeskey);
            return -1;
        }
        inner_len = ctlen;
        memcpy(inner, ct, inner_len);
        sodium_memzero(raw, sizeof raw);
        sodium_memzero(aeskey, sizeof aeskey);
    }
    memcpy(outbuf, inner, inner_len);
    *outlen = inner_len;
    return 0;
}

static int process_hop_buffer(uint8_t *pktbuf_in, size_t pktlen_in, uint8_t *pktbuf_out, size_t *pktlen_out,
                              int hop_index, int n_hops, int inject_error) {
    uint8_t key[32];
    memcpy(key, link_keys[hop_index], 32);
    uint8_t pt[MAX_PACKET_BYTES];
    size_t ptlen = 0;
    int success = 0;
    size_t used_ptlen = 0;
    uint64_t seqnum = 0;
    uint32_t flowid = 0;
    uint32_t hopidx = 0;
    uint8_t nonce[AEAD_NONCE_BYTES];
    memset(nonce, 0, AEAD_NONCE_BYTES);
    if (aead_decrypt(key, NULL, 0, pktbuf_in, pktlen_in, nonce, pt, &ptlen) == 0) {
        success = 1;
        used_ptlen = ptlen;
    } else {
        success = 0;
    }
    uint8_t out_inner[MAX_PACKET_BYTES];
    size_t out_inner_len = 0;
    if (success) {
        size_t off = 0;
        if (used_ptlen < (SEQNUM_BYTES+FLOWID_BYTES+FLAGS_BYTES+HOPIDX_BYTES+AEAD_NONCE_BYTES+ERROR_BLOCK_BYTES+SKETCH_FIELD_BYTES)) {
            success = 0;
        } else {
            memcpy(&seqnum, pt + off, SEQNUM_BYTES); off += SEQNUM_BYTES;
            memcpy(&flowid, pt + off, FLOWID_BYTES); off += FLOWID_BYTES;
            uint8_t flags = pt[off]; off += FLAGS_BYTES;
            memcpy(&hopidx, pt + off, HOPIDX_BYTES); off += HOPIDX_BYTES;
            memcpy(nonce, pt + off, AEAD_NONCE_BYTES); off += AEAD_NONCE_BYTES;
            uint8_t encblk[ENCODED_BLOCK_BYTES];
            memcpy(encblk, pt + off, ERROR_BLOCK_BYTES); off += ERROR_BLOCK_BYTES;
            uint8_t sketch[SKETCH_FIELD_BYTES];
            memcpy(sketch, pt + off, SKETCH_FIELD_BYTES); off += SKETCH_FIELD_BYTES;
            size_t inner_payload_len = used_ptlen - off;
            if (inner_payload_len > 0) memcpy(out_inner, pt + off, inner_payload_len);
            out_inner_len = inner_payload_len;
            int detected_failure = 0;
            if (inject_error && hop_index == (int)hopidx) detected_failure = 1;
            if (detected_failure) {
                uint16_t symbol = (uint16_t)hopidx;
                uint8_t enc[ENCODED_BLOCK_BYTES];
                codebook_encode(symbol, enc);
                uint8_t mask[ENCODED_BLOCK_BYTES];
                crypto_generichash(mask, ENCODED_BLOCK_BYTES, (const unsigned char *)&hopidx, sizeof hopidx, link_keys[hop_index], AES_KEY_BYTES);
                for (size_t b=0;b<ENCODED_BLOCK_BYTES;b++) enc[b] ^= mask[b];
                uint8_t new_plain[LAYER_OVERHEAD + out_inner_len];
                size_t noff = 0;
                memcpy(new_plain + noff, &seqnum, SEQNUM_BYTES); noff += SEQNUM_BYTES;
                memcpy(new_plain + noff, &flowid, FLOWID_BYTES); noff += FLOWID_BYTES;
                uint8_t newflags = 1;
                memcpy(new_plain + noff, &newflags, FLAGS_BYTES); noff += FLAGS_BYTES;
                uint32_t newhopidx = hopidx + 1;
                memcpy(new_plain + noff, &newhopidx, HOPIDX_BYTES); noff += HOPIDX_BYTES;
                uint8_t newnonce[AEAD_NONCE_BYTES];
                derive_nonce(seqnum, newhopidx, newnonce);
                memcpy(new_plain + noff, newnonce, AEAD_NONCE_BYTES); noff += AEAD_NONCE_BYTES;
                memcpy(new_plain + noff, enc, ENCODED_BLOCK_BYTES); noff += ENCODED_BLOCK_BYTES;
                uint8_t prf_key[32];
                crypto_generichash(prf_key, 32, link_keys[hop_index], AES_KEY_BYTES, (const unsigned char *)"SKETCHPRF", 8);
                uint8_t sketchW[SKETCH_FIELD_BYTES];
                uint8_t sketchnonce[SKETCH_FIELD_BYTES];
                randombytes_buf(sketchnonce, SKETCH_FIELD_BYTES);
                sketch_generate(prf_key, symbol, sketchnonce, sketchW);
                uint8_t sketchfield[SKETCH_FIELD_BYTES];
                for (size_t b=0;b<SKETCH_FIELD_BYTES;b++) sketchfield[b] = (b < SKETCH_FIELD_BYTES ? sketchnonce[b] : 0) ^ sketchW[b];
                memcpy(new_plain + noff, sketchfield, SKETCH_FIELD_BYTES); noff += SKETCH_FIELD_BYTES;
                memcpy(new_plain + noff, out_inner, out_inner_len); noff += out_inner_len;
                uint8_t next_key[32];
                memcpy(next_key, link_keys[hop_index+1], 32);
                uint8_t ad[SEQNUM_BYTES+FLOWID_BYTES+HOPIDX_BYTES];
                memcpy(ad, &seqnum, SEQNUM_BYTES);
                memcpy(ad+SEQNUM_BYTES, &flowid, FLOWID_BYTES);
                memcpy(ad+SEQNUM_BYTES+FLOWID_BYTES, &newhopidx, HOPIDX_BYTES);
                uint8_t outct[MAX_PACKET_BYTES];
                size_t outctlen = 0;
                if (aead_encrypt(next_key, ad, sizeof ad, new_plain, noff, newnonce, outct, &outctlen) != 0) {
                    sodium_memzero(prf_key, sizeof prf_key);
                    return -1;
                }
                memcpy(pktbuf_out, outct, outctlen);
                *pktlen_out = outctlen;
                sodium_memzero(prf_key, sizeof prf_key);
                sodium_memzero(next_key, sizeof next_key);
                return 0;
            } else {
                uint8_t new_plain[LAYER_OVERHEAD + out_inner_len];
                size_t noff = 0;
                memcpy(new_plain + noff, &seqnum, SEQNUM_BYTES); noff += SEQNUM_BYTES;
                memcpy(new_plain + noff, &flowid, FLOWID_BYTES); noff += FLOWID_BYTES;
                uint8_t newflags = 0;
                memcpy(new_plain + noff, &newflags, FLAGS_BYTES); noff += FLAGS_BYTES;
                uint32_t newhopidx = hopidx + 1;
                memcpy(new_plain + noff, &newhopidx, HOPIDX_BYTES); noff += HOPIDX_BYTES;
                uint8_t newnonce[AEAD_NONCE_BYTES];
                derive_nonce(seqnum, newhopidx, newnonce);
                memcpy(new_plain + noff, newnonce, AEAD_NONCE_BYTES); noff += AEAD_NONCE_BYTES;
                uint8_t encblk_null[ENCODED_BLOCK_BYTES];
                codebook_encode(0, encblk_null);
                uint8_t mask[ENCODED_BLOCK_BYTES];
                crypto_generichash(mask, ENCODED_BLOCK_BYTES, (const unsigned char *)&newhopidx, sizeof newhopidx, link_keys[hop_index+1], AES_KEY_BYTES);
                for (size_t b=0;b<ENCODED_BLOCK_BYTES;b++) new_plain[noff + b] = encblk_null[b] ^ mask[b];
                noff += ENCODED_BLOCK_BYTES;
                uint8_t sketchfield[SKETCH_FIELD_BYTES];
                memset(sketchfield, 0, SKETCH_FIELD_BYTES);
                memcpy(new_plain + noff, sketchfield, SKETCH_FIELD_BYTES); noff += SKETCH_FIELD_BYTES;
                memcpy(new_plain + noff, out_inner, out_inner_len); noff += out_inner_len;
                uint8_t next_key[32];
                memcpy(next_key, link_keys[hop_index+1], 32);
                uint8_t ad[SEQNUM_BYTES+FLOWID_BYTES+HOPIDX_BYTES];
                memcpy(ad, &seqnum, SEQNUM_BYTES);
                memcpy(ad+SEQNUM_BYTES, &flowid, FLOWID_BYTES);
                memcpy(ad+SEQNUM_BYTES+FLOWID_BYTES, &newhopidx, HOPIDX_BYTES);
                uint8_t outct[MAX_PACKET_BYTES];
                size_t outctlen = 0;
                if (aead_encrypt(next_key, ad, sizeof ad, new_plain, noff, newnonce, outct, &outctlen) != 0) {
                    sodium_memzero(next_key, sizeof next_key);
                    return -1;
                }
                memcpy(pktbuf_out, outct, outctlen);
                *pktlen_out = outctlen;
                sodium_memzero(next_key, sizeof next_key);
                return 0;
            }
        }
    }
    uint64_t fake_seq = 0;
    uint32_t fake_flow = 0;
    uint32_t newhopidx = hop_index + 1;
    uint8_t newnonce[AEAD_NONCE_BYTES];
    derive_nonce(fake_seq, newhopidx, newnonce);
    uint8_t new_plain[LAYER_OVERHEAD + 0];
    size_t noff = 0;
    memcpy(new_plain + noff, &fake_seq, SEQNUM_BYTES); noff += SEQNUM_BYTES;
    memcpy(new_plain + noff, &fake_flow, FLOWID_BYTES); noff += FLOWID_BYTES;
    uint8_t newflags = 1;
    memcpy(new_plain + noff, &newflags, FLAGS_BYTES); noff += FLAGS_BYTES;
    memcpy(new_plain + noff, &newhopidx, HOPIDX_BYTES); noff += HOPIDX_BYTES;
    memcpy(new_plain + noff, newnonce, AEAD_NONCE_BYTES); noff += AEAD_NONCE_BYTES;
    uint8_t encblk[ENCODED_BLOCK_BYTES];
    codebook_encode((uint16_t)hop_index, encblk);
    uint8_t mask[ENCODED_BLOCK_BYTES];
    crypto_generichash(mask, ENCODED_BLOCK_BYTES, (const unsigned char *)&hop_index, sizeof hop_index, link_keys[hop_index], AES_KEY_BYTES);
    for (size_t b=0;b<ENCODED_BLOCK_BYTES;b++) new_plain[noff + b] = encblk[b] ^ mask[b];
    noff += ENCODED_BLOCK_BYTES;
    uint8_t sketchfield[SKETCH_FIELD_BYTES];
    memset(sketchfield, 0, SKETCH_FIELD_BYTES);
    memcpy(new_plain + noff, sketchfield, SKETCH_FIELD_BYTES); noff += SKETCH_FIELD_BYTES;
    uint8_t next_key[32];
    memcpy(next_key, link_keys[hop_index+1], 32);
    uint8_t ad[SEQNUM_BYTES+FLOWID_BYTES+HOPIDX_BYTES];
    memcpy(ad, &fake_seq, SEQNUM_BYTES);
    memcpy(ad+SEQNUM_BYTES, &fake_flow, FLOWID_BYTES);
    memcpy(ad+SEQNUM_BYTES+FLOWID_BYTES, &newhopidx, HOPIDX_BYTES);
    uint8_t outct[MAX_PACKET_BYTES];
    size_t outctlen = 0;
    if (aead_encrypt(next_key, ad, sizeof ad, new_plain, noff, newnonce, outct, &outctlen) != 0) {
        sodium_memzero(next_key, sizeof next_key);
        return -1;
    }
    memcpy(pktbuf_out, outct, outctlen);
    *pktlen_out = outctlen;
    sodium_memzero(next_key, sizeof next_key);
    return 0;
}

/* Example multi-hops demo use; display of psv2026C buffer stats */
int main(int argc, char **argv) {
    if (sodium_init() < 0) {
        fprintf(stderr, "libsodium init failed\n");
        return 1;}
    TABLE = sodium_allocarray(TABLE_ROWS, TABLE_ROW_BYTES);
    if (!TABLE) { fprintf(stderr, "table alloc fail\n"); return 1; }
    randombytes_buf(TABLE, (size_t)TABLE_ROWS * TABLE_ROW_BYTES);
    for (int i=0;i<MAX_HOPS;i++) Vchars[i] = (uint8_t)(i*31 + 7);
    for (int i=0;i<=MAX_HOPS;i++) randombytes_buf(link_keys[i], AES_KEY_BYTES);
    uint8_t SK_end[32];
    randombytes_buf(SK_end, 32);
    const char *msg = "psv2026C________________________________";
    uint8_t outpkt[MAX_PACKET_BYTES];
    size_t outpktlen = 0;
    if (build_outermost_packet(outpkt, &outpktlen, SK_end, 1ULL, 0xAABBCCDD, (const uint8_t *)msg, strlen(msg), MAX_HOPS) != 0) {
        fprintf(stderr, "failed to build packet\n");
        return 1;
    }
    printf("built outer packet size %zu bytes\n", outpktlen);
    uint8_t buf_in[MAX_PACKET_BYTES];
    uint8_t buf_out[MAX_PACKET_BYTES];
    size_t buflen = outpktlen;
    memcpy(buf_in, outpkt, buflen);
    for (int hop = 0; hop < MAX_HOPS; ++hop) {
        size_t outlen = 0;
        int inject = (hop == 3) ? 1 : 0;
        if (process_hop_buffer(buf_in, buflen, buf_out, &outlen, hop, MAX_HOPS, inject) != 0) {
            fprintf(stderr, "hop %d processing failed\n", hop);
            return 1;
        }
        printf("hop %d processed -> %zu bytes (inject=%d)\n", hop, outlen, inject);
        memcpy(buf_in, buf_out, outlen);
        buflen = outlen;
    }
    printf("packet arrived at receiver, size %zu\n", buflen);
    sodium_free(TABLE);
    sodium_memzero(SK_end, sizeof SK_end);
    return 0;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment