use crate::encryption::{decrypt, encrypt};
use crate::signing::{KeyPair, PublicKey};
use blake3;
use rand::Rng as _;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum OnionError {
#[error("No layers to build onion")]
NoLayers,
#[error("Encryption failed")]
EncryptionFailed,
#[error("Decryption failed - invalid key or corrupted data")]
DecryptionFailed,
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Invalid onion structure")]
InvalidStructure,
}
pub type OnionResult<T> = Result<T, OnionError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnionPacket {
ciphertext: Vec<u8>,
nonce: [u8; 12],
ephemeral_hint: [u8; 32],
}
impl OnionPacket {
pub fn data(&self) -> &[u8] {
&self.ciphertext
}
pub fn to_bytes(&self) -> OnionResult<Vec<u8>> {
crate::codec::encode(self).map_err(|e| OnionError::SerializationError(e.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> OnionResult<Self> {
crate::codec::decode(bytes).map_err(|e| OnionError::SerializationError(e.to_string()))
}
pub fn peel_layer(&self, keypair: &KeyPair) -> OnionResult<(OnionLayer, Option<OnionPacket>)> {
let mut hasher = blake3::Hasher::new();
hasher.update(b"CHIE-ONION-V1");
hasher.update(&keypair.public_key());
hasher.update(&self.ephemeral_hint);
let decryption_key = *hasher.finalize().as_bytes();
let decrypted = decrypt(&self.ciphertext, &decryption_key, &self.nonce)
.map_err(|_| OnionError::DecryptionFailed)?;
let payload: OnionLayerPayload =
crate::codec::decode(&decrypted).map_err(|_| OnionError::InvalidStructure)?;
match payload {
OnionLayerPayload::Intermediate {
next_hop,
next_packet,
} => Ok((OnionLayer::Intermediate { next_hop }, Some(next_packet))),
OnionLayerPayload::Final { data } => Ok((
OnionLayer::Final,
Some(OnionPacket {
ciphertext: data,
nonce: [0; 12],
ephemeral_hint: [0; 32],
}),
)),
}
}
}
#[derive(Debug, Clone)]
pub enum OnionLayer {
Intermediate {
next_hop: PublicKey,
},
Final,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum OnionLayerPayload {
Intermediate {
next_hop: PublicKey,
next_packet: OnionPacket,
},
Final { data: Vec<u8> },
}
pub struct OnionBuilder {
data: Vec<u8>,
layers: Vec<PublicKey>,
}
impl OnionBuilder {
pub fn new(data: &[u8]) -> Self {
Self {
data: data.to_vec(),
layers: Vec::new(),
}
}
pub fn add_layer(mut self, pubkey: PublicKey) -> Self {
self.layers.push(pubkey);
self
}
pub fn add_layers(mut self, pubkeys: &[PublicKey]) -> Self {
self.layers.extend_from_slice(pubkeys);
self
}
pub fn build(self) -> OnionResult<OnionPacket> {
if self.layers.is_empty() {
return Err(OnionError::NoLayers);
}
let mut current_payload = OnionLayerPayload::Final { data: self.data };
for (i, pubkey) in self.layers.iter().enumerate() {
let payload_bytes = crate::codec::encode(¤t_payload)
.map_err(|e| OnionError::SerializationError(e.to_string()))?;
let mut ephemeral_hint = [0u8; 32];
rand::rng().fill_bytes(&mut ephemeral_hint);
let mut hasher = blake3::Hasher::new();
hasher.update(b"CHIE-ONION-V1");
hasher.update(pubkey);
hasher.update(&ephemeral_hint);
let encryption_key = *hasher.finalize().as_bytes();
let mut nonce = [0u8; 12];
rand::rng().fill_bytes(&mut nonce);
let ciphertext = encrypt(&payload_bytes, &encryption_key, &nonce)
.map_err(|_| OnionError::EncryptionFailed)?;
let packet = OnionPacket {
ciphertext,
nonce,
ephemeral_hint,
};
if i == self.layers.len() - 1 {
return Ok(packet);
}
current_payload = OnionLayerPayload::Intermediate {
next_hop: *pubkey,
next_packet: packet,
};
}
Err(OnionError::InvalidStructure)
}
}
pub struct OnionRoute {
path: Vec<PublicKey>,
}
impl OnionRoute {
pub fn new(path: Vec<PublicKey>) -> Self {
Self { path }
}
pub fn length(&self) -> usize {
self.path.len()
}
pub fn path(&self) -> &[PublicKey] {
&self.path
}
pub fn encrypt(&self, data: &[u8]) -> OnionResult<OnionPacket> {
let mut builder = OnionBuilder::new(data);
for pubkey in &self.path {
builder = builder.add_layer(*pubkey);
}
builder.build()
}
}
pub fn create_onion(data: &[u8], path: &[PublicKey]) -> OnionResult<OnionPacket> {
let mut builder = OnionBuilder::new(data);
for pubkey in path {
builder = builder.add_layer(*pubkey);
}
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signing::KeyPair;
#[test]
fn test_onion_single_layer() {
let data = b"Single layer test";
let keypair = KeyPair::generate();
let onion = OnionBuilder::new(data)
.add_layer(keypair.public_key())
.build()
.unwrap();
let (layer, next) = onion.peel_layer(&keypair).unwrap();
assert!(matches!(layer, OnionLayer::Final));
let final_data = next.unwrap();
assert_eq!(data, &final_data.ciphertext[..]);
}
#[test]
fn test_onion_two_layers() {
let data = b"Two layer test";
let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let onion = OnionBuilder::new(data)
.add_layer(keypair1.public_key())
.add_layer(keypair2.public_key())
.build()
.unwrap();
let (layer1, next1) = onion.peel_layer(&keypair2).unwrap();
if let OnionLayer::Intermediate { next_hop } = layer1 {
assert_eq!(next_hop, keypair1.public_key());
} else {
panic!("Expected intermediate layer");
}
let onion2 = next1.unwrap();
let (layer2, final_data) = onion2.peel_layer(&keypair1).unwrap();
assert!(matches!(layer2, OnionLayer::Final));
let data_packet = final_data.unwrap();
assert_eq!(data, &data_packet.ciphertext[..]);
}
#[test]
fn test_onion_three_layers() {
let data = b"Three layer test message";
let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let keypair3 = KeyPair::generate();
let onion = OnionBuilder::new(data)
.add_layer(keypair1.public_key())
.add_layer(keypair2.public_key())
.add_layer(keypair3.public_key())
.build()
.unwrap();
let (layer3, next3) = onion.peel_layer(&keypair3).unwrap();
if let OnionLayer::Intermediate { next_hop } = layer3 {
assert_eq!(next_hop, keypair2.public_key());
} else {
panic!("Expected intermediate layer");
}
let onion2 = next3.unwrap();
let (layer2, next2) = onion2.peel_layer(&keypair2).unwrap();
if let OnionLayer::Intermediate { next_hop } = layer2 {
assert_eq!(next_hop, keypair1.public_key());
} else {
panic!("Expected intermediate layer");
}
let onion1 = next2.unwrap();
let (layer1, final_data) = onion1.peel_layer(&keypair1).unwrap();
assert!(matches!(layer1, OnionLayer::Final));
let data_packet = final_data.unwrap();
assert_eq!(data, &data_packet.ciphertext[..]);
}
#[test]
fn test_onion_wrong_key() {
let data = b"Test data";
let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let wrong_keypair = KeyPair::generate();
let onion = OnionBuilder::new(data)
.add_layer(keypair1.public_key())
.add_layer(keypair2.public_key())
.build()
.unwrap();
let result = onion.peel_layer(&wrong_keypair);
assert!(matches!(result, Err(OnionError::DecryptionFailed)));
}
#[test]
fn test_onion_no_layers() {
let data = b"Test";
let result = OnionBuilder::new(data).build();
assert!(matches!(result, Err(OnionError::NoLayers)));
}
#[test]
fn test_onion_serialization() {
let data = b"Serialization test";
let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let onion = OnionBuilder::new(data)
.add_layer(keypair1.public_key())
.add_layer(keypair2.public_key())
.build()
.unwrap();
let bytes = onion.to_bytes().unwrap();
let deserialized = OnionPacket::from_bytes(&bytes).unwrap();
let (_, next) = deserialized.peel_layer(&keypair2).unwrap();
let onion2 = next.unwrap();
let (_, final_data) = onion2.peel_layer(&keypair1).unwrap();
assert_eq!(data, &final_data.unwrap().ciphertext[..]);
}
#[test]
fn test_onion_route() {
let data = b"Route test";
let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let keypair3 = KeyPair::generate();
let path = vec![
keypair1.public_key(),
keypair2.public_key(),
keypair3.public_key(),
];
let route = OnionRoute::new(path.clone());
assert_eq!(route.length(), 3);
assert_eq!(route.path(), &path[..]);
let onion = route.encrypt(data).unwrap();
let (_, next) = onion.peel_layer(&keypair3).unwrap();
let (_, next) = next.unwrap().peel_layer(&keypair2).unwrap();
let (_, final_data) = next.unwrap().peel_layer(&keypair1).unwrap();
assert_eq!(data, &final_data.unwrap().ciphertext[..]);
}
#[test]
fn test_create_onion_convenience() {
let data = b"Convenience function test";
let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let path = vec![keypair1.public_key(), keypair2.public_key()];
let onion = create_onion(data, &path).unwrap();
let (_, next) = onion.peel_layer(&keypair2).unwrap();
let (_, final_data) = next.unwrap().peel_layer(&keypair1).unwrap();
assert_eq!(data, &final_data.unwrap().ciphertext[..]);
}
#[test]
fn test_large_data() {
let data = vec![0x42u8; 10_000]; let keypair1 = KeyPair::generate();
let keypair2 = KeyPair::generate();
let onion = OnionBuilder::new(&data)
.add_layer(keypair1.public_key())
.add_layer(keypair2.public_key())
.build()
.unwrap();
let (_, next) = onion.peel_layer(&keypair2).unwrap();
let (_, final_data) = next.unwrap().peel_layer(&keypair1).unwrap();
assert_eq!(data, final_data.unwrap().ciphertext);
}
#[test]
fn test_add_layers_batch() {
let data = b"Batch layers test";
let keypairs: Vec<KeyPair> = (0..5).map(|_| KeyPair::generate()).collect();
let pubkeys: Vec<PublicKey> = keypairs.iter().map(|kp| kp.public_key()).collect();
let onion = OnionBuilder::new(data)
.add_layers(&pubkeys)
.build()
.unwrap();
let mut current = Some(onion);
for keypair in keypairs.iter().rev() {
if let Some(pkt) = current {
let (_, next) = pkt.peel_layer(keypair).unwrap();
current = next;
}
}
let final_data = current.unwrap();
assert_eq!(data, &final_data.ciphertext[..]);
}
}