Skip to main content

dryoc/
precalc.rs

1//! Precalculated secret key for use with `precalc_*` functions in
2//! [`crate::dryocbox::DryocBox`]
3//!
4//! Precalculation avoids repeating the public-key operation when encrypting or
5//! decrypting multiple messages between the same sender and receiver.
6use core::fmt;
7
8use subtle::ConstantTimeEq;
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use crate::constants::{
12    CRYPTO_BOX_BEFORENMBYTES, CRYPTO_BOX_PUBLICKEYBYTES, CRYPTO_BOX_SECRETKEYBYTES,
13};
14use crate::error::Error;
15use crate::types::{ByteArray, Bytes, MutByteArray, MutBytes, StackByteArray};
16
17type InnerKey = StackByteArray<CRYPTO_BOX_BEFORENMBYTES>;
18
19/// Precalculated secret key for use with `precalc_*` functions in
20/// [`crate::dryocbox::DryocBox`].
21///
22/// Use `precalc_*` functions to encrypt or decrypt multiple messages between
23/// the same sender and receiver. They reuse this shared secret instead of
24/// repeating the public-key operation for every message.
25///
26/// Using precalculated secret keys is compatible with libsodium's
27/// `crypto_box_beforenm`.
28#[derive(Zeroize, ZeroizeOnDrop, Clone)]
29pub struct PrecalcSecretKey<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize>(InnerKey);
30
31impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize> fmt::Debug
32    for PrecalcSecretKey<InnerKey>
33{
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_tuple("PrecalcSecretKey")
36            .field(&"[REDACTED]")
37            .finish()
38    }
39}
40
41impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize> PartialEq
42    for PrecalcSecretKey<InnerKey>
43{
44    fn eq(&self, other: &Self) -> bool {
45        self.0.as_slice().ct_eq(other.0.as_slice()).into()
46    }
47}
48
49impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize> Eq for PrecalcSecretKey<InnerKey> {}
50
51impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Bytes + Zeroize> Bytes
52    for PrecalcSecretKey<InnerKey>
53{
54    #[inline]
55    fn as_slice(&self) -> &[u8] {
56        self.0.as_slice()
57    }
58
59    #[inline]
60    fn is_empty(&self) -> bool {
61        self.0.is_empty()
62    }
63
64    #[inline]
65    fn len(&self) -> usize {
66        self.0.len()
67    }
68}
69
70impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize> ByteArray<CRYPTO_BOX_BEFORENMBYTES>
71    for PrecalcSecretKey<InnerKey>
72{
73    #[inline]
74    fn as_array(&self) -> &[u8; CRYPTO_BOX_BEFORENMBYTES] {
75        self.0.as_array()
76    }
77}
78
79impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize + MutBytes> MutBytes
80    for PrecalcSecretKey<InnerKey>
81{
82    #[inline]
83    fn as_mut_slice(&mut self) -> &mut [u8] {
84        self.0.as_mut_slice()
85    }
86
87    #[inline]
88    fn copy_from_slice(&mut self, other: &[u8]) {
89        self.0.copy_from_slice(other);
90    }
91}
92
93impl<InnerKey: MutByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize>
94    MutByteArray<CRYPTO_BOX_BEFORENMBYTES> for PrecalcSecretKey<InnerKey>
95{
96    #[inline]
97    fn as_mut_array(&mut self) -> &mut [u8; CRYPTO_BOX_BEFORENMBYTES] {
98        self.0.as_mut_array()
99    }
100}
101
102impl PrecalcSecretKey<InnerKey> {
103    /// Computes a stack-allocated shared secret key for the given
104    /// `third_party_public_key` and `secret_key`.
105    ///
106    /// Compatible with libsodium's `crypto_box_beforenm`.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if `third_party_public_key` is an unacceptable
111    /// low-order point.
112    #[inline]
113    pub fn precalculate<
114        ThirdPartyPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
115        SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
116    >(
117        third_party_public_key: &ThirdPartyPublicKey,
118        secret_key: &SecretKey,
119    ) -> Result<Self, Error> {
120        use crate::classic::crypto_box::crypto_box_beforenm;
121
122        Ok(Self(
123            crypto_box_beforenm(third_party_public_key.as_array(), secret_key.as_array())?.into(),
124        ))
125    }
126}
127
128#[cfg(any(
129    all(feature = "protected", any(unix, windows)),
130    all(doc, not(doctest), feature = "std")
131))]
132#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
133mod protected {
134    //! Locked-memory constructors for [`PrecalcSecretKey`].
135    use super::*;
136    use crate::protected::*;
137
138    type InnerKey = HeapByteArray<CRYPTO_BOX_PUBLICKEYBYTES>;
139
140    /// Shared `crypto_box_beforenm` computation placed into a locked buffer.
141    fn beforenm_into_locked<
142        ThirdPartyPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
143        SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
144    >(
145        third_party_public_key: &ThirdPartyPublicKey,
146        secret_key: &SecretKey,
147    ) -> Result<Locked<HeapByteArray<CRYPTO_BOX_BEFORENMBYTES>>, Error> {
148        use crate::classic::crypto_box::crypto_box_beforenm;
149
150        let mut precalc = HeapByteArray::<CRYPTO_BOX_BEFORENMBYTES>::new_locked()?;
151        let mut key =
152            crypto_box_beforenm(third_party_public_key.as_array(), secret_key.as_array())?;
153
154        precalc.copy_from_slice(&key);
155        key.zeroize();
156
157        Ok(precalc)
158    }
159
160    impl PrecalcSecretKey<Locked<InnerKey>> {
161        /// Computes a heap-allocated, page-aligned, locked shared secret key
162        /// for the given `third_party_public_key` and `secret_key`.
163        ///
164        /// Compatible with libsodium's `crypto_box_beforenm`.
165        ///
166        /// # Errors
167        ///
168        /// Returns an error if `third_party_public_key` is an unacceptable
169        /// low-order point or the protected allocation cannot be locked.
170        ///
171        /// # Panics
172        ///
173        /// Panics if the page-aligned allocation cannot be created or its size
174        /// cannot be represented with guard pages.
175        pub fn precalculate_locked<
176            ThirdPartyPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
177            SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
178        >(
179            third_party_public_key: &ThirdPartyPublicKey,
180            secret_key: &SecretKey,
181        ) -> Result<Self, Error> {
182            Ok(PrecalcSecretKey(beforenm_into_locked(
183                third_party_public_key,
184                secret_key,
185            )?))
186        }
187    }
188
189    impl PrecalcSecretKey<LockedRO<InnerKey>> {
190        /// Computes a heap-allocated, page-aligned, locked, read-only shared
191        /// secret key for the given `third_party_public_key` and
192        /// `secret_key`.
193        ///
194        /// Compatible with libsodium's `crypto_box_beforenm`.
195        ///
196        /// # Errors
197        ///
198        /// Returns an error if `third_party_public_key` is an unacceptable
199        /// low-order point, the protected allocation cannot be locked, or its
200        /// page permissions cannot be changed to read-only.
201        ///
202        /// # Panics
203        ///
204        /// Panics if the page-aligned allocation cannot be created or its size
205        /// cannot be represented with guard pages.
206        pub fn precalculate_readonly_locked<
207            ThirdPartyPublicKey: ByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
208            SecretKey: ByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
209        >(
210            third_party_public_key: &ThirdPartyPublicKey,
211            secret_key: &SecretKey,
212        ) -> Result<Self, Error> {
213            Ok(PrecalcSecretKey(
214                beforenm_into_locked(third_party_public_key, secret_key)?.mprotect_readonly()?,
215            ))
216        }
217    }
218}
219
220impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize> core::ops::Deref
221    for PrecalcSecretKey<InnerKey>
222{
223    type Target = InnerKey;
224
225    fn deref(&self) -> &Self::Target {
226        &self.0
227    }
228}
229
230impl<InnerKey: ByteArray<CRYPTO_BOX_BEFORENMBYTES> + Zeroize> core::ops::DerefMut
231    for PrecalcSecretKey<InnerKey>
232{
233    fn deref_mut(&mut self) -> &mut Self::Target {
234        &mut self.0
235    }
236}
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn precalculated_key_debug_redacts_contents_and_equality_is_value_based() {
243        let key = PrecalcSecretKey(StackByteArray::from([0xabu8; CRYPTO_BOX_BEFORENMBYTES]));
244        let same = key.clone();
245        let different = PrecalcSecretKey(StackByteArray::from([0xcdu8; CRYPTO_BOX_BEFORENMBYTES]));
246
247        assert_eq!(format!("{key:?}"), "PrecalcSecretKey(\"[REDACTED]\")");
248        assert_eq!(key, same);
249        assert_ne!(key, different);
250    }
251    use crate::constants::{CRYPTO_BOX_PUBLICKEYBYTES, CRYPTO_BOX_SECRETKEYBYTES};
252
253    /// NaCl `tests/box.c` / RFC 7748 section 6.1 keys: `beforenm(bobpk,
254    /// alicesk)` is NaCl's secretbox `firstkey`.
255    const ALICE_SK: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
256    const ALICE_PK: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
257    const BOB_SK: &str = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
258    const BOB_PK: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
259    const SHARED_KEY: &str = "1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389";
260
261    fn array<const N: usize>(hex: &str) -> StackByteArray<N> {
262        StackByteArray::try_from(hex::decode(hex).expect("hex").as_slice()).expect("length")
263    }
264
265    fn low_order_public_keys() -> [StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES>; 2] {
266        let mut identity = StackByteArray::<CRYPTO_BOX_PUBLICKEYBYTES>::default();
267        identity[0] = 1;
268        [StackByteArray::default(), identity]
269    }
270
271    #[test]
272    fn precalculate_matches_nacl_shared_key_from_both_sides() {
273        let alice_sk: StackByteArray<CRYPTO_BOX_SECRETKEYBYTES> = array(ALICE_SK);
274        let alice_pk: StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES> = array(ALICE_PK);
275        let bob_sk: StackByteArray<CRYPTO_BOX_SECRETKEYBYTES> = array(BOB_SK);
276        let bob_pk: StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES> = array(BOB_PK);
277        let expected: StackByteArray<CRYPTO_BOX_BEFORENMBYTES> = array(SHARED_KEY);
278
279        let alice_side = PrecalcSecretKey::precalculate(&bob_pk, &alice_sk).expect("precalc");
280        let bob_side = PrecalcSecretKey::precalculate(&alice_pk, &bob_sk).expect("precalc");
281        assert_eq!(alice_side.as_array(), expected.as_array());
282        assert_eq!(alice_side.as_slice(), expected.as_slice());
283        assert_eq!(alice_side, bob_side);
284        assert_eq!(alice_side.len(), CRYPTO_BOX_BEFORENMBYTES);
285
286        // A different secret key must not reproduce the shared key.
287        let stranger = PrecalcSecretKey::precalculate(&bob_pk, &bob_sk).expect("precalc");
288        assert_ne!(stranger, alice_side);
289
290        for low_order in low_order_public_keys() {
291            assert!(PrecalcSecretKey::precalculate(&low_order, &alice_sk).is_err());
292        }
293    }
294
295    #[cfg(all(feature = "protected", any(unix, windows)))]
296    #[test]
297    fn locked_precalculation_matches_stack_precalculation() {
298        let alice_sk: StackByteArray<CRYPTO_BOX_SECRETKEYBYTES> = array(ALICE_SK);
299        let bob_pk: StackByteArray<CRYPTO_BOX_PUBLICKEYBYTES> = array(BOB_PK);
300        let expected = hex::decode(SHARED_KEY).expect("hex");
301
302        let mut locked =
303            PrecalcSecretKey::precalculate_locked(&bob_pk, &alice_sk).expect("precalc locked");
304        assert_eq!(locked.as_slice(), expected.as_slice());
305        assert_eq!(locked.as_array(), &expected[..]);
306
307        let readonly = PrecalcSecretKey::precalculate_readonly_locked(&bob_pk, &alice_sk)
308            .expect("precalc readonly locked");
309        assert_eq!(readonly.as_slice(), expected.as_slice());
310
311        // The locked key is writable through both mutable accessors.
312        locked.as_mut_slice()[0] ^= 0xff;
313        locked.as_mut_array()[1] ^= 0xff;
314        assert_eq!(locked.as_slice()[0], expected[0] ^ 0xff);
315        assert_eq!(locked.as_slice()[1], expected[1] ^ 0xff);
316        assert_eq!(&locked.as_slice()[2..], &expected[2..]);
317
318        for low_order in low_order_public_keys() {
319            assert!(PrecalcSecretKey::precalculate_locked(&low_order, &alice_sk).is_err());
320            assert!(PrecalcSecretKey::precalculate_readonly_locked(&low_order, &alice_sk).is_err());
321        }
322    }
323
324    #[cfg(dryoc_native_tests)]
325    #[test]
326    fn precalculate_matches_libsodium_beforenm() {
327        use crate::utils::test_util::XorShift64;
328
329        let mut rng = XorShift64::new(0x7072_6563_616c_6321);
330        for _ in 0..16 {
331            let secret_key = StackByteArray::<CRYPTO_BOX_SECRETKEYBYTES>::from(rng.next_bytes32());
332            let other_secret_key =
333                StackByteArray::<CRYPTO_BOX_SECRETKEYBYTES>::from(rng.next_bytes32());
334            let mut public_key = StackByteArray::<CRYPTO_BOX_PUBLICKEYBYTES>::default();
335            crate::classic::crypto_core::crypto_scalarmult_base(
336                public_key.as_mut_array(),
337                other_secret_key.as_array(),
338            );
339
340            let precalc =
341                PrecalcSecretKey::precalculate(&public_key, &secret_key).expect("precalc");
342
343            let sodium_key = crate::native_test_util::box_beforenm(&public_key, &secret_key)
344                .expect("libsodium beforenm");
345            assert_eq!(precalc.as_array(), &sodium_key);
346        }
347    }
348}