Skip to main content

hopr_crypto_packet/sphinx/
shared_keys.rs

1use std::{marker::PhantomData, ops::Mul};
2
3use hopr_types::{
4    crypto::prelude::*,
5    primitive::hybrid_array::{Array, ArraySize},
6};
7
8use super::derivation::{create_kdf_instance, generate_key_iv};
9
10/// Represents a shared secret with a remote peer.
11pub type SharedSecret = SecretValue<hopr_types::primitive::typenum::U32>;
12
13/// Types representing a valid non-zero scalar of an additive abelian group.
14pub trait Scalar: Mul<Output = Self> + Sized {
15    /// Generates a random scalar using a cryptographically secure RNG.
16    fn random() -> Self;
17
18    /// Create scalar from bytes
19    fn from_bytes(bytes: &[u8]) -> hopr_types::crypto::errors::Result<Self>;
20}
21
22/// Represents the Alpha value of a certain length in the Sphinx protocol
23/// The length of the alpha value is directly dependent on the group element.
24pub type Alpha<A> = Array<u8, A>;
25
26/// Generic additive abelian group element with an associated scalar type.
27/// It also comes with the associated Alpha value size.
28/// A group element is considered valid if it is not neutral or a torsion element of small order.
29pub trait GroupElement<E: Scalar>: Clone + for<'a> Mul<&'a E, Output = Self> {
30    /// Length of the Alpha value - a binary representation of the group element.
31    type AlphaLen: ArraySize;
32
33    /// Converts the group element to a binary format suitable for representing the Alpha value.
34    fn to_alpha(&self) -> Alpha<Self::AlphaLen>;
35
36    /// Converts the group element from the binary format representing an Alpha value.
37    fn from_alpha(alpha: Alpha<Self::AlphaLen>) -> hopr_types::crypto::errors::Result<Self>;
38
39    /// Create a group element using the group generator and the given scalar
40    fn generate(scalar: &E) -> Self;
41
42    /// Group element is considered valid if it is not a neutral element and also not a torsion element of small order.
43    fn is_valid(&self) -> bool;
44
45    /// Generates a random pair of group element and secret scalar.
46    /// This is a convenience method that internally calls the `random` method of the associated Scalar
47    /// and constructs the group element using `generate`.
48    fn random_pair() -> (Self, E) {
49        let scalar = E::random();
50        (Self::generate(&scalar), scalar)
51    }
52
53    /// Extract a keying material from a group element using a KDF
54    fn extract_key(&self, context: &str, salt: &[u8]) -> SharedSecret {
55        let mut output = create_kdf_instance(&self.to_alpha(), context, Some(salt)).expect("invalid sphinx key length");
56        let mut out = SharedSecret::default();
57        output.fill(out.as_mut());
58        out
59    }
60}
61
62/// Structure containing shared keys for peers using the Sphinx algorithm.
63pub struct SharedKeys<E: Scalar, G: GroupElement<E>> {
64    pub alpha: Alpha<G::AlphaLen>,
65    pub secrets: Vec<SharedSecret>,
66    _d: PhantomData<(E, G)>,
67}
68
69const HASH_KEY_SPHINX_SECRET: &str = "HASH_KEY_SPHINX_SECRET";
70const HASH_KEY_SPHINX_BLINDING: &str = "HASH_KEY_SPHINX_BLINDING";
71
72impl<E: Scalar, G: GroupElement<E>> SharedKeys<E, G> {
73    /// Generates shared secrets given the group element of the peers and their Alpha value encodings.
74    ///
75    /// The order of the peer group elements is preserved for resulting shared keys.
76    pub(crate) fn generate(
77        peer_group_elements: Vec<(G, &Alpha<G::AlphaLen>)>,
78    ) -> hopr_types::crypto::errors::Result<SharedKeys<E, G>> {
79        let mut shared_keys = Vec::with_capacity(peer_group_elements.len());
80
81        // coeff_prev becomes: x * b_0 * b_1 * b_2 * ...
82        // alpha_prev becomes: x * b_0 * b_1 * b_2 * ... * G
83        let (mut alpha_prev, mut coeff_prev) = G::random_pair();
84
85        // Save the part of the result
86        let alpha = alpha_prev.to_alpha();
87
88        // Iterate through all the given peer public keys
89        let keys_len = peer_group_elements.len();
90        for (i, (group_element, salt)) in peer_group_elements.into_iter().enumerate() {
91            // Multiply the public key by the current coefficient
92            let shared_secret = group_element.mul(&coeff_prev);
93
94            // Extract the shared secret from the computed EC point and copy it into the shared keys structure
95            shared_keys.push(shared_secret.extract_key(HASH_KEY_SPHINX_SECRET, salt.as_ref()));
96
97            // Stop here, we don't need to compute anything more
98            if i == keys_len - 1 {
99                break;
100            }
101
102            // Compute the new blinding factor b_k (alpha needs compressing first)
103            let b_k = shared_secret.extract_key(HASH_KEY_SPHINX_BLINDING, &alpha_prev.to_alpha());
104            let b_k_checked = E::from_bytes(b_k.as_ref())?;
105
106            // Update coeff_prev and alpha
107            alpha_prev = alpha_prev.mul(&b_k_checked);
108            coeff_prev = coeff_prev.mul(b_k_checked);
109
110            if !alpha_prev.is_valid() {
111                return Err(CryptoError::CalculationError);
112            }
113        }
114
115        Ok(SharedKeys {
116            alpha,
117            secrets: shared_keys,
118            _d: PhantomData,
119        })
120    }
121
122    /// Calculates the forward transformation given the local private key.
123    ///
124    /// Efficiency note: the `public_element_alpha` is a precomputed group element Alpha encoding associated with the
125    /// `private_scalar`.
126    pub(crate) fn forward_transform(
127        alpha: &Alpha<G::AlphaLen>,
128        private_scalar: &E,
129        public_element_alpha: &Alpha<G::AlphaLen>,
130    ) -> hopr_types::crypto::errors::Result<(Alpha<G::AlphaLen>, SharedSecret)> {
131        let alpha_point = G::from_alpha(alpha.clone())?;
132
133        let s_k = alpha_point.clone().mul(private_scalar);
134
135        let secret = s_k.extract_key(HASH_KEY_SPHINX_SECRET, public_element_alpha);
136
137        let b_k = s_k.extract_key(HASH_KEY_SPHINX_BLINDING, alpha);
138
139        let b_k_checked = E::from_bytes(b_k.as_ref())?;
140        let alpha_new = alpha_point.mul(&b_k_checked);
141
142        if !alpha_new.is_valid() {
143            return Err(hopr_types::crypto::errors::CryptoError::CalculationError);
144        }
145
146        Ok((alpha_new.to_alpha(), secret))
147    }
148}
149
150const HASH_KEY_PRP: &str = "HASH_KEY_PRP";
151
152const HASH_KEY_REPLY_PRP: &str = "HASH_KEY_REPLY_PRP";
153
154/// Represents an instantiation of the Spinx protocol using the given EC group and corresponding public key object.
155pub trait SphinxSuite {
156    /// Keypair corresponding to the EC group
157    type P: Keypair;
158
159    /// Scalar type supported by the EC group
160    type E: Scalar + for<'a> From<&'a Self::P>;
161
162    /// Expanded form of [`Self::P`]'s public key, suitable for EC computations.
163    ///
164    /// Deriving it from the compact [`Keypair::Public`] can be CPU-intensive, which is why it is
165    /// a type of its own: a path can be expanded once and reused across several packets.
166    type ExpandedPublic;
167
168    /// EC group element
169    type G: GroupElement<Self::E> + for<'a> From<&'a Self::ExpandedPublic>;
170
171    /// Pseudo-Random Permutation used to encrypt and decrypt packet payload
172    type PRP: crypto_traits::PRP + crypto_traits::KeyIvInit;
173
174    /// Expands a compact public key into [`Self::ExpandedPublic`].
175    ///
176    /// Can be CPU-intensive. Fails if `public_key` is not a valid point on the curve.
177    fn expand_public(
178        public_key: &<Self::P as Keypair>::Public,
179    ) -> hopr_types::crypto::errors::Result<Self::ExpandedPublic>;
180
181    /// Expands an entire path. See [`SphinxSuite::expand_public`].
182    fn expand_path(
183        public_keys: &[<Self::P as Keypair>::Public],
184    ) -> hopr_types::crypto::errors::Result<Vec<Self::ExpandedPublic>> {
185        public_keys.iter().map(|pk| Self::expand_public(pk)).collect()
186    }
187
188    /// Convenience function to generate shared keys from the path of public keys.
189    ///
190    /// Expands every key on the path; use [`SphinxSuite::new_shared_keys_expanded`] when the path
191    /// has already been expanded, so that the cost is not paid once per packet.
192    fn new_shared_keys<'a>(
193        public_keys: &'a [<Self::P as Keypair>::Public],
194    ) -> hopr_types::crypto::errors::Result<SharedKeys<Self::E, Self::G>>
195    where
196        &'a Alpha<<Self::G as GroupElement<Self::E>>::AlphaLen>: From<&'a <Self::P as Keypair>::Public>,
197    {
198        let expanded = Self::expand_path(public_keys)?;
199
200        SharedKeys::generate(
201            expanded
202                .iter()
203                .zip(public_keys)
204                .map(|(expanded, compact)| (expanded.into(), compact.into()))
205                .collect(),
206        )
207    }
208
209    /// As [`SphinxSuite::new_shared_keys`], but for a path that is already expanded.
210    fn new_shared_keys_expanded<'a>(
211        public_keys: &'a [Self::ExpandedPublic],
212    ) -> hopr_types::crypto::errors::Result<SharedKeys<Self::E, Self::G>>
213    where
214        &'a Alpha<<Self::G as GroupElement<Self::E>>::AlphaLen>: From<&'a Self::ExpandedPublic>,
215    {
216        SharedKeys::generate(public_keys.iter().map(|pk| (pk.into(), pk.into())).collect())
217    }
218
219    /// Instantiates a new Pseudo-Random Permutation IV and key for general packet data.
220    fn new_prp_init(secret: &SecretKey) -> hopr_types::crypto::errors::Result<IvKey<Self::PRP>> {
221        generate_key_iv(secret, HASH_KEY_PRP, None)
222    }
223
224    /// Instantiates a new Pseudo-Random Permutation IV and key for reply data.
225    fn new_reply_prp_init(secret: &SecretKey16, salt: &[u8]) -> hopr_types::crypto::errors::Result<IvKey<Self::PRP>> {
226        generate_key_iv(secret, HASH_KEY_REPLY_PRP, Some(salt))
227    }
228}
229
230#[cfg(test)]
231pub(crate) mod tests {
232    use subtle::ConstantTimeEq;
233
234    use super::*;
235
236    #[allow(clippy::type_complexity)]
237    pub fn generic_sphinx_suite_test<S: SphinxSuite>(node_count: usize) {
238        let (pub_keys, priv_keys): (Vec<(S::G, Alpha<<S::G as GroupElement<S::E>>::AlphaLen>)>, Vec<S::E>) = (0
239            ..node_count)
240            .map(|_| {
241                let pair = S::G::random_pair();
242                ((pair.0.clone(), pair.0.to_alpha()), pair.1)
243            })
244            .unzip();
245
246        let pub_keys_alpha = pub_keys
247            .iter()
248            .map(|(pk, alpha)| (pk.clone(), alpha))
249            .collect::<Vec<_>>();
250
251        // Now generate the key shares for the public keys
252        let generated_shares = SharedKeys::<S::E, S::G>::generate(pub_keys_alpha.clone()).unwrap();
253        assert_eq!(
254            node_count,
255            generated_shares.secrets.len(),
256            "number of generated keys should be equal to the number of nodes"
257        );
258
259        let mut alpha_cpy = generated_shares.alpha.clone();
260        for (i, priv_key) in priv_keys.into_iter().enumerate() {
261            let (alpha, secret) =
262                SharedKeys::<S::E, S::G>::forward_transform(&alpha_cpy, &priv_key, &pub_keys[i].1).unwrap();
263
264            assert_eq!(
265                secret.ct_eq(&generated_shares.secrets[i]).unwrap_u8(),
266                1,
267                "forward transform should yield the same shared secret"
268            );
269
270            alpha_cpy = alpha;
271        }
272    }
273}