use oxideav_core::{Error, Result};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TencBox {
pub version: u8,
pub default_is_protected: u8,
pub default_per_sample_iv_size: u8,
pub default_kid: [u8; 16],
pub default_crypt_byte_block: u8,
pub default_skip_byte_block: u8,
pub default_constant_iv: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PsshBox {
pub version: u8,
pub system_id: [u8; 16],
pub kids: Vec<[u8; 16]>,
pub data: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SubsampleEntry {
pub bytes_of_clear_data: u16,
pub bytes_of_protected_data: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SencSample {
pub initialization_vector: Vec<u8>,
pub subsamples: Vec<SubsampleEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SencBox {
pub flags: u32,
pub samples: Vec<SencSample>,
}
impl SencBox {
pub fn uses_subsample_encryption(&self) -> bool {
(self.flags & 0x0000_0002) != 0
}
}
pub fn parse_tenc(body: &[u8]) -> Result<TencBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4 tenc: missing FullBox header"));
}
let version = body[0];
if body.len() < 24 {
return Err(Error::invalid("MP4 tenc: short payload"));
}
let (crypt_block, skip_block) = match version {
0 => {
(0u8, 0u8)
}
_ => {
let packed = body[5];
((packed >> 4) & 0x0F, packed & 0x0F)
}
};
let default_is_protected = body[6];
let default_per_sample_iv_size = body[7];
let mut default_kid = [0u8; 16];
default_kid.copy_from_slice(&body[8..24]);
let mut cursor = 24usize;
let default_constant_iv = if default_is_protected == 1 && default_per_sample_iv_size == 0 {
if body.len() < cursor + 1 {
return Err(Error::invalid(
"MP4 tenc: missing default_constant_IV_size when isProtected==1 && IV_size==0",
));
}
let civ_size = body[cursor] as usize;
cursor += 1;
if civ_size != 8 && civ_size != 16 {
return Err(Error::invalid(format!(
"MP4 tenc: default_constant_IV_size {civ_size} not in {{8, 16}}"
)));
}
if body.len() < cursor + civ_size {
return Err(Error::invalid("MP4 tenc: truncated default_constant_IV"));
}
let iv = body[cursor..cursor + civ_size].to_vec();
Some(iv)
} else {
if !(default_per_sample_iv_size == 0
|| default_per_sample_iv_size == 8
|| default_per_sample_iv_size == 16)
{
return Err(Error::invalid(format!(
"MP4 tenc: default_Per_Sample_IV_Size {default_per_sample_iv_size} not in {{0, 8, 16}}"
)));
}
None
};
Ok(TencBox {
version,
default_is_protected,
default_per_sample_iv_size,
default_kid,
default_crypt_byte_block: crypt_block,
default_skip_byte_block: skip_block,
default_constant_iv,
})
}
pub fn parse_pssh(body: &[u8]) -> Result<PsshBox> {
if body.len() < 4 + 16 {
return Err(Error::invalid("MP4 pssh: short payload"));
}
let version = body[0];
let mut system_id = [0u8; 16];
system_id.copy_from_slice(&body[4..20]);
let mut cursor = 20usize;
let mut kids: Vec<[u8; 16]> = Vec::new();
if version > 0 {
if body.len() < cursor + 4 {
return Err(Error::invalid("MP4 pssh: missing KID_count"));
}
let kid_count = u32::from_be_bytes([
body[cursor],
body[cursor + 1],
body[cursor + 2],
body[cursor + 3],
]) as usize;
cursor += 4;
let kid_bytes = kid_count
.checked_mul(16)
.ok_or_else(|| Error::invalid("MP4 pssh: KID_count overflow"))?;
if body.len() < cursor + kid_bytes {
return Err(Error::invalid("MP4 pssh: truncated KID array"));
}
kids.reserve_exact(kid_count);
for i in 0..kid_count {
let off = cursor + i * 16;
let mut k = [0u8; 16];
k.copy_from_slice(&body[off..off + 16]);
kids.push(k);
}
cursor += kid_bytes;
}
if body.len() < cursor + 4 {
return Err(Error::invalid("MP4 pssh: missing DataSize"));
}
let data_size = u32::from_be_bytes([
body[cursor],
body[cursor + 1],
body[cursor + 2],
body[cursor + 3],
]) as usize;
cursor += 4;
if body.len() < cursor + data_size {
return Err(Error::invalid("MP4 pssh: truncated Data"));
}
let data = body[cursor..cursor + data_size].to_vec();
Ok(PsshBox {
version,
system_id,
kids,
data,
})
}
pub fn parse_senc(body: &[u8], per_sample_iv_size: u8) -> Result<SencBox> {
if !(per_sample_iv_size == 0 || per_sample_iv_size == 8 || per_sample_iv_size == 16) {
return Err(Error::invalid(format!(
"MP4 senc: per_sample_iv_size {per_sample_iv_size} not in {{0, 8, 16}}"
)));
}
if body.len() < 4 + 4 {
return Err(Error::invalid("MP4 senc: short payload"));
}
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let use_subsamples = (flags & 0x0000_0002) != 0;
let sample_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let iv_size = per_sample_iv_size as usize;
let mut cursor = 8usize;
let mut samples: Vec<SencSample> = Vec::with_capacity(sample_count.min(body.len() / 8));
for _ in 0..sample_count {
if body.len() < cursor + iv_size {
return Err(Error::invalid("MP4 senc: truncated InitializationVector"));
}
let iv = body[cursor..cursor + iv_size].to_vec();
cursor += iv_size;
let mut subsamples: Vec<SubsampleEntry> = Vec::new();
if use_subsamples {
if body.len() < cursor + 2 {
return Err(Error::invalid("MP4 senc: missing subsample_count"));
}
let sub_count = u16::from_be_bytes([body[cursor], body[cursor + 1]]) as usize;
cursor += 2;
let sub_bytes = sub_count
.checked_mul(6)
.ok_or_else(|| Error::invalid("MP4 senc: subsample_count overflow"))?;
if body.len() < cursor + sub_bytes {
return Err(Error::invalid("MP4 senc: truncated subsample table"));
}
subsamples.reserve_exact(sub_count);
for _ in 0..sub_count {
let clear = u16::from_be_bytes([body[cursor], body[cursor + 1]]);
let protected = u32::from_be_bytes([
body[cursor + 2],
body[cursor + 3],
body[cursor + 4],
body[cursor + 5],
]);
subsamples.push(SubsampleEntry {
bytes_of_clear_data: clear,
bytes_of_protected_data: protected,
});
cursor += 6;
}
}
samples.push(SencSample {
initialization_vector: iv,
subsamples,
});
}
Ok(SencBox { flags, samples })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tenc_v0_per_sample_iv_16_round_trip() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]); body.push(0); body.push(0); body.push(1); body.push(16); let kid: [u8; 16] = [
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99,
];
body.extend_from_slice(&kid);
let t = parse_tenc(&body).expect("v0 parse");
assert_eq!(t.version, 0);
assert_eq!(t.default_is_protected, 1);
assert_eq!(t.default_per_sample_iv_size, 16);
assert_eq!(t.default_kid, kid);
assert_eq!(t.default_crypt_byte_block, 0);
assert_eq!(t.default_skip_byte_block, 0);
assert!(t.default_constant_iv.is_none());
}
#[test]
fn tenc_v1_pattern_with_constant_iv_8() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]); body.push(0); body.push((1 << 4) | 9); body.push(1); body.push(0); body.extend_from_slice(&[0x01; 16]); body.push(8); body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]);
let t = parse_tenc(&body).expect("v1 parse");
assert_eq!(t.version, 1);
assert_eq!(t.default_crypt_byte_block, 1);
assert_eq!(t.default_skip_byte_block, 9);
assert_eq!(t.default_per_sample_iv_size, 0);
assert_eq!(
t.default_constant_iv.as_deref(),
Some(&[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE][..])
);
}
#[test]
fn tenc_v0_iv_size_8_no_constant() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.push(0);
body.push(0);
body.push(1);
body.push(8);
body.extend_from_slice(&[0u8; 16]);
let t = parse_tenc(&body).expect("v0 parse");
assert_eq!(t.default_per_sample_iv_size, 8);
assert!(t.default_constant_iv.is_none());
}
#[test]
fn tenc_rejects_unsupported_iv_size() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.push(0);
body.push(0);
body.push(1);
body.push(4); body.extend_from_slice(&[0u8; 16]);
assert!(parse_tenc(&body).is_err());
}
#[test]
fn tenc_rejects_unsupported_constant_iv_size() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.push(0);
body.push(0);
body.push(1);
body.push(0); body.extend_from_slice(&[0u8; 16]);
body.push(4); body.extend_from_slice(&[0u8; 4]);
assert!(parse_tenc(&body).is_err());
}
#[test]
fn tenc_rejects_short_payload() {
assert!(parse_tenc(&[0u8, 0, 0]).is_err());
assert!(parse_tenc(&[0u8; 23]).is_err());
}
#[test]
fn tenc_rejects_truncated_constant_iv() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]);
body.push(0);
body.push(0);
body.push(1);
body.push(0);
body.extend_from_slice(&[0u8; 16]);
body.push(16);
body.extend_from_slice(&[0u8; 15]); assert!(parse_tenc(&body).is_err());
}
#[test]
fn pssh_v0_round_trip() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
let sysid: [u8; 16] = [
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x1E, 0x1F,
];
body.extend_from_slice(&sysid);
body.extend_from_slice(&4u32.to_be_bytes());
body.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
let p = parse_pssh(&body).expect("v0 parse");
assert_eq!(p.version, 0);
assert_eq!(p.system_id, sysid);
assert!(p.kids.is_empty());
assert_eq!(p.data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn pssh_v1_two_kids_round_trip() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]);
body.extend_from_slice(&[0x42; 16]);
body.extend_from_slice(&2u32.to_be_bytes());
body.extend_from_slice(&[0xAA; 16]);
body.extend_from_slice(&[0xBB; 16]);
body.extend_from_slice(&0u32.to_be_bytes()); let p = parse_pssh(&body).expect("v1 parse");
assert_eq!(p.version, 1);
assert_eq!(p.kids.len(), 2);
assert_eq!(p.kids[0], [0xAA; 16]);
assert_eq!(p.kids[1], [0xBB; 16]);
assert!(p.data.is_empty());
}
#[test]
fn pssh_v1_empty_kid_list_per_spec_means_apply_to_all() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]);
body.extend_from_slice(&[0u8; 16]);
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&3u32.to_be_bytes());
body.extend_from_slice(&[1, 2, 3]);
let p = parse_pssh(&body).expect("v1 empty-kid parse");
assert_eq!(p.version, 1);
assert!(p.kids.is_empty());
assert_eq!(p.data, vec![1, 2, 3]);
}
#[test]
fn pssh_rejects_kid_count_overrun() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]);
body.extend_from_slice(&[0u8; 16]);
body.extend_from_slice(&100u32.to_be_bytes()); body.extend_from_slice(&[0u8; 16]); assert!(parse_pssh(&body).is_err());
}
#[test]
fn pssh_rejects_truncated_data() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.extend_from_slice(&[0u8; 16]);
body.extend_from_slice(&8u32.to_be_bytes());
body.extend_from_slice(&[1u8; 4]); assert!(parse_pssh(&body).is_err());
}
#[test]
fn senc_v0_iv16_no_subsamples_round_trip() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.extend_from_slice(&2u32.to_be_bytes());
body.extend_from_slice(&[0x11; 16]);
body.extend_from_slice(&[0x22; 16]);
let s = parse_senc(&body, 16).expect("no-sub parse");
assert_eq!(s.flags, 0);
assert!(!s.uses_subsample_encryption());
assert_eq!(s.samples.len(), 2);
assert_eq!(s.samples[0].initialization_vector, vec![0x11; 16]);
assert_eq!(s.samples[1].initialization_vector, vec![0x22; 16]);
assert!(s.samples[0].subsamples.is_empty());
}
#[test]
fn senc_subsamples_round_trip() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x02]); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&[0x33; 8]); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&3u16.to_be_bytes());
body.extend_from_slice(&17u32.to_be_bytes());
body.extend_from_slice(&5u16.to_be_bytes());
body.extend_from_slice(&11u32.to_be_bytes());
let s = parse_senc(&body, 8).expect("sub parse");
assert!(s.uses_subsample_encryption());
assert_eq!(s.samples.len(), 1);
assert_eq!(s.samples[0].initialization_vector, vec![0x33; 8]);
assert_eq!(s.samples[0].subsamples.len(), 2);
assert_eq!(s.samples[0].subsamples[0].bytes_of_clear_data, 3);
assert_eq!(s.samples[0].subsamples[0].bytes_of_protected_data, 17);
assert_eq!(s.samples[0].subsamples[1].bytes_of_clear_data, 5);
assert_eq!(s.samples[0].subsamples[1].bytes_of_protected_data, 11);
}
#[test]
fn senc_constant_iv_scheme_iv0() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.extend_from_slice(&3u32.to_be_bytes());
let s = parse_senc(&body, 0).expect("iv0 parse");
assert_eq!(s.samples.len(), 3);
for entry in &s.samples {
assert!(entry.initialization_vector.is_empty());
}
}
#[test]
fn senc_rejects_invalid_iv_size() {
let body = vec![0u8, 0, 0, 0, 0, 0, 0, 0];
assert!(parse_senc(&body, 4).is_err());
}
#[test]
fn senc_rejects_truncated_iv() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.extend_from_slice(&2u32.to_be_bytes());
body.extend_from_slice(&[0x44; 16]);
assert!(parse_senc(&body, 16).is_err());
}
#[test]
fn senc_rejects_truncated_subsample_table() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x02]);
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&[0x55; 16]);
body.extend_from_slice(&3u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); assert!(parse_senc(&body, 16).is_err());
}
}