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(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    /// EC group element
163    type G: GroupElement<Self::E> + for<'a> From<&'a <Self::P as Keypair>::Public>;
164
165    /// Pseudo-Random Permutation used to encrypt and decrypt packet payload
166    type PRP: crypto_traits::PRP + crypto_traits::KeyIvInit;
167
168    /// Convenience function to generate shared keys from the path of public keys.
169    fn new_shared_keys<'a>(
170        public_keys: &'a [<Self::P as Keypair>::Public],
171    ) -> hopr_types::crypto::errors::Result<SharedKeys<Self::E, Self::G>>
172    where
173        &'a Alpha<<Self::G as GroupElement<Self::E>>::AlphaLen>: From<&'a <Self::P as Keypair>::Public>,
174    {
175        SharedKeys::generate(public_keys.iter().map(|pk| (pk.into(), pk.into())).collect())
176    }
177
178    /// Instantiates a new Pseudo-Random Permutation IV and key for general packet data.
179    fn new_prp_init(secret: &SecretKey) -> hopr_types::crypto::errors::Result<IvKey<Self::PRP>> {
180        generate_key_iv(secret, HASH_KEY_PRP, None)
181    }
182
183    /// Instantiates a new Pseudo-Random Permutation IV and key for reply data.
184    fn new_reply_prp_init(secret: &SecretKey16, salt: &[u8]) -> hopr_types::crypto::errors::Result<IvKey<Self::PRP>> {
185        generate_key_iv(secret, HASH_KEY_REPLY_PRP, Some(salt))
186    }
187}
188
189#[cfg(test)]
190pub(crate) mod tests {
191    use subtle::ConstantTimeEq;
192
193    use super::*;
194
195    #[allow(clippy::type_complexity)]
196    pub fn generic_sphinx_suite_test<S: SphinxSuite>(node_count: usize) {
197        let (pub_keys, priv_keys): (Vec<(S::G, Alpha<<S::G as GroupElement<S::E>>::AlphaLen>)>, Vec<S::E>) = (0
198            ..node_count)
199            .map(|_| {
200                let pair = S::G::random_pair();
201                ((pair.0.clone(), pair.0.to_alpha()), pair.1)
202            })
203            .unzip();
204
205        let pub_keys_alpha = pub_keys
206            .iter()
207            .map(|(pk, alpha)| (pk.clone(), alpha))
208            .collect::<Vec<_>>();
209
210        // Now generate the key shares for the public keys
211        let generated_shares = SharedKeys::<S::E, S::G>::generate(pub_keys_alpha.clone()).unwrap();
212        assert_eq!(
213            node_count,
214            generated_shares.secrets.len(),
215            "number of generated keys should be equal to the number of nodes"
216        );
217
218        let mut alpha_cpy = generated_shares.alpha.clone();
219        for (i, priv_key) in priv_keys.into_iter().enumerate() {
220            let (alpha, secret) =
221                SharedKeys::<S::E, S::G>::forward_transform(&alpha_cpy, &priv_key, &pub_keys[i].1).unwrap();
222
223            assert_eq!(
224                secret.ct_eq(&generated_shares.secrets[i]).unwrap_u8(),
225                1,
226                "forward transform should yield the same shared secret"
227            );
228
229            alpha_cpy = alpha;
230        }
231    }
232}