1use crate::{
4 RandomError,
5 curve25519::x25519,
6 mlkem::{self, MlKemError},
7 sha3::{Sha3_256, Shake256},
8};
9
10pub const SECRET_KEY_SIZE: usize = 32;
12pub const PUBLIC_KEY_SIZE: usize = mlkem::PUBLIC_KEY_SIZE_768 + x25519::KEY_SIZE; pub const CIPHERTEXT_SIZE: usize = mlkem::CIPHERTEXT_SIZE_768 + x25519::SHARED_SECRET_SIZE; pub const SHARED_SECRET_SIZE: usize = 32;
18
19const XWING_LABEL: &[u8; 6] = b"\\.//^\\";
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum XWingError {
24 MlKem(MlKemError),
25 Random(RandomError),
26}
27
28impl From<MlKemError> for XWingError {
29 fn from(err: MlKemError) -> Self {
30 XWingError::MlKem(err)
31 }
32}
33
34impl From<RandomError> for XWingError {
35 fn from(err: RandomError) -> Self {
36 XWingError::Random(err)
37 }
38}
39
40impl core::fmt::Display for XWingError {
41 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42 match self {
43 XWingError::MlKem(err) => write!(f, "ML-KEM error: {err}"),
44 XWingError::Random(err) => write!(f, "{err}"),
45 }
46 }
47}
48
49#[cfg(feature = "random")]
55pub fn generate_keypair() -> Result<(SecretKey, PublicKey), XWingError> {
56 let seed: [u8; SECRET_KEY_SIZE] = crate::random::bytes()?;
57 Ok(generate_keypair_derand(&seed))
58}
59
60pub fn generate_keypair_derand(secret_key: &[u8; SECRET_KEY_SIZE]) -> (SecretKey, PublicKey) {
67 let (mlkem_sk, x25519_sk, mlkem_pk, x25519_pk) = expand_decapsulation_key(secret_key);
68
69 let secret_key = SecretKey {
70 bytes: *secret_key,
71 x25519_secret_key: x25519_sk,
72 x25519_public_key_bytes: x25519_pk.to_bytes(),
73 mlkem_secret_key: mlkem_sk,
74 };
75
76 let public_key = PublicKey {
77 mlkem_public_key: mlkem_pk,
78 x25519_public_key: x25519_pk,
79 };
80
81 (secret_key, public_key)
82}
83
84#[derive(Clone, PartialEq, Eq)]
101#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
102pub struct SecretKey {
103 bytes: [u8; SECRET_KEY_SIZE],
104 x25519_secret_key: x25519::SecretKey,
105 x25519_public_key_bytes: [u8; x25519::KEY_SIZE],
106 mlkem_secret_key: mlkem::SecretKey768,
107}
108
109impl SecretKey {
110 pub fn to_bytes(&self) -> [u8; SECRET_KEY_SIZE] {
111 self.bytes
112 }
113
114 pub fn public_key(&self) -> PublicKey {
119 return PublicKey {
120 mlkem_public_key: self.mlkem_secret_key.public_key(),
121 x25519_public_key: x25519::PublicKey::from_bytes(&self.x25519_public_key_bytes),
122 };
123 }
124
125 pub fn decapsulate(&self, ct: &[u8; CIPHERTEXT_SIZE]) -> Result<[u8; SHARED_SECRET_SIZE], XWingError> {
126 let ct_m = &ct[..mlkem::CIPHERTEXT_SIZE_768].try_into().unwrap();
127 let ct_x = x25519::PublicKey::from_bytes(&ct[mlkem::CIPHERTEXT_SIZE_768..].try_into().unwrap());
128
129 let ss_m = self.mlkem_secret_key.decapsulate(ct_m)?;
130 let ss_x = self.x25519_secret_key.ecdh(&ct_x).unwrap_or_default();
136
137 Ok(combiner(&ss_m, &ss_x, &ct_x.to_bytes(), &self.x25519_public_key_bytes))
138 }
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct PublicKey {
146 mlkem_public_key: mlkem::PublicKey768,
147 x25519_public_key: x25519::PublicKey,
148}
149
150impl PublicKey {
151 pub fn from_bytes(bytes: &[u8; PUBLIC_KEY_SIZE]) -> Result<PublicKey, MlKemError> {
159 let mlkem_public_key =
160 mlkem::PublicKey768::from_bytes(&bytes[..mlkem::PUBLIC_KEY_SIZE_768].try_into().unwrap())?;
161 let x25519_public_key = x25519::PublicKey::from_bytes(&bytes[mlkem::PUBLIC_KEY_SIZE_768..].try_into().unwrap());
162
163 return Ok(PublicKey {
164 mlkem_public_key,
165 x25519_public_key,
166 });
167 }
168
169 pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] {
170 let mut bytes = [0u8; PUBLIC_KEY_SIZE];
171 bytes[..mlkem::PUBLIC_KEY_SIZE_768].copy_from_slice(&self.mlkem_public_key.to_bytes());
172 bytes[mlkem::PUBLIC_KEY_SIZE_768..].copy_from_slice(&self.x25519_public_key.to_bytes());
173 bytes
174 }
175
176 #[cfg(feature = "random")]
181 pub fn encapsulate(&self) -> Result<([u8; SHARED_SECRET_SIZE], [u8; CIPHERTEXT_SIZE]), XWingError> {
182 let eseed: [u8; 64] = crate::random::bytes()?;
183 Ok(self.encapsulate_derand(&eseed))
184 }
185
186 pub fn encapsulate_derand(&self, eseed: &[u8; 64]) -> ([u8; SHARED_SECRET_SIZE], [u8; CIPHERTEXT_SIZE]) {
194 let ek_x = x25519::SecretKey::from_bytes(&eseed[32..64].try_into().unwrap());
195 let ct_x = ek_x.public_key();
196 let ss_x = ek_x.ecdh(&self.x25519_public_key).unwrap_or_default();
199
200 let m = &eseed[..32].try_into().unwrap();
201 let (ss_m, ct_m) = self.mlkem_public_key.encapsulate_derand(m);
202
203 let ss = combiner(&ss_m, &ss_x, &ct_x.to_bytes(), &self.x25519_public_key.to_bytes());
204
205 let mut ct = [0u8; CIPHERTEXT_SIZE];
206 ct[..mlkem::CIPHERTEXT_SIZE_768].copy_from_slice(&ct_m);
207 ct[mlkem::CIPHERTEXT_SIZE_768..].copy_from_slice(&ct_x.to_bytes());
208
209 (ss, ct)
210 }
211}
212
213fn expand_decapsulation_key(
214 secret_key: &[u8; 32],
215) -> (mlkem::SecretKey768, x25519::SecretKey, mlkem::PublicKey768, x25519::PublicKey) {
216 let mut expanded_secret_key = [0u8; 96];
217 Shake256::hash(secret_key, &mut expanded_secret_key);
218
219 let (sk_m, pk_m) = mlkem::SecretKey768::generate_derand(&expanded_secret_key[..64].try_into().unwrap());
220
221 let sk_x = x25519::SecretKey::from_bytes(&expanded_secret_key[64..96].try_into().unwrap());
222 let pk_x = sk_x.public_key();
223
224 (sk_m, sk_x, pk_m, pk_x)
225}
226
227fn combiner(
228 ss_m: &[u8; mlkem::SHARED_SECRET_SIZE],
229 ss_x: &[u8; x25519::KEY_SIZE],
230 ct_x: &[u8; x25519::KEY_SIZE],
231 pk_x: &[u8; x25519::KEY_SIZE],
232) -> [u8; SHARED_SECRET_SIZE] {
233 use crate::Hasher;
234 let mut hasher = Sha3_256::new();
235 hasher.update(ss_m);
236 hasher.update(ss_x);
237 hasher.update(ct_x);
238 hasher.update(pk_x);
239 hasher.update(XWING_LABEL);
240 hasher.sum().as_ref().try_into().unwrap()
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn hex_to_array<const N: usize>(hex_str: &str) -> [u8; N] {
248 let bytes = hex::decode(hex_str).unwrap();
249 return bytes.try_into().unwrap();
250 }
251
252 #[test]
253 fn constants() {
254 assert!(PUBLIC_KEY_SIZE == 1216);
255 assert!(CIPHERTEXT_SIZE == 1120);
256 }
257
258 #[cfg(feature = "zeroize")]
259 #[test]
260 fn secret_key_zeroize_clears_bytes() {
261 use zeroize::Zeroize;
262
263 let (mut secret_key, _) = generate_keypair_derand(&[1u8; SECRET_KEY_SIZE]);
264 assert_ne!(secret_key.to_bytes(), [0u8; SECRET_KEY_SIZE]);
265 secret_key.zeroize();
266 assert_eq!(secret_key.to_bytes(), [0u8; SECRET_KEY_SIZE]);
267 }
268
269 struct TestVector {
270 seed: &'static str,
271 eseed: &'static str,
272 ss: &'static str,
273 }
274
275 const TEST_VECTORS: [TestVector; 3] = [
276 TestVector {
277 seed: "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26",
278 eseed: "3cb1eea988004b93103cfb0aeefd2a686e01fa4a58e8a3639ca8a1e3f9ae57e235b8cc873c23dc62b8d260169afa2f75ab916a58d974918835d25e6a435085b2",
279 ss: "d2df0522128f09dd8e2c92b1e905c793d8f57a54c3da25861f10bf4ca613e384",
280 },
281 TestVector {
282 seed: "badfd6dfaac359a5efbb7bcc4b59d538df9a04302e10c8bc1cbf1a0b3a5120ea",
283 eseed: "17cda7cfad765f5623474d368ccca8af0007cd9f5e4c849f167a580b14aabdefaee7eef47cb0fca9767be1fda69419dfb927e9df07348b196691abaeb580b32d",
284 ss: "f2e86241c64d60f6649fbc6c5b7d17180b780a3f34355e64a85749949c45f150",
285 },
286 TestVector {
287 seed: "ef58538b8d23f87732ea63b02b4fa0f4873360e2841928cd60dd4cee8cc0d4c9",
288 eseed: "22a96188d032675c8ac850933c7aff1533b94c834adbb69c6115bad4692d8619f90b0cdf8a7b9c264029ac185b70b83f2801f2f4b3f70c593ea3aeeb613a7f1b",
289 ss: "953f7f4e8c5b5049bdc771d1dffada0dd961477d1a2ae0988baa7ea6898d893f",
290 },
291 ];
292
293 #[test]
294 fn test_vectors_from_draft() {
295 for (i, tv) in TEST_VECTORS.iter().enumerate() {
296 let seed: [u8; 32] = hex_to_array(tv.seed);
297 let eseed: [u8; 64] = hex_to_array(tv.eseed);
298 let expected_ss: [u8; 32] = hex_to_array(tv.ss);
299
300 let (secret_key, pk) = generate_keypair_derand(&seed);
301 assert_eq!(secret_key.to_bytes(), seed, "vector {i}: sk mismatch");
302
303 let (ss, ct) = pk.encapsulate_derand(&eseed);
304 assert_eq!(ss, expected_ss, "vector {i}: encaps ss mismatch");
305
306 let decapsulated_ss = secret_key.decapsulate(&ct).unwrap();
307 assert_eq!(decapsulated_ss, expected_ss, "vector {i}: decaps ss mismatch");
308 }
309 }
310
311 #[test]
312 fn round_trip() {
313 let (secret_key, public_key) = generate_keypair().unwrap();
314 let (ss, ct) = public_key.encapsulate().unwrap();
315 let decapsulated = secret_key.decapsulate(&ct).unwrap();
316 assert_eq!(ss, decapsulated);
317 }
318
319 #[test]
320 fn round_trip_many() {
321 for _ in 0..10 {
322 let (secret_key, public_key) = generate_keypair().unwrap();
323 let (ss, ct) = public_key.encapsulate().unwrap();
324 let decapsulated = secret_key.decapsulate(&ct).unwrap();
325 assert_eq!(ss, decapsulated);
326 }
327 }
328
329 #[test]
330 fn decapsulation_with_wrong_key_produces_different_secret() {
331 let (_, pk_a) = generate_keypair().unwrap();
332 let (sk_b, _) = generate_keypair().unwrap();
333
334 let (ss_a, ct) = pk_a.encapsulate().unwrap();
335 let ss_b = sk_b.decapsulate(&ct).unwrap();
336 assert_ne!(ss_a, ss_b);
337 }
338
339 #[test]
340 fn tampered_ciphertext_produces_different_secret() {
341 let (secret_key, public_key) = generate_keypair().unwrap();
342 let (ss, mut ct) = public_key.encapsulate().unwrap();
343
344 ct[0] ^= 0x80;
345
346 let tampered_ss = secret_key.decapsulate(&ct).unwrap();
347 assert_ne!(ss, tampered_ss);
348 }
349
350 #[test]
351 fn derandomized_keygen_is_deterministic() {
352 let seed: [u8; 32] = hex_to_array("7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26");
353 let (sk1, pk1) = generate_keypair_derand(&seed);
354 let (sk2, pk2) = generate_keypair_derand(&seed);
355 assert_eq!(sk1.to_bytes(), sk2.to_bytes());
356 assert_eq!(pk1.to_bytes(), pk2.to_bytes());
357 }
358
359 #[test]
360 fn derandomized_encaps_is_deterministic() {
361 let seed: [u8; 32] = hex_to_array("7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26");
362 let eseed: [u8; 64] = hex_to_array(
363 "3cb1eea988004b93103cfb0aeefd2a686e01fa4a58e8a3639ca8a1e3f9ae57e235b8cc873c23dc62b8d260169afa2f75ab916a58d974918835d25e6a435085b2",
364 );
365 let (_, pk) = generate_keypair_derand(&seed);
366
367 let (ss1, ct1) = pk.encapsulate_derand(&eseed);
368 let (ss2, ct2) = pk.encapsulate_derand(&eseed);
369 assert_eq!(ct1, ct2);
370 assert_eq!(ss1, ss2);
371 }
372
373 #[test]
374 fn xwing_label_is_correct() {
375 assert_eq!(XWING_LABEL.len(), 6);
376 assert_eq!(hex::encode(XWING_LABEL), "5c2e2f2f5e5c");
377 }
378
379 #[test]
380 fn expand_decapsulation_key_is_deterministic() {
381 let seed: [u8; 32] = hex_to_array("7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26");
382
383 let (sk_m1, sk_x1, pk_m1, pk_x1) = expand_decapsulation_key(&seed);
384 let (sk_m2, sk_x2, pk_m2, pk_x2) = expand_decapsulation_key(&seed);
385 assert_eq!(sk_m1.to_bytes(), sk_m2.to_bytes());
386 assert_eq!(sk_x1.to_bytes(), sk_x2.to_bytes());
387 assert_eq!(pk_m1, pk_m2);
388 assert_eq!(pk_x1, pk_x2);
389 }
390
391 #[test]
392 fn combiner_is_deterministic() {
393 let ss_m = [0x01u8; 32];
394 let ss_x = [0x02u8; 32];
395 let ct_x = [0x03u8; 32];
396 let pk_x = [0x04u8; 32];
397
398 let result1 = combiner(&ss_m, &ss_x, &ct_x, &pk_x);
399 let result2 = combiner(&ss_m, &ss_x, &ct_x, &pk_x);
400 assert_eq!(result1, result2);
401 }
402}