Skip to main content

dryoc/classic/
crypto_kem_xwing.rs

1//! # X-Wing hybrid key encapsulation
2//!
3//! Implements libsodium's `crypto_kem_xwing_*` functions: X-Wing
4//! (draft-connolly-cfrg-xwing-kem), which combines ML-KEM-768 with X25519.
5//! Its shared secret stays secure as long as either ML-KEM-768 or X25519
6//! does, so it protects against quantum computers without giving up the
7//! security of elliptic-curve cryptography. [`crate::classic::crypto_kem`]
8//! uses X-Wing.
9//!
10//! The 32-byte secret key is a seed; the ML-KEM-768 and X25519 keys are
11//! derived from it with SHAKE256 whenever they are needed. The public key is
12//! the ML-KEM-768 public key followed by the X25519 public key, and the
13//! ciphertext is the ML-KEM-768 ciphertext followed by an ephemeral X25519
14//! public key. The shared secret is SHA3-256 of both component secrets, the
15//! X25519 ciphertext and public key, and a fixed label.
16//!
17//! A KEM does not authenticate the sender. Feed the shared secret to a
18//! key-derivation function before using it as an encryption key.
19//!
20//! ```
21//! use dryoc::classic::crypto_kem_xwing::*;
22//!
23//! let (public_key, secret_key) = crypto_kem_xwing_keypair();
24//!
25//! let mut ciphertext = [0u8; dryoc::constants::CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
26//! let mut sender_secret = SharedSecret::default();
27//! crypto_kem_xwing_enc(&mut ciphertext, &mut sender_secret, &public_key)
28//!     .expect("encapsulation failed");
29//!
30//! let mut recipient_secret = SharedSecret::default();
31//! crypto_kem_xwing_dec(&mut recipient_secret, &ciphertext, &secret_key)
32//!     .expect("decapsulation failed");
33//! assert_eq!(sender_secret, recipient_secret);
34//! ```
35
36use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
37
38use crate::classic::crypto_core::{crypto_scalarmult_and_base, crypto_scalarmult_base};
39use crate::constants::{
40    CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES, CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES,
41    CRYPTO_KEM_MLKEM768_SECRETKEYBYTES, CRYPTO_KEM_XWING_CIPHERTEXTBYTES,
42    CRYPTO_KEM_XWING_ENCSEEDBYTES, CRYPTO_KEM_XWING_PUBLICKEYBYTES,
43    CRYPTO_KEM_XWING_SECRETKEYBYTES, CRYPTO_KEM_XWING_SEEDBYTES,
44    CRYPTO_KEM_XWING_SHAREDSECRETBYTES, CRYPTO_SCALARMULT_BYTES,
45};
46use crate::error::Error;
47use crate::keccak::{DOMAIN_SHA3, DOMAIN_SHAKE, RATE_256, ROUNDS_FULL, Sponge, hash};
48use crate::mlkem::{self, Arith};
49use crate::rng::copy_randombytes;
50use crate::utils::zeroize_bytes;
51
52/// X-Wing public key: the ML-KEM-768 public key, then the X25519 public key.
53pub type PublicKey = [u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES];
54/// X-Wing secret key: the 32-byte seed both component keys derive from.
55pub type SecretKey = [u8; CRYPTO_KEM_XWING_SECRETKEYBYTES];
56/// X-Wing ciphertext: the ML-KEM-768 ciphertext, then an ephemeral X25519
57/// public key.
58pub type Ciphertext = [u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
59/// Shared secret produced by encapsulation and decapsulation.
60pub type SharedSecret = [u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
61/// Key-generation seed; the secret key is the seed itself.
62pub type Seed = [u8; CRYPTO_KEM_XWING_SEEDBYTES];
63/// Encapsulation seed: the ML-KEM-768 message, then the ephemeral X25519
64/// secret key.
65pub type EncSeed = [u8; CRYPTO_KEM_XWING_ENCSEEDBYTES];
66
67/// The combiner's domain-separation label, `\.//^\`.
68const LABEL: &[u8; 6] = b"\\.//^\\";
69
70/// The expanded secret key: the ML-KEM-768 key pair, and the X25519 secret
71/// and public keys. Wiped on drop, sixteen bytes per store (a derived wipe
72/// stores each of its 3.6 KB separately); [`Expanded::derive`] fills it in
73/// place so no secret is copied out of it.
74struct Expanded {
75    mlkem_public_key: [u8; CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES],
76    mlkem_secret_key: [u8; CRYPTO_KEM_MLKEM768_SECRETKEYBYTES],
77    x25519_secret_key: [u8; CRYPTO_SCALARMULT_BYTES],
78    x25519_public_key: [u8; CRYPTO_SCALARMULT_BYTES],
79}
80
81impl Zeroize for Expanded {
82    fn zeroize(&mut self) {
83        zeroize_bytes(&mut self.mlkem_public_key);
84        zeroize_bytes(&mut self.mlkem_secret_key);
85        zeroize_bytes(&mut self.x25519_secret_key);
86        zeroize_bytes(&mut self.x25519_public_key);
87    }
88}
89
90impl Drop for Expanded {
91    fn drop(&mut self) {
92        self.zeroize();
93    }
94}
95
96impl ZeroizeOnDrop for Expanded {}
97
98impl Expanded {
99    fn zeroed() -> Self {
100        Self {
101            mlkem_public_key: [0u8; CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES],
102            mlkem_secret_key: [0u8; CRYPTO_KEM_MLKEM768_SECRETKEYBYTES],
103            x25519_secret_key: [0u8; CRYPTO_SCALARMULT_BYTES],
104            x25519_public_key: [0u8; CRYPTO_SCALARMULT_BYTES],
105        }
106    }
107
108    /// Expands `seed` with SHAKE256 into the ML-KEM-768 seed `d || z` and the
109    /// X25519 secret key, then derives both key pairs into `self`, with
110    /// ML-KEM arithmetic from `arith`.
111    fn derive(&mut self, arith: Arith, seed: &SecretKey) {
112        let keys = self;
113        let mut mlkem_seed = Zeroizing::new([0u8; 64]);
114        expand(seed, &mut mlkem_seed, &mut keys.x25519_secret_key);
115        mlkem::keypair(
116            arith,
117            &mut keys.mlkem_public_key,
118            &mut keys.mlkem_secret_key,
119            &mlkem_seed,
120        );
121        crypto_scalarmult_base(&mut keys.x25519_public_key, &keys.x25519_secret_key);
122    }
123}
124
125/// Expands `seed` with SHAKE256 into the ML-KEM-768 seed `d || z` and the
126/// X25519 secret key.
127fn expand(
128    seed: &SecretKey,
129    mlkem_seed: &mut [u8; 64],
130    x25519_secret_key: &mut [u8; CRYPTO_SCALARMULT_BYTES],
131) {
132    let mut sponge = Sponge::<RATE_256, ROUNDS_FULL>::new();
133    sponge.absorb(seed);
134    sponge.pad(DOMAIN_SHAKE);
135    sponge.squeeze(mlkem_seed);
136    sponge.squeeze(x25519_secret_key);
137}
138
139/// The X-Wing combiner.
140fn combine(
141    shared_secret: &mut SharedSecret,
142    mlkem_secret: &[u8],
143    x25519_secret: &[u8],
144    x25519_ciphertext: &[u8],
145    x25519_public_key: &[u8],
146) {
147    hash::<RATE_256>(
148        shared_secret,
149        DOMAIN_SHA3,
150        &[
151            mlkem_secret,
152            x25519_secret,
153            x25519_ciphertext,
154            x25519_public_key,
155            LABEL,
156        ],
157    );
158}
159
160/// In-place variant of [`crypto_kem_xwing_seed_keypair`].
161pub fn crypto_kem_xwing_seed_keypair_inplace(
162    public_key: &mut PublicKey,
163    secret_key: &mut SecretKey,
164    seed: &Seed,
165) {
166    let mut keys = Expanded::zeroed();
167    keys.derive(Arith::detect(), seed);
168    let (mlkem_public_key, x25519_public_key) =
169        public_key.split_at_mut(CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES);
170    mlkem_public_key.copy_from_slice(&keys.mlkem_public_key);
171    x25519_public_key.copy_from_slice(&keys.x25519_public_key);
172    secret_key.copy_from_slice(seed);
173}
174
175/// Deterministically derives a key pair from `seed`. The secret key is the
176/// seed itself.
177///
178/// Compatible with libsodium's `crypto_kem_xwing_seed_keypair`.
179#[must_use]
180pub fn crypto_kem_xwing_seed_keypair(seed: &Seed) -> (PublicKey, SecretKey) {
181    let mut public_key = [0u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES];
182    let mut secret_key = [0u8; CRYPTO_KEM_XWING_SECRETKEYBYTES];
183    crypto_kem_xwing_seed_keypair_inplace(&mut public_key, &mut secret_key, seed);
184    (public_key, secret_key)
185}
186
187/// In-place variant of [`crypto_kem_xwing_keypair`].
188pub fn crypto_kem_xwing_keypair_inplace(public_key: &mut PublicKey, secret_key: &mut SecretKey) {
189    let mut seed = Zeroizing::new([0u8; CRYPTO_KEM_XWING_SEEDBYTES]);
190    copy_randombytes(seed.as_mut_slice());
191    crypto_kem_xwing_seed_keypair_inplace(public_key, secret_key, &seed);
192}
193
194/// Returns a randomly generated key pair.
195///
196/// Compatible with libsodium's `crypto_kem_xwing_keypair`.
197#[must_use]
198pub fn crypto_kem_xwing_keypair() -> (PublicKey, SecretKey) {
199    let mut public_key = [0u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES];
200    let mut secret_key = [0u8; CRYPTO_KEM_XWING_SECRETKEYBYTES];
201    crypto_kem_xwing_keypair_inplace(&mut public_key, &mut secret_key);
202    (public_key, secret_key)
203}
204
205/// Creates a random shared secret for `public_key`, writing it to
206/// `shared_secret` and its encapsulation to `ciphertext`.
207///
208/// Compatible with libsodium's `crypto_kem_xwing_enc`.
209///
210/// # Errors
211///
212/// Returns [`Error::InvalidKey`] if the ML-KEM-768 part of `public_key` is
213/// not a valid encapsulation key, or if its X25519 part is a low-order point
214/// that gives an all-zero X25519 shared secret. `ciphertext` and
215/// `shared_secret` are then left unchanged.
216pub fn crypto_kem_xwing_enc(
217    ciphertext: &mut Ciphertext,
218    shared_secret: &mut SharedSecret,
219    public_key: &PublicKey,
220) -> Result<(), Error> {
221    let mut seed = Zeroizing::new([0u8; CRYPTO_KEM_XWING_ENCSEEDBYTES]);
222    copy_randombytes(seed.as_mut_slice());
223    crypto_kem_xwing_enc_deterministic(ciphertext, shared_secret, public_key, &seed)
224}
225
226/// Deterministic variant of [`crypto_kem_xwing_enc`] with the encapsulation
227/// randomness taken from `seed`. For known-answer tests; a repeated seed
228/// repeats the shared secret.
229///
230/// Compatible with libsodium's `crypto_kem_xwing_enc_deterministic`.
231///
232/// # Errors
233///
234/// Returns the same errors as [`crypto_kem_xwing_enc`].
235pub fn crypto_kem_xwing_enc_deterministic(
236    ciphertext: &mut Ciphertext,
237    shared_secret: &mut SharedSecret,
238    public_key: &PublicKey,
239    seed: &EncSeed,
240) -> Result<(), Error> {
241    enc_deterministic(Arith::detect(), ciphertext, shared_secret, public_key, seed)
242}
243
244/// [`crypto_kem_xwing_enc_deterministic`] with ML-KEM arithmetic from
245/// `arith`, so tests can run each backend through this driver.
246pub(crate) fn enc_deterministic(
247    arith: Arith,
248    ciphertext: &mut Ciphertext,
249    shared_secret: &mut SharedSecret,
250    public_key: &PublicKey,
251    seed: &EncSeed,
252) -> Result<(), Error> {
253    let (mlkem_public_key, x25519_public_key) =
254        public_key.split_at(CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES);
255    let (mlkem_seed, x25519_ephemeral) = seed.split_at(32);
256    let x25519_public_key: &[u8; CRYPTO_SCALARMULT_BYTES] =
257        x25519_public_key.try_into().expect("32-byte X25519 key");
258    let x25519_ephemeral: &[u8; CRYPTO_SCALARMULT_BYTES] =
259        x25519_ephemeral.try_into().expect("32-byte X25519 key");
260    let (mlkem_ciphertext, x25519_ciphertext) =
261        ciphertext.split_at_mut(CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES);
262
263    // Both fallible steps run before `ciphertext` is written, so a rejected
264    // key leaves it unchanged, as in libsodium: the X25519 exchange (with
265    // the ephemeral public key, one inversion for both) first, then ML-KEM,
266    // which checks the key before encrypting.
267    let mut x25519_secret = Zeroizing::new([0u8; CRYPTO_SCALARMULT_BYTES]);
268    let mut x25519_ephemeral_public = [0u8; CRYPTO_SCALARMULT_BYTES];
269    crypto_scalarmult_and_base(
270        &mut x25519_secret,
271        &mut x25519_ephemeral_public,
272        x25519_ephemeral,
273        x25519_public_key,
274    )?;
275    let mut mlkem_secret = Zeroizing::new([0u8; 32]);
276    mlkem::encapsulate(
277        arith,
278        mlkem_ciphertext.try_into().expect("sized ciphertext"),
279        &mut mlkem_secret,
280        mlkem_public_key.try_into().expect("sized public key"),
281        mlkem_seed.try_into().expect("32-byte seed"),
282    )?;
283    let x25519_ciphertext: &mut [u8; CRYPTO_SCALARMULT_BYTES] =
284        x25519_ciphertext.try_into().expect("32-byte X25519 key");
285    *x25519_ciphertext = x25519_ephemeral_public;
286
287    combine(
288        shared_secret,
289        &*mlkem_secret,
290        &*x25519_secret,
291        x25519_ciphertext,
292        x25519_public_key,
293    );
294    Ok(())
295}
296
297/// Recovers the shared secret encapsulated in `ciphertext` with
298/// `secret_key`, writing it to `shared_secret`. A ciphertext not created for
299/// this key yields an unrelated pseudorandom secret, not an error.
300///
301/// Compatible with libsodium's `crypto_kem_xwing_dec`.
302///
303/// # Errors
304///
305/// Returns [`Error::InvalidKey`] if the X25519 part of `ciphertext` is a
306/// low-order point that gives an all-zero X25519 shared secret.
307pub fn crypto_kem_xwing_dec(
308    shared_secret: &mut SharedSecret,
309    ciphertext: &Ciphertext,
310    secret_key: &SecretKey,
311) -> Result<(), Error> {
312    dec(Arith::detect(), shared_secret, ciphertext, secret_key)
313}
314
315/// [`crypto_kem_xwing_dec`] with ML-KEM arithmetic from `arith`, so tests
316/// can run each backend through this driver.
317pub(crate) fn dec(
318    arith: Arith,
319    shared_secret: &mut SharedSecret,
320    ciphertext: &Ciphertext,
321    secret_key: &SecretKey,
322) -> Result<(), Error> {
323    // The ML-KEM key pair is not encoded: `decapsulate_seed` decapsulates
324    // under the key pair `seed_keypair` would derive, straight from its seed.
325    let mut mlkem_seed = Zeroizing::new([0u8; 64]);
326    let mut x25519_secret_key = Zeroizing::new([0u8; CRYPTO_SCALARMULT_BYTES]);
327    expand(secret_key, &mut mlkem_seed, &mut x25519_secret_key);
328    let (mlkem_ciphertext, x25519_ciphertext) =
329        ciphertext.split_at(CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES);
330    let x25519_ciphertext: &[u8; CRYPTO_SCALARMULT_BYTES] =
331        x25519_ciphertext.try_into().expect("32-byte X25519 key");
332
333    let mut x25519_secret = Zeroizing::new([0u8; CRYPTO_SCALARMULT_BYTES]);
334    let mut x25519_public_key = [0u8; CRYPTO_SCALARMULT_BYTES];
335    crypto_scalarmult_and_base(
336        &mut x25519_secret,
337        &mut x25519_public_key,
338        &x25519_secret_key,
339        x25519_ciphertext,
340    )?;
341    let mut mlkem_secret = Zeroizing::new([0u8; 32]);
342    mlkem::decapsulate_seed(
343        arith,
344        &mut mlkem_secret,
345        mlkem_ciphertext.try_into().expect("sized ciphertext"),
346        &mlkem_seed,
347    );
348
349    combine(
350        shared_secret,
351        &*mlkem_secret,
352        &*x25519_secret,
353        x25519_ciphertext,
354        &x25519_public_key,
355    );
356    Ok(())
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use crate::mlkem::tests::{field, records};
363
364    /// The draft's Appendix C vectors, which libsodium reproduces: seed to
365    /// key pair, deterministic encapsulation and decapsulation.
366    #[test]
367    fn test_draft_vectors() {
368        for record in records(include_str!("../mlkem/test-vectors/xwing_draft.txt")) {
369            let index = record["index"];
370            let (public_key, secret_key) = crypto_kem_xwing_seed_keypair(&field(&record, "seed"));
371            assert_eq!(public_key, field(&record, "pk"), "index {index}");
372            assert_eq!(secret_key, field::<32>(&record, "seed"), "index {index}");
373
374            let mut ciphertext = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
375            let mut sent = [0u8; 32];
376            crypto_kem_xwing_enc_deterministic(
377                &mut ciphertext,
378                &mut sent,
379                &public_key,
380                &field(&record, "eseed"),
381            )
382            .expect("enc");
383            assert_eq!(ciphertext, field(&record, "ct"), "index {index}");
384            assert_eq!(sent, field::<32>(&record, "ss"), "index {index}");
385
386            let mut received = [0u8; 32];
387            crypto_kem_xwing_dec(&mut received, &ciphertext, &secret_key).expect("dec");
388            assert_eq!(received, sent, "index {index}");
389        }
390    }
391
392    /// libsodium's return codes for low-order X25519 inputs and an invalid
393    /// ML-KEM key, and its shared secret for an X25519 ciphertext with the
394    /// top bit set (hashed as given, not masked). Like libsodium, a failed
395    /// call leaves its output buffers unchanged.
396    #[test]
397    fn test_libsodium_edge_cases() {
398        for record in records(include_str!(
399            "../mlkem/test-vectors/xwing_libsodium_edge.txt"
400        )) {
401            let name = record["name"];
402            let success = record["rc"] == "0";
403            let mut ciphertext = [0xa5u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
404            let mut shared_secret = [0xa5u8; 32];
405            let result = match record["op"] {
406                "enc_deterministic" => crypto_kem_xwing_enc_deterministic(
407                    &mut ciphertext,
408                    &mut shared_secret,
409                    &field(&record, "pk"),
410                    &field(&record, "eseed"),
411                ),
412                "dec" => crypto_kem_xwing_dec(
413                    &mut shared_secret,
414                    &field(&record, "ct"),
415                    &field(&record, "sk"),
416                ),
417                op => panic!("unknown op {op}"),
418            };
419            assert_eq!(result.is_ok(), success, "{name}");
420            if !success {
421                assert_eq!(
422                    ciphertext, [0xa5; CRYPTO_KEM_XWING_CIPHERTEXTBYTES],
423                    "{name}"
424                );
425                assert_eq!(shared_secret, [0xa5; 32], "{name}");
426            }
427            if let Some(ss) = record.get("ss") {
428                assert_eq!(hex::encode(shared_secret), *ss, "{name}");
429            }
430        }
431    }
432}
433
434/// Cross-checks against libsodium 1.0.22's X-Wing.
435#[cfg(all(test, dryoc_native_tests))]
436mod native_tests {
437    use super::*;
438    use crate::classic::crypto_kem_mlkem768::native_tests::{
439        seeds, tampered, with_unreduced_coefficient,
440    };
441    use crate::native_test_util as sodium;
442
443    /// Copies of `key` with its trailing X25519 part replaced by the
444    /// low-order points `u = 0` and `u = 1`.
445    fn with_low_order_x25519<const N: usize>(key: &[u8; N]) -> [[u8; N]; 2] {
446        [0u8, 1].map(|u| {
447            let mut copy = *key;
448            copy[N - CRYPTO_SCALARMULT_BYTES..].fill(0);
449            copy[N - CRYPTO_SCALARMULT_BYTES] = u;
450            copy
451        })
452    }
453
454    /// For every key seed and encapsulation seed, both libraries derive the
455    /// same key pair, encapsulate to the same ciphertext and shared secret,
456    /// decapsulate it to that secret, and agree on the secret of each
457    /// tampered ciphertext (ML-KEM implicit rejection, or a changed X25519
458    /// share).
459    #[test]
460    fn test_xwing_matches_libsodium() {
461        for seed in seeds::<CRYPTO_KEM_XWING_SEEDBYTES>() {
462            let (public_key, secret_key) = crypto_kem_xwing_seed_keypair(&seed);
463            let (so_public_key, so_secret_key) = sodium::crypto_kem_xwing_seed_keypair(&seed);
464            assert_eq!(public_key, so_public_key, "seed {seed:02x?}");
465            assert_eq!(secret_key, so_secret_key, "seed {seed:02x?}");
466
467            for enc_seed in seeds::<CRYPTO_KEM_XWING_ENCSEEDBYTES>() {
468                let mut ciphertext = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
469                let mut sent = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
470                crypto_kem_xwing_enc_deterministic(
471                    &mut ciphertext,
472                    &mut sent,
473                    &public_key,
474                    &enc_seed,
475                )
476                .expect("enc");
477                let (so_ciphertext, so_sent) =
478                    sodium::crypto_kem_xwing_enc_deterministic(&public_key, &enc_seed)
479                        .expect("libsodium enc");
480                assert_eq!(ciphertext, so_ciphertext, "enc seed {enc_seed:02x?}");
481                assert_eq!(sent, so_sent, "enc seed {enc_seed:02x?}");
482
483                for ciphertext in core::iter::once(ciphertext).chain(tampered(&ciphertext)) {
484                    let mut received = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
485                    crypto_kem_xwing_dec(&mut received, &ciphertext, &secret_key).expect("dec");
486                    let so_received = sodium::crypto_kem_xwing_dec(&ciphertext, &secret_key)
487                        .expect("libsodium dec");
488                    assert_eq!(received, so_received, "enc seed {enc_seed:02x?}");
489                    assert_eq!(received == sent, ciphertext == so_ciphertext);
490                }
491            }
492        }
493    }
494
495    /// Both libraries refuse to encapsulate to a key whose X25519 part is a
496    /// low-order point or whose ML-KEM part has an unreduced coefficient,
497    /// and refuse to decapsulate a ciphertext whose X25519 part is zero.
498    #[test]
499    fn test_xwing_invalid_inputs_rejected_like_libsodium() {
500        for seed in seeds::<CRYPTO_KEM_XWING_SEEDBYTES>() {
501            let (public_key, secret_key) = crypto_kem_xwing_seed_keypair(&seed);
502            let enc_seed = [9u8; CRYPTO_KEM_XWING_ENCSEEDBYTES];
503            let invalid_keys = with_low_order_x25519(&public_key)
504                .into_iter()
505                .chain(with_unreduced_coefficient(&public_key));
506            for invalid in invalid_keys {
507                let mut ciphertext = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
508                let mut shared_secret = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
509                assert!(
510                    crypto_kem_xwing_enc_deterministic(
511                        &mut ciphertext,
512                        &mut shared_secret,
513                        &invalid,
514                        &enc_seed,
515                    )
516                    .is_err()
517                );
518                assert!(
519                    crypto_kem_xwing_enc(&mut ciphertext, &mut shared_secret, &invalid).is_err()
520                );
521                assert!(sodium::crypto_kem_xwing_enc_deterministic(&invalid, &enc_seed).is_err());
522                assert!(sodium::crypto_kem_xwing_enc(&invalid).is_err());
523            }
524
525            let (mut ciphertext, _) =
526                sodium::crypto_kem_xwing_enc_deterministic(&public_key, &enc_seed).expect("enc");
527            ciphertext[CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES..].fill(0);
528            let mut shared_secret = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
529            assert!(crypto_kem_xwing_dec(&mut shared_secret, &ciphertext, &secret_key).is_err());
530            assert!(sodium::crypto_kem_xwing_dec(&ciphertext, &secret_key).is_err());
531        }
532    }
533
534    /// Randomized encapsulations made by either library decapsulate to the
535    /// same shared secret in the other.
536    #[test]
537    fn test_xwing_randomized_interop_with_libsodium() {
538        for _ in 0..8 {
539            let (public_key, secret_key) = crypto_kem_xwing_keypair();
540
541            let mut ciphertext = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
542            let mut sent = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
543            crypto_kem_xwing_enc(&mut ciphertext, &mut sent, &public_key).expect("enc");
544            assert_eq!(
545                sodium::crypto_kem_xwing_dec(&ciphertext, &secret_key).expect("libsodium dec"),
546                sent
547            );
548
549            let (so_ciphertext, so_sent) =
550                sodium::crypto_kem_xwing_enc(&public_key).expect("libsodium enc");
551            let mut received = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
552            crypto_kem_xwing_dec(&mut received, &so_ciphertext, &secret_key).expect("dec");
553            assert_eq!(received, so_sent);
554        }
555    }
556}
557
558#[cfg(all(test, feature = "nightly"))]
559mod benches {
560    extern crate test;
561
562    use super::*;
563
564    #[bench]
565    fn xwing_keypair_bench(b: &mut test::Bencher) {
566        b.iter(|| crypto_kem_xwing_seed_keypair(test::black_box(&[7u8; 32])));
567    }
568
569    #[bench]
570    fn xwing_enc_bench(b: &mut test::Bencher) {
571        let (public_key, _) = crypto_kem_xwing_seed_keypair(&[7u8; 32]);
572        let (mut ciphertext, mut shared_secret) =
573            ([0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES], [0u8; 32]);
574        b.iter(|| {
575            crypto_kem_xwing_enc_deterministic(
576                &mut ciphertext,
577                &mut shared_secret,
578                test::black_box(&public_key),
579                test::black_box(&[9u8; 64]),
580            )
581            .expect("enc")
582        });
583    }
584
585    /// libsodium's `crypto_kem_xwing_seed_keypair` on the same seed as
586    /// `xwing_keypair_bench`.
587    #[cfg(dryoc_native_tests)]
588    #[bench]
589    fn libsodium_xwing_keypair_bench(b: &mut test::Bencher) {
590        crate::native_test_util::init();
591        let seed = [7u8; 32];
592        let mut public_key = [0u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES];
593        let mut secret_key = [0u8; CRYPTO_KEM_XWING_SECRETKEYBYTES];
594        b.iter(|| {
595            // SAFETY: the key buffers and `seed` are arrays of libsodium's
596            // sizes.
597            let rc = unsafe {
598                libsodium_sys::crypto_kem_xwing_seed_keypair(
599                    public_key.as_mut_ptr(),
600                    secret_key.as_mut_ptr(),
601                    test::black_box(seed.as_ptr()),
602                )
603            };
604            assert_eq!(rc, 0);
605            test::black_box((&public_key, &secret_key));
606        });
607    }
608
609    /// libsodium's `crypto_kem_xwing_enc_deterministic` on the same key and
610    /// seed as `xwing_enc_bench`.
611    #[cfg(dryoc_native_tests)]
612    #[bench]
613    fn libsodium_xwing_enc_bench(b: &mut test::Bencher) {
614        crate::native_test_util::init();
615        let (public_key, _) = crypto_kem_xwing_seed_keypair(&[7u8; 32]);
616        let (mut ciphertext, mut shared_secret) =
617            ([0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES], [0u8; 32]);
618        let seed = [9u8; 64];
619        b.iter(|| {
620            // SAFETY: the output buffers, `public_key` and `seed` are arrays
621            // of the sizes libsodium reads and writes.
622            let rc = unsafe {
623                libsodium_sys::crypto_kem_xwing_enc_deterministic(
624                    ciphertext.as_mut_ptr(),
625                    shared_secret.as_mut_ptr(),
626                    test::black_box(public_key.as_ptr()),
627                    test::black_box(seed.as_ptr()),
628                )
629            };
630            assert_eq!(rc, 0);
631            test::black_box((&ciphertext, &shared_secret));
632        });
633    }
634
635    #[bench]
636    fn xwing_dec_bench(b: &mut test::Bencher) {
637        let (public_key, secret_key) = crypto_kem_xwing_seed_keypair(&[7u8; 32]);
638        let mut ciphertext = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
639        let mut shared_secret = [0u8; 32];
640        crypto_kem_xwing_enc(&mut ciphertext, &mut shared_secret, &public_key).expect("enc");
641        b.iter(|| {
642            crypto_kem_xwing_dec(
643                &mut shared_secret,
644                test::black_box(&ciphertext),
645                test::black_box(&secret_key),
646            )
647            .expect("dec")
648        });
649    }
650
651    /// libsodium's `crypto_kem_xwing_dec` with the same key and ciphertext
652    /// setup as `xwing_dec_bench`.
653    #[cfg(dryoc_native_tests)]
654    #[bench]
655    fn libsodium_xwing_dec_bench(b: &mut test::Bencher) {
656        crate::native_test_util::init();
657        let (public_key, secret_key) = crypto_kem_xwing_seed_keypair(&[7u8; 32]);
658        let mut ciphertext = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
659        let mut shared_secret = [0u8; 32];
660        crypto_kem_xwing_enc(&mut ciphertext, &mut shared_secret, &public_key).expect("enc");
661        b.iter(|| {
662            // SAFETY: `shared_secret`, `ciphertext` and `secret_key` are
663            // arrays of libsodium's sizes.
664            let rc = unsafe {
665                libsodium_sys::crypto_kem_xwing_dec(
666                    shared_secret.as_mut_ptr(),
667                    test::black_box(ciphertext.as_ptr()),
668                    test::black_box(secret_key.as_ptr()),
669                )
670            };
671            assert_eq!(rc, 0);
672            test::black_box(&shared_secret);
673        });
674    }
675}