use crate::error::CryptError;
use crate::protocol::aad::{build_aad, try_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 try_build_aad(&self, metadata: &[u8]) -> Result<Vec<u8>, CryptError> {
try_build_aad(&self.header, &self.kem_ciphertext, &self.nonce, metadata)
}
pub fn to_bytes(&self) -> Vec<u8> {
self.try_to_bytes().unwrap_or_else(|_| {
panic!("CGv2 envelope fields exceed the canonical wire representation")
})
}
pub fn try_to_bytes(&self) -> Result<Vec<u8>, CryptError> {
let hdr = self.header.try_to_bytes()?;
let kem_ct_len = u32::try_from(self.kem_ciphertext.len())
.map_err(|_| CryptError::InvalidEnvelope)?
.to_le_bytes();
let nonce_len = u32::try_from(self.nonce.len())
.map_err(|_| CryptError::InvalidEnvelope)?
.to_le_bytes();
let ct_len = u32::try_from(self.ciphertext.len())
.map_err(|_| CryptError::InvalidEnvelope)?
.to_le_bytes();
let cap = HEADER_SIZE
.checked_add(4)
.and_then(|capacity| capacity.checked_add(self.kem_ciphertext.len()))
.and_then(|capacity| capacity.checked_add(4))
.and_then(|capacity| capacity.checked_add(self.nonce.len()))
.and_then(|capacity| capacity.checked_add(4))
.and_then(|capacity| capacity.checked_add(self.ciphertext.len()))
.ok_or(CryptError::InvalidEnvelope)?;
let mut out = Vec::new();
out.try_reserve_exact(cap)
.map_err(|_| CryptError::InvalidEnvelope)?;
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);
Ok(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)?;
if kem_ciphertext.len() != header.kem_alg.ciphertext_len() {
return Err(CryptError::InvalidEnvelope);
}
let nonce = read_len_prefixed(bytes, &mut pos)?;
if nonce.len() != header.aead_alg.nonce_len() {
return Err(CryptError::InvalidEnvelope);
}
let ciphertext = read_len_prefixed(bytes, &mut pos)?;
if pos != bytes.len() {
return Err(CryptError::InvalidEnvelope);
}
Ok(Self {
header,
kem_ciphertext: copy_field(kem_ciphertext)?,
nonce: copy_field(nonce)?,
ciphertext: copy_field(ciphertext)?,
})
}
}
fn read_len_prefixed<'a>(bytes: &'a [u8], pos: &mut usize) -> Result<&'a [u8], CryptError> {
let length_end = pos.checked_add(4).ok_or(CryptError::InvalidEnvelope)?;
if length_end > bytes.len() {
return Err(CryptError::InvalidEnvelope);
}
let length_bytes = bytes[*pos..length_end]
.try_into()
.map_err(|_| CryptError::InvalidEnvelope)?;
let len = u32::from_le_bytes(length_bytes);
let len = usize::try_from(len).map_err(|_| CryptError::InvalidEnvelope)?;
*pos = length_end;
let field_end = pos.checked_add(len).ok_or(CryptError::InvalidEnvelope)?;
if field_end > bytes.len() {
return Err(CryptError::InvalidEnvelope);
}
let field = &bytes[*pos..field_end];
*pos = field_end;
Ok(field)
}
fn copy_field(field: &[u8]) -> Result<Vec<u8>, CryptError> {
let mut owned = Vec::new();
owned
.try_reserve_exact(field.len())
.map_err(|_| CryptError::InvalidEnvelope)?;
owned.extend_from_slice(field);
Ok(owned)
}
#[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; KemAlgId::MlKem768.ciphertext_len()],
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);
}
}