Skip to main content

hopr_crypto_packet/sphinx/
surb.rs

1use std::fmt::Formatter;
2
3use hopr_types::{
4    crypto::prelude::*,
5    crypto_random::Randomizable,
6    primitive::{
7        hybrid_array::{Array, typenum::Unsigned},
8        prelude::*,
9    },
10};
11use subtle::ConstantTimeEq;
12
13use super::{
14    routing::{RoutingInfo, SphinxHeaderSpec},
15    shared_keys::{Alpha, GroupElement, SharedKeys, SharedSecret, SphinxSuite},
16};
17
18/// Single Use Reply Block
19///
20/// This is delivered to the recipient, so they are able to send reply messages back
21/// anonymously (via the return path inside that SURB).
22///
23/// [`SURB`] is always created in a pair with [`ReplyOpener`], so that the sending
24/// party knows how to decrypt the data.
25///
26/// The SURB sent to the receiving party must be accompanied
27/// by a `Pseudonym`, and once the receiving party uses that SURB to send a reply, it
28/// must be accompanied by the same `Pseudonym`.
29/// Upon receiving such a reply, the reply recipient (= sender of the SURB)
30/// uses the `Pseudonym` to find the `ReplyOpener` created with the SURB to read the reply.
31///
32/// Always use [`create_surb`] to create the [`SURB`] and [`ReplyOpener`] pair.
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct SURB<S: SphinxSuite, H: SphinxHeaderSpec> {
35    /// ID of the first relayer.
36    pub first_relayer: H::KeyId,
37    /// Alpha value for the header.
38    pub alpha: Alpha<<S::G as GroupElement<S::E>>::AlphaLen>,
39    /// Sphinx routing header.
40    pub header: RoutingInfo<H>,
41    /// Encryption key to use to encrypt the data for the SURB's creator.
42    pub sender_key: SecretKey16,
43    /// Additional data for the SURB receiver.
44    pub additional_data_receiver: H::SurbReceiverData,
45}
46
47impl<S: SphinxSuite, H: SphinxHeaderSpec> SURB<S, H> {
48    /// Size of the SURB in bytes.
49    pub const SIZE: usize = H::KEY_ID_SIZE.get()
50        + <S::G as GroupElement<S::E>>::AlphaLen::USIZE
51        + RoutingInfo::<H>::SIZE
52        + SecretKey16::LENGTH
53        + H::SURB_RECEIVER_DATA_SIZE;
54
55    /// Serializes SURB into wire format.
56    pub fn into_boxed(self) -> Box<[u8]> {
57        let alpha_len = <S::G as GroupElement<S::E>>::AlphaLen::USIZE;
58
59        let mut ret = vec![0u8; Self::SIZE];
60        ret[..H::KEY_ID_SIZE.get()].copy_from_slice(self.first_relayer.as_ref());
61        ret[H::KEY_ID_SIZE.get()..H::KEY_ID_SIZE.get() + alpha_len].copy_from_slice(self.alpha.as_ref());
62        ret[H::KEY_ID_SIZE.get() + alpha_len..H::KEY_ID_SIZE.get() + alpha_len + RoutingInfo::<H>::SIZE]
63            .copy_from_slice(self.header.as_ref());
64        ret[H::KEY_ID_SIZE.get() + alpha_len + RoutingInfo::<H>::SIZE
65            ..H::KEY_ID_SIZE.get() + alpha_len + RoutingInfo::<H>::SIZE + SecretKey16::LENGTH]
66            .copy_from_slice(self.sender_key.as_ref());
67        ret[H::KEY_ID_SIZE.get() + alpha_len + RoutingInfo::<H>::SIZE + SecretKey16::LENGTH
68            ..H::KEY_ID_SIZE.get()
69                + alpha_len
70                + RoutingInfo::<H>::SIZE
71                + SecretKey16::LENGTH
72                + H::SURB_RECEIVER_DATA_SIZE]
73            .copy_from_slice(self.additional_data_receiver.as_ref());
74
75        ret.into_boxed_slice()
76    }
77
78    /// Computes Keccak256 hash of the SURB.
79    ///
80    /// The given `context` is appended to the input.
81    pub fn get_hash(&self, context: &[u8]) -> Hash {
82        Hash::create(&[
83            self.first_relayer.as_ref(),
84            self.alpha.as_ref(),
85            self.sender_key.as_ref(),
86            self.header.as_ref(),
87            context,
88        ])
89    }
90}
91
92impl<S: SphinxSuite, H: SphinxHeaderSpec> Clone for SURB<S, H>
93where
94    H::KeyId: Clone,
95    H::SurbReceiverData: Clone,
96{
97    fn clone(&self) -> Self {
98        Self {
99            first_relayer: self.first_relayer.clone(),
100            alpha: self.alpha.clone(),
101            header: self.header.clone(),
102            sender_key: self.sender_key.clone(),
103            additional_data_receiver: self.additional_data_receiver.clone(),
104        }
105    }
106}
107
108impl<S: SphinxSuite, H: SphinxHeaderSpec> std::fmt::Debug for SURB<S, H>
109where
110    H::KeyId: std::fmt::Debug,
111    H::SurbReceiverData: std::fmt::Debug,
112{
113    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
114        f.debug_struct("SURB")
115            .field("first_relayer", &self.first_relayer)
116            .field("alpha", &self.alpha)
117            .field("header", &self.header)
118            .field("sender_key", &"<redacted>")
119            .field("additional_data_receiver", &self.additional_data_receiver)
120            .finish()
121    }
122}
123
124impl<S: SphinxSuite, H: SphinxHeaderSpec> PartialEq for SURB<S, H>
125where
126    H::KeyId: PartialEq,
127    H::SurbReceiverData: PartialEq,
128{
129    fn eq(&self, other: &Self) -> bool {
130        self.first_relayer.eq(&other.first_relayer)
131            && self.alpha.eq(&other.alpha)
132            && self.header.eq(&other.header)
133            && self.sender_key.ct_eq(&other.sender_key).into()
134            && self.additional_data_receiver.eq(&other.additional_data_receiver)
135    }
136}
137
138impl<S: SphinxSuite, H: SphinxHeaderSpec> Eq for SURB<S, H>
139where
140    H::KeyId: Eq,
141    H::SurbReceiverData: Eq,
142{
143}
144
145impl<'a, S: SphinxSuite, H: SphinxHeaderSpec> TryFrom<&'a [u8]> for SURB<S, H> {
146    type Error = GeneralError;
147
148    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
149        let alpha = <S::G as GroupElement<S::E>>::AlphaLen::USIZE;
150
151        if value.len() == Self::SIZE {
152            Ok(Self {
153                first_relayer: value[0..H::KEY_ID_SIZE.get()]
154                    .try_into()
155                    .map_err(|_| GeneralError::ParseError("SURB.first_relayer".into()))?,
156                alpha: Array::<u8, <S::G as GroupElement<S::E>>::AlphaLen>::try_from(
157                    &value[H::KEY_ID_SIZE.get()..H::KEY_ID_SIZE.get() + alpha],
158                )
159                .map_err(|_| GeneralError::ParseError("SURB.alpha".into()))?,
160                header: value[H::KEY_ID_SIZE.get() + alpha..H::KEY_ID_SIZE.get() + alpha + RoutingInfo::<H>::SIZE]
161                    .try_into()
162                    .map_err(|_| GeneralError::ParseError("SURB.header".into()))?,
163                sender_key: value[H::KEY_ID_SIZE.get() + alpha + RoutingInfo::<H>::SIZE
164                    ..H::KEY_ID_SIZE.get() + alpha + RoutingInfo::<H>::SIZE + SecretKey16::LENGTH]
165                    .try_into()
166                    .map_err(|_| GeneralError::ParseError("SURB.sender_key".into()))?,
167                additional_data_receiver: value
168                    [H::KEY_ID_SIZE.get() + alpha + RoutingInfo::<H>::SIZE + SecretKey16::LENGTH..]
169                    .try_into()
170                    .map_err(|_| GeneralError::ParseError("SURB.additional_data_receiver".into()))?,
171            })
172        } else {
173            Err(GeneralError::ParseError("SURB::SIZE".into()))
174        }
175    }
176}
177
178/// Entry stored locally by the [`SURB`] creator to allow decryption
179/// of received responses.
180#[derive(Clone)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
182pub struct ReplyOpener {
183    /// Encryption key the other party should use to encrypt the data for us.
184    pub sender_key: SecretKey16,
185    /// Shared secrets for nodes along the return path.
186    pub shared_secrets: Vec<SharedSecret>,
187}
188
189impl std::fmt::Debug for ReplyOpener {
190    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("ReplyOpener")
192            .field("sender_key", &"<redacted>")
193            .field("shared_secrets", &format!("{} <redacted>", self.shared_secrets.len()))
194            .finish()
195    }
196}
197
198/// Creates a pair of [`SURB`] and [`ReplyOpener`].
199///
200/// The former is sent to the other party, the latter is kept locally.
201pub fn create_surb<S: SphinxSuite, H: SphinxHeaderSpec>(
202    shared_keys: SharedKeys<S::E, S::G>,
203    path: &[H::KeyId],
204    additional_data_relayer: &[H::RelayerData],
205    receiver_data: H::PacketReceiverData,
206    additional_data_receiver: H::SurbReceiverData,
207) -> hopr_types::crypto::errors::Result<(SURB<S, H>, ReplyOpener)>
208where
209    H::KeyId: Copy,
210{
211    let header = RoutingInfo::<H>::new(
212        path,
213        &shared_keys.secrets,
214        additional_data_relayer,
215        &receiver_data,
216        true,
217        false,
218    )?;
219
220    let sender_key = SecretKey16::random();
221
222    let surb = SURB {
223        sender_key: sender_key.clone(),
224        header,
225        first_relayer: *path.first().ok_or(CryptoError::InvalidInputValue("path is empty"))?,
226        additional_data_receiver,
227        alpha: shared_keys.alpha,
228    };
229
230    let reply_opener = ReplyOpener {
231        sender_key: sender_key.clone(),
232        shared_secrets: shared_keys.secrets,
233    };
234
235    Ok((surb, reply_opener))
236}
237
238#[cfg(test)]
239mod tests {
240    use hopr_types::crypto_random::Randomizable;
241
242    use super::{super::tests::*, *};
243
244    #[allow(type_alias_bounds)]
245    pub type HeaderSpec<S: SphinxSuite> = TestSpec<<S::P as Keypair>::Public, 4, 66>;
246
247    fn generate_surbs<S: SphinxSuite>(keypairs: Vec<S::P>) -> anyhow::Result<(SURB<S, HeaderSpec<S>>, ReplyOpener)>
248    where
249        <<S as SphinxSuite>::P as Keypair>::Public: Copy,
250        for<'a> &'a Alpha<<<S as SphinxSuite>::G as GroupElement<<S as SphinxSuite>::E>>::AlphaLen>:
251            From<&'a <<S as SphinxSuite>::P as Keypair>::Public>,
252    {
253        let pub_keys = keypairs.iter().map(|kp| *kp.public()).collect::<Vec<_>>();
254        let shares = S::new_shared_keys(&pub_keys)?;
255
256        Ok(create_surb::<S, HeaderSpec<S>>(
257            shares,
258            &pub_keys,
259            &[Default::default(); 4],
260            SimplePseudonym::random(),
261            Default::default(),
262        )?)
263    }
264
265    // Mutually exclusive cfg to prevent type alias collision under --all-features.
266    // Priority order: ed25519 > secp256k1 > x25519.
267    #[cfg(feature = "ed25519")]
268    use crate::sphinx::ec_groups::Ed25519Suite as CurrentSuite;
269    #[cfg(all(feature = "secp256k1", not(feature = "ed25519")))]
270    use crate::sphinx::ec_groups::Secp256k1Suite as CurrentSuite;
271    #[cfg(all(feature = "x25519", not(any(feature = "ed25519", feature = "secp256k1"))))]
272    use crate::sphinx::ec_groups::X25519Suite as CurrentSuite;
273
274    #[test]
275    fn surb_serialize_deserialize() -> anyhow::Result<()> {
276        let (surb_1, _) = generate_surbs::<CurrentSuite>((0..3).map(|_| OffchainKeypair::random()).collect())?;
277
278        let surb_1_enc = surb_1.into_boxed();
279
280        let surb_2 = SURB::<CurrentSuite, HeaderSpec<CurrentSuite>>::try_from(surb_1_enc.as_ref())?;
281
282        assert_eq!(surb_1_enc, surb_2.into_boxed());
283
284        Ok(())
285    }
286}