use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use broadcast_common::{Decrypt, Parse};
use crate::box_types::{BOX_HEADER_MIN_SIZE, parse_box};
pub use crate::cenc::CencScheme;
use crate::cenc::{SampleEncryptionEntry, TrackEncryptionBox};
use crate::cenc_crypto::{self, CbcsOp};
use crate::error::{Error, Result};
use crate::media::Media;
use crate::movie_fragment::{MovieFragmentBox, TrackFragmentHeaderBox, TrackFragmentRunBox};
const KEY_LEN: usize = 16;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct KeyMap {
keys: BTreeMap<[u8; KEY_LEN], [u8; KEY_LEN]>,
}
impl KeyMap {
pub fn new() -> Self {
Self {
keys: BTreeMap::new(),
}
}
pub fn with_key(mut self, kid: [u8; KEY_LEN], key: [u8; KEY_LEN]) -> Self {
self.keys.insert(kid, key);
self
}
pub fn insert(&mut self, kid: [u8; KEY_LEN], key: [u8; KEY_LEN]) {
self.keys.insert(kid, key);
}
pub fn get(&self, kid: &[u8; KEY_LEN]) -> Option<&[u8; KEY_LEN]> {
self.keys.get(kid)
}
}
#[derive(Debug, Clone)]
struct TrackCrypto {
track_id: u32,
tenc: TrackEncryptionBox,
original_format: [u8; 4],
scheme: CencScheme,
samples: Vec<SampleEncryptionEntry>,
}
#[derive(Debug, Clone)]
pub struct CencDecryptor {
file: Vec<u8>,
tracks: Vec<TrackCrypto>,
}
impl CencDecryptor {
pub fn from_fmp4(file: &[u8]) -> Result<Self> {
let mut tracks = Vec::new();
harvest_tracks(file, &mut tracks)?;
if tracks.is_empty() {
return Err(Error::UnexpectedBox {
expected: "a protected track (sinf/tenc + senc)",
});
}
Ok(Self {
file: file.to_vec(),
tracks,
})
}
pub fn original_format(&self) -> [u8; 4] {
self.tracks
.first()
.map(|t| t.original_format)
.unwrap_or(*b"\0\0\0\0")
}
pub fn scheme(&self) -> Option<CencScheme> {
self.tracks.first().map(|t| t.scheme)
}
pub fn track_encryption(&self) -> Option<&TrackEncryptionBox> {
self.tracks.first().map(|t| &t.tenc)
}
pub fn sample_entries(&self) -> &[SampleEncryptionEntry] {
self.tracks
.first()
.map(|t| t.samples.as_slice())
.unwrap_or(&[])
}
pub fn demux(&self) -> Result<Media> {
demux_protected(&self.file)
}
fn decrypt_sample(
scheme: CencScheme,
tenc: &TrackEncryptionBox,
entry: &SampleEncryptionEntry,
key: &[u8; KEY_LEN],
data: &mut bytes::Bytes,
) -> Result<bool> {
cenc_crypto::rewrite_in_place(data, |buf| match scheme {
CencScheme::Cenc => {
cenc_crypto::apply_ctr(&entry.initialization_vector, key, &entry.subsamples, buf)
}
CencScheme::Cbcs => cenc_crypto::cbcs_sample(tenc, entry, key, buf, CbcsOp::Decrypt),
})
}
}
impl Decrypt for CencDecryptor {
type Media = Media;
type Keys = KeyMap;
type Error = Error;
fn decrypt(&self, media: &mut Media, keys: &KeyMap) -> Result<()> {
for track in media.tracks.iter_mut() {
let crypto = self
.tracks
.iter()
.find(|c| c.track_id == track.spec.track_id)
.ok_or(Error::InvalidInput(
"no protected-source track matches this media track's track_id",
))?;
if crypto.tenc.default_is_protected == 0 {
continue;
}
let key = keys
.get(&crypto.tenc.default_kid)
.ok_or(Error::InvalidInput(
"no content key for the track's default_KID",
))?;
if track.samples.len() != crypto.samples.len() {
return Err(Error::InvalidInput(
"sample count mismatch between media and senc",
));
}
for (sample, entry) in track.samples.iter_mut().zip(crypto.samples.iter()) {
CencDecryptor::decrypt_sample(
crypto.scheme,
&crypto.tenc,
entry,
key,
&mut sample.data,
)?;
}
}
Ok(())
}
}
const FULL_HDR: usize = 4;
const STSD_ENTRY_COUNT: usize = 4;
const VISUAL_SAMPLE_ENTRY_HDR: usize = 78;
const SAMPLE_FLAG_IS_NON_SYNC: u32 = 0x0001_0000;
fn harvest_tracks(file: &[u8], out: &mut Vec<TrackCrypto>) -> Result<()> {
let moov = find_top_box(file, b"moov").ok_or(Error::UnexpectedBox { expected: "moov" })?;
let fragmented = find_top_box(file, b"moof").is_some();
for trak in iter_child_boxes(moov, b"trak") {
if let Some(crypto) = harvest_track(trak, fragmented)? {
out.push(crypto);
}
}
if fragmented {
harvest_fragment_senc(file, out)?;
}
Ok(())
}
fn harvest_track(trak: &[u8], fragmented: bool) -> Result<Option<TrackCrypto>> {
let Some(stbl) = descend(trak, &[b"mdia", b"minf", b"stbl"]) else {
return Ok(None);
};
let Some(stsd) = find_box(stbl, b"stsd") else {
return Ok(None);
};
let Some(sinf) = find_sinf_in_stsd(stsd) else {
return Ok(None);
};
let sinf_parsed = crate::cenc::ProtectionSchemeInfoBox::parse(sinf)?;
let scheme = sinf_parsed
.scheme_type
.as_ref()
.and_then(|s| CencScheme::from_four_cc(&s.scheme_type))
.ok_or(Error::InvalidInput(
"sinf missing or unknown schm scheme_type",
))?;
let tenc = sinf_parsed
.scheme_info
.as_ref()
.and_then(|si| si.tenc.clone())
.ok_or(Error::UnexpectedBox {
expected: "tenc inside schi",
})?;
let original_format = sinf_parsed.original_format.data_format;
let tkhd = find_box(trak, b"tkhd").ok_or(Error::UnexpectedBox { expected: "tkhd" })?;
let track_id = crate::init_segment::TrackHeaderBox::parse(tkhd)?.track_id;
let samples = if fragmented {
Vec::new()
} else {
let senc = find_box(stbl, b"senc").ok_or(Error::UnexpectedBox { expected: "senc" })?;
parse_senc_box(senc, tenc.default_per_sample_iv_size)?.entries
};
Ok(Some(TrackCrypto {
track_id,
tenc,
original_format,
scheme,
samples,
}))
}
fn parse_senc_box(senc: &[u8], per_sample_iv_size: u8) -> Result<crate::cenc::SampleEncryptionBox> {
if senc.len() < BOX_HEADER_MIN_SIZE + FULL_HDR {
return Err(Error::BufferTooShort {
need: BOX_HEADER_MIN_SIZE + FULL_HDR,
have: senc.len(),
what: "senc header",
});
}
let version = senc[BOX_HEADER_MIN_SIZE];
let flags = u32::from_be_bytes([
0,
senc[BOX_HEADER_MIN_SIZE + 1],
senc[BOX_HEADER_MIN_SIZE + 2],
senc[BOX_HEADER_MIN_SIZE + 3],
]);
crate::cenc::SampleEncryptionBox::parse_body(
&senc[BOX_HEADER_MIN_SIZE + FULL_HDR..],
version,
flags,
per_sample_iv_size,
)
}
fn harvest_fragment_senc(file: &[u8], tracks: &mut [TrackCrypto]) -> Result<()> {
for moof in iter_top_boxes(file, b"moof") {
for traf in iter_child_boxes(moof, b"traf") {
let Some(tfhd) = find_box(traf, b"tfhd") else {
continue;
};
if tfhd.len() < BOX_HEADER_MIN_SIZE + FULL_HDR {
return Err(Error::BufferTooShort {
need: BOX_HEADER_MIN_SIZE + FULL_HDR,
have: tfhd.len(),
what: "tfhd header",
});
}
let tfhd_parsed = TrackFragmentHeaderBox::parse_body(&tfhd[BOX_HEADER_MIN_SIZE..])?;
let Some(crypto) = tracks
.iter_mut()
.find(|t| t.track_id == tfhd_parsed.track_id)
else {
continue;
};
match find_box(traf, b"senc") {
Some(senc) => {
let senc_parsed = parse_senc_box(senc, crypto.tenc.default_per_sample_iv_size)?;
crypto.samples.extend(senc_parsed.entries);
}
None if crypto.tenc.default_per_sample_iv_size == 0 => {
let sample_count = traf_trun_sample_count(traf)?;
crypto.samples.extend(
core::iter::repeat_with(|| SampleEncryptionEntry {
initialization_vector: Vec::new(),
subsamples: Vec::new(),
})
.take(sample_count),
);
}
None => {
}
}
}
}
Ok(())
}
fn traf_trun_sample_count(traf: &[u8]) -> Result<usize> {
let mut total = 0usize;
for trun in
iter_boxes(&traf[BOX_HEADER_MIN_SIZE.min(traf.len())..]).filter(|b| &b[4..8] == b"trun")
{
if trun.len() < BOX_HEADER_MIN_SIZE {
return Err(Error::BufferTooShort {
need: BOX_HEADER_MIN_SIZE,
have: trun.len(),
what: "trun header",
});
}
let parsed = TrackFragmentRunBox::parse_body(&trun[BOX_HEADER_MIN_SIZE..])?;
total += parsed.samples.len();
}
Ok(total)
}
fn find_sinf_in_stsd(stsd: &[u8]) -> Option<&[u8]> {
let body_start = BOX_HEADER_MIN_SIZE + FULL_HDR + STSD_ENTRY_COUNT;
if body_start > stsd.len() {
return None;
}
for entry in iter_boxes(&stsd[body_start..]) {
let ty = &entry[4..8];
if ty == b"encv" || ty == b"enca" {
let child_start = if ty == b"encv" {
BOX_HEADER_MIN_SIZE + VISUAL_SAMPLE_ENTRY_HDR
} else {
BOX_HEADER_MIN_SIZE + 28
};
if child_start <= entry.len() {
if let Some(sinf) = iter_boxes(&entry[child_start..]).find(|b| &b[4..8] == b"sinf")
{
return Some(sinf);
}
}
}
}
None
}
fn demux_protected(file: &[u8]) -> Result<Media> {
use crate::AVCConfigurationBox;
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
let moov = find_top_box(file, b"moov").ok_or(Error::UnexpectedBox { expected: "moov" })?;
let movie_timescale = mvhd_timescale(moov).unwrap_or(1000);
let fragmented = find_top_box(file, b"moof").is_some();
let mut tracks = Vec::new();
for trak in iter_child_boxes(moov, b"trak") {
let Some(stbl) = descend(trak, &[b"mdia", b"minf", b"stbl"]) else {
continue;
};
let timescale = descend(trak, &[b"mdia"])
.and_then(|mdia| find_box(mdia, b"mdhd"))
.and_then(mdhd_timescale)
.unwrap_or(movie_timescale);
let Some(stsd) = find_box(stbl, b"stsd") else {
continue;
};
let Some(sinf) = find_sinf_in_stsd(stsd) else {
continue;
};
let sinf_parsed = crate::cenc::ProtectionSchemeInfoBox::parse(sinf)?;
if &sinf_parsed.original_format.data_format != b"avc1" {
return Err(Error::UnexpectedBox {
expected: "avc1 original_format (only protected AVC demux is supported)",
});
}
let avc_config = find_avcc_config(stsd)?;
let tkhd = find_box(trak, b"tkhd").ok_or(Error::UnexpectedBox { expected: "tkhd" })?;
let track_id = crate::init_segment::TrackHeaderBox::parse(tkhd)?.track_id;
let samples = if fragmented {
collect_fragment_samples(file, track_id)?
} else {
let sizes = stsz_sizes(stbl)?;
let sample_offsets = sample_file_offsets(stbl, &sizes)?;
let mut samples = Vec::with_capacity(sizes.len());
for (&size, &offset) in sizes.iter().zip(sample_offsets.iter()) {
let end = offset
.checked_add(size)
.ok_or(Error::InvalidInput("sample offset + size overflow"))?;
if end > file.len() {
return Err(Error::BufferTooShort {
need: end,
have: file.len(),
what: "protected sample data",
});
}
samples.push(Sample {
data: file[offset..end].to_vec().into(),
dts: None,
pts: None,
duration: None,
flags: crate::ir::SampleFlags::SYNC,
provenance: None,
});
}
samples
};
tracks.push(Track::new(
TrackSpec::new(
track_id,
timescale,
CodecConfig::Avc {
config: AVCConfigurationBox::new(avc_config),
width: 0,
height: 0,
},
),
samples,
));
}
if tracks.is_empty() {
return Err(Error::UnexpectedBox {
expected: "a protected AVC track",
});
}
Ok(Media::new(tracks, movie_timescale))
}
fn collect_fragment_samples(
file: &[u8],
target_track_id: u32,
) -> Result<Vec<crate::pipeline::Sample>> {
let mut out = Vec::new();
let mut offset = 0usize;
let mut pending_moof: Option<(usize, MovieFragmentBox)> = None;
let mut next_dts: i64 = 0;
let mut seeded = false;
while offset + BOX_HEADER_MIN_SIZE <= file.len() {
let (bx, consumed) = parse_box(&file[offset..])?;
if bx.header.box_type.is(b"moof") {
let moof = MovieFragmentBox::parse_body(bx.body)?;
pending_moof = Some((offset, moof));
} else if bx.header.box_type.is(b"mdat") {
if let Some((moof_off, moof)) = pending_moof.take() {
if !seeded {
if let Some(tfdt) = moof
.traf
.iter()
.find(|t| t.tfhd.track_id == target_track_id)
.and_then(|t| t.tfdt.as_ref())
{
next_dts = tfdt.base_media_decode_time() as i64;
}
seeded = true;
}
absorb_protected_fragment(
file,
moof_off,
&moof,
target_track_id,
&mut next_dts,
&mut out,
)?;
}
}
if consumed == 0 {
break;
}
offset += consumed;
}
Ok(out)
}
fn absorb_protected_fragment(
file: &[u8],
moof_off: usize,
moof: &MovieFragmentBox,
target_track_id: u32,
next_dts: &mut i64,
out: &mut Vec<crate::pipeline::Sample>,
) -> Result<()> {
use crate::pipeline::Sample;
for traf in &moof.traf {
let tfhd = &traf.tfhd;
if tfhd.track_id != target_track_id {
continue;
}
for trun in &traf.trun {
let base = moof_off as i64 + trun.data_offset.unwrap_or(0) as i64;
let mut cursor = base;
for (i, ts) in trun.samples.iter().enumerate() {
let size = ts
.sample_size
.or(tfhd.default_sample_size)
.ok_or(Error::InvalidInput(
"trun sample has no size (no trun.sample_size, no tfhd default_sample_size)",
))? as usize;
let duration = ts
.sample_duration
.or(tfhd.default_sample_duration)
.unwrap_or(0);
let flags = ts
.sample_flags
.or(if i == 0 {
trun.first_sample_flags
} else {
None
})
.or(tfhd.default_sample_flags)
.unwrap_or(0);
let is_sync = flags & SAMPLE_FLAG_IS_NON_SYNC == 0;
let composition_offset = ts.sample_composition_time_offset.unwrap_or(0) as i64;
let start = usize::try_from(cursor)
.map_err(|_| Error::InvalidInput("negative sample data offset"))?;
let end = start
.checked_add(size)
.ok_or(Error::InvalidInput("sample offset + size overflow"))?;
if end > file.len() {
return Err(Error::BufferTooShort {
need: end,
have: file.len(),
what: "protected fragment sample data",
});
}
let dts = *next_dts;
let pts = dts + composition_offset;
out.push(Sample {
data: file[start..end].to_vec().into(),
dts: Some(dts),
pts: Some(pts),
duration: Some(duration),
flags: crate::ir::SampleFlags::new(is_sync),
provenance: None,
});
*next_dts += duration as i64;
cursor += size as i64;
}
}
}
Ok(())
}
fn find_avcc_config(stsd: &[u8]) -> Result<crate::avc_config::AVCDecoderConfigurationRecord> {
let body_start = BOX_HEADER_MIN_SIZE + FULL_HDR + STSD_ENTRY_COUNT;
for entry in iter_boxes(&stsd[body_start.min(stsd.len())..]) {
if &entry[4..8] == b"encv" {
let child_start = BOX_HEADER_MIN_SIZE + VISUAL_SAMPLE_ENTRY_HDR;
if child_start <= entry.len() {
if let Some(avcc) = iter_boxes(&entry[child_start..]).find(|b| &b[4..8] == b"avcC")
{
let cfg = crate::AVCConfigurationBox::parse_body(&avcc[BOX_HEADER_MIN_SIZE..])?;
return Ok(cfg.config);
}
}
}
}
Err(Error::UnexpectedBox {
expected: "avcC inside encv",
})
}
fn iter_boxes(data: &[u8]) -> impl Iterator<Item = &[u8]> {
let mut offset = 0usize;
core::iter::from_fn(move || {
if offset + BOX_HEADER_MIN_SIZE > data.len() {
return None;
}
let (bx, consumed) = parse_box(&data[offset..]).ok()?;
if consumed == 0 {
return None;
}
let size = if bx.header.size == 0 {
data.len() - offset
} else {
(bx.header.size as usize).min(data.len() - offset)
};
let start = offset;
offset += consumed;
Some(&data[start..start + size])
})
}
fn iter_child_boxes<'a>(
container: &'a [u8],
fourcc: &'a [u8; 4],
) -> impl Iterator<Item = &'a [u8]> {
let body = &container[BOX_HEADER_MIN_SIZE.min(container.len())..];
iter_boxes(body).filter(move |b| &b[4..8] == fourcc)
}
fn iter_top_boxes<'a>(file: &'a [u8], fourcc: &[u8; 4]) -> impl Iterator<Item = &'a [u8]> {
iter_boxes(file).filter(move |b| b[4..8] == *fourcc)
}
fn find_box<'a>(container: &'a [u8], fourcc: &[u8; 4]) -> Option<&'a [u8]> {
let body = &container[BOX_HEADER_MIN_SIZE.min(container.len())..];
iter_boxes(body).find(|b| &b[4..8] == fourcc)
}
fn find_top_box<'a>(file: &'a [u8], fourcc: &[u8; 4]) -> Option<&'a [u8]> {
iter_boxes(file).find(|b| &b[4..8] == fourcc)
}
fn descend<'a>(start: &'a [u8], path: &[&[u8; 4]]) -> Option<&'a [u8]> {
let mut cur = start;
for fourcc in path {
cur = find_box(cur, fourcc)?;
}
Some(cur)
}
fn mvhd_timescale(moov: &[u8]) -> Option<u32> {
let mvhd = find_box(moov, b"mvhd")?;
let version = mvhd.get(BOX_HEADER_MIN_SIZE)?;
let ts_off = if *version == 1 {
BOX_HEADER_MIN_SIZE + FULL_HDR + 16
} else {
BOX_HEADER_MIN_SIZE + FULL_HDR + 8
};
Some(u32::from_be_bytes([
*mvhd.get(ts_off)?,
*mvhd.get(ts_off + 1)?,
*mvhd.get(ts_off + 2)?,
*mvhd.get(ts_off + 3)?,
]))
}
fn mdhd_timescale(mdhd: &[u8]) -> Option<u32> {
let version = mdhd.get(BOX_HEADER_MIN_SIZE)?;
let ts_off = if *version == 1 {
BOX_HEADER_MIN_SIZE + FULL_HDR + 16
} else {
BOX_HEADER_MIN_SIZE + FULL_HDR + 8
};
Some(u32::from_be_bytes([
*mdhd.get(ts_off)?,
*mdhd.get(ts_off + 1)?,
*mdhd.get(ts_off + 2)?,
*mdhd.get(ts_off + 3)?,
]))
}
fn stsz_sizes(stbl: &[u8]) -> Result<Vec<usize>> {
let stsz = find_box(stbl, b"stsz").ok_or(Error::UnexpectedBox { expected: "stsz" })?;
let base = BOX_HEADER_MIN_SIZE + FULL_HDR;
let need = base + 8;
if stsz.len() < need {
return Err(Error::BufferTooShort {
need,
have: stsz.len(),
what: "stsz header",
});
}
let sample_size =
u32::from_be_bytes([stsz[base], stsz[base + 1], stsz[base + 2], stsz[base + 3]]);
let count = u32::from_be_bytes([
stsz[base + 4],
stsz[base + 5],
stsz[base + 6],
stsz[base + 7],
]) as usize;
let mut sizes = Vec::with_capacity(count);
if sample_size != 0 {
for _ in 0..count {
sizes.push(sample_size as usize);
}
} else {
let table = base + 8;
let end = table + count * 4;
if stsz.len() < end {
return Err(Error::BufferTooShort {
need: end,
have: stsz.len(),
what: "stsz sample_size table",
});
}
for i in 0..count {
let o = table + i * 4;
sizes.push(
u32::from_be_bytes([stsz[o], stsz[o + 1], stsz[o + 2], stsz[o + 3]]) as usize,
);
}
}
Ok(sizes)
}
fn sample_file_offsets(stbl: &[u8], sizes: &[usize]) -> Result<Vec<usize>> {
let stsc = find_box(stbl, b"stsc").ok_or(Error::UnexpectedBox { expected: "stsc" })?;
let stco = find_box(stbl, b"stco").ok_or(Error::UnexpectedBox { expected: "stco" })?;
let sc_base = BOX_HEADER_MIN_SIZE + FULL_HDR;
if stco.len() < sc_base + 4 {
return Err(Error::BufferTooShort {
need: sc_base + 4,
have: stco.len(),
what: "stco header",
});
}
let chunk_count = u32::from_be_bytes([
stco[sc_base],
stco[sc_base + 1],
stco[sc_base + 2],
stco[sc_base + 3],
]) as usize;
let mut chunk_offsets = Vec::with_capacity(chunk_count);
let co_table = sc_base + 4;
if stco.len() < co_table + chunk_count * 4 {
return Err(Error::BufferTooShort {
need: co_table + chunk_count * 4,
have: stco.len(),
what: "stco chunk offsets",
});
}
for i in 0..chunk_count {
let o = co_table + i * 4;
chunk_offsets
.push(u32::from_be_bytes([stco[o], stco[o + 1], stco[o + 2], stco[o + 3]]) as usize);
}
if stsc.len() < sc_base + 4 {
return Err(Error::BufferTooShort {
need: sc_base + 4,
have: stsc.len(),
what: "stsc header",
});
}
let entry_count = u32::from_be_bytes([
stsc[sc_base],
stsc[sc_base + 1],
stsc[sc_base + 2],
stsc[sc_base + 3],
]) as usize;
let sc_table = sc_base + 4;
if stsc.len() < sc_table + entry_count * 12 {
return Err(Error::BufferTooShort {
need: sc_table + entry_count * 12,
have: stsc.len(),
what: "stsc entries",
});
}
let mut samples_per_chunk = Vec::with_capacity(chunk_count);
for c in 0..chunk_count {
let chunk_no = (c + 1) as u32;
let mut spc = 0u32;
for e in 0..entry_count {
let o = sc_table + e * 12;
let first_chunk = u32::from_be_bytes([stsc[o], stsc[o + 1], stsc[o + 2], stsc[o + 3]]);
let per = u32::from_be_bytes([stsc[o + 4], stsc[o + 5], stsc[o + 6], stsc[o + 7]]);
if first_chunk <= chunk_no {
spc = per;
} else {
break;
}
}
samples_per_chunk.push(spc);
}
let mut offsets = Vec::with_capacity(sizes.len());
let mut sample_idx = 0usize;
for (c, &chunk_base) in chunk_offsets.iter().enumerate() {
let per = samples_per_chunk.get(c).copied().unwrap_or(0) as usize;
let mut cursor = chunk_base;
for _ in 0..per {
if sample_idx >= sizes.len() {
break;
}
offsets.push(cursor);
cursor += sizes[sample_idx];
sample_idx += 1;
}
}
if offsets.len() != sizes.len() {
return Err(Error::InvalidInput(
"stsc/stco sample-to-chunk mapping did not cover all samples",
));
}
Ok(offsets)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cenc_crypto;
use crate::media::Track;
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
const VIDEO_TRACK_ID: u32 = 1;
const AUDIO_TRACK_ID: u32 = 2;
const KID: [u8; KEY_LEN] = [0xAA; KEY_LEN];
const KEY: [u8; KEY_LEN] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10,
];
const VIDEO_IV: [u8; 8] = [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11];
const AUDIO_IV: [u8; 8] = [0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22];
const SAMPLES_PER_TRACK: usize = 2;
fn tenc() -> TrackEncryptionBox {
TrackEncryptionBox {
version: 0,
default_crypt_byte_block: 0,
default_skip_byte_block: 0,
default_is_protected: 1,
default_per_sample_iv_size: 8,
default_kid: KID,
default_constant_iv: None,
}
}
fn crypto(track_id: u32, iv: &[u8; 8]) -> TrackCrypto {
TrackCrypto {
track_id,
tenc: tenc(),
original_format: *b"avc1",
scheme: CencScheme::Cenc,
samples: (0..SAMPLES_PER_TRACK)
.map(|_| SampleEncryptionEntry {
initialization_vector: iv.to_vec(),
subsamples: Vec::new(),
})
.collect(),
}
}
fn decryptor() -> CencDecryptor {
CencDecryptor {
file: Vec::new(),
tracks: alloc::vec![
crypto(VIDEO_TRACK_ID, &VIDEO_IV),
crypto(AUDIO_TRACK_ID, &AUDIO_IV),
],
}
}
fn test_codec_config() -> CodecConfig {
CodecConfig::Opus {
config: crate::opus::OpusSpecificBox {
version: 0,
output_channel_count: 2,
pre_skip: 0,
input_sample_rate: 48_000,
output_gain: 0,
channel_mapping_family: 0,
channel_mapping: None,
},
channel_count: 2,
sample_rate: 48_000,
sample_size: 16,
}
}
fn plaintext(i: usize) -> Vec<u8> {
(0u8..64).map(|b| b.wrapping_add(i as u8 * 7)).collect()
}
fn encrypted_media(track_id: u32, iv: &[u8; 8]) -> Media {
let samples = (0..SAMPLES_PER_TRACK)
.map(|i| {
let mut buf = plaintext(i);
cenc_crypto::apply_ctr(iv, &KEY, &[], &mut buf).expect("encrypt");
Sample {
data: buf.into(),
dts: None,
pts: None,
duration: None,
flags: crate::ir::SampleFlags::SYNC,
provenance: None,
}
})
.collect();
Media::new(
alloc::vec![Track::new(
TrackSpec::new(track_id, 90_000, test_codec_config()),
samples,
)],
90_000,
)
}
#[test]
fn narrowed_media_decrypts_with_its_own_tracks_ivs() {
let dec = decryptor();
let keys = KeyMap::new().with_key(KID, KEY);
let mut media = encrypted_media(AUDIO_TRACK_ID, &AUDIO_IV);
dec.decrypt(&mut media, &keys).expect("decrypt");
for (i, sample) in media.tracks[0].samples.iter().enumerate() {
assert_eq!(
&sample.data[..],
&plaintext(i)[..],
"sample {i} must be decrypted with track {AUDIO_TRACK_ID}'s IV, not \
whichever record happens to sit at the same position"
);
}
}
#[test]
fn first_track_still_decrypts_with_its_own_ivs() {
let dec = decryptor();
let keys = KeyMap::new().with_key(KID, KEY);
let mut media = encrypted_media(VIDEO_TRACK_ID, &VIDEO_IV);
dec.decrypt(&mut media, &keys).expect("decrypt");
for (i, sample) in media.tracks[0].samples.iter().enumerate() {
assert_eq!(&sample.data[..], &plaintext(i)[..]);
}
}
#[test]
fn unknown_track_id_errors() {
let dec = decryptor();
let keys = KeyMap::new().with_key(KID, KEY);
let mut media = encrypted_media(99, &AUDIO_IV);
let err = dec.decrypt(&mut media, &keys).unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)), "got {err:?}");
}
}