extern crate alloc;
use alloc::vec::Vec;
use sha3::{
digest::{ExtendableOutput, XofReader},
Digest, Sha3_256, Shake256,
};
use zeroize::Zeroize;
use crate::identity_error::IdentityError;
const HYPERCUBE_DIM: usize = 5;
const NUM_NODES: usize = 1 << HYPERCUBE_DIM; const THRESHOLD_K: usize = 3; const MODULUS_Q: u64 = 8380417;
const TOTAL_SHARES: usize = 5;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct NodeAddress(pub u8);
impl NodeAddress {
pub fn neighbor(&self, dim: usize) -> Self {
NodeAddress(self.0 ^ (1 << dim))
}
pub fn hamming_distance(&self, other: &Self) -> usize {
(self.0 ^ other.0).count_ones() as usize
}
}
#[derive(Clone, Debug)]
pub struct ZkProofSegment {
pub share_id: u8,
pub share_val: u64,
pub path_tag: [u8; 32],
}
#[derive(Clone, Debug)]
pub struct HypercubePacket {
pub source: NodeAddress,
pub destination: NodeAddress,
pub current_node: NodeAddress,
pub dimension_route: Vec<usize>,
pub route_index: usize,
pub payload: ZkProofSegment,
}
pub struct SecretSharer;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HtssShare {
pub index: u8,
pub value: Vec<u8>,
pub path: Vec<u8>,
}
const LIMB_BYTES: usize = 2;
const LIMB_EVAL_BYTES: usize = 4;
fn merkle_proof_len(index: u8) -> Option<usize> {
match index {
1..=4 => Some(3),
5 => Some(1),
_ => None,
}
}
fn merkle_leaf_hash(index: u8, value: &[u8]) -> [u8; 32] {
let mut hasher = Sha3_256::new();
hasher.update(b"AETHEL_HTSS_MERKLE_LEAF_V1");
hasher.update([index]);
hasher.update((value.len() as u32).to_le_bytes());
hasher.update(value);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
fn merkle_node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha3_256::new();
hasher.update(b"AETHEL_HTSS_MERKLE_NODE_V1");
hasher.update(left);
hasher.update(right);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
fn build_share_tree(leaves: &[[u8; 32]; TOTAL_SHARES]) -> ([u8; 32], [Vec<u8>; TOTAL_SHARES]) {
let (l0, l1, l2, l3, l4) = (leaves[0], leaves[1], leaves[2], leaves[3], leaves[4]);
let n01 = merkle_node_hash(&l0, &l1);
let n23 = merkle_node_hash(&l2, &l3);
let n0123 = merkle_node_hash(&n01, &n23);
let root = merkle_node_hash(&n0123, &l4);
let flat = |chunks: &[[u8; 32]]| -> Vec<u8> {
let mut out = Vec::with_capacity(chunks.len() * 32);
for c in chunks {
out.extend_from_slice(c);
}
out
};
let paths = [
flat(&[l1, n23, l4]), flat(&[l0, n23, l4]), flat(&[l3, n01, l4]), flat(&[l2, n01, l4]), flat(&[n0123]), ];
(root, paths)
}
fn verify_share_in_tree(index: u8, value: &[u8], path: &[u8], root: &[u8; 32]) -> bool {
let Some(expected_siblings) = merkle_proof_len(index) else {
return false;
};
if path.len() != expected_siblings * 32 {
return false;
}
let sibling = |i: usize| -> [u8; 32] {
let mut out = [0u8; 32];
out.copy_from_slice(&path[i * 32..i * 32 + 32]);
out
};
let leaf = merkle_leaf_hash(index, value);
let computed = match index {
1 | 3 => {
let pair = merkle_node_hash(&leaf, &sibling(0));
let quad = if index == 1 {
merkle_node_hash(&pair, &sibling(1))
} else {
merkle_node_hash(&sibling(1), &pair)
};
merkle_node_hash(&quad, &sibling(2))
}
2 | 4 => {
let pair = merkle_node_hash(&sibling(0), &leaf);
let quad = if index == 2 {
merkle_node_hash(&pair, &sibling(1))
} else {
merkle_node_hash(&sibling(1), &pair)
};
merkle_node_hash(&quad, &sibling(2))
}
5 => merkle_node_hash(&sibling(0), &leaf),
_ => return false,
};
let mut mismatch = 0u8;
for i in 0..32 {
mismatch |= computed[i] ^ root[i];
}
mismatch == 0
}
const MAX_SECRET_BYTES: usize = 64 * 1024;
impl SecretSharer {
pub fn split_key_material(
secret: &[u8],
nonce: &[u8],
) -> Result<(Vec<HtssShare>, [u8; 32]), IdentityError> {
if secret.is_empty() || secret.len() > MAX_SECRET_BYTES {
return Err(IdentityError::InvalidInputLength);
}
let mut payload = Vec::with_capacity(4 + secret.len() + 1);
payload.extend_from_slice(&(secret.len() as u32).to_le_bytes());
payload.extend_from_slice(secret);
while payload.len() % LIMB_BYTES != 0 {
payload.push(0);
}
let limb_count = payload.len() / LIMB_BYTES;
let mut shares: Vec<HtssShare> = (1..=TOTAL_SHARES as u8)
.map(|index| HtssShare {
index,
value: Vec::with_capacity(limb_count * LIMB_EVAL_BYTES),
path: Vec::new(), })
.collect();
let mut coeff_key = Self::derive_coeff_key(secret, nonce);
for limb_idx in 0..limb_count {
let lo = payload[limb_idx * LIMB_BYTES] as u64;
let hi = payload[limb_idx * LIMB_BYTES + 1] as u64;
let limb_value = lo | (hi << 8);
let mut coefficients = Vec::with_capacity(THRESHOLD_K);
coefficients.push(limb_value % MODULUS_Q);
for coeff_idx in 1..THRESHOLD_K {
coefficients.push(Self::derive_coeff_from_key(&coeff_key, limb_idx, coeff_idx));
}
for share in shares.iter_mut() {
let mut y = 0u64;
let mut x_pow = 1u64;
for &coeff in &coefficients {
y = (y + coeff * x_pow) % MODULUS_Q;
x_pow = (x_pow * share.index as u64) % MODULUS_Q;
}
share.value.extend_from_slice(&(y as u32).to_le_bytes());
}
coefficients.zeroize();
}
coeff_key.zeroize();
payload.zeroize();
let mut leaves = [[0u8; 32]; TOTAL_SHARES];
for (i, share) in shares.iter().enumerate() {
leaves[i] = merkle_leaf_hash(share.index, &share.value);
}
let (root, paths) = build_share_tree(&leaves);
for (share, path) in shares.iter_mut().zip(paths) {
share.path = path;
}
Ok((shares, root))
}
pub fn reconstruct_key_material(
shares: &[HtssShare],
root: &[u8; 32],
) -> Result<Vec<u8>, IdentityError> {
if shares.len() < THRESHOLD_K {
return Err(IdentityError::ThresholdNotMet);
}
if shares.len() > TOTAL_SHARES {
return Err(IdentityError::InvalidShareSet);
}
let width = shares[0].value.len();
if width == 0 || width % LIMB_EVAL_BYTES != 0 {
return Err(IdentityError::SerializationError);
}
if shares
.iter()
.any(|s| s.value.len() != width || s.index == 0)
{
return Err(IdentityError::SerializationError);
}
let mut seen = [false; 256];
for share in shares {
if seen[share.index as usize] {
return Err(IdentityError::InvalidShareSet);
}
seen[share.index as usize] = true;
}
for share in shares {
if !verify_share_in_tree(share.index, &share.value, &share.path, root) {
return Err(IdentityError::InvalidShareSet);
}
}
let limb_count = width / LIMB_EVAL_BYTES;
let mut payload = Vec::with_capacity(limb_count * LIMB_BYTES);
for limb_idx in 0..limb_count {
let points: Vec<(u8, u64)> = shares
.iter()
.map(|s| {
let off = limb_idx * LIMB_EVAL_BYTES;
let mut b = [0u8; LIMB_EVAL_BYTES];
b.copy_from_slice(&s.value[off..off + LIMB_EVAL_BYTES]);
(s.index, u32::from_le_bytes(b) as u64)
})
.collect();
let limb = Self::reconstruct_secret(&points)?;
payload.push((limb & 0xFF) as u8);
payload.push(((limb >> 8) & 0xFF) as u8);
}
if payload.len() < 4 {
return Err(IdentityError::SerializationError);
}
let mut len_bytes = [0u8; 4];
len_bytes.copy_from_slice(&payload[..4]);
let len = u32::from_le_bytes(len_bytes) as usize;
if len == 0 || 4 + len > payload.len() {
payload.zeroize();
return Err(IdentityError::SerializationError);
}
let secret = payload[4..4 + len].to_vec();
payload.zeroize();
Ok(secret)
}
pub fn split_key_material_bytes(secret: &[u8], nonce: &[u8]) -> Result<Vec<u8>, IdentityError> {
let (shares, root) = Self::split_key_material(secret, nonce)?;
let width = shares[0].value.len();
let path_bytes: usize = shares.iter().map(|s| s.path.len()).sum();
let mut out = Vec::with_capacity(32 + 1 + 4 + shares.len() * (1 + width) + path_bytes);
out.extend_from_slice(&root);
out.push(shares.len() as u8);
out.extend_from_slice(&(width as u32).to_le_bytes());
for share in &shares {
out.push(share.index);
out.extend_from_slice(&share.value);
out.extend_from_slice(&share.path);
}
Ok(out)
}
pub fn reconstruct_key_material_bytes(bytes: &[u8]) -> Result<Vec<u8>, IdentityError> {
if bytes.len() < 37 {
return Err(IdentityError::SerializationError);
}
let mut root = [0u8; 32];
root.copy_from_slice(&bytes[..32]);
let count = bytes[32] as usize;
let mut width_bytes = [0u8; 4];
width_bytes.copy_from_slice(&bytes[33..37]);
let width = u32::from_le_bytes(width_bytes) as usize;
if count == 0 || width == 0 {
return Err(IdentityError::SerializationError);
}
let mut shares = Vec::with_capacity(count);
let mut off = 37;
for _ in 0..count {
if off >= bytes.len() {
return Err(IdentityError::SerializationError);
}
let index = bytes[off];
off += 1;
let Some(sibling_count) = merkle_proof_len(index) else {
return Err(IdentityError::SerializationError);
};
let path_len = sibling_count * 32;
if off + width + path_len > bytes.len() {
return Err(IdentityError::SerializationError);
}
let value = bytes[off..off + width].to_vec();
off += width;
let path = bytes[off..off + path_len].to_vec();
off += path_len;
shares.push(HtssShare { index, value, path });
}
if off != bytes.len() {
return Err(IdentityError::SerializationError);
}
Self::reconstruct_key_material(&shares, &root)
}
fn derive_coeff_key(secret: &[u8], nonce: &[u8]) -> [u8; 32] {
let mut hasher = Shake256::default();
sha3::digest::Update::update(&mut hasher, b"AETHEL_HTSS_COEFF_V2");
sha3::digest::Update::update(&mut hasher, &(secret.len() as u32).to_le_bytes());
sha3::digest::Update::update(&mut hasher, secret);
sha3::digest::Update::update(&mut hasher, &(nonce.len() as u32).to_le_bytes());
sha3::digest::Update::update(&mut hasher, nonce);
let mut xof = hasher.finalize_xof();
let mut key = [0u8; 32];
xof.read(&mut key);
key
}
fn derive_coeff_from_key(key: &[u8; 32], limb_idx: usize, coeff_idx: usize) -> u64 {
let mut hasher = Shake256::default();
sha3::digest::Update::update(&mut hasher, b"AETHEL_HTSS_COEFF_V2_EXPAND");
sha3::digest::Update::update(&mut hasher, key);
sha3::digest::Update::update(&mut hasher, &(limb_idx as u64).to_le_bytes());
sha3::digest::Update::update(&mut hasher, &(coeff_idx as u64).to_le_bytes());
let mut xof = hasher.finalize_xof();
loop {
let mut buf = [0u8; 4];
xof.read(&mut buf);
let candidate = (u32::from_le_bytes(buf) & 0x00FF_FFFF) as u64;
if candidate < MODULUS_Q {
return candidate;
}
}
}
#[deprecated(
since = "0.1.1",
note = "threshold not enforced: coefficients derive non-cryptographically from a \
caller-supplied u64 seed, so one share plus that seed recovers the secret. \
Also truncates any secret above ~2^23. Use split_key_material."
)]
pub fn split_secret(secret: u64, k: usize, n: usize, seed: u64) -> Vec<(u8, u64)> {
let mut coefficients = Vec::with_capacity(k);
coefficients.push(secret % MODULUS_Q);
for i in 1..k {
let coeff = Self::derive_coeff(seed, i as u64) % MODULUS_Q;
coefficients.push(coeff);
}
let mut shares = Vec::with_capacity(n);
for x in 1..=(n as u8) {
let mut y = 0u64;
let mut x_pow = 1u64;
for &coeff in &coefficients {
y = (y + coeff.wrapping_mul(x_pow)) % MODULUS_Q;
x_pow = x_pow.wrapping_mul(x as u64) % MODULUS_Q;
}
shares.push((x, y));
}
coefficients.zeroize();
shares
}
fn derive_coeff(seed: u64, idx: u64) -> u64 {
let mut v = seed.wrapping_add(idx.wrapping_mul(0x9e3779b97f4a7c15));
v ^= v >> 30;
v = v.wrapping_mul(0xbf58476d1ce4e5b9);
v ^= v >> 27;
v = v.wrapping_mul(0x94d049bb133111eb);
v ^= v >> 31;
v
}
pub fn reconstruct_secret_checked(shares: &[(u8, u64)]) -> Result<u64, IdentityError> {
if shares.len() < THRESHOLD_K {
return Err(IdentityError::ThresholdNotMet);
}
Self::reconstruct_secret(shares)
}
#[allow(clippy::needless_range_loop)]
pub fn reconstruct_secret(shares: &[(u8, u64)]) -> Result<u64, IdentityError> {
let q = MODULUS_Q as i64;
let mut secret = 0i64;
for i in 0..shares.len() {
let xi = shares[i].0 as i64;
let yi = shares[i].1 as i64;
let mut num = 1i64;
let mut den = 1i64;
for j in 0..shares.len() {
if i != j {
let xj = shares[j].0 as i64;
num = ((num % q) * ((-xj % q + q) % q)) % q;
den = ((den % q) * ((xi - xj % q + q) % q)) % q;
}
}
let den_inv = Self::mod_inverse(den, q).ok_or(IdentityError::InvalidShareSet)?;
let lagrange = (num % q * den_inv % q) % q;
let term = (yi % q * lagrange % q) % q;
secret = (secret + term) % q;
}
Ok(((secret % q) + q) as u64 % MODULUS_Q)
}
fn mod_inverse(a: i64, m: i64) -> Option<i64> {
let mut t = 0i64;
let mut newt = 1i64;
let mut r = m;
let mut newr = a % m;
while newr != 0 {
let quotient = r / newr;
let temp_t = t - quotient * newt;
t = newt;
newt = temp_t;
let temp_r = r - quotient * newr;
r = newr;
newr = temp_r;
}
if r > 1 {
return None;
}
if t < 0 {
t += m;
}
Some(t)
}
}
pub struct HypercubeNetwork {
pub nodes: Vec<NodeAddress>,
}
impl HypercubeNetwork {
pub fn new() -> Self {
let nodes = (0..NUM_NODES).map(|i| NodeAddress(i as u8)).collect();
Self { nodes }
}
pub fn compute_orthogonal_paths(_src: NodeAddress, _dst: NodeAddress) -> Vec<Vec<usize>> {
let mut paths = Vec::with_capacity(HYPERCUBE_DIM);
for d_start in 0..HYPERCUBE_DIM {
let mut dim_order = Vec::with_capacity(HYPERCUBE_DIM);
for i in 0..HYPERCUBE_DIM {
dim_order.push((d_start + i) % HYPERCUBE_DIM);
}
paths.push(dim_order);
}
paths
}
pub fn route_payload_shares(
&self,
src: NodeAddress,
dst: NodeAddress,
shares: &[(u8, u64)],
) -> Vec<HypercubePacket> {
let routes = Self::compute_orthogonal_paths(src, dst);
let mut packets = Vec::new();
for (i, share) in shares.iter().enumerate() {
let route_dim_sequence = routes[i % HYPERCUBE_DIM].clone();
let mut hasher = Sha3_256::new();
hasher.update(src.0.to_le_bytes());
hasher.update(dst.0.to_le_bytes());
hasher.update([share.0]);
let mut path_tag = [0u8; 32];
path_tag.copy_from_slice(&hasher.finalize());
let packet = HypercubePacket {
source: src,
destination: dst,
current_node: src,
dimension_route: route_dim_sequence,
route_index: 0,
payload: ZkProofSegment {
share_id: share.0,
share_val: share.1,
path_tag,
},
};
packets.push(packet);
}
let mut delivered_packets = Vec::new();
for mut pkt in packets {
while pkt.current_node != pkt.destination {
let next_dim = pkt.dimension_route[pkt.route_index];
let next_node = pkt.current_node.neighbor(next_dim);
pkt.current_node = next_node;
pkt.route_index += 1;
}
delivered_packets.push(pkt);
}
delivered_packets
}
}
impl Default for HypercubeNetwork {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#[test]
fn mod_inverse_reports_a_non_invertible_input() {
assert_eq!(
SecretSharer::mod_inverse(0, MODULUS_Q as i64),
None,
"zero has no inverse mod q, and saying so with 0 is indistinguishable from success"
);
}
#[test]
fn mod_inverse_still_inverts() {
let q = MODULUS_Q as i64;
for a in [1i64, 2, 3, 7, 1234, q - 1] {
let inv = SecretSharer::mod_inverse(a, q)
.expect("a non-zero residue mod a prime is invertible");
assert_eq!(
(a * inv).rem_euclid(q),
1,
"mod_inverse({a}) is not an inverse"
);
}
}
use super::*;
#[test]
fn every_share_verifies_against_the_tree_it_was_built_from() {
let leaves: [[u8; 32]; TOTAL_SHARES] =
core::array::from_fn(|i| merkle_leaf_hash((i + 1) as u8, &[i as u8; 4]));
let (root, paths) = build_share_tree(&leaves);
for (i, path) in paths.iter().enumerate().take(TOTAL_SHARES) {
let index = (i + 1) as u8;
let value = [i as u8; 4];
assert!(
verify_share_in_tree(index, &value, path, &root),
"index {index}'s own path did not verify against the tree it came from"
);
}
}
#[test]
fn proof_length_matches_what_the_tree_builder_produces() {
let leaves = [[0u8; 32]; TOTAL_SHARES];
let (_, paths) = build_share_tree(&leaves);
for (i, path) in paths.iter().enumerate().take(TOTAL_SHARES) {
let index = (i + 1) as u8;
let expected = merkle_proof_len(index).expect("1..=5 is always Some") * 32;
assert_eq!(
path.len(),
expected,
"merkle_proof_len({index}) disagrees with build_share_tree's own output"
);
}
}
#[test]
fn verification_is_sensitive_to_every_field() {
let leaves: [[u8; 32]; TOTAL_SHARES] =
core::array::from_fn(|i| merkle_leaf_hash((i + 1) as u8, &[i as u8; 4]));
let (root, paths) = build_share_tree(&leaves);
let index = 2u8;
let value = [1u8; 4];
assert!(verify_share_in_tree(index, &value, &paths[1], &root));
assert!(!verify_share_in_tree(3, &value, &paths[1], &root));
assert!(!verify_share_in_tree(index, &[9u8; 4], &paths[1], &root));
assert!(!verify_share_in_tree(index, &value, &paths[0], &root));
let mut tampered_path = paths[1].clone();
tampered_path[0] ^= 0x01;
assert!(!verify_share_in_tree(index, &value, &tampered_path, &root));
let mut tampered_root = root;
tampered_root[0] ^= 0x01;
assert!(!verify_share_in_tree(
index,
&value,
&paths[1],
&tampered_root
));
}
#[test]
fn verification_refuses_indices_outside_the_scheme() {
assert!(!verify_share_in_tree(0, &[0u8; 4], &[0u8; 96], &[0u8; 32]));
assert!(!verify_share_in_tree(6, &[0u8; 4], &[0u8; 96], &[0u8; 32]));
assert_eq!(merkle_proof_len(0), None);
assert_eq!(merkle_proof_len(6), None);
}
#[test]
#[allow(deprecated)] fn test_htss_split_reconstruct() {
let secret: u64 = 5234123;
let seed: u64 = 0xdeadbeef_cafebabe;
let shares = SecretSharer::split_secret(secret, THRESHOLD_K, HYPERCUBE_DIM, seed);
assert_eq!(shares.len(), HYPERCUBE_DIM);
let reconstructed = SecretSharer::reconstruct_secret(&shares[0..THRESHOLD_K])
.expect("distinct indices interpolate");
assert_eq!(reconstructed, secret);
}
#[test]
#[allow(deprecated)] fn test_hypercube_routing() {
let network = HypercubeNetwork::new();
let src = NodeAddress(0b00000);
let dst = NodeAddress(0b11111);
let seed: u64 = 0x1234567890abcdef;
let shares = SecretSharer::split_secret(42u64, THRESHOLD_K, HYPERCUBE_DIM, seed);
let delivered = network.route_payload_shares(src, dst, &shares);
assert_eq!(delivered.len(), HYPERCUBE_DIM);
for pkt in &delivered {
assert_eq!(pkt.current_node, dst);
}
}
}