nym_sphinx_framing/
processing.rs

1// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::packet::FramedNymPacket;
5use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
6use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
7use nym_sphinx_forwarding::packet::MixPacket;
8use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation};
9use nym_sphinx_types::header::shared_secret::ExpandedSharedSecret;
10use nym_sphinx_types::{
11    Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
12    NymProcessedPacket, OutfoxError, OutfoxProcessedPacket, PrivateKey, ProcessedPacketData,
13    REPLAY_TAG_SIZE, SphinxError, Version as SphinxPacketVersion,
14};
15use std::fmt::Display;
16use thiserror::Error;
17use tracing::{debug, trace};
18
19#[derive(Debug)]
20pub enum MixProcessingResultData {
21    /// Contains unwrapped data that should first get delayed before being sent to next hop.
22    ForwardHop {
23        packet: MixPacket,
24        delay: Option<SphinxDelay>,
25    },
26
27    /// Contains all data extracted out of the final hop packet that could be forwarded to the destination.
28    FinalHop { final_hop_data: ProcessedFinalHop },
29}
30
31#[derive(Debug, Copy, Clone)]
32pub enum MixPacketVersion {
33    Outfox,
34    Sphinx(SphinxPacketVersion),
35}
36
37impl Display for MixPacketVersion {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
39        match self {
40            MixPacketVersion::Outfox => "outfox".fmt(f),
41            MixPacketVersion::Sphinx(sphinx_version) => {
42                write!(f, "sphinx-{}", sphinx_version.value())
43            }
44        }
45    }
46}
47
48#[derive(Debug)]
49pub struct MixProcessingResult {
50    pub packet_version: MixPacketVersion,
51    pub processing_data: MixProcessingResultData,
52}
53
54#[allow(clippy::large_enum_variant)]
55#[derive(Debug)]
56pub enum PartialMixProcessingResult {
57    Sphinx {
58        expanded_shared_secret: ExpandedSharedSecret,
59    },
60    Outfox,
61}
62
63impl PartialMixProcessingResult {
64    pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
65        match self {
66            PartialMixProcessingResult::Sphinx {
67                expanded_shared_secret,
68            } => Some(expanded_shared_secret.replay_tag()),
69            PartialMixProcessingResult::Outfox => None,
70        }
71    }
72}
73
74type ForwardAck = MixPacket;
75
76#[derive(Debug)]
77pub struct ProcessedFinalHop {
78    pub destination: DestinationAddressBytes,
79    pub forward_ack: Option<ForwardAck>,
80    pub message: Vec<u8>,
81}
82
83#[derive(Debug, Error)]
84pub enum PacketProcessingError {
85    #[error("failed to process received packet: {0}")]
86    NymPacketProcessingError(#[from] NymPacketError),
87
88    #[error("failed to process received sphinx packet: {0}")]
89    SphinxProcessingError(#[from] SphinxError),
90
91    #[error("the forward hop address was malformed: {0}")]
92    InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError),
93
94    #[error("the final hop did not contain a SURB-Ack")]
95    NoSurbAckInFinalHop,
96
97    #[error("failed to recover the expected SURB-Ack packet: {0}")]
98    MalformedSurbAck(#[from] SurbAckRecoveryError),
99
100    #[error("failed to process received outfox packet: {0}")]
101    OutfoxProcessingError(#[from] OutfoxError),
102
103    #[error("attempted to partially process an outfox packet")]
104    PartialOutfoxProcessing,
105
106    #[error("the key needed for unwrapping this packet has already expired")]
107    ExpiredKey,
108
109    #[error("this packet has already been processed before")]
110    PacketReplay,
111}
112
113pub struct PartialyUnwrappedPacketWithKeyRotation {
114    pub packet: PartiallyUnwrappedPacket,
115    pub used_key_rotation: u32,
116}
117
118pub struct PartiallyUnwrappedPacket {
119    received_data: FramedNymPacket,
120    partial_result: PartialMixProcessingResult,
121}
122
123impl PartiallyUnwrappedPacket {
124    /// Attempt to partially unwrap received packet to derive relevant keys
125    /// to allow us to reject it for obvious bad behaviour (like replay or invalid mac)
126    /// without performing full processing
127    pub fn new(
128        received_data: FramedNymPacket,
129        sphinx_key: &PrivateKey,
130    ) -> Result<Self, (FramedNymPacket, PacketProcessingError)> {
131        let partial_result = match received_data.packet() {
132            NymPacket::Sphinx(packet) => {
133                let expanded_shared_secret =
134                    packet.header.compute_expanded_shared_secret(sphinx_key);
135
136                // don't continue if the header is malformed
137                if let Err(err) = packet
138                    .header
139                    .ensure_header_integrity(&expanded_shared_secret)
140                {
141                    return Err((received_data, err.into()));
142                }
143
144                PartialMixProcessingResult::Sphinx {
145                    expanded_shared_secret,
146                }
147            }
148
149            NymPacket::Outfox(_) => PartialMixProcessingResult::Outfox,
150        };
151        Ok(PartiallyUnwrappedPacket {
152            received_data,
153            partial_result,
154        })
155    }
156
157    pub fn finalise_unwrapping(self) -> Result<MixProcessingResult, PacketProcessingError> {
158        let packet_size = self.received_data.packet_size();
159        let packet_type = self.received_data.packet_type();
160
161        let key_rotation = self.received_data.header.key_rotation;
162        let packet = self.received_data.into_inner();
163
164        // currently partial unwrapping is only implemented for sphinx packets.
165        // attempting to call it for anything else should result in a failure
166        let (
167            NymPacket::Sphinx(packet),
168            PartialMixProcessingResult::Sphinx {
169                expanded_shared_secret,
170            },
171        ) = (packet, self.partial_result)
172        else {
173            return Err(PacketProcessingError::PartialOutfoxProcessing);
174        };
175        let processed_packet = packet.process_with_expanded_secret(&expanded_shared_secret)?;
176        wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type, key_rotation)
177    }
178
179    pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
180        self.partial_result.replay_tag()
181    }
182
183    pub fn with_key_rotation(
184        self,
185        used_key_rotation: u32,
186    ) -> PartialyUnwrappedPacketWithKeyRotation {
187        PartialyUnwrappedPacketWithKeyRotation {
188            packet: self,
189            used_key_rotation,
190        }
191    }
192}
193
194impl From<(FramedNymPacket, PartialMixProcessingResult)> for PartiallyUnwrappedPacket {
195    fn from(
196        (received_data, partial_result): (FramedNymPacket, PartialMixProcessingResult),
197    ) -> Self {
198        PartiallyUnwrappedPacket {
199            received_data,
200            partial_result,
201        }
202    }
203}
204
205pub fn process_framed_packet(
206    received: FramedNymPacket,
207    sphinx_key: &PrivateKey,
208) -> Result<MixProcessingResult, PacketProcessingError> {
209    let packet_size = received.packet_size();
210    let packet_type = received.packet_type();
211    let key_rotation = received.key_rotation();
212
213    // unwrap the sphinx packet
214    let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
215
216    // for forward packets, extract next hop and set delay (but do NOT delay here)
217    // for final packets, extract SURBAck
218    perform_final_processing(processed_packet, packet_size, packet_type, key_rotation)
219}
220
221fn perform_framed_unwrapping(
222    received: FramedNymPacket,
223    sphinx_key: &PrivateKey,
224) -> Result<NymProcessedPacket, PacketProcessingError> {
225    let packet = received.into_inner();
226    perform_framed_packet_processing(packet, sphinx_key)
227}
228
229fn perform_framed_packet_processing(
230    packet: NymPacket,
231    sphinx_key: &PrivateKey,
232) -> Result<NymProcessedPacket, PacketProcessingError> {
233    packet.process(sphinx_key).map_err(|err| {
234        debug!("Failed to unwrap NymPacket packet: {err}");
235        PacketProcessingError::NymPacketProcessingError(err)
236    })
237}
238
239fn wrap_processed_sphinx_packet(
240    packet: nym_sphinx_types::ProcessedPacket,
241    packet_size: PacketSize,
242    packet_type: PacketType,
243    key_rotation: SphinxKeyRotation,
244) -> Result<MixProcessingResult, PacketProcessingError> {
245    let processing_data = match packet.data {
246        ProcessedPacketData::ForwardHop {
247            next_hop_packet,
248            next_hop_address,
249            delay,
250        } => process_forward_hop(
251            NymPacket::Sphinx(next_hop_packet),
252            next_hop_address,
253            delay,
254            packet_type,
255            key_rotation,
256        ),
257        // right now there's no use for the surb_id included in the header - probably it should get removed from the
258        // sphinx all together?
259        ProcessedPacketData::FinalHop {
260            destination,
261            identifier: _,
262            payload,
263        } => process_final_hop(
264            destination,
265            payload.recover_plaintext()?,
266            packet_size,
267            packet_type,
268            key_rotation,
269        ),
270    }?;
271
272    Ok(MixProcessingResult {
273        packet_version: MixPacketVersion::Sphinx(packet.version),
274        processing_data,
275    })
276}
277
278fn wrap_processed_outfox_packet(
279    packet: OutfoxProcessedPacket,
280    packet_size: PacketSize,
281    packet_type: PacketType,
282    key_rotation: SphinxKeyRotation,
283) -> Result<MixProcessingResult, PacketProcessingError> {
284    let next_address = *packet.next_address();
285    let packet = packet.into_packet();
286    if packet.is_final_hop() {
287        let processing_data = process_final_hop(
288            DestinationAddressBytes::from_bytes(next_address),
289            packet.recover_plaintext()?.to_vec(),
290            packet_size,
291            packet_type,
292            key_rotation,
293        )?;
294        Ok(MixProcessingResult {
295            packet_version: MixPacketVersion::Outfox,
296            processing_data,
297        })
298    } else {
299        let packet = MixPacket::new(
300            NymNodeRoutingAddress::try_from_bytes(&next_address)?,
301            NymPacket::Outfox(packet),
302            PacketType::Outfox,
303            SphinxKeyRotation::Unknown,
304        );
305        Ok(MixProcessingResult {
306            packet_version: MixPacketVersion::Outfox,
307            processing_data: MixProcessingResultData::ForwardHop {
308                packet,
309                delay: None,
310            },
311        })
312    }
313}
314
315fn perform_final_processing(
316    packet: NymProcessedPacket,
317    packet_size: PacketSize,
318    packet_type: PacketType,
319    key_rotation: SphinxKeyRotation,
320) -> Result<MixProcessingResult, PacketProcessingError> {
321    match packet {
322        NymProcessedPacket::Sphinx(packet) => {
323            wrap_processed_sphinx_packet(packet, packet_size, packet_type, key_rotation)
324        }
325        NymProcessedPacket::Outfox(packet) => {
326            wrap_processed_outfox_packet(packet, packet_size, packet_type, key_rotation)
327        }
328    }
329}
330
331fn process_final_hop(
332    destination: DestinationAddressBytes,
333    payload: Vec<u8>,
334    packet_size: PacketSize,
335    packet_type: PacketType,
336    key_rotation: SphinxKeyRotation,
337) -> Result<MixProcessingResultData, PacketProcessingError> {
338    let (forward_ack, message) =
339        split_into_ack_and_message(payload, packet_size, packet_type, key_rotation)?;
340
341    Ok(MixProcessingResultData::FinalHop {
342        final_hop_data: ProcessedFinalHop {
343            destination,
344            forward_ack,
345            message,
346        },
347    })
348}
349
350fn split_into_ack_and_message(
351    data: Vec<u8>,
352    packet_size: PacketSize,
353    packet_type: PacketType,
354    key_rotation: SphinxKeyRotation,
355) -> Result<(Option<MixPacket>, Vec<u8>), PacketProcessingError> {
356    match packet_size {
357        PacketSize::AckPacket | PacketSize::OutfoxAckPacket => {
358            trace!("received an ack packet!");
359            Ok((None, data))
360        }
361        PacketSize::RegularPacket
362        | PacketSize::ExtendedPacket8
363        | PacketSize::ExtendedPacket16
364        | PacketSize::ExtendedPacket32
365        | PacketSize::OutfoxRegularPacket => {
366            trace!("received a normal packet!");
367            cfg_if::cfg_if! {
368                if #[cfg(feature = "no-mix-acks")] {
369                    let _ = packet_type;
370                    let _ = key_rotation;
371
372                    // AIDEV-NOTE: When no-mix-acks is enabled, skip ack extraction entirely.
373                    // The full payload (including ack portion) is returned as the message.
374                    Ok((None, data))
375                } else {
376                    let (ack_data, message) = split_hop_data_into_ack_and_message(data, packet_type)?;
377                    let (ack_first_hop, ack_packet) =
378                        match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) {
379                            Ok((first_hop, packet)) => (first_hop, packet),
380                            Err(err) => {
381                                tracing::info!("Failed to recover first hop from ack data: {err}");
382                                return Err(err.into());
383                            }
384                        };
385                    let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type, key_rotation);
386                    Ok((Some(forward_ack), message))
387                }
388            }
389        }
390    }
391}
392
393#[allow(dead_code)]
394fn split_hop_data_into_ack_and_message(
395    mut extracted_data: Vec<u8>,
396    packet_type: PacketType,
397) -> Result<(Vec<u8>, Vec<u8>), PacketProcessingError> {
398    let ack_len = SurbAck::len(Some(packet_type));
399
400    // in theory it's impossible for this to fail since it managed to go into correct `match`
401    // branch at the caller
402    if extracted_data.len() < ack_len {
403        return Err(PacketProcessingError::NoSurbAckInFinalHop);
404    }
405
406    let message = extracted_data.split_off(ack_len);
407    let ack_data = extracted_data;
408    Ok((ack_data, message))
409}
410
411fn process_forward_hop(
412    packet: NymPacket,
413    forward_address: NodeAddressBytes,
414    delay: SphinxDelay,
415    packet_type: PacketType,
416    key_rotation: SphinxKeyRotation,
417) -> Result<MixProcessingResultData, PacketProcessingError> {
418    let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
419
420    let packet = MixPacket::new(next_hop_address, packet, packet_type, key_rotation);
421    Ok(MixProcessingResultData::ForwardHop {
422        packet,
423        delay: Some(delay),
424    })
425}
426
427// TODO: what more could we realistically test here?
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[tokio::test]
433    async fn splitting_hop_data_works_for_sufficiently_long_payload() {
434        let short_data = vec![42u8];
435        assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Mix).is_err());
436
437        let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))];
438        let (ack, data) =
439            split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix).unwrap();
440        assert_eq!(sufficient_data, ack);
441        assert!(data.is_empty());
442
443        let long_data: Vec<u8> = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5];
444        let (ack, data) = split_hop_data_into_ack_and_message(long_data, PacketType::Mix).unwrap();
445        assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix)));
446        assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4)
447    }
448
449    #[tokio::test]
450    async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() {
451        let short_data = vec![42u8];
452        assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Outfox).is_err());
453
454        let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))];
455        let (ack, data) =
456            split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox)
457                .unwrap();
458        assert_eq!(sufficient_data, ack);
459        assert!(data.is_empty());
460
461        let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5];
462        let (ack, data) =
463            split_hop_data_into_ack_and_message(long_data, PacketType::Outfox).unwrap();
464        assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox)));
465        assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4)
466    }
467
468    #[tokio::test]
469    async fn splitting_into_ack_and_message_returns_whole_data_for_ack() {
470        let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10];
471        let (ack, message) = split_into_ack_and_message(
472            data.clone(),
473            PacketSize::AckPacket,
474            PacketType::Mix,
475            SphinxKeyRotation::EvenRotation,
476        )
477        .unwrap();
478        assert!(ack.is_none());
479        assert_eq!(data, message)
480    }
481
482    #[tokio::test]
483    async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() {
484        let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10];
485        let (ack, message) = split_into_ack_and_message(
486            data.clone(),
487            PacketSize::OutfoxAckPacket,
488            PacketType::Outfox,
489            SphinxKeyRotation::EvenRotation,
490        )
491        .unwrap();
492        assert!(ack.is_none());
493        assert_eq!(data, message)
494    }
495}