Skip to main content

dryoc/
kx.rs

1//! # Key exchange functions
2//!
3//! [`Session`] implements libsodium's client/server key-exchange construction.
4//! A client and server use their own secret key and the other party's public
5//! key to derive two shared session keys: one for receiving and one for
6//! sending.
7//!
8//! Use these session keys with a shared-key encryption API. The exchange does
9//! not encrypt messages by itself. Public keys must be authenticated through a
10//! trusted channel; otherwise an attacker can replace them and sit between the
11//! two parties.
12//!
13//! # Rustaceous API example
14//!
15//! ```
16//! use dryoc::kx::*;
17//!
18//! // Generate random client/server keypairs
19//! let client_keypair = StackKeyPair::generate();
20//! let server_keypair = StackKeyPair::generate();
21//!
22//! // Compute the client's receive and transmit keys.
23//! let client_session_keys = StackSession::new_client(&client_keypair, &server_keypair.public_key)
24//!     .expect("compute client failed");
25//!
26//! // Compute the server's receive and transmit keys.
27//! let server_session_keys = StackSession::new_server(&server_keypair, &client_keypair.public_key)
28//!     .expect("compute server failed");
29//!
30//! let (client_rx, client_tx) = client_session_keys.into_parts();
31//! let (server_rx, server_tx) = server_session_keys.into_parts();
32//!
33//! // Each transmit key matches the other party's receive key.
34//! assert_eq!(client_rx, server_tx);
35//! assert_eq!(client_tx, server_rx);
36//! ```
37//!
38//! ## Additional resources
39//!
40//! * See the [libsodium documentation](https://doc.libsodium.org/key_exchange)
41//!   for more about key exchange
42
43use core::fmt;
44
45#[cfg(feature = "serde")]
46use serde::{Deserialize, Serialize};
47use zeroize::{Zeroize, ZeroizeOnDrop};
48
49use crate::classic::crypto_kx::{crypto_kx_client_session_keys, crypto_kx_server_session_keys};
50use crate::constants::{
51    CRYPTO_KX_PUBLICKEYBYTES, CRYPTO_KX_SECRETKEYBYTES, CRYPTO_KX_SESSIONKEYBYTES,
52};
53use crate::error::Error;
54use crate::types::*;
55
56/// Stack-allocated session key type alias
57pub type SessionKey = StackByteArray<CRYPTO_KX_SESSIONKEYBYTES>;
58/// Stack-allocated public key type alias
59pub type PublicKey = StackByteArray<CRYPTO_KX_PUBLICKEYBYTES>;
60/// Stack-allocated secret key type alias
61pub type SecretKey = StackByteArray<CRYPTO_KX_SECRETKEYBYTES>;
62/// Stack-allocated keypair type alias
63pub type StackKeyPair = crate::keypair::KeyPair<PublicKey, SecretKey>;
64
65#[derive(Zeroize, Clone)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67/// Key derivation implementation based on Curve25519, Diffie-Hellman, and
68/// Blake2b. Compatible with libsodium's `crypto_kx_*` functions.
69///
70/// The session-key type must implement [`ZeroizeOnDrop`] so keys remain
71/// self-wiping after [`Session::into_parts`] transfers ownership to the caller.
72pub struct Session<SessionKey: ByteArray<CRYPTO_KX_SESSIONKEYBYTES> + Zeroize + ZeroizeOnDrop> {
73    rx_key: SessionKey,
74    tx_key: SessionKey,
75}
76
77impl<SessionKey: ByteArray<CRYPTO_KX_SESSIONKEYBYTES> + Zeroize + ZeroizeOnDrop> fmt::Debug
78    for Session<SessionKey>
79{
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.debug_struct("Session")
82            .field("rx_key", &"[REDACTED]")
83            .field("tx_key", &"[REDACTED]")
84            .finish()
85    }
86}
87
88/// Stack-allocated type alias for [`Session`]. Provided for convenience.
89pub type StackSession = Session<SessionKey>;
90
91#[cfg(any(
92    all(feature = "protected", any(unix, windows)),
93    all(doc, not(doctest), feature = "std")
94))]
95#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
96pub mod protected {
97    //! # Protected memory type aliases for [`Session`]
98    //!
99    //! Protected-memory aliases for key exchange.
100    //!
101    //! ## Example
102    //!
103    //! ```
104    //! use dryoc::kx::Session;
105    //! use dryoc::kx::protected::*;
106    //!
107    //! // Generate random client/server keypairs
108    //! let client_keypair = LockedROKeyPair::generate_readonly_locked_keypair()
109    //!     .expect("couldn't generate client keypair");
110    //! let server_keypair = LockedROKeyPair::generate_readonly_locked_keypair()
111    //!     .expect("couldn't generate server keypair");
112    //!
113    //! // Compute client session keys, into default stack-allocated byte array
114    //! let client_session_keys: LockedSession =
115    //!     Session::new_client(&client_keypair, &server_keypair.public_key)
116    //!         .expect("compute client failed");
117    //!
118    //! // Compute server session keys, into default stack-allocated byte array
119    //! let server_session_keys: LockedSession =
120    //!     Session::new_server(&server_keypair, &client_keypair.public_key)
121    //!         .expect("compute client failed");
122    //!
123    //! let (client_rx, client_tx) = client_session_keys.into_parts();
124    //! let (server_rx, server_tx) = server_session_keys.into_parts();
125    //!
126    //! // Client Rx should match server Tx keys
127    //! assert_eq!(client_rx.as_slice(), server_tx.as_slice());
128    //! // Client Tx should match server Rx keys
129    //! assert_eq!(client_tx.as_slice(), server_rx.as_slice());
130    //! ```
131    use super::*;
132    pub use crate::protected::*;
133
134    /// Heap-allocated, page-aligned session key type alias for use with
135    /// protected memory
136    pub type SessionKey = HeapByteArray<CRYPTO_KX_SESSIONKEYBYTES>;
137    /// Heap-allocated, page-aligned public key type alias for use with
138    /// protected memory
139    pub type PublicKey = HeapByteArray<CRYPTO_KX_PUBLICKEYBYTES>;
140    /// Heap-allocated, page-aligned secret key type alias for use with
141    /// protected memory
142    pub type SecretKey = HeapByteArray<CRYPTO_KX_SECRETKEYBYTES>;
143
144    /// Heap-allocated, page-aligned keypair type alias for use with
145    /// protected memory
146    pub type LockedKeyPair = crate::keypair::KeyPair<Locked<PublicKey>, Locked<SecretKey>>;
147    /// Heap-allocated, page-aligned keypair type alias for use with
148    /// protected memory
149    pub type LockedROKeyPair = crate::keypair::KeyPair<LockedRO<PublicKey>, LockedRO<SecretKey>>;
150    /// Locked session keys type alias, for use with protected memory
151    pub type LockedSession = Session<Locked<SessionKey>>;
152}
153
154impl<SessionKey: NewByteArray<CRYPTO_KX_SESSIONKEYBYTES> + Zeroize + ZeroizeOnDrop>
155    Session<SessionKey>
156{
157    /// Computes client session keys, given `client_keypair` and
158    /// `server_public_key`, returning a new session upon success.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if `server_public_key` is unacceptable, including a
163    /// low-order point that would produce an all-zero shared secret.
164    pub fn new_client<
165        PublicKey: ByteArray<CRYPTO_KX_PUBLICKEYBYTES> + Zeroize,
166        SecretKey: ByteArray<CRYPTO_KX_SECRETKEYBYTES> + Zeroize,
167    >(
168        client_keypair: &crate::keypair::KeyPair<PublicKey, SecretKey>,
169        server_public_key: &PublicKey,
170    ) -> Result<Self, Error> {
171        let mut rx_key = SessionKey::new_byte_array();
172        let mut tx_key = SessionKey::new_byte_array();
173
174        crypto_kx_client_session_keys(
175            rx_key.as_mut_array(),
176            tx_key.as_mut_array(),
177            client_keypair.public_key.as_array(),
178            client_keypair.secret_key.as_array(),
179            server_public_key.as_array(),
180        )?;
181
182        Ok(Self { rx_key, tx_key })
183    }
184
185    /// Computes server session keys, given `server_keypair` and
186    /// `client_public_key`, returning a new session upon success.
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if `client_public_key` is unacceptable, including a
191    /// low-order point that would produce an all-zero shared secret.
192    pub fn new_server<
193        PublicKey: ByteArray<CRYPTO_KX_PUBLICKEYBYTES> + Zeroize,
194        SecretKey: ByteArray<CRYPTO_KX_SECRETKEYBYTES> + Zeroize,
195    >(
196        server_keypair: &crate::keypair::KeyPair<PublicKey, SecretKey>,
197        client_public_key: &PublicKey,
198    ) -> Result<Self, Error> {
199        let mut rx_key = SessionKey::new_byte_array();
200        let mut tx_key = SessionKey::new_byte_array();
201
202        crypto_kx_server_session_keys(
203            rx_key.as_mut_array(),
204            tx_key.as_mut_array(),
205            server_keypair.public_key.as_array(),
206            server_keypair.secret_key.as_array(),
207            client_public_key.as_array(),
208        )?;
209
210        Ok(Self { rx_key, tx_key })
211    }
212}
213
214impl<SessionKey: ByteArray<CRYPTO_KX_SESSIONKEYBYTES> + Zeroize + ZeroizeOnDrop>
215    Session<SessionKey>
216{
217    /// Moves the rx_key and tx_key out of this instance, returning them as a
218    /// tuple with `(rx_key, tx_key)`.
219    #[must_use]
220    pub fn into_parts(self) -> (SessionKey, SessionKey) {
221        (self.rx_key, self.tx_key)
222    }
223
224    /// Returns a reference to a slice of the Rx session key.
225    #[inline]
226    pub fn rx_as_slice(&self) -> &[u8] {
227        self.rx_key.as_slice()
228    }
229
230    /// Returns a reference to a slice of the Tx session key.
231    #[inline]
232    pub fn tx_as_slice(&self) -> &[u8] {
233        self.tx_key.as_slice()
234    }
235
236    /// Returns a reference to an array of the Rx session key.
237    #[inline]
238    pub fn rx_as_array(&self) -> &[u8; CRYPTO_KX_SESSIONKEYBYTES] {
239        self.rx_key.as_array()
240    }
241
242    /// Returns a reference to an array of the Tx session key.
243    #[inline]
244    pub fn tx_as_array(&self) -> &[u8; CRYPTO_KX_SESSIONKEYBYTES] {
245        self.tx_key.as_array()
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn session_debug_redacts_keys() {
255        let session = StackSession {
256            rx_key: SessionKey::from([1u8; CRYPTO_KX_SESSIONKEYBYTES]),
257            tx_key: SessionKey::from([2u8; CRYPTO_KX_SESSIONKEYBYTES]),
258        };
259
260        assert_eq!(
261            format!("{session:?}"),
262            "Session { rx_key: \"[REDACTED]\", tx_key: \"[REDACTED]\" }"
263        );
264    }
265
266    use crate::classic::crypto_kx::{crypto_kx_client_session_keys, crypto_kx_server_session_keys};
267    use crate::constants::CRYPTO_BOX_SEEDBYTES;
268    use crate::utils::test_util::XorShift64;
269
270    /// Client and server keypairs from `crypto_box_seed_keypair([1; 32])` and
271    /// `[2; 32]`, with the session keys libsodium's `crypto_kx_*_session_keys`
272    /// derive for them.
273    const CLIENT_SEED: [u8; CRYPTO_BOX_SEEDBYTES] = [1u8; CRYPTO_BOX_SEEDBYTES];
274    const SERVER_SEED: [u8; CRYPTO_BOX_SEEDBYTES] = [2u8; CRYPTO_BOX_SEEDBYTES];
275    const CLIENT_RX: &str = "4081524abf55a75021ebd5e98e08552fb2bd26315c40e563b74e64abff1be442";
276    const CLIENT_TX: &str = "2f9c2f944f504caf772db17affc91e3ba8886a806ba53ab37881d15c042f3410";
277
278    fn kat_keypairs() -> (StackKeyPair, StackKeyPair) {
279        (
280            StackKeyPair::from_seed(&CLIENT_SEED),
281            StackKeyPair::from_seed(&SERVER_SEED),
282        )
283    }
284
285    fn low_order_public_keys() -> [PublicKey; 2] {
286        let mut identity = PublicKey::default();
287        identity[0] = 1;
288        [PublicKey::default(), identity]
289    }
290
291    #[test]
292    fn seeded_sessions_match_libsodium_known_answers_with_rx_tx_crossed() {
293        let (client, server) = kat_keypairs();
294        let client_rx = hex::decode(CLIENT_RX).expect("hex");
295        let client_tx = hex::decode(CLIENT_TX).expect("hex");
296
297        let client_session = StackSession::new_client(&client, &server.public_key).expect("client");
298        let server_session = StackSession::new_server(&server, &client.public_key).expect("server");
299
300        assert_eq!(client_session.rx_as_slice(), client_rx.as_slice());
301        assert_eq!(client_session.tx_as_slice(), client_tx.as_slice());
302        assert_eq!(client_session.rx_as_array(), &client_rx[..]);
303        assert_eq!(client_session.tx_as_array(), &client_tx[..]);
304        assert_eq!(server_session.rx_as_slice(), client_tx.as_slice());
305        assert_eq!(server_session.tx_as_slice(), client_rx.as_slice());
306
307        let (rx, tx) = client_session.into_parts();
308        assert_eq!(rx.as_slice(), client_rx.as_slice());
309        assert_eq!(tx.as_slice(), client_tx.as_slice());
310        let (rx, tx) = server_session.into_parts();
311        assert_eq!(rx.as_slice(), client_tx.as_slice());
312        assert_eq!(tx.as_slice(), client_rx.as_slice());
313
314        // Roles are part of the derivation: swapping them changes the keys.
315        let swapped = StackSession::new_client(&server, &client.public_key).expect("kx");
316        assert_ne!(swapped.rx_as_slice(), client_rx.as_slice());
317        assert_ne!(swapped.rx_as_slice(), client_tx.as_slice());
318    }
319
320    #[test]
321    fn sessions_match_classic_session_keys() {
322        let mut rng = XorShift64::new(0x6b78_5f73_6573_7300);
323        for _ in 0..8 {
324            let client = StackKeyPair::from_seed(&rng.next_bytes32());
325            let server = StackKeyPair::from_seed(&rng.next_bytes32());
326
327            let mut rx = [0u8; CRYPTO_KX_SESSIONKEYBYTES];
328            let mut tx = [0u8; CRYPTO_KX_SESSIONKEYBYTES];
329            crypto_kx_client_session_keys(
330                &mut rx,
331                &mut tx,
332                client.public_key.as_array(),
333                client.secret_key.as_array(),
334                server.public_key.as_array(),
335            )
336            .expect("classic client");
337            let session: Session<SessionKey> =
338                Session::new_client(&client, &server.public_key).expect("client");
339            assert_eq!(session.rx_as_array(), &rx);
340            assert_eq!(session.tx_as_array(), &tx);
341
342            crypto_kx_server_session_keys(
343                &mut rx,
344                &mut tx,
345                server.public_key.as_array(),
346                server.secret_key.as_array(),
347                client.public_key.as_array(),
348            )
349            .expect("classic server");
350            let session: Session<SessionKey> =
351                Session::new_server(&server, &client.public_key).expect("server");
352            assert_eq!(session.rx_as_array(), &rx);
353            assert_eq!(session.tx_as_array(), &tx);
354        }
355    }
356
357    #[test]
358    fn low_order_peer_keys_are_rejected_on_both_sides() {
359        let (client, server) = kat_keypairs();
360        for low_order in low_order_public_keys() {
361            assert!(StackSession::new_client(&client, &low_order).is_err());
362            assert!(StackSession::new_server(&server, &low_order).is_err());
363        }
364    }
365
366    #[cfg(all(feature = "serde", feature = "alloc"))]
367    #[test]
368    fn serde_round_trip_keeps_rx_and_tx_in_place() {
369        use crate::dryocsecretbox::{DryocSecretBox, Nonce, VecBox};
370
371        let (client, server) = kat_keypairs();
372        let client_session = StackSession::new_client(&client, &server.public_key).expect("client");
373        let server_session = StackSession::new_server(&server, &client.public_key).expect("server");
374
375        let json = serde_json::to_string(&client_session).expect("serialize");
376        let decoded: StackSession = serde_json::from_str(&json).expect("deserialize");
377        assert_eq!(decoded.rx_as_slice(), hex::decode(CLIENT_RX).expect("hex"));
378        assert_eq!(decoded.tx_as_slice(), hex::decode(CLIENT_TX).expect("hex"));
379
380        // The decoded client tx key opens what the server encrypts with its rx
381        // key's counterpart, so the field order survived the round trip.
382        let nonce = Nonce::from([7u8; crate::constants::CRYPTO_SECRETBOX_NONCEBYTES]);
383        let (server_rx, server_tx) = server_session.into_parts();
384        let (decoded_rx, decoded_tx) = decoded.into_parts();
385        let from_server = DryocSecretBox::encrypt_to_vecbox(b"server says", &nonce, &server_tx)
386            .expect("encrypt failed");
387        assert_eq!(
388            VecBox::from_bytes(&from_server.to_vec())
389                .expect("parse")
390                .decrypt_to_vec(&nonce, &decoded_rx)
391                .expect("decrypt"),
392            b"server says"
393        );
394        let from_client = DryocSecretBox::encrypt_to_vecbox(b"client says", &nonce, &decoded_tx)
395            .expect("encrypt failed");
396        assert_eq!(
397            from_client
398                .decrypt_to_vec(&nonce, &server_rx)
399                .expect("decrypt"),
400            b"client says"
401        );
402        assert!(from_client.decrypt_to_vec(&nonce, &server_tx).is_err());
403    }
404
405    #[cfg(all(feature = "protected", any(unix, windows)))]
406    #[test]
407    fn locked_sessions_match_stack_sessions() {
408        use crate::kx::protected::*;
409
410        let (client, server) = kat_keypairs();
411        let locked_client = LockedKeyPair {
412            public_key: protected::PublicKey::from_slice_into_locked(client.public_key.as_slice())
413                .expect("lock client pk"),
414            secret_key: protected::SecretKey::from_slice_into_locked(client.secret_key.as_slice())
415                .expect("lock client sk"),
416        };
417        let locked_server_pk =
418            protected::PublicKey::from_slice_into_locked(server.public_key.as_slice())
419                .expect("lock server pk");
420
421        let session: LockedSession =
422            Session::new_client(&locked_client, &locked_server_pk).expect("client");
423        assert_eq!(session.rx_as_slice(), hex::decode(CLIENT_RX).expect("hex"));
424        assert_eq!(session.tx_as_slice(), hex::decode(CLIENT_TX).expect("hex"));
425
426        let locked_low_order = protected::PublicKey::new_locked().expect("lock low-order key");
427        let rejected: Result<LockedSession, Error> =
428            Session::new_client(&locked_client, &locked_low_order);
429        assert!(rejected.is_err());
430    }
431
432    #[cfg(dryoc_native_tests)]
433    #[test]
434    fn sessions_match_libsodium_session_keys() {
435        use crate::native_test_util::{kx_client_session_keys, kx_server_session_keys};
436
437        let mut rng = XorShift64::new(0x6c69_6273_6f64_6b78);
438        for _ in 0..8 {
439            let client = StackKeyPair::from_seed(&rng.next_bytes32());
440            let server = StackKeyPair::from_seed(&rng.next_bytes32());
441            let client_session =
442                StackSession::new_client(&client, &server.public_key).expect("client");
443            let server_session =
444                StackSession::new_server(&server, &client.public_key).expect("server");
445
446            let (rx, tx) =
447                kx_client_session_keys(&client.public_key, &client.secret_key, &server.public_key)
448                    .expect("libsodium client");
449            assert_eq!(client_session.rx_as_array(), &rx);
450            assert_eq!(client_session.tx_as_array(), &tx);
451
452            let (rx, tx) =
453                kx_server_session_keys(&server.public_key, &server.secret_key, &client.public_key)
454                    .expect("libsodium server");
455            assert_eq!(server_session.rx_as_array(), &rx);
456            assert_eq!(server_session.tx_as_array(), &tx);
457        }
458    }
459}