use crate::error::PrivacyError;
use crate::keyhop::{hkdf_derive, aes_gcm_encrypt, aes_gcm_decrypt};
use crate::mesh::keys;
use crate::types::SphinxPacket;
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};
extern crate alloc;
use alloc::{string::String, vec, vec::Vec};
#[allow(dead_code)]
const ML_KEM_768_CT_LEN: usize = 1088;
const ML_KEM_768_EK_LEN: usize = 1184;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SphinxLayer {
pub ciphertext: Vec<u8>,
pub encrypted_payload: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SphinxOnionPacket {
pub layers: Vec<SphinxLayer>,
pub chaos_tag: Vec<u8>,
pub decoy: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SphinxConfig {
pub hops: u8,
pub lambda: f64,
pub chaos_seed: [u8; 32],
}
impl Default for SphinxConfig {
fn default() -> Self {
Self { hops: 7, lambda: 10.0, chaos_seed: [0u8; 32] }
}
}
pub fn build_sphinx_onion(
hops: &[(String, Vec<u8>)],
payload: &[u8],
chaos_seed: &[u8],
) -> Result<SphinxOnionPacket, PrivacyError> {
if hops.is_empty() {
return Err(PrivacyError::SphinxFailed("No hops provided".into()));
}
if hops.len() < 5 || hops.len() > 9 {
return Err(PrivacyError::SphinxFailed(
alloc::format!("hops must be 5–9, got {}", hops.len())
));
}
for (i, (node_id, ek)) in hops.iter().enumerate() {
if ek.len() != ML_KEM_768_EK_LEN {
return Err(PrivacyError::SphinxFailed(
alloc::format!("hop {} ({}): encapsulation key must be {} bytes, got {}",
i, node_id, ML_KEM_768_EK_LEN, ek.len())
));
}
}
let n = hops.len();
let mut layers: Vec<SphinxLayer> = Vec::with_capacity(n);
let mut current_payload = payload.to_vec();
for i in (0..n).rev() {
let (node_id, ek_bytes) = &hops[i];
let ek_for_encap = derive_hop_ek(ek_bytes, chaos_seed, i as u64);
let (ct, ss) = encapsulate_to_hop(&ek_for_encap)?;
let mut layer_key = [0u8; 32];
hkdf_derive(&ss, node_id.as_bytes(), b"sphinx-layer-v1", &mut layer_key)?;
let encrypted = aes_gcm_encrypt(&layer_key, ¤t_payload)?;
let layer = SphinxLayer {
ciphertext: ct.clone(),
encrypted_payload: encrypted.clone(),
};
let mut next_payload = Vec::with_capacity(ct.len() + encrypted.len());
next_payload.extend_from_slice(&ct);
next_payload.extend_from_slice(&encrypted);
current_payload = next_payload;
layers.push(layer);
}
layers.reverse();
let mut tag_hasher = Sha256::new();
for layer in &layers {
tag_hasher.update(&layer.ciphertext);
}
tag_hasher.update(chaos_seed);
tag_hasher.update(b"sphinx-chaos-tag-v1");
let chaos_tag = tag_hasher.finalize().to_vec();
Ok(SphinxOnionPacket {
layers,
chaos_tag,
decoy: false,
})
}
pub fn unwrap_sphinx_layer(
layer: &SphinxLayer,
decapsulation_key: &[u8],
node_id: &str,
) -> Result<Vec<u8>, PrivacyError> {
let ss = keys::decapsulate(decapsulation_key, &layer.ciphertext)?;
let mut layer_key = [0u8; 32];
hkdf_derive(&ss, node_id.as_bytes(), b"sphinx-layer-v1", &mut layer_key)?;
aes_gcm_decrypt(&layer_key, &layer.encrypted_payload)
}
pub fn generate_decoys(
lambda: f64,
chaos_seed: &[u8],
hop_count: usize,
) -> Vec<SphinxOnionPacket> {
let count = poisson_sample(lambda, chaos_seed);
let mut decoys = Vec::with_capacity(count);
for i in 0..count {
let mut decoy_seed = [0u8; 32];
let mut hasher = Sha256::new();
hasher.update(chaos_seed);
hasher.update((i as u64).to_le_bytes());
hasher.update(b"decoy-seed-v1");
decoy_seed.copy_from_slice(&hasher.finalize());
let actual_hops = hop_count.clamp(5, 9);
let mut fake_hops: Vec<(String, Vec<u8>)> = Vec::with_capacity(actual_hops);
for h in 0..actual_hops {
let mut hop_seed = [0u8; 32];
let mut hs = Sha256::new();
hs.update(decoy_seed);
hs.update((h as u64).to_le_bytes());
hs.update(b"decoy-hop-key-v1");
hop_seed.copy_from_slice(&hs.finalize());
match keys::generate_keypair(&hop_seed) {
Ok((ek, _dk)) => {
let node_id = alloc::format!("decoy-{i}-hop-{h}");
fake_hops.push((node_id, ek));
}
Err(_) => {
break;
}
}
}
if fake_hops.len() < 5 {
continue; }
let decoy_payload = vec![0u8; 32];
if let Ok(mut pkt) = build_sphinx_onion(&fake_hops, &decoy_payload, &decoy_seed) {
pkt.decoy = true;
decoys.push(pkt);
}
}
decoys
}
pub fn build_sphinx_packet(
payload: Vec<u8>,
config: &SphinxConfig,
is_decoy: bool,
) -> Result<SphinxPacket, PrivacyError> {
if config.hops < 5 || config.hops > 9 {
return Err(PrivacyError::SphinxFailed(
alloc::format!("hops must be 5–9, got {}", config.hops)
));
}
let n = config.hops as usize;
let mut hops: Vec<(String, Vec<u8>)> = Vec::with_capacity(n);
for i in 0..n {
let mut hop_seed = [0u8; 32];
let mut hasher = Sha256::new();
hasher.update(config.chaos_seed);
hasher.update((i as u64).to_le_bytes());
hasher.update(b"sphinx-hop-keygen-v1");
hop_seed.copy_from_slice(&hasher.finalize());
let (ek, _dk) = keys::generate_keypair(&hop_seed)?;
let node_id = alloc::format!("hop-{i}");
hops.push((node_id, ek));
}
let onion = build_sphinx_onion(&hops, &payload, &config.chaos_seed)?;
let mut serialized = Vec::new();
serialized.extend_from_slice(&onion.chaos_tag);
for layer in &onion.layers {
let ct_len = layer.ciphertext.len() as u32;
serialized.extend_from_slice(&ct_len.to_le_bytes());
serialized.extend_from_slice(&layer.ciphertext);
let enc_len = layer.encrypted_payload.len() as u32;
serialized.extend_from_slice(&enc_len.to_le_bytes());
serialized.extend_from_slice(&layer.encrypted_payload);
}
Ok(SphinxPacket {
payload: serialized,
hops: config.hops,
is_decoy,
chaos_seed: hex::encode(config.chaos_seed),
})
}
pub fn generate_decoys_legacy(
count: usize,
config: &SphinxConfig,
) -> Result<Vec<SphinxPacket>, PrivacyError> {
let mut decoys = Vec::with_capacity(count);
for i in 0..count {
let mut seed = config.chaos_seed;
seed[0] ^= i as u8;
let decoy_payload = vec![0u8; 32]; let cfg = SphinxConfig { chaos_seed: seed, ..config.clone() };
decoys.push(build_sphinx_packet(decoy_payload, &cfg, true)?);
}
Ok(decoys)
}
pub fn sphinx_route_obfuscated(
hops_json: &str,
hop_keys_hex_json: &str,
payload: &[u8],
qem_metadata_json: &str,
chaos_seed: &[u8],
) -> Result<SphinxPacket, PrivacyError> {
let hop_ids: Vec<String> = serde_json::from_str(hops_json)
.map_err(|e| PrivacyError::SphinxFailed(alloc::format!("hops_json parse error: {e}")))?;
let hop_keys_hex: Vec<String> = serde_json::from_str(hop_keys_hex_json)
.map_err(|e| PrivacyError::SphinxFailed(alloc::format!("hop_keys_hex_json parse error: {e}")))?;
if hop_ids.len() != hop_keys_hex.len() {
return Err(PrivacyError::SphinxFailed(alloc::format!(
"hops_json length ({}) != hop_keys_hex_json length ({})",
hop_ids.len(), hop_keys_hex.len()
)));
}
let mut hops: Vec<(String, Vec<u8>)> = Vec::with_capacity(hop_ids.len());
for (id, key_hex) in hop_ids.iter().zip(hop_keys_hex.iter()) {
let key_bytes = hex::decode(key_hex)
.map_err(|e| PrivacyError::SphinxFailed(alloc::format!("hex decode error for hop {id}: {e}")))?;
hops.push((id.clone(), key_bytes));
}
let seed_32: [u8; 32] = {
use sha2::{Digest, Sha256};
Sha256::digest(chaos_seed).into()
};
let qem_bytes = qem_metadata_json.as_bytes();
let mut header: Vec<u8> = Vec::with_capacity(8 + qem_bytes.len() + 4 + seed_32.len());
header.extend_from_slice(b"QEMH");
header.extend_from_slice(&(qem_bytes.len() as u32).to_le_bytes());
header.extend_from_slice(qem_bytes);
header.extend_from_slice(&(seed_32.len() as u32).to_le_bytes());
header.extend_from_slice(&seed_32);
let mut full_payload: Vec<u8> = Vec::with_capacity(header.len() + payload.len());
full_payload.extend_from_slice(&header);
full_payload.extend_from_slice(payload);
let onion = build_sphinx_onion(&hops, &full_payload, &seed_32)?;
let mut serialized: Vec<u8> = Vec::new();
serialized.extend_from_slice(&onion.chaos_tag);
for layer in &onion.layers {
let ct_len = layer.ciphertext.len() as u32;
serialized.extend_from_slice(&ct_len.to_le_bytes());
serialized.extend_from_slice(&layer.ciphertext);
let enc_len = layer.encrypted_payload.len() as u32;
serialized.extend_from_slice(&enc_len.to_le_bytes());
serialized.extend_from_slice(&layer.encrypted_payload);
}
Ok(SphinxPacket {
payload: serialized,
hops: hops.len() as u8,
is_decoy: false,
chaos_seed: hex::encode(seed_32),
})
}
pub fn unwrap_layer(packet: &SphinxPacket, hop: u8, chaos_seed: &[u8; 32]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(&packet.payload);
hasher.update([hop]);
hasher.update(chaos_seed);
hasher.update(b"sphinx-unwrap-v1");
hasher.finalize().to_vec()
}
fn derive_hop_ek(ek_bytes: &[u8], _chaos_seed: &[u8], _hop_index: u64) -> Vec<u8> {
ek_bytes.to_vec()
}
fn encapsulate_to_hop(ek_bytes: &[u8]) -> Result<(Vec<u8>, Vec<u8>), PrivacyError> {
keys::encapsulate(ek_bytes)
}
fn poisson_sample(lambda: f64, chaos_seed: &[u8]) -> usize {
if lambda <= 0.0 {
return 0;
}
let threshold = (-lambda).exp();
let mut product = 1.0_f64;
let mut count = 0usize;
let mut counter = 0u64;
loop {
let mut hasher = Sha256::new();
hasher.update(chaos_seed);
hasher.update(counter.to_le_bytes());
hasher.update(b"poisson-v1");
let hash = hasher.finalize();
let raw = u64::from_le_bytes(hash[..8].try_into().unwrap_or([0u8; 8]));
let u = (raw as f64 / u64::MAX as f64).max(1e-15);
product *= u;
counter += 1;
if product <= threshold {
break;
}
count += 1;
if count >= 50 {
break;
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_hops(n: usize, seed: &[u8]) -> Vec<(String, Vec<u8>)> {
(0..n).map(|i| {
let mut hop_seed = [0u8; 32];
let mut hasher = Sha256::new();
hasher.update(seed);
hasher.update((i as u64).to_le_bytes());
hop_seed.copy_from_slice(&hasher.finalize());
let (ek, _dk) = keys::generate_keypair(&hop_seed).unwrap();
(alloc::format!("node-{i}"), ek)
}).collect()
}
#[test]
fn test_build_sphinx_onion_5_hops() {
let seed = [1u8; 32];
let hops = make_test_hops(5, &seed);
let payload = b"hello sphinx";
let pkt = build_sphinx_onion(&hops, payload, &seed).unwrap();
assert_eq!(pkt.layers.len(), 5);
assert!(!pkt.decoy);
assert_eq!(pkt.chaos_tag.len(), 32);
}
#[test]
fn test_build_sphinx_onion_7_hops() {
let seed = [2u8; 32];
let hops = make_test_hops(7, &seed);
let payload = b"test payload for 7 hops";
let pkt = build_sphinx_onion(&hops, payload, &seed).unwrap();
assert_eq!(pkt.layers.len(), 7);
for layer in &pkt.layers {
assert_eq!(layer.ciphertext.len(), ML_KEM_768_CT_LEN);
assert!(!layer.encrypted_payload.is_empty());
}
}
#[test]
fn test_invalid_hop_count() {
let seed = [0u8; 32];
let hops = make_test_hops(3, &seed); assert!(build_sphinx_onion(&hops, b"x", &seed).is_err());
}
#[test]
fn test_unwrap_sphinx_layer() {
let seed = [3u8; 32];
let n = 5;
let mut hop_keys: Vec<(String, Vec<u8>, Vec<u8>)> = Vec::new(); for i in 0..n {
let mut hop_seed = [0u8; 32];
let mut hasher = Sha256::new();
hasher.update(seed);
hasher.update((i as u64).to_le_bytes());
hop_seed.copy_from_slice(&hasher.finalize());
let (ek, dk) = keys::generate_keypair(&hop_seed).unwrap();
hop_keys.push((alloc::format!("node-{i}"), ek, dk));
}
let hops: Vec<(String, Vec<u8>)> = hop_keys.iter()
.map(|(id, ek, _)| (id.clone(), ek.clone()))
.collect();
let payload = b"secret message";
let pkt = build_sphinx_onion(&hops, payload, &seed).unwrap();
let (node_id, _ek, dk) = &hop_keys[0];
let inner = unwrap_sphinx_layer(&pkt.layers[0], dk, node_id).unwrap();
assert!(!inner.is_empty());
}
#[test]
fn test_build_sphinx_packet_legacy() {
let cfg = SphinxConfig::default();
let pkt = build_sphinx_packet(b"hello".to_vec(), &cfg, false).unwrap();
assert_eq!(pkt.hops, 7);
assert!(!pkt.is_decoy);
assert!(!pkt.payload.is_empty());
}
#[test]
fn test_invalid_hops_legacy() {
let cfg = SphinxConfig { hops: 3, ..SphinxConfig::default() };
assert!(build_sphinx_packet(b"x".to_vec(), &cfg, false).is_err());
}
#[test]
fn test_generate_decoys_legacy() {
let cfg = SphinxConfig::default();
let decoys = generate_decoys_legacy(3, &cfg).unwrap();
assert_eq!(decoys.len(), 3);
assert!(decoys.iter().all(|d| d.is_decoy));
}
#[test]
fn test_poisson_sample_zero_lambda() {
let seed = [0u8; 32];
assert_eq!(poisson_sample(0.0, &seed), 0);
}
#[test]
fn test_poisson_sample_positive() {
let seed = [5u8; 32];
let count = poisson_sample(10.0, &seed);
assert!(count <= 50);
}
#[test]
fn test_chaos_tag_deterministic() {
let seed = [7u8; 32];
let hops = make_test_hops(5, &seed);
let payload = b"deterministic test";
let pkt1 = build_sphinx_onion(&hops, payload, &seed).unwrap();
assert_eq!(pkt1.chaos_tag.len(), 32);
}
}