hopr_crypto_packet/
packet.rs

1use hopr_crypto_sphinx::{
2    derivation::derive_packet_tag,
3    prp::{PRPParameters, PRP},
4    routing::{forward_header, header_length, ForwardedHeader, RoutingInfo},
5    shared_keys::{Alpha, GroupElement, SharedKeys, SharedSecret, SphinxSuite},
6};
7use hopr_crypto_types::{
8    keypairs::Keypair,
9    primitives::{DigestLike, SimpleMac},
10    types::PacketTag,
11};
12use hopr_internal_types::prelude::*;
13use hopr_primitive_types::prelude::*;
14use std::fmt::{Debug, Formatter};
15use std::marker::PhantomData;
16use typenum::Unsigned;
17
18use crate::{
19    errors::{PacketError::PacketDecodingError, Result},
20    packet::ForwardedMetaPacket::{Final, Relayed},
21    por::POR_SECRET_LENGTH,
22};
23
24/// Tag used to separate padding from data
25const PADDING_TAG: &[u8] = b"HOPR";
26
27/// Determines the total length (header + payload) of the packet given the header information.
28pub const fn packet_length<S: SphinxSuite>(
29    max_hops: usize,
30    additional_data_relayer_len: usize,
31    additional_data_last_hop_len: usize,
32) -> usize {
33    <S::P as Keypair>::Public::SIZE
34        + header_length::<S>(max_hops, additional_data_relayer_len, additional_data_last_hop_len)
35        + SimpleMac::SIZE
36        + PAYLOAD_SIZE
37        + PADDING_TAG.len()
38}
39
40// TODO: Make padding length prefixed in 3.0
41
42fn add_padding(msg: &[u8]) -> Box<[u8]> {
43    assert!(msg.len() <= PAYLOAD_SIZE, "message too long for padding");
44
45    let padded_len = PAYLOAD_SIZE + PADDING_TAG.len();
46    let mut ret = vec![0u8; padded_len];
47    ret[padded_len - msg.len()..padded_len].copy_from_slice(msg);
48
49    // Zeroes and the PADDING_TAG are prepended to the message to form padding
50    ret[padded_len - msg.len() - PADDING_TAG.len()..padded_len - msg.len()].copy_from_slice(PADDING_TAG);
51    ret.into_boxed_slice()
52}
53
54fn remove_padding(msg: &[u8]) -> Option<&[u8]> {
55    assert_eq!(
56        PAYLOAD_SIZE + PADDING_TAG.len(),
57        msg.len(),
58        "padded message must be {} bytes long",
59        PAYLOAD_SIZE + PADDING_TAG.len()
60    );
61    let pos = msg
62        .windows(PADDING_TAG.len())
63        .position(|window| window == PADDING_TAG)?;
64    Some(&msg.split_at(pos).1[PADDING_TAG.len()..])
65}
66
67/// An encrypted packet.
68///
69/// A sender can create a new packet via [MetaPacket::new] and send it.
70/// Once received by the recipient, it is parsed first by calling [MetaPacket::try_from]
71/// and then it can be transformed into [ForwardedMetaPacket] by calling
72/// the [MetaPacket::into_forwarded] method. The [ForwardedMetaPacket] then contains the information
73/// about the next recipient of this packet.
74///
75/// The packet format is directly dependent on the used [SphinxSuite].
76pub struct MetaPacket<S: SphinxSuite> {
77    packet: Box<[u8]>,
78    _s: PhantomData<S>,
79}
80
81impl<S: SphinxSuite> Debug for MetaPacket<S> {
82    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83        write!(f, "[{}]", hex::encode(&self.packet))
84    }
85}
86
87// Needs manual Clone implementation to not impose Clone restriction on `S`
88impl<S: SphinxSuite> Clone for MetaPacket<S> {
89    fn clone(&self) -> Self {
90        Self {
91            packet: self.packet.clone(),
92            _s: PhantomData,
93        }
94    }
95}
96
97/// Represent a [MetaPacket] with one layer of encryption removed, exposing the details
98/// about the next hop.
99///
100/// There are two possible states - either the packet is intended for the recipient,
101/// and is thus [Final], or it is meant to be sent (relayed)
102/// to the next hop - thus it is [Relayed].
103#[allow(dead_code)]
104pub enum ForwardedMetaPacket<S: SphinxSuite> {
105    /// The content is another [MetaPacket] meant to be sent to the next hop.
106    Relayed {
107        /// Packet for the next hop.
108        packet: MetaPacket<S>,
109        /// Public key of the next hop.
110        next_node: <S::P as Keypair>::Public,
111        /// Position in the channel path of this packet.
112        path_pos: u8,
113        /// Contains the PoR challenge that will be solved when we receive
114        /// the acknowledgement after we forward the inner packet to the next hop.
115        additional_info: Box<[u8]>,
116        /// Shared secret that was used to encrypt the removed layer.
117        derived_secret: SharedSecret,
118        /// Packet checksum.
119        packet_tag: PacketTag,
120    },
121    /// The content is the actual payload for the packet's destination.
122    Final {
123        /// Decrypted payload
124        plain_text: Box<[u8]>,
125        /// Reserved for SURBs. Currently not used
126        additional_data: Box<[u8]>,
127        /// Shared secret that was used to encrypt the removed layer.
128        derived_secret: SharedSecret,
129        /// Packet checksum.
130        packet_tag: PacketTag,
131    },
132}
133
134impl<S: SphinxSuite> MetaPacket<S> {
135    /// The fixed length of the Sphinx packet header.
136    pub const HEADER_LEN: usize = header_length::<S>(INTERMEDIATE_HOPS + 1, POR_SECRET_LENGTH, 0);
137
138    /// The fixed length of the padded packet.
139    pub const PACKET_LEN: usize = packet_length::<S>(INTERMEDIATE_HOPS + 1, POR_SECRET_LENGTH, 0);
140
141    /// Creates a new outgoing packet with the given payload `msg`, `path` and `shared_keys` computed along the path.
142    ///
143    /// The size of the `msg` must be less or equal [PAYLOAD_SIZE], otherwise the
144    /// constructor will panic. The caller **must** ensure the size is correct beforehand.
145    /// The `additional_data_relayer` contains the PoR challenges for the individual relayers along the path,
146    /// each of the challenges has the same size of `additional_relayer_data_len`.
147    ///
148    /// Optionally, there could be some additional data (`additional_data_last_hop`) for the packet destination.
149    /// This is reserved for the future use by SURBs.
150    pub fn new(
151        shared_keys: SharedKeys<S::E, S::G>,
152        msg: &[u8],
153        path: &[<S::P as Keypair>::Public],
154        max_hops: usize,
155        additional_relayer_data_len: usize,
156        additional_data_relayer: &[&[u8]],
157        additional_data_last_hop: Option<&[u8]>,
158    ) -> Self {
159        assert!(msg.len() <= PAYLOAD_SIZE, "message too long to fit into a packet");
160
161        let mut payload = add_padding(msg);
162
163        let routing_info = RoutingInfo::new::<S>(
164            max_hops,
165            path,
166            &shared_keys.secrets,
167            additional_relayer_data_len,
168            additional_data_relayer,
169            additional_data_last_hop,
170        );
171
172        // Encrypt packet payload using the derived shared secrets
173        for secret in shared_keys.secrets.iter().rev() {
174            let prp = PRP::from_parameters(PRPParameters::new(secret));
175            prp.forward_inplace(&mut payload)
176                .unwrap_or_else(|e| panic!("onion encryption error {e}"))
177        }
178
179        Self::new_from_parts(shared_keys.alpha, routing_info, &payload)
180    }
181
182    fn new_from_parts(
183        alpha: Alpha<<S::G as GroupElement<S::E>>::AlphaLen>,
184        routing_info: RoutingInfo,
185        payload: &[u8],
186    ) -> Self {
187        assert!(
188            !routing_info.routing_information.is_empty(),
189            "routing info must not be empty"
190        );
191        assert_eq!(
192            PAYLOAD_SIZE + PADDING_TAG.len(),
193            payload.len(),
194            "payload has incorrect length"
195        );
196
197        let mut packet = Vec::with_capacity(Self::SIZE);
198        packet.extend_from_slice(&alpha);
199        packet.extend_from_slice(&routing_info.routing_information);
200        packet.extend_from_slice(&routing_info.mac);
201        packet.extend_from_slice(payload);
202
203        Self {
204            packet: packet.into_boxed_slice(),
205            _s: PhantomData,
206        }
207    }
208
209    /// Returns the Alpha value subslice from the packet data.
210    fn alpha(&self) -> &[u8] {
211        let len = <S::G as GroupElement<S::E>>::AlphaLen::USIZE;
212        &self.packet[..len]
213    }
214
215    /// Returns the routing info subslice from the packet data.
216    fn routing_info(&self) -> &[u8] {
217        let base = <S::G as GroupElement<S::E>>::AlphaLen::USIZE;
218        &self.packet[base..base + Self::HEADER_LEN]
219    }
220
221    /// Returns the packet checksum (MAC) subslice from the packet data.
222    fn mac(&self) -> &[u8] {
223        let base = <S::G as GroupElement<S::E>>::AlphaLen::USIZE + Self::HEADER_LEN;
224        &self.packet[base..base + SimpleMac::SIZE]
225    }
226
227    /// Returns the payload subslice from the packet data.
228    fn payload(&self) -> &[u8] {
229        let base = <S::G as GroupElement<S::E>>::AlphaLen::USIZE + Self::HEADER_LEN + SimpleMac::SIZE;
230        &self.packet[base..base + PAYLOAD_SIZE + PADDING_TAG.len()]
231    }
232
233    /// Attempts to remove the layer of encryption of this packet by using the given `node_keypair`.
234    /// This will transform this packet into the [ForwardedMetaPacket].
235    pub fn into_forwarded(
236        self,
237        node_keypair: &S::P,
238        max_hops: usize,
239        additional_data_relayer_len: usize,
240        additional_data_last_hop_len: usize,
241    ) -> Result<ForwardedMetaPacket<S>> {
242        let (alpha, secret) = SharedKeys::<S::E, S::G>::forward_transform(
243            Alpha::<<S::G as GroupElement<S::E>>::AlphaLen>::from_slice(self.alpha()),
244            &(node_keypair.into()),
245            &(node_keypair.public().into()),
246        )?;
247
248        let mut routing_info_cpy: Vec<u8> = self.routing_info().into();
249        let fwd_header = forward_header::<S>(
250            &secret,
251            &mut routing_info_cpy,
252            self.mac(),
253            max_hops,
254            additional_data_relayer_len,
255            additional_data_last_hop_len,
256        )?;
257
258        let prp = PRP::from_parameters(PRPParameters::new(&secret));
259        let decrypted = prp.inverse(self.payload())?;
260
261        Ok(match fwd_header {
262            ForwardedHeader::RelayNode {
263                header,
264                mac,
265                path_pos,
266                next_node,
267                additional_info,
268            } => Relayed {
269                packet: Self::new_from_parts(
270                    alpha,
271                    RoutingInfo {
272                        routing_information: header,
273                        mac,
274                    },
275                    &decrypted,
276                ),
277                packet_tag: derive_packet_tag(&secret),
278                derived_secret: secret,
279                next_node: <S::P as Keypair>::Public::try_from(&next_node)
280                    .map_err(|_| PacketDecodingError("couldn't parse next node id".into()))?,
281                path_pos,
282                additional_info,
283            },
284            ForwardedHeader::FinalNode { additional_data } => Final {
285                packet_tag: derive_packet_tag(&secret),
286                derived_secret: secret,
287                plain_text: remove_padding(&decrypted)
288                    .ok_or(PacketDecodingError(format!(
289                        "couldn't remove padding: {}",
290                        hex::encode(decrypted.as_ref())
291                    )))?
292                    .into(),
293                additional_data,
294            },
295        })
296    }
297}
298
299impl<S: SphinxSuite> AsRef<[u8]> for MetaPacket<S> {
300    fn as_ref(&self) -> &[u8] {
301        self.packet.as_ref()
302    }
303}
304
305impl<S: SphinxSuite> TryFrom<&[u8]> for MetaPacket<S> {
306    type Error = GeneralError;
307
308    fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
309        if value.len() == Self::SIZE {
310            Ok(Self {
311                packet: value.into(),
312                _s: PhantomData,
313            })
314        } else {
315            Err(GeneralError::ParseError("MetaPacket".into()))
316        }
317    }
318}
319
320impl<S: SphinxSuite> BytesRepresentable for MetaPacket<S> {
321    const SIZE: usize = <S::G as GroupElement<S::E>>::AlphaLen::USIZE
322        + Self::HEADER_LEN
323        + SimpleMac::SIZE
324        + PAYLOAD_SIZE
325        + PADDING_TAG.len();
326}
327
328#[cfg(test)]
329mod tests {
330    use crate::packet::{add_padding, remove_padding, ForwardedMetaPacket, MetaPacket, PADDING_TAG};
331    use crate::por::{ProofOfRelayString, POR_SECRET_LENGTH};
332    use hopr_crypto_sphinx::{
333        ec_groups::{Ed25519Suite, Secp256k1Suite, X25519Suite},
334        shared_keys::SphinxSuite,
335    };
336    use hopr_crypto_types::keypairs::{ChainKeypair, Keypair, OffchainKeypair};
337    use hopr_internal_types::protocol::INTERMEDIATE_HOPS;
338    use parameterized::parameterized;
339
340    #[test]
341    fn test_padding() {
342        let data = b"test";
343        let padded = add_padding(data);
344
345        let mut expected = vec![0u8; 492];
346        expected.extend_from_slice(PADDING_TAG);
347        expected.extend_from_slice(data);
348        assert_eq!(&expected, padded.as_ref());
349
350        let unpadded = remove_padding(&padded);
351        assert!(unpadded.is_some());
352        assert_eq!(data, &unpadded.unwrap());
353    }
354
355    fn generic_test_meta_packet<S: SphinxSuite>(keypairs: Vec<S::P>) -> anyhow::Result<()> {
356        let pubkeys = keypairs.iter().map(|kp| kp.public().clone()).collect::<Vec<_>>();
357
358        let shared_keys = S::new_shared_keys(&pubkeys)?;
359        let por_strings = ProofOfRelayString::from_shared_secrets(&shared_keys.secrets);
360
361        assert_eq!(shared_keys.secrets.len() - 1, por_strings.len());
362
363        let msg = b"some random test message";
364
365        let mut mp = MetaPacket::<S>::new(
366            shared_keys,
367            msg,
368            &pubkeys,
369            INTERMEDIATE_HOPS + 1,
370            POR_SECRET_LENGTH,
371            &por_strings.iter().map(|v| v.as_ref()).collect::<Vec<_>>(),
372            None,
373        );
374
375        assert!(mp.as_ref().len() < 1492, "metapacket too long {}", mp.as_ref().len());
376
377        for (i, pair) in keypairs.iter().enumerate() {
378            let fwd = mp
379                .clone()
380                .into_forwarded(pair, INTERMEDIATE_HOPS + 1, POR_SECRET_LENGTH, 0)
381                .unwrap_or_else(|_| panic!("failed to unwrap at {i}"));
382
383            match fwd {
384                ForwardedMetaPacket::Relayed { packet, .. } => {
385                    assert!(i < keypairs.len() - 1);
386                    mp = packet;
387                }
388                ForwardedMetaPacket::Final {
389                    plain_text,
390                    additional_data,
391                    ..
392                } => {
393                    assert_eq!(keypairs.len() - 1, i);
394                    assert_eq!(msg, plain_text.as_ref());
395                    assert!(additional_data.is_empty());
396                }
397            }
398        }
399
400        Ok(())
401    }
402
403    #[parameterized(amount = { 4, 3, 2 })]
404    fn test_x25519_meta_packet(amount: usize) -> anyhow::Result<()> {
405        generic_test_meta_packet::<X25519Suite>((0..amount).map(|_| OffchainKeypair::random()).collect())
406    }
407
408    #[parameterized(amount = { 4, 3, 2 })]
409    fn test_ed25519_meta_packet(amount: usize) -> anyhow::Result<()> {
410        generic_test_meta_packet::<Ed25519Suite>((0..amount).map(|_| OffchainKeypair::random()).collect())
411    }
412
413    #[parameterized(amount = { 4, 3, 2 })]
414    fn test_secp256k1_meta_packet(amount: usize) -> anyhow::Result<()> {
415        generic_test_meta_packet::<Secp256k1Suite>((0..amount).map(|_| ChainKeypair::random()).collect())
416    }
417}