use crate::constants::*;
use crate::error::{Error, Result};
use crate::keys::{KeyPair, PublicKeyBundle};
use alloc::boxed::Box;
use ml_kem::kem::{Decapsulate as _, KeyExport as _};
use sha3::{Digest, Sha3_256};
use zeroize::Zeroizing;
pub(crate) struct KemCiphertext {
pub(crate) epk_x25519: [u8; X25519_PK_LEN],
pub(crate) ct_mlkem: Box<[u8; MLKEM1024_CT_LEN]>,
}
pub(crate) fn encapsulate(
recipient: &PublicKeyBundle,
) -> Result<(KemCiphertext, Zeroizing<[u8; 32]>)> {
let mut eph_bytes = Zeroizing::new([0u8; X25519_SK_LEN]);
getrandom::fill(eph_bytes.as_mut()).map_err(|_| Error::RandomnessUnavailable)?;
let mut m = Zeroizing::new([0u8; 32]);
getrandom::fill(m.as_mut()).map_err(|_| Error::RandomnessUnavailable)?;
Ok(encapsulate_deterministic(recipient, *eph_bytes, &m))
}
fn encapsulate_deterministic(
recipient: &PublicKeyBundle,
eph_bytes: [u8; X25519_SK_LEN],
m: &[u8; 32],
) -> (KemCiphertext, Zeroizing<[u8; 32]>) {
let eph_sk = x25519_dalek::StaticSecret::from(eph_bytes);
let epk = x25519_dalek::PublicKey::from(&eph_sk);
let ss_x25519 = eph_sk.diffie_hellman(&recipient.x25519);
let (ct_mlkem, ss_mlkem) = recipient.mlkem.encapsulate_deterministic(&(*m).into());
let ct_mlkem_bytes: [u8; MLKEM1024_CT_LEN] = ct_mlkem.into();
let ss = combine(
&ss_mlkem,
ss_x25519.as_bytes(),
&ct_mlkem_bytes,
epk.as_bytes(),
recipient,
);
(
KemCiphertext {
epk_x25519: *epk.as_bytes(),
ct_mlkem: Box::new(ct_mlkem_bytes),
},
ss,
)
}
pub(crate) fn decapsulate(keypair: &KeyPair, ct: &KemCiphertext) -> Zeroizing<[u8; 32]> {
let epk = x25519_dalek::PublicKey::from(ct.epk_x25519);
let ss_x25519 = keypair.x25519_sk.diffie_hellman(&epk);
let ct_mlkem = ml_kem::ml_kem_1024::Ciphertext::from(*ct.ct_mlkem);
let ss_mlkem = keypair.mlkem_dk.decapsulate(&ct_mlkem);
combine(
&ss_mlkem,
ss_x25519.as_bytes(),
ct.ct_mlkem.as_ref(),
&ct.epk_x25519,
keypair.public_keys(),
)
}
fn combine(
ss_mlkem: &[u8],
ss_x25519: &[u8; 32],
ct_mlkem: &[u8; MLKEM1024_CT_LEN],
epk_x25519: &[u8; X25519_PK_LEN],
recipient: &PublicKeyBundle,
) -> Zeroizing<[u8; 32]> {
debug_assert_eq!(ss_mlkem.len(), 32);
let mut hasher = Sha3_256::new();
hasher.update(KEM_COMBINER_LABEL);
hasher.update(ss_mlkem);
hasher.update(ss_x25519);
hasher.update(ct_mlkem);
hasher.update(epk_x25519);
hasher.update(recipient.mlkem.to_bytes());
hasher.update(recipient.x25519.as_bytes());
Zeroizing::new(hasher.finalize().into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encap_decap_agree() {
let kp = KeyPair::generate().unwrap();
let (ct, ss_sender) = encapsulate(kp.public_keys()).unwrap();
let ss_recipient = decapsulate(&kp, &ct);
assert_eq!(*ss_sender, *ss_recipient);
}
#[test]
fn wrong_recipient_disagrees() {
let alice = KeyPair::generate().unwrap();
let mallory = KeyPair::generate().unwrap();
let (ct, ss_sender) = encapsulate(alice.public_keys()).unwrap();
let ss_mallory = decapsulate(&mallory, &ct);
assert_ne!(*ss_sender, *ss_mallory);
}
#[test]
fn tampered_mlkem_ciphertext_changes_secret() {
let kp = KeyPair::generate().unwrap();
let (mut ct, ss_sender) = encapsulate(kp.public_keys()).unwrap();
ct.ct_mlkem[0] ^= 0x01;
let ss_recipient = decapsulate(&kp, &ct);
assert_ne!(*ss_sender, *ss_recipient);
}
#[test]
fn tampered_epk_changes_secret() {
let kp = KeyPair::generate().unwrap();
let (mut ct, ss_sender) = encapsulate(kp.public_keys()).unwrap();
ct.epk_x25519[0] ^= 0x01;
let ss_recipient = decapsulate(&kp, &ct);
assert_ne!(*ss_sender, *ss_recipient);
}
#[test]
fn deterministic_kat() {
use crate::keys::Seeds;
let kp = KeyPair::from_seeds(Seeds {
x25519_sk: [0x01; 32],
mlkem_seed: [0x02; 64],
ed25519_seed: [0x03; 32],
mldsa_seed: [0x04; 32],
});
let (ct, ss) = encapsulate_deterministic(kp.public_keys(), [0x05; 32], &[0x06; 32]);
let (ct2, ss2) = encapsulate_deterministic(kp.public_keys(), [0x05; 32], &[0x06; 32]);
assert_eq!(ct.epk_x25519, ct2.epk_x25519);
assert_eq!(ct.ct_mlkem, ct2.ct_mlkem);
assert_eq!(*ss, *ss2);
assert_eq!(*decapsulate(&kp, &ct), *ss);
assert_eq!(
hex::encode(*ss),
"bf0314748b5e99e0c147a4ab1e51bbdea39798abcec5af015ca4f8689daa5556"
);
assert_eq!(
hex::encode(ct.epk_x25519),
"50a61409b1ddd0325e9b16b700e719e9772c07000b1bd7786e907c653d20495d"
);
assert_eq!(
hex::encode(sha3::Sha3_256::digest(ct.ct_mlkem.as_ref())),
"99a43f1aa309cf15a3109784e11bb28956c35aede3663811a66a4a24d3ebe771"
);
}
#[test]
fn combiner_matches_spec() {
let kp = KeyPair::generate().unwrap();
let ss_mlkem = [0x11u8; 32];
let ss_x25519 = [0x22u8; 32];
let ct_mlkem = [0x33u8; MLKEM1024_CT_LEN];
let epk = [0x44u8; X25519_PK_LEN];
let got = combine(&ss_mlkem, &ss_x25519, &ct_mlkem, &epk, kp.public_keys());
let mut manual = Vec::new();
manual.extend_from_slice(b"quantum-shield/v2/kem:X25519+ML-KEM-1024\0");
manual.extend_from_slice(&ss_mlkem);
manual.extend_from_slice(&ss_x25519);
manual.extend_from_slice(&ct_mlkem);
manual.extend_from_slice(&epk);
manual.extend_from_slice(
&kp.public_keys().to_bytes()[HEADER_LEN + X25519_PK_LEN..][..MLKEM1024_EK_LEN],
);
manual.extend_from_slice(kp.public_keys().x25519.as_bytes());
let expected: [u8; 32] = Sha3_256::digest(&manual).into();
assert_eq!(*got, expected);
}
}