use std::future::Future;
use basil_proto::broker::v1 as pb;
use hkdf::Hkdf;
use ml_kem::kem::{Decapsulate, Encapsulate, FromSeed, KeyExport, TryKeyInit};
use ml_kem::{MlKem512, MlKem768, MlKem1024};
use sha2::Sha256;
use zeroize::Zeroizing;
use super::format::{
CEK_LEN, ChunkAead, NONCE_LEN, StreamError, StreamResult, aead_open, aead_seal,
};
use super::{MlKemSuite, fill_random};
use crate::client::Client;
const KEM_ENVELOPE_LABEL: &[u8] = b"basil-ml-kem-envelope-v1";
const ENV_TOKEN_AES256GCM: &[u8] = b"aes-256-gcm";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamKemEnvelope {
pub encapsulated_key: Vec<u8>,
pub nonce: [u8; NONCE_LEN],
pub ciphertext: Vec<u8>,
}
pub trait CekRecovery {
fn recover_cek(
&self,
envelope: &StreamKemEnvelope,
aad: &[u8],
) -> impl Future<Output = StreamResult<Zeroizing<Vec<u8>>>> + Send;
}
#[derive(Clone)]
pub struct BrokerCekRecovery {
client: Client,
key_id: String,
suite: MlKemSuite,
}
impl BrokerCekRecovery {
#[must_use]
pub fn new(client: Client, key_id: impl Into<String>, suite: MlKemSuite) -> Self {
Self {
client,
key_id: key_id.into(),
suite,
}
}
}
impl CekRecovery for BrokerCekRecovery {
fn recover_cek(
&self,
envelope: &StreamKemEnvelope,
aad: &[u8],
) -> impl Future<Output = StreamResult<Zeroizing<Vec<u8>>>> + Send {
let mut client = self.client.clone();
let key_id = self.key_id.clone();
let pb_env = pb::KemEnvelope {
kem_algorithm: i32::from(self.suite.proto_kem_algorithm()),
envelope_algorithm: i32::from(pb::EnvelopeAlgorithm::Aes256Gcm),
key_version: 0,
encapsulated_key: envelope.encapsulated_key.clone(),
nonce: envelope.nonce.to_vec(),
ciphertext: envelope.ciphertext.clone(),
};
let aad = aad.to_vec();
async move {
let cek = client.unwrap_envelope(&key_id, pb_env, Some(&aad)).await?;
if cek.len() != CEK_LEN {
return Err(StreamError::BadCekLength);
}
Ok(Zeroizing::new(cek))
}
}
}
pub struct LocalSeedCekRecovery {
seed: Zeroizing<Vec<u8>>,
suite: MlKemSuite,
}
impl LocalSeedCekRecovery {
#[must_use]
pub fn new(seed: impl Into<Vec<u8>>, suite: MlKemSuite) -> Self {
Self {
seed: Zeroizing::new(seed.into()),
suite,
}
}
}
impl CekRecovery for LocalSeedCekRecovery {
fn recover_cek(
&self,
envelope: &StreamKemEnvelope,
aad: &[u8],
) -> impl Future<Output = StreamResult<Zeroizing<Vec<u8>>>> + Send {
let recovered = open_cek_local(&self.seed, self.suite, envelope, aad);
async move { recovered }
}
}
pub fn wrap_cek(
public_key: &[u8],
suite: MlKemSuite,
cek: &[u8],
aad: &[u8],
) -> StreamResult<StreamKemEnvelope> {
let (encapsulated_key, shared_secret, derived_public) = match suite {
MlKemSuite::MlKem512 => encapsulate_to::<MlKem512>(public_key)?,
MlKemSuite::MlKem768 => encapsulate_to::<MlKem768>(public_key)?,
MlKemSuite::MlKem1024 => encapsulate_to::<MlKem1024>(public_key)?,
};
let aead_key = derive_kem_aead_key(suite, &shared_secret, &encapsulated_key, &derived_public)?;
let mut nonce = [0u8; NONCE_LEN];
fill_random(&mut nonce);
let ciphertext = aead_seal(ChunkAead::Aes256Gcm, &aead_key, &nonce, cek, aad)?;
Ok(StreamKemEnvelope {
encapsulated_key,
nonce,
ciphertext,
})
}
pub fn open_cek_local(
seed: &[u8],
suite: MlKemSuite,
envelope: &StreamKemEnvelope,
aad: &[u8],
) -> StreamResult<Zeroizing<Vec<u8>>> {
let seed = seed_from_slice(seed)?;
let (shared_secret, derived_public) = match suite {
MlKemSuite::MlKem512 => {
decapsulate_with_seed::<MlKem512>(&seed, &envelope.encapsulated_key)?
}
MlKemSuite::MlKem768 => {
decapsulate_with_seed::<MlKem768>(&seed, &envelope.encapsulated_key)?
}
MlKemSuite::MlKem1024 => {
decapsulate_with_seed::<MlKem1024>(&seed, &envelope.encapsulated_key)?
}
};
let aead_key = derive_kem_aead_key(
suite,
&shared_secret,
&envelope.encapsulated_key,
&derived_public,
)?;
aead_open(
ChunkAead::Aes256Gcm,
&aead_key,
&envelope.nonce,
&envelope.ciphertext,
aad,
)
}
pub fn serialize_kem_header(env: &StreamKemEnvelope) -> StreamResult<Vec<u8>> {
let kem_ct_len =
u32::try_from(env.encapsulated_key.len()).map_err(|_| StreamError::SealFailed)?;
let wrapped_len = u32::try_from(env.ciphertext.len()).map_err(|_| StreamError::SealFailed)?;
let mut out =
Vec::with_capacity(8 + env.encapsulated_key.len() + NONCE_LEN + env.ciphertext.len());
out.extend_from_slice(&kem_ct_len.to_be_bytes());
out.extend_from_slice(&env.encapsulated_key);
out.extend_from_slice(&env.nonce);
out.extend_from_slice(&wrapped_len.to_be_bytes());
out.extend_from_slice(&env.ciphertext);
Ok(out)
}
fn derive_kem_aead_key(
suite: MlKemSuite,
shared_secret: &[u8],
encapsulated_key: &[u8],
public_key: &[u8],
) -> StreamResult<Zeroizing<[u8; CEK_LEN]>> {
let kem_token = suite.kem_token().as_bytes();
let mut info = Vec::with_capacity(
KEM_ENVELOPE_LABEL.len()
+ kem_token.len()
+ ENV_TOKEN_AES256GCM.len()
+ encapsulated_key.len()
+ public_key.len(),
);
info.extend_from_slice(KEM_ENVELOPE_LABEL);
info.extend_from_slice(kem_token);
info.extend_from_slice(ENV_TOKEN_AES256GCM);
info.extend_from_slice(encapsulated_key);
info.extend_from_slice(public_key);
let hk = Hkdf::<Sha256>::new(None, shared_secret);
let mut okm = Zeroizing::new([0u8; CEK_LEN]);
hk.expand(&info, okm.as_mut_slice())
.map_err(|_| StreamError::KdfFailed)?;
Ok(okm)
}
fn to_vec_bytes(value: &impl AsRef<[u8]>) -> Vec<u8> {
value.as_ref().to_vec()
}
type Encapsulation = (Vec<u8>, Zeroizing<Vec<u8>>, Vec<u8>);
fn encapsulate_to<K>(public_key: &[u8]) -> StreamResult<Encapsulation>
where
K: ml_kem::kem::Kem,
K::EncapsulationKey: Encapsulate<Kem = K>,
{
let ek = <K::EncapsulationKey as TryKeyInit>::new_from_slice(public_key)
.map_err(|_| StreamError::BadPublicKey)?;
let derived_public = <K::EncapsulationKey as KeyExport>::to_bytes(&ek);
let (encapsulated_key, shared_secret) = ek.encapsulate();
Ok((
to_vec_bytes(&encapsulated_key),
Zeroizing::new(to_vec_bytes(&shared_secret)),
to_vec_bytes(&derived_public),
))
}
fn decapsulate_with_seed<K>(
seed: &ml_kem::Seed,
encapsulated_key: &[u8],
) -> StreamResult<(Zeroizing<Vec<u8>>, Vec<u8>)>
where
K: ml_kem::kem::Kem + FromSeed<SeedSize = ml_kem::array::sizes::U64>,
K::DecapsulationKey: Decapsulate<Kem = K>,
K::EncapsulationKey: KeyExport,
{
let (decapsulation_key, encapsulation_key) = K::from_seed(seed);
let shared_secret = decapsulation_key
.decapsulate_slice(encapsulated_key)
.map_err(|_| StreamError::BadKemCiphertext)?;
let derived_public = encapsulation_key.to_bytes();
Ok((
Zeroizing::new(to_vec_bytes(&shared_secret)),
to_vec_bytes(&derived_public),
))
}
fn seed_from_slice(seed: &[u8]) -> StreamResult<Zeroizing<ml_kem::Seed>> {
let seed = seed.try_into().map_err(|_| StreamError::BadKemCiphertext)?;
Ok(Zeroizing::new(seed))
}
#[cfg(test)]
pub fn public_from_seed(seed: &[u8], suite: MlKemSuite) -> StreamResult<Vec<u8>> {
let seed = seed_from_slice(seed)?;
let public = match suite {
MlKemSuite::MlKem512 => public_from_seed_for::<MlKem512>(&seed),
MlKemSuite::MlKem768 => public_from_seed_for::<MlKem768>(&seed),
MlKemSuite::MlKem1024 => public_from_seed_for::<MlKem1024>(&seed),
};
Ok(public)
}
#[cfg(test)]
fn public_from_seed_for<K>(seed: &ml_kem::Seed) -> Vec<u8>
where
K: ml_kem::kem::Kem + FromSeed<SeedSize = ml_kem::array::sizes::U64>,
K::EncapsulationKey: KeyExport,
{
let (_decapsulation_key, encapsulation_key) = K::from_seed(seed);
to_vec_bytes(&encapsulation_key.to_bytes())
}