1use 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
52pub type PublicKey = [u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES];
54pub type SecretKey = [u8; CRYPTO_KEM_XWING_SECRETKEYBYTES];
56pub type Ciphertext = [u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
59pub type SharedSecret = [u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
61pub type Seed = [u8; CRYPTO_KEM_XWING_SEEDBYTES];
63pub type EncSeed = [u8; CRYPTO_KEM_XWING_ENCSEEDBYTES];
66
67const LABEL: &[u8; 6] = b"\\.//^\\";
69
70struct 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 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
125fn 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
139fn 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
160pub 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#[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
187pub 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#[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
205pub 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
226pub 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
244pub(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 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
297pub 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
315pub(crate) fn dec(
318 arith: Arith,
319 shared_secret: &mut SharedSecret,
320 ciphertext: &Ciphertext,
321 secret_key: &SecretKey,
322) -> Result<(), Error> {
323 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 #[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 #[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#[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 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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 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}