use oxideav_core::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CencScheme {
Cenc,
Cbc1,
Cens,
Cbcs,
Unknown([u8; 4]),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CipherMode {
Ctr,
Cbc,
}
impl CencScheme {
pub fn from_fourcc(fourcc: &[u8; 4]) -> CencScheme {
match fourcc {
b"cenc" => CencScheme::Cenc,
b"cbc1" => CencScheme::Cbc1,
b"cens" => CencScheme::Cens,
b"cbcs" => CencScheme::Cbcs,
other => CencScheme::Unknown(*other),
}
}
pub fn fourcc(&self) -> [u8; 4] {
match self {
CencScheme::Cenc => *b"cenc",
CencScheme::Cbc1 => *b"cbc1",
CencScheme::Cens => *b"cens",
CencScheme::Cbcs => *b"cbcs",
CencScheme::Unknown(fc) => *fc,
}
}
pub fn cipher_mode(&self) -> Option<CipherMode> {
match self {
CencScheme::Cenc | CencScheme::Cens => Some(CipherMode::Ctr),
CencScheme::Cbc1 | CencScheme::Cbcs => Some(CipherMode::Cbc),
CencScheme::Unknown(_) => None,
}
}
pub fn uses_pattern_encryption(&self) -> bool {
matches!(self, CencScheme::Cens | CencScheme::Cbcs)
}
pub fn required_tenc_version(&self) -> Option<u8> {
match self {
CencScheme::Cenc | CencScheme::Cbc1 => Some(0),
CencScheme::Cens | CencScheme::Cbcs => Some(1),
CencScheme::Unknown(_) => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CencSchemeDecision {
pub scheme: CencScheme,
pub tenc: TencBox,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IvSupply {
PerSample { size: u8 },
Constant,
None,
}
impl CencSchemeDecision {
pub fn new(scheme: CencScheme, tenc: TencBox) -> Result<CencSchemeDecision> {
if let Some(req) = scheme.required_tenc_version() {
if tenc.version != req {
return Err(Error::invalid(format!(
"CENC scheme decision: scheme {} requires tenc version {} but tenc.version = {}",
fourcc_str(&scheme.fourcc()),
req,
tenc.version
)));
}
}
if scheme.uses_pattern_encryption()
&& tenc.default_crypt_byte_block == 0
&& tenc.default_skip_byte_block == 0
{
return Err(Error::invalid(format!(
"CENC scheme decision: pattern-encryption scheme {} requires non-zero (crypt_byte_block, skip_byte_block)",
fourcc_str(&scheme.fourcc())
)));
}
Ok(CencSchemeDecision { scheme, tenc })
}
pub fn cipher_mode(&self) -> Option<CipherMode> {
self.scheme.cipher_mode()
}
pub fn uses_pattern_encryption(&self) -> bool {
self.scheme.uses_pattern_encryption()
}
pub fn iv_supply(&self) -> IvSupply {
if self.tenc.default_is_protected != 1 {
return IvSupply::None;
}
if self.tenc.default_per_sample_iv_size == 0 {
IvSupply::Constant
} else {
IvSupply::PerSample {
size: self.tenc.default_per_sample_iv_size,
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SeigEntry {
pub crypt_byte_block: u8,
pub skip_byte_block: u8,
pub is_protected: u8,
pub per_sample_iv_size: u8,
pub kid: [u8; 16],
pub constant_iv: Option<Vec<u8>>,
}
impl SeigEntry {
pub fn iv_supply(&self) -> IvSupply {
if self.is_protected != 1 {
return IvSupply::None;
}
if self.per_sample_iv_size == 0 {
IvSupply::Constant
} else {
IvSupply::PerSample {
size: self.per_sample_iv_size,
}
}
}
pub fn uses_pattern_encryption(&self) -> bool {
self.crypt_byte_block != 0 || self.skip_byte_block != 0
}
}
pub fn parse_seig(body: &[u8]) -> Result<SeigEntry> {
if body.len() < 20 {
return Err(Error::invalid("CENC seig: short payload"));
}
let packed = body[1];
let crypt_byte_block = (packed >> 4) & 0x0F;
let skip_byte_block = packed & 0x0F;
let is_protected = body[2];
let per_sample_iv_size = body[3];
let mut kid = [0u8; 16];
kid.copy_from_slice(&body[4..20]);
let mut cursor = 20usize;
let constant_iv = if is_protected == 1 && per_sample_iv_size == 0 {
if body.len() < cursor + 1 {
return Err(Error::invalid(
"CENC seig: missing 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!(
"CENC seig: constant_IV_size {civ_size} not in {{8, 16}}"
)));
}
if body.len() < cursor + civ_size {
return Err(Error::invalid("CENC seig: truncated constant_IV"));
}
Some(body[cursor..cursor + civ_size].to_vec())
} else {
if !(per_sample_iv_size == 0 || per_sample_iv_size == 8 || per_sample_iv_size == 16) {
return Err(Error::invalid(format!(
"CENC seig: Per_Sample_IV_Size {per_sample_iv_size} not in {{0, 8, 16}}"
)));
}
None
};
Ok(SeigEntry {
crypt_byte_block,
skip_byte_block,
is_protected,
per_sample_iv_size,
kid,
constant_iv,
})
}
fn fourcc_str(fc: &[u8; 4]) -> String {
let mut out = String::with_capacity(4);
for &b in fc {
if (0x20..=0x7E).contains(&b) {
out.push(b as char);
} else {
out.push('?');
}
}
out
}
#[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 })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CipherStepKind {
Clear,
Encrypted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CipherStep {
pub offset: u64,
pub len: u64,
pub kind: CipherStepKind,
pub iv_restart: bool,
}
pub fn plan_sample_cipher(
decision: &CencSchemeDecision,
subsamples: Option<&[SubsampleEntry]>,
sample_len: u64,
) -> Result<Vec<CipherStep>> {
if decision.iv_supply() == IvSupply::None {
return Err(Error::invalid(
"MP4 cenc cipher plan: track default is unprotected (IvSupply::None) — no plan",
));
}
let mode = decision.cipher_mode().ok_or_else(|| {
Error::invalid(
"MP4 cenc cipher plan: unknown scheme — caller supplies its own dialect walker",
)
})?;
let uses_pattern = decision.uses_pattern_encryption();
let crypt_blocks = decision.tenc.default_crypt_byte_block as u64;
let skip_blocks = decision.tenc.default_skip_byte_block as u64;
let mut plan: Vec<CipherStep> = Vec::new();
match subsamples {
None => {
let leave_partial_clear = match mode {
CipherMode::Cbc => true,
CipherMode::Ctr => uses_pattern, };
let whole_blocks = (sample_len / 16) * 16;
let tail = sample_len - whole_blocks;
if leave_partial_clear && tail != 0 {
if whole_blocks > 0 {
plan.push(CipherStep {
offset: 0,
len: whole_blocks,
kind: CipherStepKind::Encrypted,
iv_restart: false,
});
}
plan.push(CipherStep {
offset: whole_blocks,
len: tail,
kind: CipherStepKind::Clear,
iv_restart: false,
});
} else if sample_len > 0 {
plan.push(CipherStep {
offset: 0,
len: sample_len,
kind: CipherStepKind::Encrypted,
iv_restart: false,
});
}
return Ok(plan);
}
Some(subs) => {
let mut cursor: u64 = 0;
let cbcs_restart_per_sub = matches!(decision.scheme, CencScheme::Cbcs);
for (i, s) in subs.iter().enumerate() {
let clear = s.bytes_of_clear_data as u64;
let protected = s.bytes_of_protected_data as u64;
if clear == 0 && protected == 0 {
return Err(Error::invalid(format!(
"MP4 cenc cipher plan: subsample {i} is both-zero (§9.5.1 prohibition)"
)));
}
let row_len = clear.checked_add(protected).ok_or_else(|| {
Error::invalid(format!(
"MP4 cenc cipher plan: subsample {i} clear+protected overflow"
))
})?;
let row_end = cursor.checked_add(row_len).ok_or_else(|| {
Error::invalid("MP4 cenc cipher plan: subsample run-length overflow")
})?;
if row_end > sample_len {
return Err(Error::invalid(format!(
"MP4 cenc cipher plan: subsample {i} ends at {row_end} past sample_len {sample_len}"
)));
}
if clear > 0 {
plan.push(CipherStep {
offset: cursor,
len: clear,
kind: CipherStepKind::Clear,
iv_restart: false,
});
cursor += clear;
}
if protected > 0 {
if uses_pattern {
plan_pattern_run(
&mut plan,
cursor,
protected,
crypt_blocks,
skip_blocks,
cbcs_restart_per_sub,
);
} else {
plan.push(CipherStep {
offset: cursor,
len: protected,
kind: CipherStepKind::Encrypted,
iv_restart: false,
});
}
cursor += protected;
}
}
if cursor != sample_len {
return Err(Error::invalid(format!(
"MP4 cenc cipher plan: subsample total {cursor} != sample_len {sample_len} (§9.5.1)"
)));
}
}
}
Ok(plan)
}
fn plan_pattern_run(
plan: &mut Vec<CipherStep>,
offset: u64,
protected_len: u64,
crypt_blocks: u64,
skip_blocks: u64,
cbcs_restart_per_sub: bool,
) {
if protected_len == 0 {
return;
}
if crypt_blocks == 0 {
plan.push(CipherStep {
offset,
len: protected_len,
kind: CipherStepKind::Clear,
iv_restart: false,
});
return;
}
let crypt_bytes = crypt_blocks * 16;
let skip_bytes = skip_blocks * 16;
let pattern_bytes = crypt_bytes + skip_bytes;
let mut remaining = protected_len;
let mut cursor = offset;
let mut first_encrypted_in_sub = true;
while remaining > 0 {
let take_full = crypt_bytes.min(remaining - (remaining % 16));
if take_full > 0 {
plan.push(CipherStep {
offset: cursor,
len: take_full,
kind: CipherStepKind::Encrypted,
iv_restart: cbcs_restart_per_sub && first_encrypted_in_sub,
});
cursor += take_full;
remaining -= take_full;
first_encrypted_in_sub = false;
}
if take_full < crypt_bytes {
if remaining > 0 {
plan.push(CipherStep {
offset: cursor,
len: remaining,
kind: CipherStepKind::Clear,
iv_restart: false,
});
cursor += remaining;
}
break;
}
if skip_bytes > 0 && remaining > 0 {
let take_skip = skip_bytes.min(remaining);
plan.push(CipherStep {
offset: cursor,
len: take_skip,
kind: CipherStepKind::Clear,
iv_restart: false,
});
cursor += take_skip;
remaining -= take_skip;
}
if pattern_bytes == 0 {
break;
}
}
let _ = cursor;
}
#[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());
}
#[test]
fn scheme_fourcc_round_trip() {
for fc in [b"cenc", b"cbc1", b"cens", b"cbcs"] {
let s = CencScheme::from_fourcc(fc);
assert_eq!(&s.fourcc(), fc, "round trip {fc:?}");
}
let priv_fc = b"priv";
let s = CencScheme::from_fourcc(priv_fc);
assert!(matches!(s, CencScheme::Unknown(b) if &b == priv_fc));
assert_eq!(s.fourcc(), *priv_fc);
}
#[test]
fn scheme_cipher_mode_routes_ctr_vs_cbc() {
assert_eq!(
CencScheme::Cenc.cipher_mode(),
Some(CipherMode::Ctr),
"cenc → CTR (§10.1)"
);
assert_eq!(
CencScheme::Cens.cipher_mode(),
Some(CipherMode::Ctr),
"cens → CTR (§10.3)"
);
assert_eq!(
CencScheme::Cbc1.cipher_mode(),
Some(CipherMode::Cbc),
"cbc1 → CBC (§10.2)"
);
assert_eq!(
CencScheme::Cbcs.cipher_mode(),
Some(CipherMode::Cbc),
"cbcs → CBC (§10.4)"
);
assert_eq!(CencScheme::Unknown(*b"priv").cipher_mode(), None);
}
#[test]
fn scheme_pattern_flag() {
assert!(!CencScheme::Cenc.uses_pattern_encryption());
assert!(!CencScheme::Cbc1.uses_pattern_encryption());
assert!(CencScheme::Cens.uses_pattern_encryption());
assert!(CencScheme::Cbcs.uses_pattern_encryption());
assert!(!CencScheme::Unknown(*b"priv").uses_pattern_encryption());
}
#[test]
fn scheme_required_tenc_version() {
assert_eq!(CencScheme::Cenc.required_tenc_version(), Some(0));
assert_eq!(CencScheme::Cbc1.required_tenc_version(), Some(0));
assert_eq!(CencScheme::Cens.required_tenc_version(), Some(1));
assert_eq!(CencScheme::Cbcs.required_tenc_version(), Some(1));
assert_eq!(CencScheme::Unknown(*b"priv").required_tenc_version(), None);
}
fn cenc_tenc_v0_per_sample() -> TencBox {
TencBox {
version: 0,
default_is_protected: 1,
default_per_sample_iv_size: 16,
default_kid: [0xAA; 16],
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
}
}
fn cbcs_tenc_v1_constant_iv() -> TencBox {
TencBox {
version: 1,
default_is_protected: 1,
default_per_sample_iv_size: 0,
default_kid: [0xBB; 16],
default_crypt_byte_block: 1,
default_skip_byte_block: 9,
default_constant_iv: Some(vec![0xCA; 16]),
}
}
#[test]
fn decision_routes_cenc_to_ctr_per_sample() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample())
.expect("cenc decision");
assert_eq!(d.cipher_mode(), Some(CipherMode::Ctr));
assert!(!d.uses_pattern_encryption());
assert_eq!(d.iv_supply(), IvSupply::PerSample { size: 16 });
}
#[test]
fn decision_routes_cbcs_to_cbc_constant_iv_pattern() {
let d = CencSchemeDecision::new(CencScheme::Cbcs, cbcs_tenc_v1_constant_iv())
.expect("cbcs decision");
assert_eq!(d.cipher_mode(), Some(CipherMode::Cbc));
assert!(d.uses_pattern_encryption());
assert_eq!(d.iv_supply(), IvSupply::Constant);
assert_eq!(d.tenc.default_constant_iv.as_deref(), Some(&[0xCA; 16][..]));
}
#[test]
fn decision_rejects_version_mismatch() {
let bad = TencBox {
version: 1,
..cenc_tenc_v0_per_sample()
};
let err = CencSchemeDecision::new(CencScheme::Cenc, bad).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("requires tenc version 0"), "msg={msg}");
}
#[test]
fn decision_rejects_pattern_scheme_with_zero_pattern() {
let bad = TencBox {
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
..cbcs_tenc_v1_constant_iv()
};
let err = CencSchemeDecision::new(CencScheme::Cbcs, bad).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("pattern-encryption"), "msg={msg}");
}
#[test]
fn decision_unprotected_track_is_iv_none() {
let plain = TencBox {
version: 0,
default_is_protected: 0,
default_per_sample_iv_size: 0,
default_kid: [0u8; 16],
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
};
let d = CencSchemeDecision::new(CencScheme::Cenc, plain).expect("plaintext decision");
assert_eq!(d.iv_supply(), IvSupply::None);
}
#[test]
fn decision_unknown_scheme_routes_unconstrained() {
let d =
CencSchemeDecision::new(CencScheme::from_fourcc(b"priv"), cenc_tenc_v0_per_sample())
.expect("unknown scheme decision");
assert!(matches!(d.scheme, CencScheme::Unknown(b) if &b == b"priv"));
assert_eq!(d.cipher_mode(), None);
assert!(!d.uses_pattern_encryption());
}
#[test]
fn seig_per_sample_iv_round_trip() {
let mut body: Vec<u8> = vec![
0, 0, 1, 16, ];
let kid: [u8; 16] = [
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
0x1F, 0x20,
];
body.extend_from_slice(&kid);
let s = parse_seig(&body).expect("seig per-sample IV");
assert_eq!(s.is_protected, 1);
assert_eq!(s.per_sample_iv_size, 16);
assert_eq!(s.kid, kid);
assert_eq!(s.crypt_byte_block, 0);
assert_eq!(s.skip_byte_block, 0);
assert!(s.constant_iv.is_none());
assert_eq!(s.iv_supply(), IvSupply::PerSample { size: 16 });
assert!(!s.uses_pattern_encryption());
}
#[test]
fn seig_pattern_constant_iv_round_trip() {
let mut body: Vec<u8> = vec![
0, (1 << 4) | 9, 1, 0, ];
body.extend_from_slice(&[0x77; 16]); body.push(16); body.extend_from_slice(&[0xDD; 16]);
let s = parse_seig(&body).expect("seig pattern + constant IV");
assert_eq!(s.crypt_byte_block, 1);
assert_eq!(s.skip_byte_block, 9);
assert_eq!(s.per_sample_iv_size, 0);
assert_eq!(s.constant_iv.as_deref(), Some(&[0xDD; 16][..]));
assert_eq!(s.iv_supply(), IvSupply::Constant);
assert!(s.uses_pattern_encryption());
}
#[test]
fn seig_unprotected_group_override() {
let mut body: Vec<u8> = vec![
0, 0, 0, 0, ];
body.extend_from_slice(&[0u8; 16]); let s = parse_seig(&body).expect("seig unprotected group");
assert_eq!(s.is_protected, 0);
assert_eq!(s.iv_supply(), IvSupply::None);
}
#[test]
fn seig_ignores_trailing_bytes_per_spec_note() {
let mut body: Vec<u8> = vec![0, 0, 1, 8];
body.extend_from_slice(&[0x33; 16]);
body.extend_from_slice(&[0xFE, 0xED, 0xBE, 0xEF]);
let s = parse_seig(&body).expect("seig with trailing bytes");
assert_eq!(s.per_sample_iv_size, 8);
}
#[test]
fn seig_rejects_unsupported_iv_size() {
let mut body: Vec<u8> = vec![0, 0, 1, 4]; body.extend_from_slice(&[0u8; 16]);
assert!(parse_seig(&body).is_err());
}
#[test]
fn seig_rejects_unsupported_constant_iv_size() {
let mut body: Vec<u8> = vec![0, 0, 1, 0];
body.extend_from_slice(&[0u8; 16]);
body.push(4); body.extend_from_slice(&[0u8; 4]);
assert!(parse_seig(&body).is_err());
}
#[test]
fn seig_rejects_short_payload() {
assert!(parse_seig(&[0u8; 19]).is_err());
}
#[test]
fn seig_rejects_truncated_constant_iv() {
let mut body: Vec<u8> = vec![0, 0, 1, 0];
body.extend_from_slice(&[0u8; 16]);
body.push(16);
body.extend_from_slice(&[0u8; 15]); assert!(parse_seig(&body).is_err());
}
fn cbc1_tenc_v0_per_sample() -> TencBox {
TencBox {
version: 0,
default_is_protected: 1,
default_per_sample_iv_size: 16,
default_kid: [0x12; 16],
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
}
}
fn cens_tenc_v1_pattern_1_9() -> TencBox {
TencBox {
version: 1,
default_is_protected: 1,
default_per_sample_iv_size: 8,
default_kid: [0x44; 16],
default_crypt_byte_block: 1,
default_skip_byte_block: 9,
default_constant_iv: None,
}
}
fn sub(clear: u16, protected: u32) -> SubsampleEntry {
SubsampleEntry {
bytes_of_clear_data: clear,
bytes_of_protected_data: protected,
}
}
#[test]
fn plan_cenc_full_sample_ctr_one_encrypted_span() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let plan = plan_sample_cipher(&d, None, 137).expect("full-sample cenc");
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].offset, 0);
assert_eq!(plan[0].len, 137);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert!(!plan[0].iv_restart);
}
#[test]
fn plan_cbc1_full_sample_leaves_trailing_partial_clear() {
let d = CencSchemeDecision::new(CencScheme::Cbc1, cbc1_tenc_v0_per_sample()).unwrap();
let plan = plan_sample_cipher(&d, None, 137).expect("full-sample cbc1");
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert_eq!(plan[0].len, 128);
assert_eq!(plan[1].kind, CipherStepKind::Clear);
assert_eq!(plan[1].offset, 128);
assert_eq!(plan[1].len, 9);
}
#[test]
fn plan_cbc1_full_sample_aligned_no_clear_tail() {
let d = CencSchemeDecision::new(CencScheme::Cbc1, cbc1_tenc_v0_per_sample()).unwrap();
let plan = plan_sample_cipher(&d, None, 64).expect("aligned cbc1");
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert_eq!(plan[0].len, 64);
}
#[test]
fn plan_cenc_subsamples_clear_then_encrypted() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(5, 32), sub(3, 16)];
let plan = plan_sample_cipher(&d, Some(&subs), 56).expect("subsamples cenc");
assert_eq!(plan.len(), 4);
assert_eq!(
plan[0],
CipherStep {
offset: 0,
len: 5,
kind: CipherStepKind::Clear,
iv_restart: false
}
);
assert_eq!(
plan[1],
CipherStep {
offset: 5,
len: 32,
kind: CipherStepKind::Encrypted,
iv_restart: false
}
);
assert_eq!(
plan[2],
CipherStep {
offset: 37,
len: 3,
kind: CipherStepKind::Clear,
iv_restart: false
}
);
assert_eq!(
plan[3],
CipherStep {
offset: 40,
len: 16,
kind: CipherStepKind::Encrypted,
iv_restart: false
}
);
}
#[test]
fn plan_cbc1_subsamples_no_iv_restart() {
let d = CencSchemeDecision::new(CencScheme::Cbc1, cbc1_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(0, 32), sub(0, 16)];
let plan = plan_sample_cipher(&d, Some(&subs), 48).expect("subsamples cbc1");
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert_eq!(plan[1].kind, CipherStepKind::Encrypted);
assert!(plan.iter().all(|s| !s.iv_restart));
}
#[test]
fn plan_cbcs_subsamples_iv_restart_per_subsample() {
let d = CencSchemeDecision::new(CencScheme::Cbcs, cbcs_tenc_v1_constant_iv()).unwrap();
let subs = vec![sub(8, 160), sub(8, 160)];
let plan = plan_sample_cipher(&d, Some(&subs), 336).expect("subsamples cbcs");
assert_eq!(plan.len(), 6);
assert_eq!(plan[1].kind, CipherStepKind::Encrypted);
assert!(plan[1].iv_restart);
assert_eq!(plan[4].kind, CipherStepKind::Encrypted);
assert!(plan[4].iv_restart);
}
#[test]
fn plan_cens_subsamples_no_iv_restart_under_ctr() {
let d = CencSchemeDecision::new(CencScheme::Cens, cens_tenc_v1_pattern_1_9()).unwrap();
let subs = vec![sub(8, 160), sub(8, 160)];
let plan = plan_sample_cipher(&d, Some(&subs), 336).expect("subsamples cens");
assert!(plan.iter().all(|s| !s.iv_restart));
}
#[test]
fn plan_pattern_1_9_one_repetition_exact() {
let d = CencSchemeDecision::new(CencScheme::Cbcs, cbcs_tenc_v1_constant_iv()).unwrap();
let subs = vec![sub(0, 160)];
let plan = plan_sample_cipher(&d, Some(&subs), 160).expect("one-rep pattern");
assert_eq!(plan.len(), 2);
assert_eq!(
plan[0],
CipherStep {
offset: 0,
len: 16,
kind: CipherStepKind::Encrypted,
iv_restart: true
}
);
assert_eq!(
plan[1],
CipherStep {
offset: 16,
len: 144,
kind: CipherStepKind::Clear,
iv_restart: false
}
);
}
#[test]
fn plan_pattern_trailing_partial_encrypted_block_goes_clear() {
let pattern_tenc = TencBox {
version: 1,
default_is_protected: 1,
default_per_sample_iv_size: 16,
default_kid: [0x77; 16],
default_crypt_byte_block: 2,
default_skip_byte_block: 1,
default_constant_iv: None,
};
let d = CencSchemeDecision::new(CencScheme::Cens, pattern_tenc).unwrap();
let subs = vec![sub(0, 37)];
let plan = plan_sample_cipher(&d, Some(&subs), 37).expect("partial crypt → clear");
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert_eq!(plan[0].len, 32);
assert_eq!(plan[1].kind, CipherStepKind::Clear);
assert_eq!(plan[1].offset, 32);
assert_eq!(plan[1].len, 5);
}
#[test]
fn plan_pattern_truncated_mid_skip_run() {
let d = CencSchemeDecision::new(CencScheme::Cbcs, cbcs_tenc_v1_constant_iv()).unwrap();
let subs = vec![sub(0, 66)];
let plan = plan_sample_cipher(&d, Some(&subs), 66).expect("truncated skip run");
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert_eq!(plan[0].len, 16);
assert_eq!(plan[1].kind, CipherStepKind::Clear);
assert_eq!(plan[1].len, 50);
}
#[test]
fn plan_rejects_subsample_totals_short_of_sample_len() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(5, 32)];
let err = plan_sample_cipher(&d, Some(&subs), 100).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("subsample total") || msg.contains("!="),
"msg={msg}"
);
}
#[test]
fn plan_rejects_subsample_totals_over_sample_len() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(50, 100)];
let err = plan_sample_cipher(&d, Some(&subs), 100).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("past sample_len"), "msg={msg}");
}
#[test]
fn plan_rejects_both_zero_subsample() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(10, 10), sub(0, 0), sub(5, 5)];
let err = plan_sample_cipher(&d, Some(&subs), 30).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("both-zero"), "msg={msg}");
}
#[test]
fn plan_rejects_unprotected_track_default() {
let plain = TencBox {
version: 0,
default_is_protected: 0,
default_per_sample_iv_size: 0,
default_kid: [0u8; 16],
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
};
let d = CencSchemeDecision::new(CencScheme::Cenc, plain).unwrap();
let err = plan_sample_cipher(&d, None, 100).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("unprotected"), "msg={msg}");
}
#[test]
fn plan_rejects_unknown_scheme() {
let d =
CencSchemeDecision::new(CencScheme::from_fourcc(b"priv"), cenc_tenc_v0_per_sample())
.unwrap();
let err = plan_sample_cipher(&d, None, 100).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("unknown scheme"), "msg={msg}");
}
#[test]
fn plan_empty_sample_yields_empty_plan() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let plan = plan_sample_cipher(&d, None, 0).expect("empty sample");
assert!(plan.is_empty());
}
#[test]
fn plan_subsample_with_only_clear_emits_clear_only() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(20, 0), sub(0, 16)];
let plan = plan_sample_cipher(&d, Some(&subs), 36).expect("clear-only sub");
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].kind, CipherStepKind::Clear);
assert_eq!(plan[0].len, 20);
assert_eq!(plan[1].kind, CipherStepKind::Encrypted);
assert_eq!(plan[1].offset, 20);
assert_eq!(plan[1].len, 16);
}
#[test]
fn plan_subsample_with_only_protected_emits_encrypted_only() {
let d = CencSchemeDecision::new(CencScheme::Cenc, cenc_tenc_v0_per_sample()).unwrap();
let subs = vec![sub(0, 32)];
let plan = plan_sample_cipher(&d, Some(&subs), 32).expect("protected-only sub");
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].kind, CipherStepKind::Encrypted);
assert_eq!(plan[0].len, 32);
}
#[test]
fn plan_partitions_cover_sample_len_exactly() {
let d = CencSchemeDecision::new(CencScheme::Cbcs, cbcs_tenc_v1_constant_iv()).unwrap();
let subs = vec![sub(8, 160), sub(4, 156), sub(0, 16)];
let total: u64 = subs
.iter()
.map(|s| s.bytes_of_clear_data as u64 + s.bytes_of_protected_data as u64)
.sum();
let plan = plan_sample_cipher(&d, Some(&subs), total).expect("partition cover");
let mut prev_end = 0u64;
let mut sum = 0u64;
for step in &plan {
assert_eq!(step.offset, prev_end, "non-contiguous step");
assert!(step.len > 0, "zero-length step");
prev_end = step.offset + step.len;
sum += step.len;
}
assert_eq!(sum, total);
assert_eq!(prev_end, total);
}
}