Skip to main content

x_wing/
lib.rs

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//! # Usage
10//!
11//! This crate implements the X-Wing Key Encapsulation Method (X-Wing-KEM) algorithm.
12//! X-Wing-KEM is a KEM in the sense that it creates an (decapsulation key, encapsulation key) pair,
13//! such that anyone can use the encapsulation key to establish a shared key with the holder of the
14//! decapsulation key. X-Wing-KEM is a general-purpose hybrid post-quantum KEM, combining x25519 and ML-KEM-768.
15#![cfg_attr(feature = "getrandom", doc = "```")]
16#![cfg_attr(not(feature = "getrandom"), doc = "```ignore")]
17//! // NOTE: requires the `getrandom` feature is enabled
18//! use x_wing::{
19//!     XWingKem,
20//!     kem::{Decapsulate, Encapsulate, Kem}
21//! };
22//!
23//! let (sk, pk) = XWingKem::generate_keypair();
24//! let (ct, sk_sender) = pk.encapsulate();
25//! let sk_receiver = sk.decapsulate(&ct);
26//! assert_eq!(sk_sender, sk_receiver);
27//! ```
28
29pub use kem::{
30    self, Decapsulate, Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport,
31    KeyInit, KeySizeUser, TryKeyInit,
32    common::rand_core::{CryptoRng, TryCryptoRng},
33};
34
35use core::fmt::{self, Debug};
36use ml_kem::{
37    FromSeed, MlKem768,
38    array::{
39        Array, ArrayN, AsArrayRef,
40        sizes::{U32, U1120, U1184, U1216},
41    },
42    ml_kem_768,
43};
44use sha3::Sha3_256;
45use shake::{ExtendableOutput, Shake256, Shake256Reader, XofReader};
46use x25519_dalek::{PublicKey, StaticSecret};
47
48#[cfg(feature = "zeroize")]
49use zeroize::{Zeroize, ZeroizeOnDrop};
50
51type MlKem768DecapsulationKey = ml_kem_768::DecapsulationKey;
52type MlKem768EncapsulationKey = ml_kem_768::EncapsulationKey;
53
54const X_WING_LABEL: &[u8; 6] = br"\.//^\";
55
56/// Size in bytes of the `EncapsulationKey`.
57pub const ENCAPSULATION_KEY_SIZE: usize = 1216;
58/// Size in bytes of the `DecapsulationKey`.
59pub const DECAPSULATION_KEY_SIZE: usize = 32;
60/// Size in bytes of the `Ciphertext`.
61pub const CIPHERTEXT_SIZE: usize = 1120;
62/// Number of bytes necessary to encapsulate a key
63pub const ENCAPSULATION_RANDOMNESS_SIZE: usize = 64;
64
65/// Serialized ciphertext.
66pub type Ciphertext = kem::Ciphertext<XWingKem>;
67/// Shared secret key.
68pub type SharedKey = Array<u8, U32>;
69
70/// X-Wing Key Encapsulation Method (X-Wing-KEM).
71#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
72pub struct XWingKem;
73
74impl Kem for XWingKem {
75    type DecapsulationKey = DecapsulationKey;
76    type EncapsulationKey = EncapsulationKey;
77    type CiphertextSize = U1120;
78    type SharedKeySize = U32;
79}
80
81// The naming convention of variables matches the RFC.
82// ss -> Shared Secret
83// ct -> Cipher Text
84// ek -> Ephemeral Key
85// pk -> Public Key
86// sk -> Secret Key
87// Postfixes:
88// _m -> ML-Kem related key
89// _x -> x25519 related key
90
91/// X-Wing encapsulation or public key.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct EncapsulationKey {
94    pk_m: MlKem768EncapsulationKey,
95    pk_x: PublicKey,
96}
97
98impl EncapsulationKey {
99    /// Encapsulates with the given randomness. Uses the first 32 bytes for ML-KEM and the last 32
100    /// bytes for x25519. This is useful for testing against known vectors.
101    ///
102    /// # Warning
103    /// Do NOT use this function unless you know what you're doing. If you fail to use all uniform
104    /// random bytes even once, you can have catastrophic security failure.
105    #[doc(hidden)]
106    #[cfg_attr(not(feature = "hazmat"), doc(hidden))]
107    #[expect(clippy::must_use_candidate)]
108    pub fn encapsulate_deterministic(
109        &self,
110        randomness: &ArrayN<u8, ENCAPSULATION_RANDOMNESS_SIZE>,
111    ) -> (Ciphertext, SharedKey) {
112        // Split randomness into two 32-byte arrays
113        let (rand_m, rand_x) = randomness.split::<U32>();
114
115        // Encapsulate with ML-KEM first. This is infallible
116        let (ct_m, ss_m) = self.pk_m.encapsulate_deterministic(&rand_m);
117
118        let ek_x = StaticSecret::from(rand_x.0);
119        // Equal to ct_x = x25519(ek_x, BASE_POINT)
120        let ct_x = PublicKey::from(&ek_x);
121        // Equal to ss_x = x25519(ek_x, pk_x)
122        let ss_x = ek_x.diffie_hellman(&self.pk_x);
123
124        let ss = combiner(&ss_m, &ss_x, &ct_x, &self.pk_x);
125        let ct = CiphertextMessage { ct_m, ct_x };
126
127        (ct.into(), ss)
128    }
129}
130
131impl Encapsulate for EncapsulationKey {
132    type Kem = XWingKem;
133
134    fn encapsulate_with_rng<R>(&self, rng: &mut R) -> (Ciphertext, SharedKey)
135    where
136        R: CryptoRng + ?Sized,
137    {
138        #[allow(unused_mut)]
139        let mut randomness = Array::generate_from_rng(rng);
140        let res = self.encapsulate_deterministic(&randomness);
141
142        #[cfg(feature = "zeroize")]
143        randomness.zeroize();
144
145        res
146    }
147}
148
149impl KeySizeUser for EncapsulationKey {
150    type KeySize = U1216;
151}
152
153impl KeyExport for EncapsulationKey {
154    fn to_bytes(&self) -> Key<Self> {
155        let mut key_bytes = Key::<Self>::default();
156        let (m, x) = key_bytes.split_at_mut(1184);
157        m.copy_from_slice(&self.pk_m.to_bytes());
158        x.copy_from_slice(self.pk_x.as_bytes());
159        key_bytes
160    }
161}
162
163impl TryKeyInit for EncapsulationKey {
164    fn new(key_bytes: &Key<Self>) -> Result<Self, InvalidKey> {
165        let (m_bytes, x_bytes) = key_bytes.split_ref::<U1184>();
166
167        let pk_m = MlKem768EncapsulationKey::new(m_bytes)?;
168        let pk_x = PublicKey::from(x_bytes.0);
169
170        Ok(EncapsulationKey { pk_m, pk_x })
171    }
172}
173
174impl TryFrom<&[u8]> for EncapsulationKey {
175    type Error = InvalidKey;
176
177    fn try_from(key_bytes: &[u8]) -> Result<Self, InvalidKey> {
178        Self::new_from_slice(key_bytes)
179    }
180}
181
182/// X-Wing decapsulation key or private key.
183#[derive(Clone)]
184pub struct DecapsulationKey {
185    sk: [u8; DECAPSULATION_KEY_SIZE],
186    ek: EncapsulationKey,
187}
188
189impl DecapsulationKey {
190    /// Private key as bytes.
191    #[must_use]
192    pub fn as_bytes(&self) -> &[u8; DECAPSULATION_KEY_SIZE] {
193        &self.sk
194    }
195}
196
197impl Debug for DecapsulationKey {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.debug_struct("DecapsulationKey")
200            .field("ek", &self.ek)
201            .finish_non_exhaustive()
202    }
203}
204
205impl Decapsulate for DecapsulationKey {
206    #[allow(clippy::similar_names)] // So we can use the names as in the RFC
207    fn decapsulate(&self, ct: &Ciphertext) -> SharedKey {
208        let ct = CiphertextMessage::from(ct);
209        let (sk_m, sk_x, _pk_m, pk_x) = expand_key(&self.sk);
210
211        let ss_m = sk_m.decapsulate(&ct.ct_m);
212
213        // equal to ss_x = x25519(sk_x, ct_x)
214        let ss_x = sk_x.diffie_hellman(&ct.ct_x);
215
216        combiner(&ss_m, &ss_x, &ct.ct_x, &pk_x)
217    }
218}
219
220impl Decapsulator for DecapsulationKey {
221    type Kem = XWingKem;
222
223    fn encapsulation_key(&self) -> &EncapsulationKey {
224        &self.ek
225    }
226}
227
228impl Drop for DecapsulationKey {
229    fn drop(&mut self) {
230        #[cfg(feature = "zeroize")]
231        self.sk.zeroize();
232    }
233}
234
235impl From<[u8; DECAPSULATION_KEY_SIZE]> for DecapsulationKey {
236    fn from(sk: [u8; DECAPSULATION_KEY_SIZE]) -> Self {
237        DecapsulationKey::new(sk.as_array_ref())
238    }
239}
240
241impl Generate for DecapsulationKey {
242    fn try_generate_from_rng<R>(rng: &mut R) -> Result<Self, R::Error>
243    where
244        R: TryCryptoRng + ?Sized,
245    {
246        <[u8; DECAPSULATION_KEY_SIZE]>::try_generate_from_rng(rng).map(Into::into)
247    }
248}
249
250impl KeySizeUser for DecapsulationKey {
251    type KeySize = U32;
252}
253
254impl KeyInit for DecapsulationKey {
255    fn new(key: &Key<Self>) -> Self {
256        let (_sk_m, _sk_x, pk_m, pk_x) = expand_key(key.as_ref());
257        let ek = EncapsulationKey { pk_m, pk_x };
258        Self { sk: key.0, ek }
259    }
260}
261
262impl KeyExport for DecapsulationKey {
263    fn to_bytes(&self) -> Key<Self> {
264        self.sk.into()
265    }
266}
267
268#[cfg(feature = "zeroize")]
269impl ZeroizeOnDrop for DecapsulationKey {}
270
271fn expand_key(
272    sk: &[u8; DECAPSULATION_KEY_SIZE],
273) -> (
274    MlKem768DecapsulationKey,
275    StaticSecret,
276    MlKem768EncapsulationKey,
277    PublicKey,
278) {
279    use sha3::digest::Update;
280    let mut shaker = Shake256::default();
281    shaker.update(sk);
282    let mut expanded: Shake256Reader = shaker.finalize_xof();
283
284    let seed = read_from(&mut expanded).into();
285    let (sk_m, pk_m) = MlKem768::from_seed(&seed);
286
287    let sk_x = read_from(&mut expanded);
288    let sk_x = StaticSecret::from(sk_x);
289    let pk_x = PublicKey::from(&sk_x);
290
291    (sk_m, sk_x, pk_m, pk_x)
292}
293
294/// X-Wing ciphertext.
295#[derive(Clone, Debug, PartialEq, Eq)]
296pub struct CiphertextMessage {
297    ct_m: ArrayN<u8, 1088>,
298    ct_x: PublicKey,
299}
300
301impl CiphertextMessage {
302    /// Convert the ciphertext to the following format:
303    /// ML-KEM-768 ciphertext(1088 bytes) || X25519 ciphertext(32 bytes).
304    #[must_use]
305    pub fn to_bytes(&self) -> Ciphertext {
306        let mut buffer = Ciphertext::default();
307        buffer[0..1088].copy_from_slice(&self.ct_m);
308        buffer[1088..].copy_from_slice(self.ct_x.as_bytes());
309        buffer
310    }
311}
312
313impl From<&Ciphertext> for CiphertextMessage {
314    fn from(value: &Ciphertext) -> Self {
315        let mut ct_m = [0; 1088];
316        ct_m.copy_from_slice(&value[0..1088]);
317        let mut ct_x = [0; 32];
318        ct_x.copy_from_slice(&value[1088..]);
319
320        CiphertextMessage {
321            ct_m: ct_m.into(),
322            ct_x: ct_x.into(),
323        }
324    }
325}
326
327impl From<&CiphertextMessage> for Ciphertext {
328    #[inline]
329    fn from(msg: &CiphertextMessage) -> Self {
330        msg.to_bytes()
331    }
332}
333
334impl From<CiphertextMessage> for Ciphertext {
335    #[inline]
336    fn from(msg: CiphertextMessage) -> Self {
337        Self::from(&msg)
338    }
339}
340
341fn combiner(
342    ss_m: &ArrayN<u8, 32>,
343    ss_x: &x25519_dalek::SharedSecret,
344    ct_x: &PublicKey,
345    pk_x: &PublicKey,
346) -> SharedKey {
347    use sha3::Digest;
348
349    let mut hasher = Sha3_256::new();
350    hasher.update(ss_m);
351    hasher.update(ss_x);
352    hasher.update(ct_x);
353    hasher.update(pk_x.as_bytes());
354    hasher.update(X_WING_LABEL);
355    hasher.finalize()
356}
357
358fn read_from<const N: usize>(reader: &mut Shake256Reader) -> [u8; N] {
359    let mut data = [0; N];
360    reader.read(&mut data);
361    data
362}
363
364#[cfg(test)]
365mod tests {
366    #[cfg(feature = "getrandom")]
367    use super::*;
368
369    #[test]
370    #[cfg(feature = "getrandom")]
371    fn key_serialize() {
372        let (sk, pk) = XWingKem::generate_keypair();
373
374        let sk_bytes = sk.as_bytes();
375        let pk_bytes = pk.to_bytes();
376
377        let sk_b = DecapsulationKey::from(*sk_bytes);
378        let pk_b = EncapsulationKey::new(&pk_bytes).unwrap();
379
380        assert_eq!(sk.sk, sk_b.sk);
381        assert!(pk == pk_b);
382    }
383}