use crate::{
api::{AuthenticatedAead, Decryptor, Encryptor},
core::hub::{DecryptData, EncryptData, Kyber, KyberSizeVariant},
error::CryptError,
markers::{Data, Decryption, Encryption},
protocol::Envelope,
};
const HPKE_FRAME_MAGIC: &[u8; 4] = b"HFv1";
pub fn seal<K, A>(
recipient_pk: &[u8],
info: &[u8],
aad: &[u8],
plaintext: &[u8],
) -> Result<Envelope, CryptError>
where
K: KyberSizeVariant,
A: AuthenticatedAead,
Kyber<Encryption, K, Data, A>: EncryptData,
{
if recipient_pk.is_empty() {
return Err(CryptError::MissingPublicKey);
}
let framed = frame_plaintext(info, aad, plaintext);
Encryptor::<K, A>::new()
.recipient(recipient_pk.to_vec())
.plaintext(framed)
.seal()
}
pub fn open<K, A>(
recipient_sk: &[u8],
info: &[u8],
aad: &[u8],
envelope: &Envelope,
) -> Result<Vec<u8>, CryptError>
where
K: KyberSizeVariant,
A: AuthenticatedAead,
Kyber<Decryption, K, Data, A>: DecryptData,
{
if recipient_sk.is_empty() {
return Err(CryptError::MissingSecretKey);
}
let framed = Decryptor::<K, A>::new()
.secret_key(recipient_sk.to_vec())
.open(envelope)?;
unframe_plaintext(&framed, info, aad)
}
fn frame_plaintext(info: &[u8], aad: &[u8], plaintext: &[u8]) -> Vec<u8> {
let cap = HPKE_FRAME_MAGIC.len() + 4 + info.len() + 4 + aad.len() + plaintext.len();
let mut out = Vec::with_capacity(cap);
out.extend_from_slice(HPKE_FRAME_MAGIC);
out.extend_from_slice(&(info.len() as u32).to_le_bytes());
out.extend_from_slice(info);
out.extend_from_slice(&(aad.len() as u32).to_le_bytes());
out.extend_from_slice(aad);
out.extend_from_slice(plaintext);
out
}
fn unframe_plaintext(
framed: &[u8],
expected_info: &[u8],
expected_aad: &[u8],
) -> Result<Vec<u8>, CryptError> {
if framed.len() < 12 {
return Err(CryptError::InvalidEnvelope);
}
let mut pos = 0usize;
if &framed[pos..pos + 4] != HPKE_FRAME_MAGIC {
return Err(CryptError::InvalidEnvelope);
}
pos += 4;
let info = read_field(framed, &mut pos)?;
let aad = read_field(framed, &mut pos)?;
if info != expected_info {
return Err(CryptError::InvalidEnvelope);
}
if aad != expected_aad {
return Err(CryptError::InvalidEnvelope);
}
Ok(framed[pos..].to_vec())
}
fn read_field<'a>(data: &'a [u8], pos: &mut usize) -> Result<&'a [u8], CryptError> {
if *pos + 4 > data.len() {
return Err(CryptError::InvalidEnvelope);
}
let len =
u32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]) as usize;
*pos += 4;
if *pos + len > data.len() {
return Err(CryptError::InvalidEnvelope);
}
let field = &data[*pos..*pos + len];
*pos += len;
Ok(field)
}
#[cfg(all(test, feature = "ml-kem-backend"))]
mod tests {
use super::*;
use crate::core::hub::MlKem768;
use crate::kem::{backend::OsRng, ml_kem::MlKem768Impl, KemBackend};
use crate::markers::XChaCha20Poly1305;
fn keygen() -> (Vec<u8>, Vec<u8>) {
let mut rng = OsRng;
let (pk, sk) = MlKem768Impl::keypair(&mut rng).expect("ML-KEM-768 keygen must not fail");
(pk.as_ref().to_vec(), sk.as_ref().to_vec())
}
#[test]
fn test_frame_roundtrip_empty_fields() {
let info = b"";
let aad = b"";
let pt = b"secret payload";
let framed = frame_plaintext(info, aad, pt);
let recovered = unframe_plaintext(&framed, info, aad).expect("unframe must succeed");
assert_eq!(recovered, pt);
}
#[test]
fn test_frame_roundtrip_non_empty_fields() {
let info = b"crypt_guard:hpke:v1";
let aad = b"request-id=abc123";
let pt = b"the actual plaintext bytes";
let framed = frame_plaintext(info, aad, pt);
let recovered = unframe_plaintext(&framed, info, aad).expect("unframe must succeed");
assert_eq!(recovered, pt);
}
#[test]
fn test_frame_wrong_info_rejected() {
let framed = frame_plaintext(b"correct-info", b"aad", b"pt");
let err = unframe_plaintext(&framed, b"wrong-info", b"aad");
assert!(
matches!(err, Err(CryptError::InvalidEnvelope)),
"mismatched info must return InvalidEnvelope, got: {:?}",
err
);
}
#[test]
fn test_frame_wrong_aad_rejected() {
let framed = frame_plaintext(b"info", b"correct-aad", b"pt");
let err = unframe_plaintext(&framed, b"info", b"wrong-aad");
assert!(
matches!(err, Err(CryptError::InvalidEnvelope)),
"mismatched aad must return InvalidEnvelope, got: {:?}",
err
);
}
#[test]
fn test_frame_truncated_rejected() {
let framed = frame_plaintext(b"info", b"aad", b"pt");
let err = unframe_plaintext(&framed[..5], b"info", b"aad");
assert!(matches!(err, Err(CryptError::InvalidEnvelope)));
}
#[test]
fn test_frame_bad_magic_rejected() {
let mut framed = frame_plaintext(b"info", b"aad", b"pt");
framed[0] ^= 0xFF; let err = unframe_plaintext(&framed, b"info", b"aad");
assert!(matches!(err, Err(CryptError::InvalidEnvelope)));
}
#[test]
fn test_hpke_seal_open_roundtrip() {
let (pk, sk) = keygen();
let info = b"app:crypt_guard:v2";
let aad = b"session-id=deadbeef";
let plaintext = b"hello, post-quantum world";
let envelope = seal::<MlKem768, XChaCha20Poly1305>(&pk, info, aad, plaintext)
.expect("seal must succeed");
let recovered = open::<MlKem768, XChaCha20Poly1305>(&sk, info, aad, &envelope)
.expect("open must succeed");
assert_eq!(
recovered, plaintext,
"recovered plaintext must equal original"
);
}
#[test]
fn test_hpke_open_wrong_info_fails() {
let (pk, sk) = keygen();
let info = b"correct-info";
let aad = b"aad";
let plaintext = b"sensitive data";
let envelope = seal::<MlKem768, XChaCha20Poly1305>(&pk, info, aad, plaintext)
.expect("seal must succeed");
let result = open::<MlKem768, XChaCha20Poly1305>(&sk, b"wrong-info", aad, &envelope);
assert!(
result.is_err(),
"open with wrong info must fail, but got Ok({:?})",
result.ok()
);
}
#[test]
fn test_hpke_open_wrong_aad_fails() {
let (pk, sk) = keygen();
let info = b"info";
let aad = b"correct-aad";
let plaintext = b"sensitive data";
let envelope = seal::<MlKem768, XChaCha20Poly1305>(&pk, info, aad, plaintext)
.expect("seal must succeed");
let result = open::<MlKem768, XChaCha20Poly1305>(&sk, info, b"wrong-aad", &envelope);
assert!(
result.is_err(),
"open with wrong aad must fail, but got Ok({:?})",
result.ok()
);
}
#[test]
fn test_hpke_open_wrong_key_fails() {
let (pk, _sk_correct) = keygen();
let (_pk2, sk_wrong) = keygen();
let info = b"info";
let aad = b"aad";
let plaintext = b"sensitive data";
let envelope = seal::<MlKem768, XChaCha20Poly1305>(&pk, info, aad, plaintext)
.expect("seal must succeed");
let result = open::<MlKem768, XChaCha20Poly1305>(&sk_wrong, info, aad, &envelope);
assert!(
result.is_err(),
"open with wrong secret key must fail, but got Ok({:?})",
result.ok()
);
}
#[test]
fn test_hpke_empty_info_and_aad() {
let (pk, sk) = keygen();
let envelope = seal::<MlKem768, XChaCha20Poly1305>(&pk, b"", b"", b"payload")
.expect("seal with empty info/aad must succeed");
let recovered = open::<MlKem768, XChaCha20Poly1305>(&sk, b"", b"", &envelope)
.expect("open with matching empty info/aad must succeed");
assert_eq!(recovered, b"payload");
}
#[test]
fn test_hpke_empty_pk_error() {
let err = seal::<MlKem768, XChaCha20Poly1305>(b"", b"info", b"aad", b"pt");
assert!(matches!(err, Err(CryptError::MissingPublicKey)));
}
#[test]
fn test_hpke_empty_sk_error() {
let (pk, _sk) = keygen();
let envelope =
seal::<MlKem768, XChaCha20Poly1305>(&pk, b"i", b"a", b"pt").expect("seal must succeed");
let err = open::<MlKem768, XChaCha20Poly1305>(b"", b"i", b"a", &envelope);
assert!(matches!(err, Err(CryptError::MissingSecretKey)));
}
}