use crate::{
error::{AttributesError, ConversionsError, SealError},
views::{aead, Views},
AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView,
SealView,
};
use multi_codec::Codec;
use multi_hash::{mh, Multihash};
use multi_trait::TryDecodeFrom;
use multi_util::Varbytes;
use rand_chacha::ChaCha20Rng;
use rand_core_06::SeedableRng;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
const X25519_SEED_LEN: usize = 32;
const MCELIECE_SEED_LEN: usize = 32;
const PRIV_SEED_LEN: usize = X25519_SEED_LEN + MCELIECE_SEED_LEN; const X25519_PUB_LEN: usize = 32;
type SealedParts = (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>);
fn is_aead_allowed(codec: Codec) -> bool {
codec == Codec::Chacha20Poly1305
}
pub(crate) struct View<'a> {
mk: &'a Multikey,
}
impl<'a> TryFrom<&'a Multikey> for View<'a> {
type Error = Error;
fn try_from(mk: &'a Multikey) -> Result<Self, Self::Error> {
Ok(Self { mk })
}
}
impl<'a> AttrView for View<'a> {
fn is_encrypted(&self) -> bool {
false
}
fn is_secret_key(&self) -> bool {
self.mk.codec == Codec::X25519Mceliece348864Priv
}
fn is_public_key(&self) -> bool {
self.mk.codec == Codec::X25519Mceliece348864Pub
}
fn is_secret_key_share(&self) -> bool {
false
}
}
impl<'a> DataView for View<'a> {
fn key_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
let key = self
.mk
.attributes
.get(&AttrId::KeyData)
.ok_or(AttributesError::MissingKey)?;
Ok(key.clone())
}
fn secret_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
if !self.is_secret_key() {
return Err(AttributesError::NotSecretKey(self.mk.codec).into());
}
self.key_bytes()
}
}
impl<'a> ConvView for View<'a> {
fn to_public_key(&self) -> Result<Multikey, Error> {
let secret_bytes = {
let kd = self.mk.data_view()?;
kd.secret_bytes()?
};
if secret_bytes.len() != PRIV_SEED_LEN {
return Err(
ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(),
);
}
let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN]
.try_into()
.map_err(|_| ConversionsError::SecretKeyFailure("invalid x25519 seed".into()))?;
let x25519_secret = StaticSecret::from(x25519_seed);
let x25519_pub = PublicKey::from(&x25519_secret);
let mceliece_seed: [u8; 32] = secret_bytes[X25519_SEED_LEN..PRIV_SEED_LEN]
.try_into()
.map_err(|_| ConversionsError::SecretKeyFailure("invalid mceliece seed".into()))?;
let mut rng = ChaCha20Rng::from_seed(mceliece_seed);
let (pk, _sk) = mceliece348864::keypair_boxed(&mut rng);
let mut pub_bytes = Vec::with_capacity(X25519_PUB_LEN + pk.as_ref().len());
pub_bytes.extend_from_slice(x25519_pub.as_bytes());
pub_bytes.extend_from_slice(pk.as_ref());
Builder::new(Codec::X25519Mceliece348864Pub)
.with_comment(&self.mk.comment)
.with_key_bytes(&pub_bytes)
.try_build()
}
fn to_ssh_public_key(&self) -> Result<ssh_key::PublicKey, Error> {
Err(ConversionsError::UnsupportedAlgorithm(
"X25519-Classic-McEliece-348864 not supported in SSH key format".into(),
)
.into())
}
fn to_ssh_private_key(&self) -> Result<ssh_key::PrivateKey, Error> {
Err(ConversionsError::UnsupportedAlgorithm(
"X25519-Classic-McEliece-348864 not supported in SSH key format".into(),
)
.into())
}
}
impl<'a> FingerprintView for View<'a> {
fn fingerprint(&self, codec: Codec) -> Result<Multihash, Error> {
let pub_bytes = if self.is_secret_key() {
let pk = self.to_public_key()?;
let dv = pk.data_view()?;
dv.key_bytes()?
} else {
self.key_bytes()?
};
Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?)
}
}
fn combine_shared_secrets(
ss_mceliece: &[u8],
ss_x25519: &[u8],
mceliece_ct: &[u8],
ephemeral_x25519_pub: &[u8],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"x25519-mceliece348864-hpke");
hasher.update(ss_mceliece);
hasher.update(ss_x25519);
hasher.update(mceliece_ct);
hasher.update(ephemeral_x25519_pub);
*hasher.finalize().as_bytes()
}
fn encode_sealed(ephemeral_pub: &[u8], mceliece_ct: &[u8], nonce: &[u8], ct_tag: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.append(&mut Varbytes::new(ephemeral_pub.to_vec()).into());
out.append(&mut Varbytes::new(mceliece_ct.to_vec()).into());
out.append(&mut Varbytes::new(nonce.to_vec()).into());
out.append(&mut Varbytes::new(ct_tag.to_vec()).into());
out
}
fn decode_sealed(data: &[u8]) -> Result<SealedParts, SealError> {
let (ephemeral_pub, ptr) = Varbytes::try_decode_from(data)
.map_err(|_| SealError::InvalidFormat("missing ephemeral public key".into()))?;
let (mceliece_ct, ptr) = Varbytes::try_decode_from(ptr)
.map_err(|_| SealError::InvalidFormat("missing mceliece ciphertext".into()))?;
let (nonce, ptr) = Varbytes::try_decode_from(ptr)
.map_err(|_| SealError::InvalidFormat("missing nonce".into()))?;
let (ct_tag, _) = Varbytes::try_decode_from(ptr)
.map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?;
Ok((
ephemeral_pub.to_inner(),
mceliece_ct.to_inner(),
nonce.to_inner(),
ct_tag.to_inner(),
))
}
impl<'a> SealView for View<'a> {
fn seal(
&self,
plaintext: &[u8],
aead_codec: Codec,
aad: &[u8],
) -> Result<(Vec<u8>, Option<Multikey>), Error> {
if !self.is_public_key() {
return Err(SealError::NotEncapsulationKey.into());
}
if !is_aead_allowed(aead_codec) {
return Err(SealError::UnsupportedAeadCodec(aead_codec).into());
}
let pub_bytes = self.key_bytes()?;
if pub_bytes.len() != X25519_PUB_LEN + mceliece348864::CRYPTO_PUBLICKEYBYTES {
return Err(
SealError::EncapsulationFailed("invalid hybrid public key length".into()).into(),
);
}
let x25519_pub_arr: [u8; 32] = pub_bytes[..X25519_PUB_LEN]
.try_into()
.map_err(|_| SealError::EncapsulationFailed("invalid x25519 public key".into()))?;
let recipient_x25519_pub = PublicKey::from(x25519_pub_arr);
let ephemeral_secret = StaticSecret::random_from_rng(&mut rand::rng());
let ephemeral_pub = PublicKey::from(&ephemeral_secret);
let ss_x25519 = ephemeral_secret.diffie_hellman(&recipient_x25519_pub);
let pk_array: Box<[u8; mceliece348864::CRYPTO_PUBLICKEYBYTES]> = pub_bytes
[X25519_PUB_LEN..]
.to_vec()
.into_boxed_slice()
.try_into()
.map_err(|_| SealError::EncapsulationFailed("invalid mceliece public key".into()))?;
let pk = mceliece348864::PublicKey::from(pk_array);
let (ct, ss_mceliece) = mceliece348864::encapsulate_boxed(&pk, &mut rand_core_06::OsRng);
let mceliece_ct: &[u8] = ct.as_ref();
let combined_ss = combine_shared_secrets(
ss_mceliece.as_ref(),
ss_x25519.as_bytes(),
mceliece_ct,
ephemeral_pub.as_bytes(),
);
let (nonce, ct_tag) = aead::aead_seal(aead_codec, &combined_ss, plaintext, aad)?;
Ok((
encode_sealed(ephemeral_pub.as_bytes(), mceliece_ct, &nonce, &ct_tag),
None,
))
}
}
impl<'a> OpenView for View<'a> {
fn open(
&self,
sealed_msg: &[u8],
_ephemeral: Option<&Multikey>,
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>, Error> {
if !self.is_secret_key() {
return Err(SealError::NotDecapsulationKey.into());
}
let (ephemeral_pub_bytes, mceliece_ct_bytes, nonce, ct_tag) = decode_sealed(sealed_msg)?;
if ephemeral_pub_bytes.len() != X25519_PUB_LEN {
return Err(
SealError::InvalidFormat("invalid ephemeral public key length".into()).into(),
);
}
let secret_bytes = {
let kd = self.mk.data_view()?;
kd.secret_bytes()?
};
if secret_bytes.len() != PRIV_SEED_LEN {
return Err(
ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(),
);
}
let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN]
.try_into()
.map_err(|_| SealError::DecapsulationFailed("invalid x25519 seed".into()))?;
let x25519_secret = StaticSecret::from(x25519_seed);
let ephemeral_pub_arr: [u8; 32] = ephemeral_pub_bytes
.as_slice()
.try_into()
.map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?;
let ephemeral_pub = PublicKey::from(ephemeral_pub_arr);
let ss_x25519 = x25519_secret.diffie_hellman(&ephemeral_pub);
let mceliece_seed: [u8; 32] = secret_bytes[X25519_SEED_LEN..PRIV_SEED_LEN]
.try_into()
.map_err(|_| SealError::DecapsulationFailed("invalid mceliece seed".into()))?;
let mut rng = ChaCha20Rng::from_seed(mceliece_seed);
let (_pk, sk) = mceliece348864::keypair_boxed(&mut rng);
let ct_array: [u8; mceliece348864::CRYPTO_CIPHERTEXTBYTES] = mceliece_ct_bytes
.clone()
.try_into()
.map_err(|_| SealError::DecapsulationFailed("invalid mceliece ciphertext".into()))?;
let ct = mceliece348864::Ciphertext::from(ct_array);
let ss_mceliece = mceliece348864::decapsulate_boxed(&ct, &sk);
let combined_ss = combine_shared_secrets(
ss_mceliece.as_ref(),
ss_x25519.as_bytes(),
mceliece_ct_bytes.as_slice(),
ephemeral_pub_bytes.as_slice(),
);
Ok(aead::aead_open(
Codec::Chacha20Poly1305,
&combined_ss,
&nonce,
&ct_tag,
aad,
)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::views::Views;
#[test]
fn test_seal_open_roundtrip() {
let mut rng = rand::rng();
let sk = Builder::new_from_random_bytes(Codec::X25519Mceliece348864Priv, &mut rng)
.unwrap()
.with_comment("x25519-mceliece hybrid test")
.try_build()
.unwrap();
let pk = sk.conv_view().unwrap().to_public_key().unwrap();
let plaintext = b"hello X25519-Classic-McEliece-348864 hybrid KEM!";
let (sealed, _) = pk
.seal_view()
.unwrap()
.seal(plaintext, Codec::Chacha20Poly1305, b"")
.unwrap();
let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap();
assert_eq!(plaintext.as_slice(), opened.as_slice());
}
#[test]
fn test_wrong_key_fails() {
let mut rng = rand::rng();
let sk1 = Builder::new_from_random_bytes(Codec::X25519Mceliece348864Priv, &mut rng)
.unwrap()
.try_build()
.unwrap();
let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap();
let sk2 = Builder::new_from_random_bytes(Codec::X25519Mceliece348864Priv, &mut rng)
.unwrap()
.try_build()
.unwrap();
let (sealed, _) = pk1
.seal_view()
.unwrap()
.seal(b"secret", Codec::Chacha20Poly1305, b"")
.unwrap();
assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err());
}
}