1use ml_kem::{Decapsulate, MlKem768, MlKem1024};
43use ml_kem::{DecapsulationKey, EncapsulationKey, KeyExport};
44use sha3::Shake256;
45use sha3::digest::{ExtendableOutput, Update, XofReader};
46use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret};
47use zeroize::Zeroize;
48
49use crypto_secretbox::aead::Aead;
50use crypto_secretbox::aead::generic_array::GenericArray;
51use crypto_secretbox::{KeyInit, XSalsa20Poly1305};
52
53use crate::CryptoError;
54use crate::b64;
55
56const VERSION_HYBRID_768: u8 = 0x02;
60const VERSION_HYBRID_1024: u8 = 0x03;
62const NONCE_LEN: usize = 24;
64const X25519_LEN: usize = 32;
66const SEED_LEN: usize = 32;
68const MAC_LEN: usize = 16;
70const LABEL: &[u8] = b"\\.//^\\";
72
73const MLKEM768_EK_LEN: usize = 1184;
76const MLKEM768_CT_LEN: usize = 1088;
78const MLKEM768_SEED_LEN: usize = 64;
80const EXPANDED_SEED_768_LEN: usize = 96;
82const COMBINED_PK_768_LEN: usize = MLKEM768_EK_LEN + X25519_LEN;
84const COMBINED_CT_768_LEN: usize = MLKEM768_CT_LEN + X25519_LEN;
86
87const MLKEM1024_EK_LEN: usize = 1568;
90const MLKEM1024_CT_LEN: usize = 1568;
92const MLKEM1024_SEED_LEN: usize = 64;
94const EXPANDED_SEED_1024_LEN: usize = 96;
96const COMBINED_PK_1024_LEN: usize = MLKEM1024_EK_LEN + X25519_LEN;
98const COMBINED_CT_1024_LEN: usize = MLKEM1024_CT_LEN + X25519_LEN;
100
101#[derive(Debug, Clone)]
105pub struct HybridKeyPair {
106 pub public_key: String,
108 pub secret_key: String,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
114pub enum SecurityLevel {
115 #[default]
117 Cat3,
118 Cat5,
120}
121
122#[inline]
126fn random_bytes(buf: &mut [u8]) {
127 getrandom::getrandom(buf).expect("OS CSPRNG unavailable");
128}
129
130fn expand_seed(seed: &[u8; SEED_LEN], output_len: usize) -> Vec<u8> {
132 let mut hasher = Shake256::default();
133 hasher.update(seed);
134 let mut reader = hasher.finalize_xof();
135 let mut out = vec![0u8; output_len];
136 reader.read(&mut out);
137 out
138}
139
140fn combine(
142 ss_mlkem: &[u8],
143 ss_x25519: &[u8],
144 ct_x25519: &[u8; X25519_LEN],
145 pk_x25519: &[u8; X25519_LEN],
146) -> [u8; 32] {
147 use sha3::Digest;
148 let mut hasher = sha3::Sha3_256::new();
149 Digest::update(&mut hasher, ss_mlkem);
150 Digest::update(&mut hasher, ss_x25519);
151 Digest::update(&mut hasher, ct_x25519);
152 Digest::update(&mut hasher, pk_x25519);
153 Digest::update(&mut hasher, LABEL);
154 hasher.finalize().into()
155}
156
157fn secretbox_encrypt(
159 shared_secret: &[u8; 32],
160 plaintext: &[u8],
161) -> Result<(Vec<u8>, [u8; NONCE_LEN]), CryptoError> {
162 let cipher = XSalsa20Poly1305::new(GenericArray::from_slice(shared_secret));
163 let mut nonce_buf = [0u8; NONCE_LEN];
164 random_bytes(&mut nonce_buf);
165 let nonce = GenericArray::from_slice(&nonce_buf);
166 let ct = cipher
167 .encrypt(nonce, plaintext)
168 .map_err(|_| CryptoError::Hybrid("secretbox encrypt failed".into()))?;
169 Ok((ct, nonce_buf))
170}
171
172fn secretbox_decrypt(
174 shared_secret: &[u8; 32],
175 nonce: &[u8],
176 ciphertext: &[u8],
177) -> Result<Vec<u8>, CryptoError> {
178 let cipher = XSalsa20Poly1305::new(GenericArray::from_slice(shared_secret));
179 let nonce = GenericArray::from_slice(nonce);
180 cipher
181 .decrypt(nonce, ciphertext)
182 .map_err(|_| CryptoError::Decryption)
183}
184
185pub fn generate_hybrid_keypair() -> HybridKeyPair {
189 generate_hybrid_keypair_with_level(SecurityLevel::Cat3)
190}
191
192pub fn hybrid_seal(plaintext: &[u8], combined_pk_b64: &str) -> Result<String, CryptoError> {
196 hybrid_seal_with_level(plaintext, combined_pk_b64, SecurityLevel::Cat3)
197}
198
199pub fn hybrid_open(ct_b64: &str, seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
201 let combined = b64::decode(ct_b64)?;
202 match combined.first() {
203 Some(&VERSION_HYBRID_768) => hybrid_open_768(&combined, seed_b64),
204 Some(&VERSION_HYBRID_1024) => hybrid_open_1024(&combined, seed_b64),
205 _ => Err(CryptoError::Hybrid(
206 "not a hybrid ciphertext (bad version tag)".into(),
207 )),
208 }
209}
210
211pub fn is_hybrid_ciphertext(ct_b64: &str) -> bool {
213 b64::decode(ct_b64)
214 .map(|bytes| {
215 matches!(
216 bytes.first(),
217 Some(&VERSION_HYBRID_768) | Some(&VERSION_HYBRID_1024)
218 )
219 })
220 .unwrap_or(false)
221}
222
223pub fn generate_hybrid_keypair_1024() -> HybridKeyPair {
227 generate_hybrid_keypair_with_level(SecurityLevel::Cat5)
228}
229
230pub fn hybrid_seal_1024(plaintext: &[u8], combined_pk_b64: &str) -> Result<String, CryptoError> {
234 hybrid_seal_with_level(plaintext, combined_pk_b64, SecurityLevel::Cat5)
235}
236
237pub fn generate_hybrid_keypair_with_level(level: SecurityLevel) -> HybridKeyPair {
241 let mut seed = [0u8; SEED_LEN];
242 random_bytes(&mut seed);
243
244 let expanded_len = match level {
245 SecurityLevel::Cat3 => EXPANDED_SEED_768_LEN,
246 SecurityLevel::Cat5 => EXPANDED_SEED_1024_LEN,
247 };
248 let mlkem_seed_len = match level {
249 SecurityLevel::Cat3 => MLKEM768_SEED_LEN,
250 SecurityLevel::Cat5 => MLKEM1024_SEED_LEN,
251 };
252
253 let mut expanded = expand_seed(&seed, expanded_len);
254 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[mlkem_seed_len..].try_into().unwrap();
255
256 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
258 let x25519_pk = X25519PublicKey::from(&x25519_sk);
259
260 let combined_pk = match level {
261 SecurityLevel::Cat3 => {
262 let mlkem_seed: [u8; MLKEM768_SEED_LEN] =
263 expanded[..MLKEM768_SEED_LEN].try_into().unwrap();
264 let dk = DecapsulationKey::<MlKem768>::from_seed(mlkem_seed.into());
265 let ek = dk.encapsulation_key();
266 let ek_bytes = ek.to_bytes();
267 let mut pk = Vec::with_capacity(COMBINED_PK_768_LEN);
268 pk.extend_from_slice(&ek_bytes);
269 pk.extend_from_slice(x25519_pk.as_bytes());
270 pk
271 }
272 SecurityLevel::Cat5 => {
273 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] =
274 expanded[..MLKEM1024_SEED_LEN].try_into().unwrap();
275 let dk = DecapsulationKey::<MlKem1024>::from_seed(mlkem_seed.into());
276 let ek = dk.encapsulation_key();
277 let ek_bytes = ek.to_bytes();
278 let mut pk = Vec::with_capacity(COMBINED_PK_1024_LEN);
279 pk.extend_from_slice(&ek_bytes);
280 pk.extend_from_slice(x25519_pk.as_bytes());
281 pk
282 }
283 };
284
285 let pair = HybridKeyPair {
286 public_key: b64::encode(&combined_pk),
287 secret_key: b64::encode(&seed),
288 };
289
290 seed.zeroize();
291 expanded.zeroize();
292 pair
293}
294
295pub fn hybrid_seal_with_level(
297 plaintext: &[u8],
298 combined_pk_b64: &str,
299 level: SecurityLevel,
300) -> Result<String, CryptoError> {
301 let pk_bytes = b64::decode(combined_pk_b64)?;
302
303 let (expected_pk_len, mlkem_ek_len, version_tag) = match level {
304 SecurityLevel::Cat3 => (COMBINED_PK_768_LEN, MLKEM768_EK_LEN, VERSION_HYBRID_768),
305 SecurityLevel::Cat5 => (COMBINED_PK_1024_LEN, MLKEM1024_EK_LEN, VERSION_HYBRID_1024),
306 };
307
308 if pk_bytes.len() != expected_pk_len {
309 return Err(CryptoError::InvalidLength {
310 expected: expected_pk_len,
311 got: pk_bytes.len(),
312 });
313 }
314
315 let mlkem_ek_bytes = &pk_bytes[..mlkem_ek_len];
317 let x25519_pk_bytes: [u8; X25519_LEN] = pk_bytes[mlkem_ek_len..].try_into().unwrap();
318
319 let mut mlkem_coins = [0u8; 32];
321 random_bytes(&mut mlkem_coins);
322
323 let (mlkem_ct_bytes, ss_mlkem_bytes) = match level {
324 SecurityLevel::Cat3 => {
325 let ek = EncapsulationKey::<MlKem768>::new(
326 mlkem_ek_bytes
327 .try_into()
328 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 ek".into()))?,
329 )
330 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 encapsulation key".into()))?;
331 let (ct, ss) = ek.encapsulate_deterministic(&mlkem_coins.into());
332 (ct.as_slice().to_vec(), ss.as_slice().to_vec())
333 }
334 SecurityLevel::Cat5 => {
335 let ek = EncapsulationKey::<MlKem1024>::new(
336 mlkem_ek_bytes
337 .try_into()
338 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 ek".into()))?,
339 )
340 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 encapsulation key".into()))?;
341 let (ct, ss) = ek.encapsulate_deterministic(&mlkem_coins.into());
342 (ct.as_slice().to_vec(), ss.as_slice().to_vec())
343 }
344 };
345 mlkem_coins.zeroize();
346
347 let mut x25519_eph_bytes = [0u8; X25519_LEN];
349 random_bytes(&mut x25519_eph_bytes);
350 let x25519_eph_sk = X25519StaticSecret::from(x25519_eph_bytes);
351 let x25519_eph_pk = X25519PublicKey::from(&x25519_eph_sk);
352 let x25519_recipient_pk = X25519PublicKey::from(x25519_pk_bytes);
353 let ss_x25519 = x25519_eph_sk.diffie_hellman(&x25519_recipient_pk);
354 x25519_eph_bytes.zeroize();
355
356 let ct_x25519: [u8; X25519_LEN] = *x25519_eph_pk.as_bytes();
358 let mut shared_secret = combine(
359 &ss_mlkem_bytes,
360 ss_x25519.as_bytes(),
361 &ct_x25519,
362 &x25519_pk_bytes,
363 );
364
365 let (secretbox_ct, nonce_buf) = secretbox_encrypt(&shared_secret, plaintext)?;
367 shared_secret.zeroize();
368
369 let combined_ct_len = mlkem_ct_bytes.len() + X25519_LEN;
371 let mut out = Vec::with_capacity(1 + combined_ct_len + NONCE_LEN + secretbox_ct.len());
372 out.push(version_tag);
373 out.extend_from_slice(&mlkem_ct_bytes);
374 out.extend_from_slice(&ct_x25519);
375 out.extend_from_slice(&nonce_buf);
376 out.extend_from_slice(&secretbox_ct);
377
378 Ok(b64::encode(&out))
379}
380
381fn hybrid_open_768(combined: &[u8], seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
384 let seed_bytes = b64::decode(seed_b64)?;
385 if seed_bytes.len() != SEED_LEN {
386 return Err(CryptoError::InvalidLength {
387 expected: SEED_LEN,
388 got: seed_bytes.len(),
389 });
390 }
391 if combined.len() < 1 + COMBINED_CT_768_LEN + NONCE_LEN + MAC_LEN {
392 return Err(CryptoError::TooShort);
393 }
394
395 let seed: [u8; SEED_LEN] = seed_bytes.try_into().unwrap();
396 let mut expanded = expand_seed(&seed, EXPANDED_SEED_768_LEN);
397 let mlkem_seed: [u8; MLKEM768_SEED_LEN] = expanded[..MLKEM768_SEED_LEN].try_into().unwrap();
398 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[MLKEM768_SEED_LEN..].try_into().unwrap();
399 expanded.zeroize();
400
401 let mlkem_ct = &combined[1..1 + MLKEM768_CT_LEN];
403 let x25519_eph_pk_bytes: [u8; X25519_LEN] = combined
404 [1 + MLKEM768_CT_LEN..1 + COMBINED_CT_768_LEN]
405 .try_into()
406 .unwrap();
407 let nonce_slice = &combined[1 + COMBINED_CT_768_LEN..1 + COMBINED_CT_768_LEN + NONCE_LEN];
408 let encrypted = &combined[1 + COMBINED_CT_768_LEN + NONCE_LEN..];
409
410 let dk = DecapsulationKey::<MlKem768>::from_seed(mlkem_seed.into());
412 let kem_ct = mlkem_ct
413 .try_into()
414 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-768 ciphertext".into()))?;
415 let ss_mlkem = dk.decapsulate(kem_ct);
416
417 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
419 let x25519_eph_pk = X25519PublicKey::from(x25519_eph_pk_bytes);
420 let ss_x25519 = x25519_sk.diffie_hellman(&x25519_eph_pk);
421
422 let x25519_pk = X25519PublicKey::from(&x25519_sk);
423 let pk_x25519: [u8; X25519_LEN] = *x25519_pk.as_bytes();
424
425 let mut shared_secret = combine(
426 ss_mlkem.as_slice(),
427 ss_x25519.as_bytes(),
428 &x25519_eph_pk_bytes,
429 &pk_x25519,
430 );
431
432 let result = secretbox_decrypt(&shared_secret, nonce_slice, encrypted);
433 shared_secret.zeroize();
434 result
435}
436
437fn hybrid_open_1024(combined: &[u8], seed_b64: &str) -> Result<Vec<u8>, CryptoError> {
440 let seed_bytes = b64::decode(seed_b64)?;
441 if seed_bytes.len() != SEED_LEN {
442 return Err(CryptoError::InvalidLength {
443 expected: SEED_LEN,
444 got: seed_bytes.len(),
445 });
446 }
447 if combined.len() < 1 + COMBINED_CT_1024_LEN + NONCE_LEN + MAC_LEN {
448 return Err(CryptoError::TooShort);
449 }
450
451 let seed: [u8; SEED_LEN] = seed_bytes.try_into().unwrap();
452 let mut expanded = expand_seed(&seed, EXPANDED_SEED_1024_LEN);
453 let mlkem_seed: [u8; MLKEM1024_SEED_LEN] = expanded[..MLKEM1024_SEED_LEN].try_into().unwrap();
454 let x25519_sk_bytes: [u8; X25519_LEN] = expanded[MLKEM1024_SEED_LEN..].try_into().unwrap();
455 expanded.zeroize();
456
457 let mlkem_ct = &combined[1..1 + MLKEM1024_CT_LEN];
459 let x25519_eph_pk_bytes: [u8; X25519_LEN] = combined
460 [1 + MLKEM1024_CT_LEN..1 + COMBINED_CT_1024_LEN]
461 .try_into()
462 .unwrap();
463 let nonce_slice = &combined[1 + COMBINED_CT_1024_LEN..1 + COMBINED_CT_1024_LEN + NONCE_LEN];
464 let encrypted = &combined[1 + COMBINED_CT_1024_LEN + NONCE_LEN..];
465
466 let dk = DecapsulationKey::<MlKem1024>::from_seed(mlkem_seed.into());
468 let kem_ct = mlkem_ct
469 .try_into()
470 .map_err(|_| CryptoError::Hybrid("invalid ML-KEM-1024 ciphertext".into()))?;
471 let ss_mlkem = dk.decapsulate(kem_ct);
472
473 let x25519_sk = X25519StaticSecret::from(x25519_sk_bytes);
475 let x25519_eph_pk = X25519PublicKey::from(x25519_eph_pk_bytes);
476 let ss_x25519 = x25519_sk.diffie_hellman(&x25519_eph_pk);
477
478 let x25519_pk = X25519PublicKey::from(&x25519_sk);
479 let pk_x25519: [u8; X25519_LEN] = *x25519_pk.as_bytes();
480
481 let mut shared_secret = combine(
482 ss_mlkem.as_slice(),
483 ss_x25519.as_bytes(),
484 &x25519_eph_pk_bytes,
485 &pk_x25519,
486 );
487
488 let result = secretbox_decrypt(&shared_secret, nonce_slice, encrypted);
489 shared_secret.zeroize();
490 result
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
500 fn cat3_roundtrip() {
501 let kp = generate_hybrid_keypair();
502 let pt = b"32-byte symmetric context key!!!";
503 let ct = hybrid_seal(pt, &kp.public_key).unwrap();
504 assert!(is_hybrid_ciphertext(&ct));
505 let opened = hybrid_open(&ct, &kp.secret_key).unwrap();
506 assert_eq!(opened, pt);
507 }
508
509 #[test]
510 fn cat3_wrong_key_fails() {
511 let kp1 = generate_hybrid_keypair();
512 let kp2 = generate_hybrid_keypair();
513 let ct = hybrid_seal(b"x", &kp1.public_key).unwrap();
514 assert!(hybrid_open(&ct, &kp2.secret_key).is_err());
515 }
516
517 #[test]
518 fn cat3_version_tag() {
519 let kp = generate_hybrid_keypair();
520 let raw = b64::decode(&hybrid_seal(b"x", &kp.public_key).unwrap()).unwrap();
521 assert_eq!(raw[0], VERSION_HYBRID_768);
522 }
523
524 #[test]
525 fn cat3_nondeterministic() {
526 let kp = generate_hybrid_keypair();
527 let c1 = hybrid_seal(b"x", &kp.public_key).unwrap();
528 let c2 = hybrid_seal(b"x", &kp.public_key).unwrap();
529 assert_ne!(c1, c2);
530 }
531
532 #[test]
533 fn cat3_empty_plaintext() {
534 let kp = generate_hybrid_keypair();
535 let ct = hybrid_seal(b"", &kp.public_key).unwrap();
536 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"");
537 }
538
539 #[test]
540 fn cat3_key_sizes() {
541 let kp = generate_hybrid_keypair();
542 let pk = b64::decode(&kp.public_key).unwrap();
543 let sk = b64::decode(&kp.secret_key).unwrap();
544 assert_eq!(pk.len(), COMBINED_PK_768_LEN); assert_eq!(sk.len(), SEED_LEN); }
547
548 #[test]
549 fn cat3_ciphertext_size() {
550 let kp = generate_hybrid_keypair();
551 let pt = b"exactly 32 bytes of key material";
552 let raw = b64::decode(&hybrid_seal(pt, &kp.public_key).unwrap()).unwrap();
553 assert_eq!(
555 raw.len(),
556 1 + COMBINED_CT_768_LEN + NONCE_LEN + 32 + MAC_LEN
557 );
558 }
559
560 #[test]
563 fn cat5_roundtrip() {
564 let kp = generate_hybrid_keypair_1024();
565 let pt = b"32-byte symmetric context key!!!";
566 let ct = hybrid_seal_1024(pt, &kp.public_key).unwrap();
567 assert!(is_hybrid_ciphertext(&ct));
568 let opened = hybrid_open(&ct, &kp.secret_key).unwrap();
569 assert_eq!(opened, pt);
570 }
571
572 #[test]
573 fn cat5_version_tag() {
574 let kp = generate_hybrid_keypair_1024();
575 let raw = b64::decode(&hybrid_seal_1024(b"x", &kp.public_key).unwrap()).unwrap();
576 assert_eq!(raw[0], VERSION_HYBRID_1024);
577 }
578
579 #[test]
580 fn cat5_wrong_key_fails() {
581 let kp1 = generate_hybrid_keypair_1024();
582 let kp2 = generate_hybrid_keypair_1024();
583 let ct = hybrid_seal_1024(b"x", &kp1.public_key).unwrap();
584 assert!(hybrid_open(&ct, &kp2.secret_key).is_err());
585 }
586
587 #[test]
588 fn cat5_key_sizes() {
589 let kp = generate_hybrid_keypair_1024();
590 let pk = b64::decode(&kp.public_key).unwrap();
591 let sk = b64::decode(&kp.secret_key).unwrap();
592 assert_eq!(pk.len(), COMBINED_PK_1024_LEN); assert_eq!(sk.len(), SEED_LEN); }
595
596 #[test]
597 fn cat5_ciphertext_size() {
598 let kp = generate_hybrid_keypair_1024();
599 let pt = b"exactly 32 bytes of key material";
600 let raw = b64::decode(&hybrid_seal_1024(pt, &kp.public_key).unwrap()).unwrap();
601 assert_eq!(
603 raw.len(),
604 1 + COMBINED_CT_1024_LEN + NONCE_LEN + 32 + MAC_LEN
605 );
606 }
607
608 #[test]
609 fn cat5_nondeterministic() {
610 let kp = generate_hybrid_keypair_1024();
611 let c1 = hybrid_seal_1024(b"x", &kp.public_key).unwrap();
612 let c2 = hybrid_seal_1024(b"x", &kp.public_key).unwrap();
613 assert_ne!(c1, c2);
614 }
615
616 #[test]
617 fn cat5_empty_plaintext() {
618 let kp = generate_hybrid_keypair_1024();
619 let ct = hybrid_seal_1024(b"", &kp.public_key).unwrap();
620 assert_eq!(hybrid_open(&ct, &kp.secret_key).unwrap(), b"");
621 }
622
623 #[test]
626 fn cat3_ct_cannot_open_with_cat5_key() {
627 let kp3 = generate_hybrid_keypair();
628 let kp5 = generate_hybrid_keypair_1024();
629 let ct = hybrid_seal(b"test", &kp3.public_key).unwrap();
630 assert!(hybrid_open(&ct, &kp5.secret_key).is_err());
631 }
632
633 #[test]
634 fn cat5_ct_cannot_open_with_cat3_key() {
635 let kp3 = generate_hybrid_keypair();
636 let kp5 = generate_hybrid_keypair_1024();
637 let ct = hybrid_seal_1024(b"test", &kp5.public_key).unwrap();
638 assert!(hybrid_open(&ct, &kp3.secret_key).is_err());
639 }
640
641 #[test]
642 fn legacy_not_hybrid() {
643 let legacy = b64::encode(&[0x01, 0x02, 0x03]);
644 assert!(!is_hybrid_ciphertext(&legacy));
645 }
646
647 #[test]
648 fn seed_expansion_deterministic() {
649 let seed = [0x42u8; SEED_LEN];
650 let expanded = expand_seed(&seed, 96);
651 let expanded2 = expand_seed(&seed, 96);
652 assert_eq!(expanded, expanded2);
653 }
654
655 #[test]
656 fn combiner_uses_label() {
657 let ss_mlkem = [0xAAu8; 32];
658 let ss_x25519 = [0xBBu8; 32];
659 let ct_x25519 = [0xCCu8; 32];
660 let pk_x25519 = [0xDDu8; 32];
661 let result = combine(&ss_mlkem, &ss_x25519, &ct_x25519, &pk_x25519);
662 assert_eq!(result.len(), 32);
663
664 let ss_mlkem2 = [0xEEu8; 32];
665 let result2 = combine(&ss_mlkem2, &ss_x25519, &ct_x25519, &pk_x25519);
666 assert_ne!(result, result2);
667 }
668}