use crate::encryption::EncryptionError;
use sha2::{Digest, Sha256};
const CEK_VERSION_BYTE: u8 = 0x01;
#[derive(Debug)]
pub struct CekEnvelope<'a> {
pub ciphertext: &'a [u8],
pub signature: &'a [u8],
pub signed_portion: &'a [u8],
}
impl CekEnvelope<'_> {
pub fn signed_digest(&self) -> [u8; 32] {
Sha256::digest(self.signed_portion).into()
}
}
pub fn parse(data: &[u8]) -> Result<CekEnvelope<'_>, EncryptionError> {
if data.len() < 5 {
return Err(EncryptionError::CekDecryptionFailed(
"Encrypted CEK too short".into(),
));
}
if data[0] != CEK_VERSION_BYTE {
return Err(EncryptionError::CekDecryptionFailed(format!(
"Invalid CEK version: expected {:#04x}, got {:#04x}",
CEK_VERSION_BYTE, data[0]
)));
}
let key_path_len = u16::from_le_bytes([data[1], data[2]]) as usize;
let ciphertext_len = u16::from_le_bytes([data[3], data[4]]) as usize;
let ciphertext_offset = 5 + key_path_len;
let signature_offset = ciphertext_offset + ciphertext_len;
if data.len() <= signature_offset {
return Err(EncryptionError::CekDecryptionFailed(format!(
"Encrypted CEK truncated: {} bytes, need key path ({key_path_len}) + \
ciphertext ({ciphertext_len}) + signature after a 5-byte header",
data.len()
)));
}
Ok(CekEnvelope {
ciphertext: &data[ciphertext_offset..signature_offset],
signature: &data[signature_offset..],
signed_portion: &data[..signature_offset],
})
}
pub fn build_signed_portion(key_path: &str, ciphertext: &[u8]) -> Vec<u8> {
let key_path_utf16: Vec<u8> = key_path
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let mut out = Vec::with_capacity(5 + key_path_utf16.len() + ciphertext.len());
out.push(CEK_VERSION_BYTE);
out.extend_from_slice(&(key_path_utf16.len() as u16).to_le_bytes());
out.extend_from_slice(&(ciphertext.len() as u16).to_le_bytes());
out.extend_from_slice(&key_path_utf16);
out.extend_from_slice(ciphertext);
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn sample_envelope() -> Vec<u8> {
let mut envelope = build_signed_portion("Test", &[0xAB; 256]);
envelope.extend_from_slice(&[0xCD; 256]); envelope
}
#[test]
fn parse_extracts_fields() {
let envelope = sample_envelope();
let parsed = parse(&envelope).unwrap();
assert_eq!(parsed.ciphertext, &[0xAB; 256][..]);
assert_eq!(parsed.signature, &[0xCD; 256][..]);
assert_eq!(parsed.signed_portion.len(), 5 + 8 + 256);
assert_eq!(parsed.signed_portion, &envelope[..envelope.len() - 256]);
}
#[test]
fn layout_puts_both_lengths_before_payloads() {
let portion = build_signed_portion("Test", &[0xAB; 256]);
assert_eq!(portion[0], 0x01);
assert_eq!(u16::from_le_bytes([portion[1], portion[2]]), 8); assert_eq!(u16::from_le_bytes([portion[3], portion[4]]), 256);
assert_eq!(&portion[5..7], &[b'T', 0x00]);
}
#[test]
fn parse_rejects_bad_version() {
let mut envelope = sample_envelope();
envelope[0] = 0x02;
let err = parse(&envelope).unwrap_err();
assert!(err.to_string().contains("Invalid CEK version"));
}
#[test]
fn parse_rejects_missing_signature() {
let portion = build_signed_portion("Test", &[0xAB; 256]);
assert!(parse(&portion).is_err()); }
#[test]
fn parse_rejects_truncation() {
assert!(parse(&[0x01, 0x00]).is_err());
let envelope = sample_envelope();
assert!(parse(&envelope[..envelope.len() - 300]).is_err());
}
#[test]
fn signed_digest_is_sha256_of_signed_portion() {
let envelope = sample_envelope();
let parsed = parse(&envelope).unwrap();
let expected: [u8; 32] = Sha256::digest(&envelope[..envelope.len() - 256]).into();
assert_eq!(parsed.signed_digest(), expected);
}
}