1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4#![doc(
5 html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg",
6 html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg"
7)]
8
9#![cfg_attr(feature = "getrandom", doc = "```")]
16#![cfg_attr(not(feature = "getrandom"), doc = "```ignore")]
17mod error;
30
31pub use crate::error::DecapsulationError;
32pub use kem::{
33 self, Decapsulate, Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport,
34 KeyInit, KeySizeUser, TryDecapsulate, TryKeyInit,
35 common::rand_core::{CryptoRng, TryCryptoRng},
36};
37
38use core::fmt::{self, Debug};
39use ml_kem::{
40 FromSeed, MlKem768,
41 array::{
42 Array, ArrayN, AsArrayRef,
43 sizes::{U32, U1120, U1184, U1216},
44 },
45 ml_kem_768,
46};
47use sha3::Sha3_256;
48use shake::{ExtendableOutput, Shake256, Shake256Reader, XofReader};
49use x25519_dalek::{PublicKey, StaticSecret};
50
51#[cfg(feature = "zeroize")]
52use zeroize::{Zeroize, ZeroizeOnDrop};
53
54type MlKem768DecapsulationKey = ml_kem_768::DecapsulationKey;
55type MlKem768EncapsulationKey = ml_kem_768::EncapsulationKey;
56
57const X_WING_LABEL: &[u8; 6] = br"\.//^\";
58
59pub const ENCAPSULATION_KEY_SIZE: usize = 1216;
61pub const DECAPSULATION_KEY_SIZE: usize = 32;
63pub const CIPHERTEXT_SIZE: usize = 1120;
65pub const ENCAPSULATION_RANDOMNESS_SIZE: usize = 64;
67
68pub type Ciphertext = kem::Ciphertext<XWingKem>;
70pub type SharedKey = Array<u8, U32>;
72
73#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
75pub struct XWingKem;
76
77impl Kem for XWingKem {
78 type DecapsulationKey = DecapsulationKey;
79 type EncapsulationKey = EncapsulationKey;
80 type CiphertextSize = U1120;
81 type SharedKeySize = U32;
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct EncapsulationKey {
97 pk_m: MlKem768EncapsulationKey,
98 pk_x: PublicKey,
99}
100
101impl EncapsulationKey {
102 #[doc(hidden)]
109 #[cfg_attr(not(feature = "hazmat"), doc(hidden))]
110 #[expect(clippy::must_use_candidate)]
111 pub fn encapsulate_deterministic(
112 &self,
113 randomness: &ArrayN<u8, ENCAPSULATION_RANDOMNESS_SIZE>,
114 ) -> (Ciphertext, SharedKey) {
115 let (rand_m, rand_x) = randomness.split::<U32>();
117
118 let (ct_m, ss_m) = self.pk_m.encapsulate_deterministic(&rand_m);
120
121 let ek_x = StaticSecret::from(rand_x.0);
122 let ct_x = PublicKey::from(&ek_x);
124 let ss_x = ek_x.diffie_hellman(&self.pk_x);
126
127 let ss = combiner(&ss_m, &ss_x, &ct_x, &self.pk_x);
128 let ct = CiphertextMessage { ct_m, ct_x };
129
130 (ct.into(), ss)
131 }
132}
133
134impl Encapsulate for EncapsulationKey {
135 type Kem = XWingKem;
136
137 fn encapsulate_with_rng<R>(&self, rng: &mut R) -> (Ciphertext, SharedKey)
138 where
139 R: CryptoRng + ?Sized,
140 {
141 #[allow(unused_mut)]
142 let mut randomness = Array::generate_from_rng(rng);
143 let res = self.encapsulate_deterministic(&randomness);
144
145 #[cfg(feature = "zeroize")]
146 randomness.zeroize();
147
148 res
149 }
150}
151
152impl KeySizeUser for EncapsulationKey {
153 type KeySize = U1216;
154}
155
156impl KeyExport for EncapsulationKey {
157 fn to_bytes(&self) -> Key<Self> {
158 let mut key_bytes = Key::<Self>::default();
159 let (m, x) = key_bytes.split_at_mut(1184);
160 m.copy_from_slice(&self.pk_m.to_bytes());
161 x.copy_from_slice(self.pk_x.as_bytes());
162 key_bytes
163 }
164}
165
166impl TryKeyInit for EncapsulationKey {
167 fn new(key_bytes: &Key<Self>) -> Result<Self, InvalidKey> {
168 let (m_bytes, x_bytes) = key_bytes.split_ref::<U1184>();
169
170 let pk_m = MlKem768EncapsulationKey::new(m_bytes)?;
171 let pk_x = PublicKey::from(x_bytes.0);
172
173 Ok(EncapsulationKey { pk_m, pk_x })
174 }
175}
176
177impl TryFrom<&[u8]> for EncapsulationKey {
178 type Error = InvalidKey;
179
180 fn try_from(key_bytes: &[u8]) -> Result<Self, InvalidKey> {
181 Self::new_from_slice(key_bytes)
182 }
183}
184
185#[derive(Clone)]
190pub struct DecapsulationKey {
191 sk: [u8; DECAPSULATION_KEY_SIZE],
192 ek: EncapsulationKey,
193}
194
195impl DecapsulationKey {
196 #[must_use]
198 pub fn as_bytes(&self) -> &[u8; DECAPSULATION_KEY_SIZE] {
199 &self.sk
200 }
201}
202
203impl Debug for DecapsulationKey {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.debug_struct("DecapsulationKey")
206 .field("ek", &self.ek)
207 .finish_non_exhaustive()
208 }
209}
210
211impl DecapsulationKey {
212 fn decapsulate_inner(
213 &self,
214 ct: &CiphertextMessage,
215 ) -> (SharedKey, x25519_dalek::SharedSecret, PublicKey) {
216 let (sk_m, sk_x, _pk_m, pk_x) = expand_key(&self.sk);
217
218 let ss_m = sk_m.decapsulate(&ct.ct_m);
219
220 let ss_x = sk_x.diffie_hellman(&ct.ct_x);
222
223 (ss_m, ss_x, pk_x)
224 }
225}
226
227impl Decapsulate for DecapsulationKey {
228 #[allow(clippy::similar_names)] fn decapsulate(&self, ct: &Ciphertext) -> SharedKey {
230 let ct = CiphertextMessage::from(ct);
231 let (ss_m, ss_x, pk_x) = self.decapsulate_inner(&ct);
232
233 combiner(&ss_m, &ss_x, &ct.ct_x, &pk_x)
234 }
235}
236
237impl Decapsulator for DecapsulationKey {
238 type Kem = XWingKem;
239
240 fn encapsulation_key(&self) -> &EncapsulationKey {
241 &self.ek
242 }
243}
244
245impl Drop for DecapsulationKey {
246 fn drop(&mut self) {
247 #[cfg(feature = "zeroize")]
248 self.sk.zeroize();
249 }
250}
251
252impl From<[u8; DECAPSULATION_KEY_SIZE]> for DecapsulationKey {
253 fn from(sk: [u8; DECAPSULATION_KEY_SIZE]) -> Self {
254 DecapsulationKey::new(sk.as_array_ref())
255 }
256}
257
258impl Generate for DecapsulationKey {
259 fn try_generate_from_rng<R>(rng: &mut R) -> Result<Self, R::Error>
260 where
261 R: TryCryptoRng + ?Sized,
262 {
263 <[u8; DECAPSULATION_KEY_SIZE]>::try_generate_from_rng(rng).map(Into::into)
264 }
265}
266
267impl KeySizeUser for DecapsulationKey {
268 type KeySize = U32;
269}
270
271impl KeyInit for DecapsulationKey {
272 fn new(key: &Key<Self>) -> Self {
273 let (_sk_m, _sk_x, pk_m, pk_x) = expand_key(key.as_ref());
274 let ek = EncapsulationKey { pk_m, pk_x };
275 Self { sk: key.0, ek }
276 }
277}
278
279impl KeyExport for DecapsulationKey {
280 fn to_bytes(&self) -> Key<Self> {
281 self.sk.into()
282 }
283}
284
285#[cfg(feature = "zeroize")]
286impl ZeroizeOnDrop for DecapsulationKey {}
287
288#[derive(Clone, Debug)]
316pub struct DecapsulationKeyRejectNonContrib(DecapsulationKey);
317
318impl TryDecapsulate for DecapsulationKeyRejectNonContrib {
319 type Error = DecapsulationError;
320
321 #[allow(clippy::similar_names)] fn try_decapsulate(&self, ct: &Ciphertext) -> Result<SharedKey, Self::Error> {
323 let ct = CiphertextMessage::from(ct);
324 let (ss_m, ss_x, pk_x) = self.0.decapsulate_inner(&ct);
325
326 if !ss_x.was_contributory() {
327 return Err(DecapsulationError);
328 }
329
330 Ok(combiner(&ss_m, &ss_x, &ct.ct_x, &pk_x))
331 }
332}
333
334impl Decapsulator for DecapsulationKeyRejectNonContrib {
335 type Kem = XWingKem;
336
337 fn encapsulation_key(&self) -> &EncapsulationKey {
338 self.0.encapsulation_key()
339 }
340}
341
342impl From<[u8; DECAPSULATION_KEY_SIZE]> for DecapsulationKeyRejectNonContrib {
343 fn from(sk: [u8; DECAPSULATION_KEY_SIZE]) -> Self {
344 Self(sk.into())
345 }
346}
347
348impl Generate for DecapsulationKeyRejectNonContrib {
349 fn try_generate_from_rng<R>(rng: &mut R) -> Result<Self, R::Error>
350 where
351 R: TryCryptoRng + ?Sized,
352 {
353 DecapsulationKey::try_generate_from_rng(rng).map(Self)
354 }
355}
356
357impl KeySizeUser for DecapsulationKeyRejectNonContrib {
358 type KeySize = U32;
359}
360
361impl KeyInit for DecapsulationKeyRejectNonContrib {
362 fn new(key: &Key<Self>) -> Self {
363 Self(DecapsulationKey::new(key))
364 }
365}
366
367impl KeyExport for DecapsulationKeyRejectNonContrib {
368 fn to_bytes(&self) -> Key<Self> {
369 self.0.to_bytes()
370 }
371}
372
373fn expand_key(
374 sk: &[u8; DECAPSULATION_KEY_SIZE],
375) -> (
376 MlKem768DecapsulationKey,
377 StaticSecret,
378 MlKem768EncapsulationKey,
379 PublicKey,
380) {
381 use sha3::digest::Update;
382 let mut shaker = Shake256::default();
383 shaker.update(sk);
384 let mut expanded: Shake256Reader = shaker.finalize_xof();
385
386 let seed = read_from(&mut expanded).into();
387 let (sk_m, pk_m) = MlKem768::from_seed(&seed);
388
389 let sk_x = read_from(&mut expanded);
390 let sk_x = StaticSecret::from(sk_x);
391 let pk_x = PublicKey::from(&sk_x);
392
393 (sk_m, sk_x, pk_m, pk_x)
394}
395
396#[derive(Clone, Debug, PartialEq, Eq)]
398pub struct CiphertextMessage {
399 ct_m: ArrayN<u8, 1088>,
400 ct_x: PublicKey,
401}
402
403impl CiphertextMessage {
404 #[must_use]
407 pub fn to_bytes(&self) -> Ciphertext {
408 let mut buffer = Ciphertext::default();
409 buffer[0..1088].copy_from_slice(&self.ct_m);
410 buffer[1088..].copy_from_slice(self.ct_x.as_bytes());
411 buffer
412 }
413}
414
415impl From<&Ciphertext> for CiphertextMessage {
416 fn from(value: &Ciphertext) -> Self {
417 let mut ct_m = [0; 1088];
418 ct_m.copy_from_slice(&value[0..1088]);
419 let mut ct_x = [0; 32];
420 ct_x.copy_from_slice(&value[1088..]);
421
422 CiphertextMessage {
423 ct_m: ct_m.into(),
424 ct_x: ct_x.into(),
425 }
426 }
427}
428
429impl From<&CiphertextMessage> for Ciphertext {
430 #[inline]
431 fn from(msg: &CiphertextMessage) -> Self {
432 msg.to_bytes()
433 }
434}
435
436impl From<CiphertextMessage> for Ciphertext {
437 #[inline]
438 fn from(msg: CiphertextMessage) -> Self {
439 Self::from(&msg)
440 }
441}
442
443fn combiner(
444 ss_m: &ArrayN<u8, 32>,
445 ss_x: &x25519_dalek::SharedSecret,
446 ct_x: &PublicKey,
447 pk_x: &PublicKey,
448) -> SharedKey {
449 use sha3::Digest;
450
451 let mut hasher = Sha3_256::new();
452 hasher.update(ss_m);
453 hasher.update(ss_x);
454 hasher.update(ct_x);
455 hasher.update(pk_x.as_bytes());
456 hasher.update(X_WING_LABEL);
457 hasher.finalize()
458}
459
460fn read_from<const N: usize>(reader: &mut Shake256Reader) -> [u8; N] {
461 let mut data = [0; N];
462 reader.read(&mut data);
463 data
464}
465
466#[cfg(test)]
467mod tests {
468 #[cfg(feature = "getrandom")]
469 use super::*;
470
471 #[test]
472 #[cfg(feature = "getrandom")]
473 fn key_serialize() {
474 let (sk, pk) = XWingKem::generate_keypair();
475
476 let sk_bytes = sk.as_bytes();
477 let pk_bytes = pk.to_bytes();
478
479 let sk_b = DecapsulationKey::from(*sk_bytes);
480 let pk_b = EncapsulationKey::new(&pk_bytes).unwrap();
481
482 assert_eq!(sk.sk, sk_b.sk);
483 assert!(pk == pk_b);
484 }
485
486 #[test]
487 #[cfg(feature = "getrandom")]
488 fn non_contributory() {
489 let (sk, pk) = XWingKem::generate_keypair();
490
491 let ct = CiphertextMessage {
493 ct_m: pk.pk_m.encapsulate().0,
494 ct_x: PublicKey::from([0; 32]),
495 }
496 .into();
497
498 assert!(sk.try_decapsulate(&ct).is_ok());
500
501 let sk = DecapsulationKeyRejectNonContrib(sk);
503 assert!(matches!(sk.try_decapsulate(&ct), Err(DecapsulationError)));
504 }
505}