use crate::error::CryptError;
use crate::protocol::aad::build_aad;
use crate::protocol::header::{Header, HEADER_SIZE};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Envelope {
pub header: Header,
pub kem_ciphertext: Vec<u8>,
pub nonce: Vec<u8>,
pub ciphertext: Vec<u8>,
}
impl Envelope {
pub fn new(
header: Header,
kem_ciphertext: Vec<u8>,
nonce: Vec<u8>,
ciphertext: Vec<u8>,
) -> Self {
Self {
header,
kem_ciphertext,
nonce,
ciphertext,
}
}
pub fn build_aad(&self, metadata: &[u8]) -> Vec<u8> {
build_aad(&self.header, &self.kem_ciphertext, &self.nonce, metadata)
}
pub fn to_bytes(&self) -> Vec<u8> {
let hdr = self.header.to_bytes();
let kem_ct_len = (self.kem_ciphertext.len() as u32).to_le_bytes();
let nonce_len = (self.nonce.len() as u32).to_le_bytes();
let ct_len = (self.ciphertext.len() as u32).to_le_bytes();
let cap = HEADER_SIZE
+ 4
+ self.kem_ciphertext.len()
+ 4
+ self.nonce.len()
+ 4
+ self.ciphertext.len();
let mut out = Vec::with_capacity(cap);
out.extend_from_slice(&hdr);
out.extend_from_slice(&kem_ct_len);
out.extend_from_slice(&self.kem_ciphertext);
out.extend_from_slice(&nonce_len);
out.extend_from_slice(&self.nonce);
out.extend_from_slice(&ct_len);
out.extend_from_slice(&self.ciphertext);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptError> {
if bytes.len() < HEADER_SIZE {
return Err(CryptError::InvalidEnvelope);
}
let header = Header::from_bytes(&bytes[..HEADER_SIZE])?;
let mut pos = HEADER_SIZE;
let kem_ciphertext = read_len_prefixed(bytes, &mut pos)?;
let nonce = read_len_prefixed(bytes, &mut pos)?;
let ciphertext = read_len_prefixed(bytes, &mut pos)?;
Ok(Self {
header,
kem_ciphertext,
nonce,
ciphertext,
})
}
}
fn read_len_prefixed(bytes: &[u8], pos: &mut usize) -> Result<Vec<u8>, CryptError> {
if *pos + 4 > bytes.len() {
return Err(CryptError::InvalidEnvelope);
}
let len = u32::from_le_bytes([
bytes[*pos],
bytes[*pos + 1],
bytes[*pos + 2],
bytes[*pos + 3],
]) as usize;
*pos += 4;
if *pos + len > bytes.len() {
return Err(CryptError::InvalidEnvelope);
}
let field = bytes[*pos..*pos + len].to_vec();
*pos += len;
Ok(field)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::header::{AeadAlgId, KdfAlgId, KemAlgId};
fn make_env() -> Envelope {
let hdr = Header::new(
KemAlgId::MlKem768,
AeadAlgId::XChaCha20Poly1305,
KdfAlgId::HkdfSha256,
);
Envelope::new(hdr, vec![0u8; 32], vec![1u8; 24], vec![2u8; 64])
}
#[test]
fn test_envelope_roundtrip() {
let env = make_env();
let bytes = env.to_bytes();
let parsed = Envelope::from_bytes(&bytes).unwrap();
assert_eq!(env, parsed);
}
#[test]
fn test_envelope_short_parse_error() {
assert!(matches!(
Envelope::from_bytes(&[0u8; 5]),
Err(CryptError::InvalidEnvelope)
));
}
#[test]
fn test_tamper_kem_ct_detected_in_aad() {
let env = make_env();
let mut tampered = env.clone();
tampered.kem_ciphertext[0] ^= 0xFF;
assert_ne!(env.build_aad(b""), tampered.build_aad(b""));
}
#[test]
fn test_tamper_nonce_detected_in_aad() {
let env = make_env();
let mut tampered = env.clone();
tampered.nonce[0] ^= 0x01;
assert_ne!(env.build_aad(b""), tampered.build_aad(b""));
}
#[test]
fn test_tamper_ciphertext_roundtrip_still_parses() {
let env = make_env();
let mut bytes = env.to_bytes();
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
let parsed = Envelope::from_bytes(&bytes).unwrap();
assert_ne!(parsed.ciphertext, env.ciphertext);
}
}