sphinx_packet/header/
shared_secret.rs

1// Copyright 2025 Nym Technologies SA
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::constants::{
16    BLINDING_FACTOR_SIZE, EXPANDED_SHARED_SECRET_HKDF_INFO, EXPANDED_SHARED_SECRET_HKDF_SALT,
17    EXPANDED_SHARED_SECRET_LENGTH, INTEGRITY_MAC_KEY_SIZE, PAYLOAD_KEY_SEED_SIZE, PAYLOAD_KEY_SIZE,
18    REPLAY_TAG_SIZE,
19};
20use crate::crypto::STREAM_CIPHER_KEY_SIZE;
21use crate::header::keys::{HeaderIntegrityMacKey, StreamCipherKey};
22use crate::header::SphinxHeader;
23use crate::payload::key::PayloadKey;
24use arrayref::array_ref;
25use hkdf::Hkdf;
26use sha2::Sha256;
27use x25519_dalek::{PublicKey, SharedSecret, StaticSecret};
28use zeroize::{Zeroize, ZeroizeOnDrop};
29
30pub(crate) trait ExpandSecret {
31    fn expand_shared_secret(&self) -> ExpandedSharedSecret;
32}
33
34impl ExpandSecret for PublicKey {
35    fn expand_shared_secret(&self) -> ExpandedSharedSecret {
36        self.as_bytes().expand_shared_secret()
37    }
38}
39
40impl ExpandSecret for SharedSecret {
41    fn expand_shared_secret(&self) -> ExpandedSharedSecret {
42        self.as_bytes().expand_shared_secret()
43    }
44}
45
46impl ExpandSecret for [u8; 32] {
47    fn expand_shared_secret(&self) -> ExpandedSharedSecret {
48        expand_shared_secret(self)
49    }
50}
51
52#[derive(Zeroize, ZeroizeOnDrop, Clone, PartialEq, Debug)]
53pub struct ExpandedSharedSecret([u8; EXPANDED_SHARED_SECRET_LENGTH]);
54
55impl ExpandedSharedSecret {
56    // order of bytes is quite important here to preserve backwards compatibility.
57    // replay tag has not been used before so it **has to** be created last
58
59    /// Output of the hρ random oracle
60    pub(crate) fn stream_cipher_key(&self) -> &StreamCipherKey {
61        array_ref!(&self.0, 0, STREAM_CIPHER_KEY_SIZE)
62    }
63
64    /// Output of the hμ random oracle
65    pub(crate) fn header_integrity_hmac_key(&self) -> &HeaderIntegrityMacKey {
66        array_ref!(&self.0, STREAM_CIPHER_KEY_SIZE, INTEGRITY_MAC_KEY_SIZE)
67    }
68
69    /// Legacy output of the hπ random oracle
70    // NOTE: currently we expand it to full PRP key
71    pub(crate) fn legacy_payload_key(&self) -> &PayloadKey {
72        array_ref!(
73            &self.0,
74            STREAM_CIPHER_KEY_SIZE + INTEGRITY_MAC_KEY_SIZE,
75            PAYLOAD_KEY_SIZE
76        )
77    }
78
79    /// Output of the hπ random oracle
80    pub(crate) fn payload_key_seed(&self) -> &[u8; PAYLOAD_KEY_SEED_SIZE] {
81        array_ref!(
82            &self.0,
83            STREAM_CIPHER_KEY_SIZE + INTEGRITY_MAC_KEY_SIZE,
84            PAYLOAD_KEY_SEED_SIZE
85        )
86    }
87
88    /// Output of the hb random oracle
89    pub(crate) fn blinding_factor_bytes(&self) -> &[u8; BLINDING_FACTOR_SIZE] {
90        array_ref!(
91            &self.0,
92            STREAM_CIPHER_KEY_SIZE + INTEGRITY_MAC_KEY_SIZE + PAYLOAD_KEY_SIZE,
93            BLINDING_FACTOR_SIZE
94        )
95    }
96
97    pub(crate) fn blinding_factor(&self) -> StaticSecret {
98        StaticSecret::from(*self.blinding_factor_bytes())
99    }
100
101    pub(crate) fn blind_shared_secret(&self, shared_secret: PublicKey) -> PublicKey {
102        SphinxHeader::blind_the_shared_secret(shared_secret, self.blinding_factor())
103    }
104
105    #[deprecated]
106    #[allow(deprecated)]
107    pub(crate) fn legacy_blind_share_secret(&self, shared_secret: PublicKey) -> PublicKey {
108        SphinxHeader::legacy_blind_shared_secret(shared_secret, self.blinding_factor())
109    }
110
111    /// Output of the h𝜏 random oracle
112    pub fn replay_tag(&self) -> &[u8; REPLAY_TAG_SIZE] {
113        array_ref!(
114            &self.0,
115            STREAM_CIPHER_KEY_SIZE
116                + INTEGRITY_MAC_KEY_SIZE
117                + PAYLOAD_KEY_SIZE
118                + BLINDING_FACTOR_SIZE,
119            REPLAY_TAG_SIZE
120        )
121    }
122}
123
124pub(crate) fn expand_shared_secret(shared_secret: &[u8; 32]) -> ExpandedSharedSecret {
125    let hkdf = Hkdf::<Sha256>::new(Some(EXPANDED_SHARED_SECRET_HKDF_SALT), shared_secret);
126
127    let mut output = [0u8; EXPANDED_SHARED_SECRET_LENGTH];
128    // SAFETY: the length of the provided okm is within the allowed range
129    #[allow(clippy::unwrap_used)]
130    hkdf.expand(EXPANDED_SHARED_SECRET_HKDF_INFO, &mut output)
131        .unwrap();
132
133    ExpandedSharedSecret(output)
134}
135
136#[cfg(test)]
137mod expanding_shared_secret {
138    use super::*;
139    use crate::test_utils::fixtures::mock_shared_secret;
140    use crate::test_utils::{assert_zeroize, assert_zeroize_on_drop, seeded_rng};
141
142    #[test]
143    fn expanded_shared_secret_is_zeroized() {
144        assert_zeroize::<ExpandedSharedSecret>();
145        assert_zeroize_on_drop::<ExpandedSharedSecret>();
146    }
147
148    // using old values from legacy `RoutingKeys`
149    #[test]
150    fn results_in_same_values_as_old_implementation() {
151        let mut rng = seeded_rng([1u8; 32]);
152        let ss = mock_shared_secret(&mut rng);
153
154        let expected_sck = [
155            186, 234, 152, 113, 202, 124, 191, 228, 173, 89, 91, 8, 127, 251, 214, 200,
156        ];
157        let expected_hihk = [
158            28, 222, 17, 227, 46, 180, 170, 7, 34, 52, 177, 142, 150, 137, 142, 222,
159        ];
160        let expected_pk = [
161            210, 209, 81, 241, 254, 123, 36, 81, 155, 85, 115, 40, 101, 210, 97, 8, 196, 104, 61,
162            23, 165, 190, 191, 236, 203, 69, 15, 230, 70, 100, 161, 136, 53, 88, 116, 118, 81, 57,
163            58, 181, 232, 102, 149, 93, 239, 255, 156, 205, 0, 146, 110, 117, 137, 59, 102, 170,
164            87, 250, 175, 207, 193, 107, 112, 154, 247, 220, 110, 135, 32, 106, 20, 152, 14, 132,
165            89, 154, 249, 24, 176, 40, 30, 182, 195, 209, 124, 59, 58, 201, 209, 255, 80, 151, 109,
166            226, 157, 232, 48, 128, 56, 159, 90, 168, 229, 60, 106, 14, 50, 215, 198, 200, 168, 24,
167            159, 224, 240, 119, 23, 242, 61, 129, 54, 36, 140, 245, 127, 159, 230, 5, 52, 142, 254,
168            52, 168, 171, 139, 100, 206, 16, 94, 219, 68, 113, 141, 159, 4, 233, 189, 144, 164,
169            202, 180, 74, 214, 66, 96, 185, 70, 191, 155, 18, 210, 52, 123, 71, 231, 225, 79, 0,
170            196, 25, 217, 231, 133, 191, 96, 119, 103, 182, 200, 36, 215, 62, 203, 149, 214, 139,
171            32, 70, 66, 30, 63, 102,
172        ];
173        let expected_bf = [
174            227, 64, 184, 235, 140, 62, 232, 172, 235, 42, 58, 169, 241, 253, 245, 2, 136, 149, 74,
175            48, 6, 165, 145, 133, 190, 105, 222, 218, 248, 172, 49, 188,
176        ];
177
178        let expanded = expand_shared_secret(ss.as_bytes());
179        assert_eq!(expanded.stream_cipher_key(), &expected_sck);
180        assert_eq!(expanded.header_integrity_hmac_key(), &expected_hihk);
181        assert_eq!(expanded.legacy_payload_key(), &expected_pk);
182        assert_eq!(expanded.blinding_factor_bytes(), &expected_bf);
183    }
184}