Skip to main content

nox_crypto/sphinx/
mod.rs

1pub mod lioness;
2pub mod packet;
3pub mod pow;
4pub mod surb;
5
6use chacha20::cipher::{KeyIvInit, StreamCipher};
7use chacha20::{ChaCha20, Key, Nonce};
8use curve25519_dalek::constants::X25519_BASEPOINT;
9use curve25519_dalek::montgomery::MontgomeryPoint;
10use curve25519_dalek::scalar::Scalar;
11use hmac::{Hmac, Mac};
12use serde::{Deserialize, Serialize};
13use serde_big_array::BigArray;
14use sha2::{Digest, Sha256};
15use subtle::ConstantTimeEq;
16use thiserror::Error;
17use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519SecretKey};
18/// Constant-time ISO 7816-4 unpadding. Returns `None` if padding is invalid.
19pub(crate) fn unpad_iso7816_inner(data: &[u8]) -> Option<Vec<u8>> {
20    use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
21
22    if data.is_empty() {
23        return None;
24    }
25
26    let mut marker_pos: i64 = -1;
27    let mut found_marker = Choice::from(0u8);
28
29    for i in (0..data.len()).rev() {
30        let is_marker = data[i].ct_eq(&0x80);
31        let should_update = is_marker & !found_marker;
32        marker_pos = i64::conditional_select(&marker_pos, &(i as i64), should_update);
33        found_marker |= is_marker;
34    }
35
36    if marker_pos < 0 {
37        return None;
38    }
39
40    let marker_idx = marker_pos as usize;
41
42    let mut all_zeros = Choice::from(1u8);
43    for &byte in &data[marker_idx + 1..] {
44        all_zeros &= byte.ct_eq(&0x00);
45    }
46
47    if !bool::from(all_zeros) {
48        return None;
49    }
50
51    Some(data[..marker_idx].to_vec())
52}
53
54pub const ROUTING_INFO_SIZE: usize = 400;
55pub const MAC_SIZE: usize = 32;
56pub const NONCE_SIZE: usize = 8;
57pub const HEADER_SIZE: usize = 32 + ROUTING_INFO_SIZE + MAC_SIZE + NONCE_SIZE;
58/// Per-hop routing info shift: Flag(1) + Len(1) + MAC(32) + Address(max ~90) = 124, rounded to 128.
59pub const SHIFT_SIZE: usize = 128;
60
61#[derive(Debug, Error)]
62pub enum SphinxError {
63    #[error("Integrity Check Failed (Invalid MAC)")]
64    MacMismatch,
65    #[error("Serialization Error: {0}")]
66    Serialization(String),
67    #[error("Packet too short")]
68    InvalidSize,
69    #[error("Crypto Error: {0}")]
70    Crypto(String),
71    #[error("PoW error: {0}")]
72    Pow(#[from] pow::PowError),
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct SphinxHeader {
77    pub ephemeral_key: X25519PublicKey,
78    #[serde(with = "BigArray")]
79    pub routing_info: [u8; ROUTING_INFO_SIZE],
80    pub mac: [u8; MAC_SIZE],
81    pub nonce: u64,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85pub struct PathHop {
86    pub public_key: X25519PublicKey,
87    pub address: String,
88}
89
90/// Per-hop timing breakdown (nanoseconds). Only with `hop-metrics` feature.
91#[cfg(feature = "hop-metrics")]
92#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
93pub struct HopTimings {
94    pub ecdh_ns: u64,
95    pub key_derive_ns: u64,
96    pub mac_verify_ns: u64,
97    pub routing_decrypt_ns: u64,
98    pub body_decrypt_ns: u64,
99    pub blinding_ns: u64,
100    pub total_sphinx_ns: u64,
101}
102
103#[derive(Debug)]
104pub enum ProcessResult {
105    Forward {
106        next_hop: String,
107        next_packet: Box<SphinxHeader>,
108        processed_body: Vec<u8>,
109    },
110    Exit {
111        payload: Vec<u8>,
112    },
113}
114
115#[cfg(feature = "hop-metrics")]
116pub type ProcessOutput = (ProcessResult, HopTimings);
117
118#[cfg(not(feature = "hop-metrics"))]
119pub type ProcessOutput = ProcessResult;
120
121/// Extract the [`ProcessResult`], discarding timing data if present.
122#[inline]
123#[must_use]
124pub fn into_result(output: ProcessOutput) -> ProcessResult {
125    #[cfg(feature = "hop-metrics")]
126    {
127        output.0
128    }
129    #[cfg(not(feature = "hop-metrics"))]
130    {
131        output
132    }
133}
134impl SphinxHeader {
135    /// Blake3 hash of (`ephemeral_key`, `mac`, `nonce`) for replay detection.
136    #[must_use]
137    pub fn compute_replay_tag(&self) -> [u8; 32] {
138        let mut hasher = blake3::Hasher::new();
139        hasher.update(self.ephemeral_key.as_bytes());
140        hasher.update(&self.mac);
141        hasher.update(&self.nonce.to_le_bytes());
142        *hasher.finalize().as_bytes()
143    }
144
145    /// Process a Sphinx packet at this hop: ECDH, MAC verify, decrypt routing + body, blind key.
146    pub fn process(
147        &self,
148        node_sk: &X25519SecretKey,
149        body: Vec<u8>,
150    ) -> Result<ProcessOutput, SphinxError> {
151        #[cfg(feature = "hop-metrics")]
152        let total_start = std::time::Instant::now();
153
154        #[cfg(feature = "hop-metrics")]
155        let t0 = std::time::Instant::now();
156
157        let shared_secret = node_sk.diffie_hellman(&self.ephemeral_key);
158
159        #[cfg(feature = "hop-metrics")]
160        let ecdh_ns = t0.elapsed().as_nanos() as u64;
161
162        #[cfg(feature = "hop-metrics")]
163        let t1 = std::time::Instant::now();
164
165        let (rho, mu, pi, blinding_factor) = derive_keys(shared_secret.as_bytes());
166
167        #[cfg(feature = "hop-metrics")]
168        let key_derive_ns = t1.elapsed().as_nanos() as u64;
169
170        #[cfg(feature = "hop-metrics")]
171        let t2 = std::time::Instant::now();
172
173        let calculated_mac = compute_mac(&mu, &self.routing_info);
174        if calculated_mac.ct_eq(&self.mac).unwrap_u8() == 0 {
175            return Err(SphinxError::MacMismatch);
176        }
177
178        #[cfg(feature = "hop-metrics")]
179        let mac_verify_ns = t2.elapsed().as_nanos() as u64;
180
181        #[cfg(feature = "hop-metrics")]
182        let t3 = std::time::Instant::now();
183
184        let mut extended_routing = [0u8; ROUTING_INFO_SIZE + SHIFT_SIZE];
185        extended_routing[..ROUTING_INFO_SIZE].copy_from_slice(&self.routing_info);
186
187        // Zero nonce is safe: rho is unique per hop, so each (key, nonce) pair is used exactly once.
188        apply_stream_cipher(&rho, &[0u8; 12], &mut extended_routing);
189
190        let decrypted_routing: [u8; ROUTING_INFO_SIZE] = extended_routing[..ROUTING_INFO_SIZE]
191            .try_into()
192            .map_err(|_| SphinxError::Serialization("Routing size mismatch".into()))?;
193        let shifted_routing: [u8; ROUTING_INFO_SIZE] = extended_routing
194            [SHIFT_SIZE..SHIFT_SIZE + ROUTING_INFO_SIZE]
195            .try_into()
196            .map_err(|_| SphinxError::Serialization("Routing size mismatch".into()))?;
197
198        #[cfg(feature = "hop-metrics")]
199        let routing_decrypt_ns = t3.elapsed().as_nanos() as u64;
200
201        #[cfg(feature = "hop-metrics")]
202        let t4 = std::time::Instant::now();
203
204        let mut processed_body = body;
205        if processed_body.len() < lioness::MIN_BODY_SIZE {
206            return Err(SphinxError::Serialization(format!(
207                "Body too small for Lioness SPRP: {} bytes, minimum {}",
208                processed_body.len(),
209                lioness::MIN_BODY_SIZE
210            )));
211        }
212        let lioness_keys = lioness::LionessKeys::from_pi(&pi);
213        lioness::lioness_decrypt(&lioness_keys, &mut processed_body);
214
215        #[cfg(feature = "hop-metrics")]
216        let body_decrypt_ns = t4.elapsed().as_nanos() as u64;
217
218        let hop_type = decrypted_routing[0];
219
220        #[cfg(feature = "hop-metrics")]
221        let mut blinding_ns = 0u64;
222
223        let result = if hop_type == 0x00 {
224            let addr_len = decrypted_routing[1] as usize;
225            let next_mac_start = 2;
226            let next_mac_end = 34;
227            let addr_start = 34;
228            let addr_end = 34 + addr_len;
229
230            if addr_end > SHIFT_SIZE {
231                return Err(SphinxError::Serialization(
232                    "Address length exceeds SHIFT_SIZE".into(),
233                ));
234            }
235
236            let mut next_mac = [0u8; 32];
237            next_mac.copy_from_slice(&decrypted_routing[next_mac_start..next_mac_end]);
238
239            let next_hop = std::str::from_utf8(&decrypted_routing[addr_start..addr_end])
240                .map(str::to_owned)
241                .map_err(|_| SphinxError::Serialization("Invalid Address UTF8".into()))?;
242
243            #[cfg(feature = "hop-metrics")]
244            let t5 = std::time::Instant::now();
245
246            let point = MontgomeryPoint(self.ephemeral_key.to_bytes());
247            let blinded_point = point * blinding_factor;
248            let next_ephemeral_key = X25519PublicKey::from(blinded_point.to_bytes());
249
250            #[cfg(feature = "hop-metrics")]
251            {
252                blinding_ns = t5.elapsed().as_nanos() as u64;
253            }
254
255            ProcessResult::Forward {
256                next_hop,
257                next_packet: Box::new(SphinxHeader {
258                    ephemeral_key: next_ephemeral_key,
259                    routing_info: shifted_routing,
260                    mac: next_mac,
261                    nonce: 0,
262                }),
263                processed_body,
264            }
265        } else {
266            ProcessResult::Exit {
267                payload: processed_body,
268            }
269        };
270
271        #[cfg(feature = "hop-metrics")]
272        {
273            let total_sphinx_ns = total_start.elapsed().as_nanos() as u64;
274            Ok((
275                result,
276                HopTimings {
277                    ecdh_ns,
278                    key_derive_ns,
279                    mac_verify_ns,
280                    routing_decrypt_ns,
281                    body_decrypt_ns,
282                    blinding_ns,
283                    total_sphinx_ns,
284                },
285            ))
286        }
287
288        #[cfg(not(feature = "hop-metrics"))]
289        Ok(result)
290    }
291
292    pub fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), SphinxError> {
293        if bytes.len() < HEADER_SIZE {
294            return Err(SphinxError::InvalidSize);
295        }
296
297        let pk_end = 32;
298        let route_end = pk_end + ROUTING_INFO_SIZE;
299        let mac_end = route_end + MAC_SIZE;
300        let nonce_end = mac_end + NONCE_SIZE;
301
302        let pk_bytes: [u8; 32] = bytes[0..pk_end]
303            .try_into()
304            .map_err(|_| SphinxError::InvalidSize)?;
305        let ephemeral_key = X25519PublicKey::from(pk_bytes);
306
307        let mut routing_info = [0u8; ROUTING_INFO_SIZE];
308        routing_info.copy_from_slice(&bytes[pk_end..route_end]);
309
310        let mut mac = [0u8; MAC_SIZE];
311        mac.copy_from_slice(&bytes[route_end..mac_end]);
312
313        let nonce_bytes: [u8; 8] = bytes[mac_end..nonce_end]
314            .try_into()
315            .map_err(|_| SphinxError::InvalidSize)?;
316        let nonce = u64::from_be_bytes(nonce_bytes);
317
318        let header = SphinxHeader {
319            ephemeral_key,
320            routing_info,
321            mac,
322            nonce,
323        };
324
325        let payload = &bytes[nonce_end..];
326        Ok((header, payload))
327    }
328
329    #[must_use]
330    pub fn to_bytes(&self, payload: &[u8]) -> Vec<u8> {
331        let mut out = Vec::with_capacity(HEADER_SIZE + payload.len());
332        out.extend_from_slice(self.ephemeral_key.as_bytes());
333        out.extend_from_slice(&self.routing_info);
334        out.extend_from_slice(&self.mac);
335        out.extend_from_slice(&self.nonce.to_be_bytes());
336        out.extend_from_slice(payload);
337        out
338    }
339
340    #[must_use]
341    pub fn verify_pow(&self, difficulty: u32) -> bool {
342        if difficulty == 0 {
343            return true;
344        }
345        let hash = self.compute_hash();
346        pow::count_leading_zeros(&hash) >= difficulty
347    }
348
349    /// Solves `PoW` by brute-forcing nonces (single-threaded).
350    pub fn solve_pow(&mut self, difficulty: u32) -> Result<(), SphinxError> {
351        if difficulty == 0 {
352            return Ok(());
353        }
354        if difficulty > pow::MAX_DIFFICULTY {
355            return Err(pow::PowError::DifficultyTooHigh { difficulty }.into());
356        }
357        loop {
358            if self.verify_pow(difficulty) {
359                break;
360            }
361            self.nonce = self.nonce.wrapping_add(1);
362        }
363        Ok(())
364    }
365
366    /// Solves `PoW` using the parallel solver.
367    pub fn solve_pow_parallel(
368        &mut self,
369        difficulty: u32,
370        threads: usize,
371    ) -> Result<(), SphinxError> {
372        if difficulty == 0 {
373            return Ok(());
374        }
375
376        let solver = pow::PowSolver::new(pow::Sha256Pow, threads);
377        let hash_data = self.compute_hash_for_pow();
378        self.nonce = solver.solve(&hash_data, difficulty, self.nonce)?;
379        Ok(())
380    }
381
382    /// Header data without the nonce, for the `PoW` solver.
383    fn compute_hash_for_pow(&self) -> [u8; 32 + ROUTING_INFO_SIZE + MAC_SIZE] {
384        let mut data = [0u8; 32 + ROUTING_INFO_SIZE + MAC_SIZE];
385        data[..32].copy_from_slice(self.ephemeral_key.as_bytes());
386        data[32..32 + ROUTING_INFO_SIZE].copy_from_slice(&self.routing_info);
387        data[32 + ROUTING_INFO_SIZE..].copy_from_slice(&self.mac);
388        data
389    }
390
391    fn compute_hash(&self) -> [u8; 32] {
392        let mut hasher = Sha256::new();
393        hasher.update(self.ephemeral_key.as_bytes());
394        hasher.update(self.routing_info);
395        hasher.update(self.mac);
396        hasher.update(self.nonce.to_be_bytes());
397        hasher.finalize().into()
398    }
399}
400
401#[inline]
402#[must_use]
403pub fn derive_keys(shared_secret: &[u8; 32]) -> ([u8; 32], [u8; 32], [u8; 32], Scalar) {
404    let ss_bytes = shared_secret;
405
406    let mut hasher = Sha256::new();
407    hasher.update(b"rho");
408    hasher.update(ss_bytes);
409    let rho: [u8; 32] = hasher.finalize().into();
410
411    let mut hasher = Sha256::new();
412    hasher.update(b"mu");
413    hasher.update(ss_bytes);
414    let mu: [u8; 32] = hasher.finalize().into();
415
416    let mut hasher = Sha256::new();
417    hasher.update(b"pi");
418    hasher.update(ss_bytes);
419    let pi: [u8; 32] = hasher.finalize().into();
420
421    let mut hasher = Sha256::new();
422    hasher.update(b"blind");
423    hasher.update(ss_bytes);
424    let blind_bytes: [u8; 32] = hasher.finalize().into();
425    let blind_scalar = Scalar::from_bytes_mod_order(blind_bytes);
426
427    (rho, mu, pi, blind_scalar)
428}
429
430#[inline]
431#[allow(clippy::expect_used)]
432pub(crate) fn compute_mac(key: &[u8; 32], data: &[u8]) -> [u8; 32] {
433    let mut mac =
434        Hmac::<Sha256>::new_from_slice(key).expect("HMAC key length is always valid for SHA256");
435    mac.update(data);
436    mac.finalize().into_bytes().into()
437}
438
439#[inline]
440pub(crate) fn apply_stream_cipher(key: &[u8; 32], iv: &[u8; 12], data: &mut [u8]) {
441    let key = Key::from_slice(key);
442    let nonce = Nonce::from_slice(iv);
443    let mut cipher = ChaCha20::new(key, nonce);
444    cipher.apply_keystream(data);
445}
446
447/// Single-hop convenience wrapper for tests only.
448#[cfg(test)]
449pub fn build_packet(
450    node_pk: X25519PublicKey,
451    payload: &[u8],
452    _is_exit: bool,
453    pow_difficulty: u32,
454) -> Result<Vec<u8>, SphinxError> {
455    let path = vec![PathHop {
456        public_key: node_pk,
457        address: "0xEXIT".into(),
458    }];
459    build_multi_hop_packet(&path, payload, pow_difficulty)
460}
461
462pub fn build_multi_hop_packet(
463    path: &[PathHop],
464    payload: &[u8],
465    pow_difficulty: u32,
466) -> Result<Vec<u8>, SphinxError> {
467    if path.is_empty() {
468        return Err(SphinxError::Serialization("Empty path".into()));
469    }
470
471    let mut rng = rand::rngs::OsRng;
472
473    // Body must be padded to full capacity BEFORE Lioness encryption.
474    // Lioness output depends on the full block size -- a size mismatch between
475    // sender and relay produces garbage.
476    let body_capacity = packet::PACKET_SIZE - HEADER_SIZE;
477    let mut current_body = vec![0u8; body_capacity];
478    let copy_len = payload.len().min(body_capacity);
479    current_body[..copy_len].copy_from_slice(&payload[..copy_len]);
480
481    let mut ephemeral_public_keys = Vec::with_capacity(path.len());
482    let mut shared_secrets = Vec::with_capacity(path.len());
483    let mut blinding_factors = Vec::with_capacity(path.len());
484
485    // Raw Scalar avoids X25519 clamping interference during blinding
486    let mut current_secret_scalar = Scalar::random(&mut rng);
487    let initial_pk_point = X25519_BASEPOINT * current_secret_scalar;
488    let mut accumulated_blinding = Scalar::ONE;
489
490    for hop in path {
491        let hop_pk_point = MontgomeryPoint(hop.public_key.to_bytes());
492
493        let shared_point = hop_pk_point * current_secret_scalar;
494        let shared_bytes = shared_point.to_bytes();
495        shared_secrets.push(shared_bytes);
496
497        let (_, _, _, blinding) = derive_keys(&shared_bytes);
498        blinding_factors.push(blinding);
499
500        let blinded_point = initial_pk_point * accumulated_blinding;
501        let hop_pk = X25519PublicKey::from(blinded_point.to_bytes());
502        ephemeral_public_keys.push(hop_pk);
503
504        accumulated_blinding *= blinding;
505        current_secret_scalar *= blinding;
506    }
507
508    let mut filler = Vec::new();
509    for shared_secret in shared_secrets.iter().take(path.len().saturating_sub(1)) {
510        let (rho, _, _, _) = derive_keys(shared_secret);
511        let mut keystream = [0u8; ROUTING_INFO_SIZE + SHIFT_SIZE];
512        apply_stream_cipher(&rho, &[0u8; 12], &mut keystream);
513
514        let filler_start_in_keystream = ROUTING_INFO_SIZE - filler.len();
515        for (j, byte) in filler.iter_mut().enumerate() {
516            *byte ^= keystream[filler_start_in_keystream + j];
517        }
518        filler.extend_from_slice(&keystream[ROUTING_INFO_SIZE..ROUTING_INFO_SIZE + SHIFT_SIZE]);
519    }
520
521    if !filler.is_empty() && !path.is_empty() {
522        let (rho, _, _, _) = derive_keys(&shared_secrets[path.len() - 1]);
523        let mut keystream = [0u8; ROUTING_INFO_SIZE];
524        apply_stream_cipher(&rho, &[0u8; 12], &mut keystream);
525        let filler_start = ROUTING_INFO_SIZE - filler.len();
526        for (j, byte) in filler.iter_mut().enumerate() {
527            *byte ^= keystream[filler_start + j];
528        }
529    }
530
531    let mut routing_info = [0u8; ROUTING_INFO_SIZE];
532    let mut next_mac = [0u8; 32];
533
534    for i in (0..path.len()).rev() {
535        let shared = shared_secrets[i];
536        let (rho, mu, pi, _) = derive_keys(&shared);
537
538        let body_keys = lioness::LionessKeys::from_pi(&pi);
539        lioness::lioness_encrypt(&body_keys, &mut current_body);
540
541        let mut current_routing = [0u8; ROUTING_INFO_SIZE];
542        if i == path.len() - 1 {
543            current_routing[0] = 0x01;
544            if !filler.is_empty() {
545                let filler_start = ROUTING_INFO_SIZE - filler.len();
546                current_routing[filler_start..].copy_from_slice(&filler);
547            }
548        } else {
549            current_routing[0] = 0x00;
550            let next_addr_bytes = path[i + 1].address.as_bytes();
551            current_routing[1] = u8::try_from(next_addr_bytes.len()).map_err(|_| {
552                SphinxError::Serialization(format!(
553                    "Address too long ({} bytes, max 255)",
554                    next_addr_bytes.len()
555                ))
556            })?;
557            current_routing[2..34].copy_from_slice(&next_mac);
558            current_routing[34..34 + next_addr_bytes.len()].copy_from_slice(next_addr_bytes);
559
560            let remainder_len = ROUTING_INFO_SIZE - SHIFT_SIZE;
561            current_routing[SHIFT_SIZE..].copy_from_slice(&routing_info[0..remainder_len]);
562        }
563
564        apply_stream_cipher(&rho, &[0u8; 12], &mut current_routing);
565        let mac = compute_mac(&mu, &current_routing);
566
567        routing_info = current_routing;
568        next_mac = mac;
569    }
570
571    let mut final_header = SphinxHeader {
572        ephemeral_key: ephemeral_public_keys[0],
573        routing_info,
574        mac: next_mac,
575        nonce: 0,
576    };
577
578    final_header.solve_pow(pow_difficulty)?;
579    Ok(final_header.to_bytes(&current_body))
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    #[test]
587    // #[ignore]
588    fn test_multi_hop_integrity() {
589        let mut rng = rand::thread_rng();
590
591        let sks: Vec<X25519SecretKey> = (0..3)
592            .map(|_| X25519SecretKey::random_from_rng(&mut rng))
593            .collect();
594        let pks: Vec<X25519PublicKey> = sks.iter().map(X25519PublicKey::from).collect();
595
596        let path = vec![
597            PathHop {
598                public_key: pks[0],
599                address: "node_0_addr".into(),
600            },
601            PathHop {
602                public_key: pks[1],
603                address: "node_1_addr".into(),
604            },
605            PathHop {
606                public_key: pks[2],
607                address: "EXIT_ADDR".into(),
608            },
609        ];
610
611        let payload = b"DeepOnion".to_vec();
612        println!("Original Payload: {:?}", payload);
613
614        let packet = build_multi_hop_packet(&path, &payload, 0).expect("Build failed");
615
616        println!("\n>>> Processing Hop 0");
617        let (header0, body0) = SphinxHeader::from_bytes(&packet).unwrap();
618        let res0 = into_result(
619            header0
620                .process(&sks[0], body0.to_vec())
621                .expect("Hop 0 failed"),
622        );
623
624        let (next_hop0, packet0, body0_peeled) = match res0 {
625            ProcessResult::Forward {
626                next_hop,
627                next_packet,
628                processed_body,
629                ..
630            } => (next_hop, next_packet, processed_body),
631            _ => panic!("Hop 0 should be forward"),
632        };
633        assert_eq!(next_hop0, "node_1_addr");
634
635        let bytes1 = packet0.to_bytes(&body0_peeled);
636        let (header1, body1) = SphinxHeader::from_bytes(&bytes1).unwrap();
637
638        println!("\n>>> Processing Hop 1");
639        let res1 = into_result(
640            header1
641                .process(&sks[1], body1.to_vec())
642                .expect("Hop 1 failed"),
643        );
644
645        let (next_hop1, packet1, body1_peeled) = match res1 {
646            ProcessResult::Forward {
647                next_hop,
648                next_packet,
649                processed_body,
650                ..
651            } => (next_hop, next_packet, processed_body),
652            _ => panic!("Hop 1 should be forward"),
653        };
654        assert_eq!(next_hop1, "EXIT_ADDR");
655        let bytes2 = packet1.to_bytes(&body1_peeled);
656        let (header2, body2) = SphinxHeader::from_bytes(&bytes2).unwrap();
657
658        println!("\n>>> Processing Hop 2");
659        let res2 = into_result(
660            header2
661                .process(&sks[2], body2.to_vec())
662                .expect("Hop 2 failed"),
663        );
664
665        match res2 {
666            ProcessResult::Exit {
667                payload: final_payload,
668            } => {
669                // Body is padded to full capacity (32296 bytes). The payload
670                // sits at the start, followed by zero padding.
671                let body_capacity = packet::PACKET_SIZE - HEADER_SIZE;
672                assert_eq!(final_payload.len(), body_capacity);
673                assert_eq!(&final_payload[..payload.len()], payload.as_slice(),);
674                // Remaining bytes must be zero padding
675                assert!(final_payload[payload.len()..].iter().all(|&b| b == 0),);
676            }
677            _ => panic!("Hop 2 should be exit"),
678        }
679    }
680
681    /// Regression: body must be padded BEFORE Lioness encryption to match relay body size.
682    #[test]
683    fn test_lioness_body_size_consistency() {
684        let mut rng = rand::thread_rng();
685        let sks: Vec<X25519SecretKey> = (0..3)
686            .map(|_| X25519SecretKey::random_from_rng(&mut rng))
687            .collect();
688        let pks: Vec<X25519PublicKey> = sks.iter().map(X25519PublicKey::from).collect();
689
690        let path = vec![
691            PathHop {
692                public_key: pks[0],
693                address: "entry".into(),
694            },
695            PathHop {
696                public_key: pks[1],
697                address: "mix".into(),
698            },
699            PathHop {
700                public_key: pks[2],
701                address: "exit".into(),
702            },
703        ];
704
705        let payload = vec![0x42u8; 91];
706        let packet = build_multi_hop_packet(&path, &payload, 0).expect("Build failed");
707
708        assert_eq!(packet.len(), packet::PACKET_SIZE);
709
710        let (h0, b0) = SphinxHeader::from_bytes(&packet).expect("Parse failed");
711        let body_capacity = packet::PACKET_SIZE - HEADER_SIZE;
712        assert_eq!(b0.len(), body_capacity);
713
714        let r0 = into_result(h0.process(&sks[0], b0.to_vec()).expect("Hop 0 failed"));
715        let (p1, b1) = match r0 {
716            ProcessResult::Forward {
717                next_packet,
718                processed_body,
719                ..
720            } => (next_packet, processed_body),
721            _ => panic!("Expected forward at hop 0"),
722        };
723        assert_eq!(b1.len(), body_capacity);
724
725        let bytes1 = p1.to_bytes(&b1);
726        let (h1, b1r) = SphinxHeader::from_bytes(&bytes1).expect("Parse failed");
727        let r1 = into_result(h1.process(&sks[1], b1r.to_vec()).expect("Hop 1 failed"));
728        let (p2, b2) = match r1 {
729            ProcessResult::Forward {
730                next_packet,
731                processed_body,
732                ..
733            } => (next_packet, processed_body),
734            _ => panic!("Expected forward at hop 1"),
735        };
736        assert_eq!(b2.len(), body_capacity);
737
738        let bytes2 = p2.to_bytes(&b2);
739        let (h2, b2r) = SphinxHeader::from_bytes(&bytes2).expect("Parse failed");
740        let r2 = into_result(h2.process(&sks[2], b2r.to_vec()).expect("Hop 2 failed"));
741
742        match r2 {
743            ProcessResult::Exit {
744                payload: exit_payload,
745            } => {
746                assert_eq!(exit_payload.len(), body_capacity);
747                assert_eq!(&exit_payload[..payload.len()], payload.as_slice());
748                assert!(exit_payload[payload.len()..].iter().all(|&b| b == 0));
749            }
750            _ => panic!("Expected exit at hop 2"),
751        }
752    }
753
754    #[test]
755    fn test_sphinx_peel_hop() {
756        let mut rng = rand::thread_rng();
757
758        let node_sk = X25519SecretKey::random_from_rng(&mut rng);
759        let node_pk = X25519PublicKey::from(&node_sk);
760
761        let sender_ephemeral_sk = X25519SecretKey::random_from_rng(&mut rng);
762        let sender_ephemeral_pk = X25519PublicKey::from(&sender_ephemeral_sk);
763
764        let shared_secret = sender_ephemeral_sk.diffie_hellman(&node_pk);
765        let (rho, mu, _pi, _blind) = derive_keys(shared_secret.as_bytes());
766
767        let next_hop = "NEXT";
768        let mut routing_data = [0u8; ROUTING_INFO_SIZE];
769        routing_data[0] = 0x00; // Forward
770        routing_data[1] = next_hop.len() as u8;
771        routing_data[2..34].copy_from_slice(&[0u8; 32]); // Dummy next MAC
772        routing_data[34..34 + next_hop.len()].copy_from_slice(next_hop.as_bytes());
773
774        apply_stream_cipher(&rho, &[0u8; 12], &mut routing_data);
775        let mac = compute_mac(&mu, &routing_data);
776
777        let header = SphinxHeader {
778            ephemeral_key: sender_ephemeral_pk,
779            routing_info: routing_data,
780            mac,
781            nonce: 0,
782        };
783
784        let body = vec![0u8; lioness::MIN_BODY_SIZE];
785        let result = into_result(header.process(&node_sk, body).unwrap());
786
787        match result {
788            ProcessResult::Forward { next_hop: h, .. } => {
789                assert_eq!(h, "NEXT");
790            }
791            _ => panic!("Expected Forward result"),
792        }
793    }
794
795    #[test]
796    fn test_integrity_failure() {
797        let mut rng = rand::thread_rng();
798        let node_sk = X25519SecretKey::random_from_rng(&mut rng);
799        let dummy_sk = X25519SecretKey::random_from_rng(&mut rng);
800        let dummy_pk = X25519PublicKey::from(&dummy_sk);
801
802        let header = SphinxHeader {
803            ephemeral_key: dummy_pk,
804            routing_info: [0u8; ROUTING_INFO_SIZE],
805            mac: [0u8; 32],
806            nonce: 0,
807        };
808
809        let result = header.process(&node_sk, vec![]);
810        assert!(matches!(result, Err(SphinxError::MacMismatch)));
811    }
812
813    #[test]
814    fn test_identical_payloads_produce_unique_packets() {
815        use sha2::{Digest, Sha256};
816
817        let mut rng = rand::thread_rng();
818
819        let node_sk = X25519SecretKey::random_from_rng(&mut rng);
820        let node_pk = X25519PublicKey::from(&node_sk);
821
822        let payload = b"PAYLOAD_REPLAY_CHECK".to_vec();
823
824        let packet_1 = build_packet(node_pk, &payload, true, 0).unwrap();
825        let packet_2 = build_packet(node_pk, &payload, true, 0).unwrap();
826
827        assert_ne!(packet_1, packet_2);
828
829        let mut hasher1 = Sha256::new();
830        hasher1.update(&packet_1);
831        let hash1 = hasher1.finalize();
832
833        let mut hasher2 = Sha256::new();
834        hasher2.update(&packet_2);
835        let hash2 = hasher2.finalize();
836
837        assert_ne!(hash1, hash2);
838    }
839
840    #[test]
841    fn test_sphinx_fuzzing_no_panic() {
842        use rand::Rng;
843
844        let mut rng = rand::thread_rng();
845
846        for _ in 0..1000 {
847            let len = rng.gen_range(0..2000);
848            let mut bytes = vec![0u8; len];
849            rng.fill(&mut bytes[..]);
850
851            let result = SphinxHeader::from_bytes(&bytes);
852            if let Err(e) = result {
853                assert!(!format!("{}", e).is_empty());
854            }
855        }
856    }
857
858    #[test]
859    fn test_derive_keys_deterministic() {
860        let ss = [0x42u8; 32];
861        let (rho1, mu1, pi1, blind1) = derive_keys(&ss);
862        let (rho2, mu2, pi2, blind2) = derive_keys(&ss);
863        assert_eq!(rho1, rho2);
864        assert_eq!(mu1, mu2);
865        assert_eq!(pi1, pi2);
866        assert_eq!(blind1, blind2);
867    }
868
869    #[test]
870    fn test_derive_keys_different_shared_secrets() {
871        let ss_a = [0x01u8; 32];
872        let ss_b = [0x02u8; 32];
873        let (rho_a, mu_a, pi_a, _) = derive_keys(&ss_a);
874        let (rho_b, mu_b, pi_b, _) = derive_keys(&ss_b);
875        assert_ne!(rho_a, rho_b);
876        assert_ne!(mu_a, mu_b);
877        assert_ne!(pi_a, pi_b);
878    }
879
880    #[test]
881    fn test_derive_keys_outputs_are_distinct() {
882        let ss = [0x55u8; 32];
883        let (rho, mu, pi, blind) = derive_keys(&ss);
884        let blind_bytes = blind.to_bytes();
885        assert_ne!(rho, mu);
886        assert_ne!(rho, pi);
887        assert_ne!(mu, pi);
888        assert_ne!(rho, blind_bytes);
889    }
890
891    #[test]
892    fn test_compute_mac_known_vector() {
893        let key = [0u8; 32];
894        let data = b"test";
895        let mac = compute_mac(&key, data);
896
897        let mac2 = compute_mac(&key, data);
898        assert_eq!(mac, mac2);
899
900        assert_ne!(mac, [0u8; 32]);
901        assert_eq!(mac.len(), 32);
902
903        let mac_diff = compute_mac(&key, b"different");
904        assert_ne!(mac, mac_diff);
905    }
906
907    /// ChaCha20 XOR is its own inverse -- foundational property for Sphinx.
908    #[test]
909    fn test_apply_stream_cipher_identity() {
910        let key = [0xABu8; 32];
911        let iv = [0u8; 12];
912        let original = b"sphinx routing information payload".to_vec();
913
914        let mut data = original.clone();
915        apply_stream_cipher(&key, &iv, &mut data);
916        assert_ne!(data, original);
917
918        apply_stream_cipher(&key, &iv, &mut data);
919        assert_eq!(data, original);
920    }
921
922    #[test]
923    fn test_apply_stream_cipher_different_keys() {
924        let key_a = [0x11u8; 32];
925        let key_b = [0x22u8; 32];
926        let iv = [0u8; 12];
927        let plaintext = b"hello sphinx world".to_vec();
928
929        let mut ct_a = plaintext.clone();
930        apply_stream_cipher(&key_a, &iv, &mut ct_a);
931
932        let mut ct_b = plaintext.clone();
933        apply_stream_cipher(&key_b, &iv, &mut ct_b);
934
935        assert_ne!(ct_a, ct_b);
936    }
937
938    #[test]
939    fn test_replay_tag_uniqueness() {
940        use rand::Rng;
941        let mut rng = rand::thread_rng();
942
943        let mut tags = std::collections::HashSet::new();
944        for _ in 0..20 {
945            let mut routing_info = [0u8; ROUTING_INFO_SIZE];
946            rng.fill(&mut routing_info[..]);
947            let mut mac = [0u8; MAC_SIZE];
948            rng.fill(&mut mac[..]);
949            let nonce: u64 = rng.gen();
950
951            let header = SphinxHeader {
952                ephemeral_key: X25519PublicKey::from(&X25519SecretKey::random_from_rng(&mut rng)),
953                routing_info,
954                mac,
955                nonce,
956            };
957            let tag = header.compute_replay_tag();
958            tags.insert(tag);
959        }
960        assert_eq!(tags.len(), 20);
961    }
962
963    /// HEADER_SIZE must match the actual wire size -- SURB parsing depends on it.
964    #[test]
965    fn test_sphinx_header_size_constant() {
966        use rand::Rng;
967        let mut rng = rand::thread_rng();
968
969        let mut routing_info = [0u8; ROUTING_INFO_SIZE];
970        rng.fill(&mut routing_info[..]);
971        let mut mac = [0u8; MAC_SIZE];
972        rng.fill(&mut mac[..]);
973
974        let header = SphinxHeader {
975            ephemeral_key: X25519PublicKey::from(&X25519SecretKey::random_from_rng(&mut rng)),
976            routing_info,
977            mac,
978            nonce: 12345,
979        };
980
981        let serialized = header.to_bytes(&[]);
982        assert_eq!(
983            serialized.len(),
984            HEADER_SIZE,
985            "to_bytes(&[]) produced {} bytes, expected HEADER_SIZE={HEADER_SIZE}",
986            serialized.len()
987        );
988
989        assert_eq!(HEADER_SIZE, 32 + ROUTING_INFO_SIZE + MAC_SIZE + NONCE_SIZE);
990    }
991}