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 samples = parse_senc_sample_table(body, 4, use_subsamples, per_sample_iv_size as usize)?;
Ok(SencBox { flags, samples })
}
fn parse_senc_sample_table(
body: &[u8],
cursor: usize,
use_subsamples: bool,
iv_size: usize,
) -> Result<Vec<SencSample>> {
if body.len() < cursor + 4 {
return Err(Error::invalid("MP4 senc: short payload"));
}
let sample_count = u32::from_be_bytes([
body[cursor],
body[cursor + 1],
body[cursor + 2],
body[cursor + 3],
]) as usize;
let mut cursor = cursor + 4;
let min_entry = iv_size + if use_subsamples { 2 } else { 0 };
let remaining = body.len().saturating_sub(cursor);
let backed = match remaining.checked_div(min_entry) {
None => sample_count <= (1 << 20),
Some(max_backed) => max_backed >= sample_count,
};
if !backed {
return Err(Error::invalid(
"MP4 senc: sample_count exceeds what the box bytes can back",
));
}
let mut samples: Vec<SencSample> = Vec::with_capacity(sample_count);
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(samples)
}
fn wrap_full_box(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = 8 + body.len() as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(fourcc);
out.extend_from_slice(body);
out
}
pub fn build_tenc_box(tenc: &TencBox) -> Result<Vec<u8>> {
if tenc.version > 1 {
return Err(Error::invalid(format!(
"MP4 tenc build: undefined version {}",
tenc.version
)));
}
if tenc.version == 0
&& (tenc.default_crypt_byte_block != 0 || tenc.default_skip_byte_block != 0)
{
return Err(Error::invalid(
"MP4 tenc build: pattern pair requires version 1",
));
}
if tenc.default_crypt_byte_block > 0x0F || tenc.default_skip_byte_block > 0x0F {
return Err(Error::invalid(
"MP4 tenc build: pattern component exceeds its 4-bit field",
));
}
if !matches!(tenc.default_per_sample_iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"MP4 tenc build: default_Per_Sample_IV_Size {} not in {{0, 8, 16}}",
tenc.default_per_sample_iv_size
)));
}
let needs_constant_iv = tenc.default_is_protected == 1 && tenc.default_per_sample_iv_size == 0;
match (&tenc.default_constant_iv, needs_constant_iv) {
(Some(iv), true) => {
if iv.len() != 8 && iv.len() != 16 {
return Err(Error::invalid(format!(
"MP4 tenc build: default_constant_IV length {} not in {{8, 16}}",
iv.len()
)));
}
}
(None, true) => {
return Err(Error::invalid(
"MP4 tenc build: isProtected==1 && IV_size==0 requires a constant IV",
));
}
(Some(_), false) => {
return Err(Error::invalid(
"MP4 tenc build: constant IV present but not required (would not round-trip)",
));
}
(None, false) => {}
}
let mut body = Vec::with_capacity(24 + 17);
body.extend_from_slice(&[tenc.version, 0, 0, 0]); body.push(0); if tenc.version == 0 {
body.push(0); } else {
body.push((tenc.default_crypt_byte_block << 4) | tenc.default_skip_byte_block);
}
body.push(tenc.default_is_protected);
body.push(tenc.default_per_sample_iv_size);
body.extend_from_slice(&tenc.default_kid);
if let Some(iv) = &tenc.default_constant_iv {
body.push(iv.len() as u8);
body.extend_from_slice(iv);
}
Ok(wrap_full_box(b"tenc", &body))
}
pub fn build_pssh_box(pssh: &PsshBox) -> Result<Vec<u8>> {
if pssh.version == 0 && !pssh.kids.is_empty() {
return Err(Error::invalid(
"MP4 pssh build: KID list requires version >= 1",
));
}
let kid_count = u32::try_from(pssh.kids.len())
.map_err(|_| Error::invalid("MP4 pssh build: KID_count exceeds u32"))?;
let data_size = u32::try_from(pssh.data.len())
.map_err(|_| Error::invalid("MP4 pssh build: DataSize exceeds u32"))?;
let mut body = Vec::with_capacity(4 + 16 + 4 + pssh.kids.len() * 16 + 4 + pssh.data.len());
body.extend_from_slice(&[pssh.version, 0, 0, 0]);
body.extend_from_slice(&pssh.system_id);
if pssh.version > 0 {
body.extend_from_slice(&kid_count.to_be_bytes());
for kid in &pssh.kids {
body.extend_from_slice(kid);
}
}
body.extend_from_slice(&data_size.to_be_bytes());
body.extend_from_slice(&pssh.data);
Ok(wrap_full_box(b"pssh", &body))
}
pub fn build_senc_box(senc: &SencBox) -> Result<Vec<u8>> {
if senc.flags > 0x00FF_FFFF {
return Err(Error::invalid(
"MP4 senc build: flags exceed the 24-bit field",
));
}
let iv_size = senc
.samples
.first()
.map(|s| s.initialization_vector.len())
.unwrap_or(0);
if !matches!(iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"MP4 senc build: per-sample IV length {iv_size} not in {{0, 8, 16}}"
)));
}
let use_subsamples = senc.uses_subsample_encryption();
let mut body = Vec::with_capacity(8 + senc.samples.len() * (iv_size + 8));
body.push(0); body.extend_from_slice(&senc.flags.to_be_bytes()[1..4]);
build_senc_sample_table(&mut body, &senc.samples, iv_size, use_subsamples)?;
Ok(wrap_full_box(b"senc", &body))
}
fn build_senc_sample_table(
body: &mut Vec<u8>,
samples: &[SencSample],
iv_size: usize,
use_subsamples: bool,
) -> Result<()> {
let sample_count = u32::try_from(samples.len())
.map_err(|_| Error::invalid("MP4 senc build: sample_count exceeds u32"))?;
body.extend_from_slice(&sample_count.to_be_bytes());
for (i, sample) in samples.iter().enumerate() {
if sample.initialization_vector.len() != iv_size {
return Err(Error::invalid(format!(
"MP4 senc build: sample {i} IV length {} differs from the shared width {iv_size}",
sample.initialization_vector.len()
)));
}
body.extend_from_slice(&sample.initialization_vector);
if use_subsamples {
let sub_count = u16::try_from(sample.subsamples.len()).map_err(|_| {
Error::invalid(format!(
"MP4 senc build: sample {i} subsample_count exceeds u16"
))
})?;
body.extend_from_slice(&sub_count.to_be_bytes());
for sub in &sample.subsamples {
body.extend_from_slice(&sub.bytes_of_clear_data.to_be_bytes());
body.extend_from_slice(&sub.bytes_of_protected_data.to_be_bytes());
}
} else if !sample.subsamples.is_empty() {
return Err(Error::invalid(format!(
"MP4 senc build: sample {i} carries subsamples but UseSubSampleEncryption is clear"
)));
}
}
Ok(())
}
pub fn build_seig_entry(entry: &SeigEntry) -> Result<Vec<u8>> {
if entry.crypt_byte_block > 0x0F || entry.skip_byte_block > 0x0F {
return Err(Error::invalid(
"CENC seig build: pattern component exceeds its 4-bit field",
));
}
if !matches!(entry.per_sample_iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"CENC seig build: Per_Sample_IV_Size {} not in {{0, 8, 16}}",
entry.per_sample_iv_size
)));
}
let needs_constant_iv = entry.is_protected == 1 && entry.per_sample_iv_size == 0;
match (&entry.constant_iv, needs_constant_iv) {
(Some(iv), true) => {
if iv.len() != 8 && iv.len() != 16 {
return Err(Error::invalid(format!(
"CENC seig build: constant_IV length {} not in {{8, 16}}",
iv.len()
)));
}
}
(None, true) => {
return Err(Error::invalid(
"CENC seig build: isProtected==1 && IV_size==0 requires a constant IV",
));
}
(Some(_), false) => {
return Err(Error::invalid(
"CENC seig build: constant IV present but not required (would not round-trip)",
));
}
(None, false) => {}
}
let mut out = Vec::with_capacity(20 + 17);
out.push(0); out.push((entry.crypt_byte_block << 4) | entry.skip_byte_block);
out.push(entry.is_protected);
out.push(entry.per_sample_iv_size);
out.extend_from_slice(&entry.kid);
if let Some(iv) = &entry.constant_iv {
out.push(iv.len() as u8);
out.extend_from_slice(iv);
}
Ok(out)
}
pub fn build_sinf_box(
original_format: [u8; 4],
scheme_type: [u8; 4],
scheme_version: u32,
tenc: &TencBox,
) -> Result<Vec<u8>> {
CencSchemeDecision::new(CencScheme::from_fourcc(&scheme_type), tenc.clone())?;
let tenc_bytes = build_tenc_box(tenc)?;
let frma = wrap_full_box(b"frma", &original_format);
let mut schm_body = Vec::with_capacity(12);
schm_body.extend_from_slice(&[0, 0, 0, 0]); schm_body.extend_from_slice(&scheme_type);
schm_body.extend_from_slice(&scheme_version.to_be_bytes());
let schm = wrap_full_box(b"schm", &schm_body);
let schi = wrap_full_box(b"schi", &tenc_bytes);
let mut sinf_body = Vec::with_capacity(frma.len() + schm.len() + schi.len());
sinf_body.extend_from_slice(&frma);
sinf_body.extend_from_slice(&schm);
sinf_body.extend_from_slice(&schi);
Ok(wrap_full_box(b"sinf", &sinf_body))
}
pub const PIFF_SENC_USERTYPE: [u8; 16] = [
0xA2, 0x39, 0x4F, 0x52, 0x5A, 0x9B, 0x4F, 0x14, 0xA2, 0x44, 0x6C, 0x42, 0x7C, 0x64, 0x8D, 0xF4,
];
pub const PIFF_TENC_USERTYPE: [u8; 16] = [
0x89, 0x74, 0xDB, 0xCE, 0x7B, 0xE7, 0x4C, 0x51, 0x84, 0xF9, 0x71, 0x48, 0xF9, 0x88, 0x25, 0x54,
];
pub const PIFF_PSSH_USERTYPE: [u8; 16] = [
0xD0, 0x8A, 0x4F, 0x18, 0x10, 0xF3, 0x4A, 0x82, 0xB6, 0xC8, 0x32, 0xD8, 0xAB, 0xA1, 0x83, 0xD3,
];
pub const PIFF_ALGORITHM_NONE: u32 = 0x0;
pub const PIFF_ALGORITHM_AES_CTR: u32 = 0x1;
pub const PIFF_ALGORITHM_AES_CBC: u32 = 0x2;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PiffTencBox {
pub algorithm_id: u32,
pub iv_size: u8,
pub kid: [u8; 16],
}
impl PiffTencBox {
pub fn scheme(&self) -> Option<CencScheme> {
match self.algorithm_id {
PIFF_ALGORITHM_AES_CTR => Some(CencScheme::Cenc),
PIFF_ALGORITHM_AES_CBC => Some(CencScheme::Cbc1),
_ => None,
}
}
pub fn cipher_mode(&self) -> Option<CipherMode> {
self.scheme().and_then(|s| s.cipher_mode())
}
pub fn to_tenc(&self) -> TencBox {
TencBox {
version: 0,
default_is_protected: u8::from(self.algorithm_id != PIFF_ALGORITHM_NONE),
default_per_sample_iv_size: self.iv_size,
default_kid: self.kid,
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
}
}
pub fn scheme_decision(&self) -> Result<CencSchemeDecision> {
let scheme = self.scheme().ok_or_else(|| {
Error::invalid(format!(
"PIFF tenc: AlgorithmID {:#x} names no cipher",
self.algorithm_id
))
})?;
CencSchemeDecision::new(scheme, self.to_tenc())
}
}
pub fn parse_piff_tenc(body: &[u8]) -> Result<PiffTencBox> {
if body.len() < 4 {
return Err(Error::invalid("PIFF tenc: missing FullBox header"));
}
if body[0] != 0 {
return Err(Error::invalid(format!(
"PIFF tenc: undefined version {}",
body[0]
)));
}
if body.len() < 4 + 20 {
return Err(Error::invalid("PIFF tenc: short payload"));
}
let algorithm_id = u32::from_be_bytes([0, body[4], body[5], body[6]]);
let iv_size = body[7];
if !matches!(iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"PIFF tenc: default_IV_size {iv_size} not in {{0, 8, 16}}"
)));
}
let mut kid = [0u8; 16];
kid.copy_from_slice(&body[8..24]);
Ok(PiffTencBox {
algorithm_id,
iv_size,
kid,
})
}
pub fn build_piff_tenc_box(tenc: &PiffTencBox) -> Result<Vec<u8>> {
if tenc.algorithm_id > 0x00FF_FFFF {
return Err(Error::invalid(
"PIFF tenc build: AlgorithmID exceeds its 24-bit field",
));
}
if !matches!(tenc.iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"PIFF tenc build: default_IV_size {} not in {{0, 8, 16}}",
tenc.iv_size
)));
}
let mut body = Vec::with_capacity(24);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&tenc.algorithm_id.to_be_bytes()[1..4]);
body.push(tenc.iv_size);
body.extend_from_slice(&tenc.kid);
Ok(wrap_uuid_box(&PIFF_TENC_USERTYPE, &body))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PiffSencOverride {
pub algorithm_id: u32,
pub iv_size: u8,
pub kid: [u8; 16],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PiffSencBox {
pub flags: u32,
pub override_params: Option<PiffSencOverride>,
pub samples: Vec<SencSample>,
}
impl PiffSencBox {
pub fn uses_subsample_encryption(&self) -> bool {
(self.flags & 0x0000_0002) != 0
}
pub fn has_override(&self) -> bool {
(self.flags & 0x0000_0001) != 0
}
pub fn to_senc(&self) -> SencBox {
SencBox {
flags: self.flags & 0x0000_0002,
samples: self.samples.clone(),
}
}
}
pub fn parse_piff_senc(body: &[u8], default_iv_size: u8) -> Result<PiffSencBox> {
if body.len() < 4 {
return Err(Error::invalid("PIFF senc: missing FullBox header"));
}
if body[0] != 0 {
return Err(Error::invalid(format!(
"PIFF senc: undefined version {}",
body[0]
)));
}
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let use_subsamples = (flags & 0x0000_0002) != 0;
let mut cursor = 4usize;
let override_params = if (flags & 0x0000_0001) != 0 {
if body.len() < cursor + 20 {
return Err(Error::invalid("PIFF senc: truncated override triple"));
}
let algorithm_id =
u32::from_be_bytes([0, body[cursor], body[cursor + 1], body[cursor + 2]]);
let iv_size = body[cursor + 3];
let mut kid = [0u8; 16];
kid.copy_from_slice(&body[cursor + 4..cursor + 20]);
cursor += 20;
Some(PiffSencOverride {
algorithm_id,
iv_size,
kid,
})
} else {
None
};
let iv_size = override_params
.as_ref()
.map(|o| o.iv_size)
.unwrap_or(default_iv_size);
if !matches!(iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"PIFF senc: IV_size {iv_size} not in {{0, 8, 16}}"
)));
}
let samples = parse_senc_sample_table(body, cursor, use_subsamples, iv_size as usize)?;
Ok(PiffSencBox {
flags,
override_params,
samples,
})
}
pub fn build_piff_senc_box(senc: &PiffSencBox) -> Result<Vec<u8>> {
if senc.flags > 0x00FF_FFFF {
return Err(Error::invalid(
"PIFF senc build: flags exceed the 24-bit field",
));
}
if senc.has_override() != senc.override_params.is_some() {
return Err(Error::invalid(
"PIFF senc build: flags & 1 disagrees with the override triple's presence",
));
}
let iv_size = match &senc.override_params {
Some(o) => {
if o.algorithm_id > 0x00FF_FFFF {
return Err(Error::invalid(
"PIFF senc build: override AlgorithmID exceeds its 24-bit field",
));
}
o.iv_size as usize
}
None => senc
.samples
.first()
.map(|s| s.initialization_vector.len())
.unwrap_or(0),
};
if !matches!(iv_size, 0 | 8 | 16) {
return Err(Error::invalid(format!(
"PIFF senc build: per-sample IV length {iv_size} not in {{0, 8, 16}}"
)));
}
let mut body = Vec::with_capacity(4 + 20 + 4 + senc.samples.len() * (iv_size + 8));
body.push(0); body.extend_from_slice(&senc.flags.to_be_bytes()[1..4]);
if let Some(o) = &senc.override_params {
body.extend_from_slice(&o.algorithm_id.to_be_bytes()[1..4]);
body.push(o.iv_size);
body.extend_from_slice(&o.kid);
}
build_senc_sample_table(
&mut body,
&senc.samples,
iv_size,
senc.uses_subsample_encryption(),
)?;
Ok(wrap_uuid_box(&PIFF_SENC_USERTYPE, &body))
}
pub fn parse_piff_pssh(body: &[u8]) -> Result<PsshBox> {
if !body.is_empty() && body[0] != 0 {
return Err(Error::invalid(format!(
"PIFF pssh: undefined version {}",
body[0]
)));
}
parse_pssh(body)
}
pub fn build_piff_pssh_box(pssh: &PsshBox) -> Result<Vec<u8>> {
if pssh.version != 0 {
return Err(Error::invalid(
"PIFF pssh build: only the version-0 pssh layout exists in PIFF",
));
}
if !pssh.kids.is_empty() {
return Err(Error::invalid(
"PIFF pssh build: KID list requires the CENC v1 pssh, not the PIFF uuid form",
));
}
let data_size = u32::try_from(pssh.data.len())
.map_err(|_| Error::invalid("PIFF pssh build: DataSize exceeds u32"))?;
let mut body = Vec::with_capacity(4 + 16 + 4 + pssh.data.len());
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&pssh.system_id);
body.extend_from_slice(&data_size.to_be_bytes());
body.extend_from_slice(&pssh.data);
Ok(wrap_uuid_box(&PIFF_PSSH_USERTYPE, &body))
}
fn wrap_uuid_box(usertype: &[u8; 16], body: &[u8]) -> Vec<u8> {
let total = 8 + 16 + body.len() as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(b"uuid");
out.extend_from_slice(usertype);
out.extend_from_slice(body);
out
}
#[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::*;
fn box_body(bytes: &[u8], fourcc: &[u8; 4]) -> Vec<u8> {
assert_eq!(
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize,
bytes.len(),
"box size field must match total length"
);
assert_eq!(&bytes[4..8], fourcc);
bytes[8..].to_vec()
}
#[test]
fn build_tenc_v0_round_trips() {
let rec = TencBox {
version: 0,
default_is_protected: 1,
default_per_sample_iv_size: 16,
default_kid: [0x42; 16],
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
};
let bytes = build_tenc_box(&rec).unwrap();
assert_eq!(parse_tenc(&box_body(&bytes, b"tenc")).unwrap(), rec);
}
#[test]
fn build_tenc_v1_pattern_constant_iv_round_trips() {
let rec = TencBox {
version: 1,
default_is_protected: 1,
default_per_sample_iv_size: 0,
default_kid: [0x0F; 16],
default_crypt_byte_block: 1,
default_skip_byte_block: 9,
default_constant_iv: Some(vec![0xCC; 16]),
};
let bytes = build_tenc_box(&rec).unwrap();
assert_eq!(parse_tenc(&box_body(&bytes, b"tenc")).unwrap(), rec);
}
#[test]
fn build_tenc_rejects_non_round_trippable_records() {
let base = TencBox {
version: 0,
default_is_protected: 1,
default_per_sample_iv_size: 8,
default_kid: [0; 16],
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_constant_iv: None,
};
let mut r = base.clone();
r.default_crypt_byte_block = 1;
assert!(build_tenc_box(&r).is_err());
let mut r = base.clone();
r.version = 2;
assert!(build_tenc_box(&r).is_err());
let mut r = base.clone();
r.default_per_sample_iv_size = 4;
assert!(build_tenc_box(&r).is_err());
let mut r = base.clone();
r.default_per_sample_iv_size = 0;
assert!(build_tenc_box(&r).is_err());
let mut r = base.clone();
r.default_constant_iv = Some(vec![0xAA; 8]);
assert!(build_tenc_box(&r).is_err());
let mut r = base;
r.default_per_sample_iv_size = 0;
r.default_constant_iv = Some(vec![0xAA; 12]);
assert!(build_tenc_box(&r).is_err());
}
#[test]
fn build_seig_per_sample_iv_round_trips() {
let rec = SeigEntry {
crypt_byte_block: 0,
skip_byte_block: 0,
is_protected: 1,
per_sample_iv_size: 8,
kid: [0x7E; 16],
constant_iv: None,
};
let bytes = build_seig_entry(&rec).unwrap();
assert_eq!(bytes.len(), 20, "fixed-prefix-only entry is 20 bytes");
assert_eq!(parse_seig(&bytes).unwrap(), rec);
}
#[test]
fn build_seig_pattern_constant_iv_round_trips() {
let rec = SeigEntry {
crypt_byte_block: 1,
skip_byte_block: 9,
is_protected: 1,
per_sample_iv_size: 0,
kid: [0xC4; 16],
constant_iv: Some(vec![0x5D; 16]),
};
let bytes = build_seig_entry(&rec).unwrap();
assert_eq!(bytes[1], 0x19, "crypt:4|skip:4 packing");
assert_eq!(parse_seig(&bytes).unwrap(), rec);
}
#[test]
fn build_seig_unprotected_group_round_trips() {
let rec = SeigEntry {
crypt_byte_block: 0,
skip_byte_block: 0,
is_protected: 0,
per_sample_iv_size: 0,
kid: [0u8; 16],
constant_iv: None,
};
let bytes = build_seig_entry(&rec).unwrap();
assert_eq!(parse_seig(&bytes).unwrap(), rec);
}
#[test]
fn build_seig_rejects_non_round_trippable_records() {
let base = SeigEntry {
crypt_byte_block: 0,
skip_byte_block: 0,
is_protected: 1,
per_sample_iv_size: 8,
kid: [0x11; 16],
constant_iv: None,
};
let mut r = base.clone();
r.crypt_byte_block = 16;
assert!(build_seig_entry(&r).is_err());
let mut r = base.clone();
r.per_sample_iv_size = 4;
assert!(build_seig_entry(&r).is_err());
let mut r = base.clone();
r.per_sample_iv_size = 0;
assert!(build_seig_entry(&r).is_err());
let mut r = base.clone();
r.constant_iv = Some(vec![0xAA; 8]);
assert!(build_seig_entry(&r).is_err());
let mut r = base;
r.per_sample_iv_size = 0;
r.constant_iv = Some(vec![0xAA; 12]);
assert!(build_seig_entry(&r).is_err());
}
#[test]
fn build_pssh_v0_and_v1_round_trip() {
let v0 = PsshBox {
version: 0,
system_id: [0x11; 16],
kids: Vec::new(),
data: vec![1, 2, 3, 4, 5],
};
let bytes = build_pssh_box(&v0).unwrap();
assert_eq!(parse_pssh(&box_body(&bytes, b"pssh")).unwrap(), v0);
let v1 = PsshBox {
version: 1,
system_id: [0x22; 16],
kids: vec![[0xA0; 16], [0xB1; 16]],
data: Vec::new(),
};
let bytes = build_pssh_box(&v1).unwrap();
assert_eq!(parse_pssh(&box_body(&bytes, b"pssh")).unwrap(), v1);
}
#[test]
fn build_pssh_rejects_v0_with_kids() {
let rec = PsshBox {
version: 0,
system_id: [0; 16],
kids: vec![[1; 16]],
data: Vec::new(),
};
assert!(build_pssh_box(&rec).is_err());
}
#[test]
fn build_senc_plain_iv_round_trips() {
let rec = SencBox {
flags: 0,
samples: vec![
SencSample {
initialization_vector: vec![1; 8],
subsamples: Vec::new(),
},
SencSample {
initialization_vector: vec![2; 8],
subsamples: Vec::new(),
},
],
};
let bytes = build_senc_box(&rec).unwrap();
assert_eq!(parse_senc(&box_body(&bytes, b"senc"), 8).unwrap(), rec);
}
#[test]
fn build_senc_subsample_round_trips() {
let rec = SencBox {
flags: 0x0000_0002,
samples: vec![SencSample {
initialization_vector: vec![7; 16],
subsamples: vec![
SubsampleEntry {
bytes_of_clear_data: 13,
bytes_of_protected_data: 96,
},
SubsampleEntry {
bytes_of_clear_data: 4,
bytes_of_protected_data: 32,
},
],
}],
};
let bytes = build_senc_box(&rec).unwrap();
assert_eq!(parse_senc(&box_body(&bytes, b"senc"), 16).unwrap(), rec);
}
#[test]
fn build_senc_constant_iv_scheme_round_trips() {
let rec = SencBox {
flags: 0x0000_0002,
samples: vec![SencSample {
initialization_vector: Vec::new(),
subsamples: vec![SubsampleEntry {
bytes_of_clear_data: 5,
bytes_of_protected_data: 160,
}],
}],
};
let bytes = build_senc_box(&rec).unwrap();
assert_eq!(parse_senc(&box_body(&bytes, b"senc"), 0).unwrap(), rec);
}
#[test]
fn build_senc_rejects_inconsistent_records() {
let rec = SencBox {
flags: 0,
samples: vec![
SencSample {
initialization_vector: vec![1; 8],
subsamples: Vec::new(),
},
SencSample {
initialization_vector: vec![2; 16],
subsamples: Vec::new(),
},
],
};
assert!(build_senc_box(&rec).is_err());
let rec = SencBox {
flags: 0,
samples: vec![SencSample {
initialization_vector: vec![1; 8],
subsamples: vec![SubsampleEntry {
bytes_of_clear_data: 1,
bytes_of_protected_data: 2,
}],
}],
};
assert!(build_senc_box(&rec).is_err());
let rec = SencBox {
flags: 0x0100_0000,
samples: Vec::new(),
};
assert!(build_senc_box(&rec).is_err());
let rec = SencBox {
flags: 0,
samples: vec![SencSample {
initialization_vector: vec![1; 4],
subsamples: Vec::new(),
}],
};
assert!(build_senc_box(&rec).is_err());
}
#[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);
}
fn piff_tenc_ctr() -> PiffTencBox {
PiffTencBox {
algorithm_id: PIFF_ALGORITHM_AES_CTR,
iv_size: 8,
kid: [0x11; 16],
}
}
#[test]
fn piff_tenc_round_trip() {
let t = piff_tenc_ctr();
let bytes = build_piff_tenc_box(&t).unwrap();
assert_eq!(bytes.len(), 8 + 16 + 4 + 20);
assert_eq!(&bytes[4..8], b"uuid");
assert_eq!(&bytes[8..24], &PIFF_TENC_USERTYPE);
let back = parse_piff_tenc(&bytes[24..]).unwrap();
assert_eq!(back, t);
}
#[test]
fn piff_tenc_scheme_bridges() {
let ctr = piff_tenc_ctr();
assert_eq!(ctr.scheme(), Some(CencScheme::Cenc));
assert_eq!(ctr.cipher_mode(), Some(CipherMode::Ctr));
let tenc = ctr.to_tenc();
assert_eq!(tenc.version, 0);
assert_eq!(tenc.default_is_protected, 1);
assert_eq!(tenc.default_per_sample_iv_size, 8);
assert_eq!(tenc.default_kid, [0x11; 16]);
assert_eq!(tenc.default_constant_iv, None);
let decision = ctr.scheme_decision().expect("CTR routes to cenc");
assert_eq!(decision.cipher_mode(), Some(CipherMode::Ctr));
let cbc = PiffTencBox {
algorithm_id: PIFF_ALGORITHM_AES_CBC,
iv_size: 16,
kid: [0x22; 16],
};
assert_eq!(cbc.scheme(), Some(CencScheme::Cbc1));
assert_eq!(cbc.cipher_mode(), Some(CipherMode::Cbc));
let none = PiffTencBox {
algorithm_id: PIFF_ALGORITHM_NONE,
iv_size: 0,
kid: [0; 16],
};
assert_eq!(none.scheme(), None);
assert!(none.scheme_decision().is_err());
assert_eq!(none.to_tenc().default_is_protected, 0);
}
#[test]
fn piff_tenc_hostile_inputs() {
assert!(parse_piff_tenc(&[0, 0]).is_err());
assert!(parse_piff_tenc(&[1, 0, 0, 0, 0, 0, 1, 8]).is_err());
assert!(parse_piff_tenc(&[0, 0, 0, 0]).is_err());
let mut body = vec![0u8; 24];
body[6] = 1; body[7] = 9;
let err = parse_piff_tenc(&body).expect_err("IV size 9 must fail");
assert!(format!("{err}").contains("IV_size"), "{err}");
let bad = PiffTencBox {
algorithm_id: 0x0100_0000,
iv_size: 8,
kid: [0; 16],
};
assert!(build_piff_tenc_box(&bad).is_err());
}
fn piff_senc_with_override() -> PiffSencBox {
PiffSencBox {
flags: 0x0000_0003, override_params: Some(PiffSencOverride {
algorithm_id: PIFF_ALGORITHM_AES_CTR,
iv_size: 16,
kid: [0xAB; 16],
}),
samples: vec![
SencSample {
initialization_vector: vec![0x01; 16],
subsamples: vec![SubsampleEntry {
bytes_of_clear_data: 9,
bytes_of_protected_data: 480,
}],
},
SencSample {
initialization_vector: vec![0x02; 16],
subsamples: vec![
SubsampleEntry {
bytes_of_clear_data: 5,
bytes_of_protected_data: 128,
},
SubsampleEntry {
bytes_of_clear_data: 0,
bytes_of_protected_data: 64,
},
],
},
],
}
}
#[test]
fn piff_senc_override_round_trip() {
let s = piff_senc_with_override();
let bytes = build_piff_senc_box(&s).unwrap();
assert_eq!(&bytes[4..8], b"uuid");
assert_eq!(&bytes[8..24], &PIFF_SENC_USERTYPE);
let back = parse_piff_senc(&bytes[24..], 8).unwrap();
assert_eq!(back, s);
assert!(back.has_override());
assert!(back.uses_subsample_encryption());
}
#[test]
fn piff_senc_default_iv_round_trip() {
let s = PiffSencBox {
flags: 0,
override_params: None,
samples: vec![
SencSample {
initialization_vector: vec![0x0A; 8],
subsamples: Vec::new(),
},
SencSample {
initialization_vector: vec![0x0B; 8],
subsamples: Vec::new(),
},
],
};
let bytes = build_piff_senc_box(&s).unwrap();
let back = parse_piff_senc(&bytes[24..], 8).unwrap();
assert_eq!(back, s);
assert!(parse_piff_senc(&bytes[24..], 16).is_err());
}
#[test]
fn piff_senc_to_senc_masks_override_bit() {
let s = piff_senc_with_override();
let senc = s.to_senc();
assert_eq!(senc.flags, 0x0000_0002, "only the §7.2 bit survives");
assert!(senc.uses_subsample_encryption());
assert_eq!(senc.samples, s.samples);
let senc_bytes = build_senc_box(&senc).unwrap();
let reparsed = parse_senc(&senc_bytes[8..], 16).unwrap();
assert_eq!(reparsed.samples, s.samples);
}
#[test]
fn piff_senc_hostile_inputs() {
assert!(parse_piff_senc(&[1, 0, 0, 0], 8).is_err());
let body = [0u8, 0, 0, 1, 0xAA, 0xBB];
let err = parse_piff_senc(&body, 8).expect_err("truncated override");
assert!(format!("{err}").contains("override"), "{err}");
let mut body = vec![0u8, 0, 0, 0];
body.extend_from_slice(&1000u32.to_be_bytes());
body.extend_from_slice(&[0x01; 8]); assert!(parse_piff_senc(&body, 8).is_err());
let mut s = piff_senc_with_override();
s.flags = 0x0000_0002; assert!(build_piff_senc_box(&s).is_err());
let mut s2 = piff_senc_with_override();
s2.override_params = None; assert!(build_piff_senc_box(&s2).is_err());
let mut s3 = piff_senc_with_override();
s3.samples[0].initialization_vector = vec![0x01; 8];
assert!(build_piff_senc_box(&s3).is_err());
let s4 = PiffSencBox {
flags: 0,
override_params: None,
samples: vec![SencSample {
initialization_vector: vec![0x0A; 8],
subsamples: vec![SubsampleEntry {
bytes_of_clear_data: 1,
bytes_of_protected_data: 2,
}],
}],
};
assert!(build_piff_senc_box(&s4).is_err());
}
#[test]
fn piff_pssh_round_trip() {
let p = PsshBox {
version: 0,
system_id: [0x5E; 16],
kids: Vec::new(),
data: b"licence-acquisition-blob".to_vec(),
};
let bytes = build_piff_pssh_box(&p).unwrap();
assert_eq!(&bytes[4..8], b"uuid");
assert_eq!(&bytes[8..24], &PIFF_PSSH_USERTYPE);
let back = parse_piff_pssh(&bytes[24..]).unwrap();
assert_eq!(back, p);
let cenc_pssh = build_pssh_box(&p).unwrap();
assert_eq!(&bytes[24..], &cenc_pssh[8..]);
}
#[test]
fn piff_bodies_survive_every_truncation() {
let tenc_bytes = build_piff_tenc_box(&piff_tenc_ctr()).unwrap();
let tenc_body = &tenc_bytes[24..];
for cut in 0..tenc_body.len() {
assert!(
parse_piff_tenc(&tenc_body[..cut]).is_err(),
"tenc is fixed-size; every truncation must fail (cut {cut})"
);
}
let senc = piff_senc_with_override();
let senc_bytes = build_piff_senc_box(&senc).unwrap();
let senc_body = &senc_bytes[24..];
for cut in 0..senc_body.len() {
assert!(
parse_piff_senc(&senc_body[..cut], 8).is_err(),
"senc table over-runs must fail (cut {cut})"
);
}
let pssh = PsshBox {
version: 0,
system_id: [0x5E; 16],
kids: Vec::new(),
data: b"blob".to_vec(),
};
let pssh_bytes = build_piff_pssh_box(&pssh).unwrap();
let pssh_body = &pssh_bytes[24..];
for cut in 0..pssh_body.len() {
assert!(
parse_piff_pssh(&pssh_body[..cut]).is_err(),
"pssh DataSize is length-prefixed; every truncation must fail (cut {cut})"
);
}
}
#[test]
fn piff_pssh_rejects_v1_shapes() {
let mut body = vec![1u8, 0, 0, 0];
body.extend_from_slice(&[0x5E; 16]);
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); let err = parse_piff_pssh(&body).expect_err("v1 must fail");
assert!(format!("{err}").contains("version"), "{err}");
let v1 = PsshBox {
version: 1,
system_id: [0x5E; 16],
kids: Vec::new(),
data: Vec::new(),
};
assert!(build_piff_pssh_box(&v1).is_err());
let with_kids = PsshBox {
version: 0,
system_id: [0x5E; 16],
kids: vec![[0x01; 16]],
data: Vec::new(),
};
assert!(build_piff_pssh_box(&with_kids).is_err());
}
}