use alloc::collections::BTreeSet;
use alloc::vec::Vec;
use broadcast_common::Encrypt;
use crate::annexb::{NAL_LENGTH_SIZE, iter_length_prefixed_nals};
use crate::cenc::{CencScheme, SampleEncryptionEntry, SubSampleEntry, TrackEncryptionBox};
use crate::cenc_crypto::{self, CbcsOp};
use crate::error::{Error, Result};
use crate::media::{Media, TrackEncryption};
use crate::nal::NalCodec;
use crate::pipeline::CodecConfig;
const KEY_LEN: usize = 16;
const PER_SAMPLE_IV_SIZE: u8 = 8;
const DEFAULT_CBCS_PATTERN: (u8, u8) = (1, 9);
const CBCS_PATTERN_MAX: u8 = 0x0F;
const VALID_EXPLICIT_IV_LENS: [usize; 2] = [8, 16];
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum IvGen {
#[default]
Counter,
Explicit(Vec<Vec<u8>>),
Constant([u8; KEY_LEN]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SubsamplePolicy {
Video,
WholeSample,
}
#[derive(Debug, Clone)]
pub struct EncryptConfig {
pub scheme: CencScheme,
pub kid: [u8; KEY_LEN],
pub iv: IvGen,
pub pattern: Option<(u8, u8)>,
pub subsample: SubsamplePolicy,
}
#[derive(Debug)]
pub struct CencEncryptor {
key: [u8; KEY_LEN],
next_counter: u64,
}
impl CencEncryptor {
pub fn new(key: [u8; KEY_LEN]) -> Self {
Self {
key,
next_counter: 0,
}
}
pub fn resume(key: [u8; KEY_LEN], next_counter: u64) -> Self {
Self { key, next_counter }
}
pub fn next_counter(&self) -> u64 {
self.next_counter
}
}
impl Encrypt for CencEncryptor {
type Media = Media;
type Config = EncryptConfig;
type Error = Error;
fn encrypt(&mut self, media: &mut Media, cfg: &EncryptConfig) -> Result<()> {
if cfg.scheme == CencScheme::Cenc && matches!(cfg.iv, IvGen::Constant(_)) {
return Err(Error::InvalidInput(
"IvGen::Constant is cbcs-only: a constant IV under cenc (AES-CTR) derives one \
counter block for every sample, reusing a single keystream (two-time pad)",
));
}
let pattern = match cfg.scheme {
CencScheme::Cbcs => {
let p = cfg.pattern.unwrap_or(DEFAULT_CBCS_PATTERN);
if p.0 > CBCS_PATTERN_MAX || p.1 > CBCS_PATTERN_MAX {
return Err(Error::InvalidInput(
"cbcs pattern block counts must each be 0..=15",
));
}
p
}
CencScheme::Cenc => (0, 0),
};
let (per_sample_iv_size, default_constant_iv) = tenc_iv_fields(&cfg.iv)?;
let tenc = TrackEncryptionBox {
version: if cfg.scheme == CencScheme::Cbcs { 1 } else { 0 },
default_crypt_byte_block: pattern.0,
default_skip_byte_block: pattern.1,
default_is_protected: 1,
default_per_sample_iv_size: per_sample_iv_size,
default_kid: cfg.kid,
default_constant_iv,
};
let total_samples: usize = media.tracks.iter().map(|t| t.samples.len()).sum();
self.validate_iv_gen(&cfg.iv, total_samples)?;
let plan = self.plan_sample_ivs(media, &cfg.iv)?;
assert_ivs_unique(&plan)?;
for (track, track_ivs) in media.tracks.iter_mut().zip(plan.iter()) {
let nal_codec = nal_codec_for(&track.spec.config);
let sample_count = track.samples.len();
let mut entries = Vec::with_capacity(sample_count);
for (sample, iv) in track.samples.iter_mut().zip(track_ivs.iter()) {
let subsamples = match (cfg.subsample, nal_codec) {
(SubsamplePolicy::Video, Some(codec)) => nal_subsamples(codec, &sample.data)?,
_ => Vec::new(),
};
let entry = SampleEncryptionEntry {
initialization_vector: iv.clone(),
subsamples,
};
match cfg.scheme {
CencScheme::Cenc => cenc_crypto::rewrite_in_place(&mut sample.data, |buf| {
cenc_crypto::apply_ctr(
&entry.initialization_vector,
&self.key,
&entry.subsamples,
buf,
)
})?,
CencScheme::Cbcs => cenc_crypto::rewrite_in_place(&mut sample.data, |buf| {
cenc_crypto::cbcs_sample(&tenc, &entry, &self.key, buf, CbcsOp::Encrypt)
})?,
};
entries.push(entry);
}
track.encryption = Some(TrackEncryption {
scheme: cfg.scheme,
tenc: tenc.clone(),
samples: entries,
});
}
if matches!(cfg.iv, IvGen::Counter) {
self.next_counter += total_samples as u64;
}
Ok(())
}
}
impl CencEncryptor {
fn validate_iv_gen(&self, iv_gen: &IvGen, total_samples: usize) -> Result<()> {
match iv_gen {
IvGen::Counter => {
let last = total_samples.saturating_sub(1) as u64;
self.next_counter
.checked_add(last)
.ok_or(Error::InvalidInput(
"CENC IV counter overflow (next_counter + sample_index)",
))?;
Ok(())
}
IvGen::Explicit(ivs) => {
if ivs.len() != total_samples {
return Err(Error::InvalidInput(
"IvGen::Explicit must supply exactly one IV per sample of the whole Media \
(the sum of every track's sample count), consumed in (track, sample) order — \
one content key covers every track, so IV uniqueness is per key, not per track",
));
}
let mut seen: BTreeSet<&[u8]> = BTreeSet::new();
for iv in ivs {
if !VALID_EXPLICIT_IV_LENS.contains(&iv.len()) {
return Err(Error::InvalidInput(
"CENC per-sample IV must be 8 or 16 bytes",
));
}
if !seen.insert(iv.as_slice()) {
return Err(Error::InvalidInput(
"duplicate IvGen::Explicit per-sample IV: an IV must be unique per \
content key (ISO/IEC 23001-7 §9.2) — reusing one under cenc (AES-CTR) \
reuses its keystream (two-time pad)",
));
}
}
Ok(())
}
IvGen::Constant(_) => Ok(()),
}
}
fn plan_sample_ivs(&self, media: &Media, iv_gen: &IvGen) -> Result<Vec<Vec<Vec<u8>>>> {
let mut media_sample_idx = 0usize;
let mut plan = Vec::with_capacity(media.tracks.len());
for track in &media.tracks {
let mut track_ivs = Vec::with_capacity(track.samples.len());
for _ in &track.samples {
track_ivs.push(self.resolve_iv(iv_gen, media_sample_idx)?);
media_sample_idx += 1;
}
plan.push(track_ivs);
}
Ok(plan)
}
fn resolve_iv(&self, iv_gen: &IvGen, idx: usize) -> Result<Vec<u8>> {
match iv_gen {
IvGen::Counter => {
let v = self
.next_counter
.checked_add(idx as u64)
.ok_or(Error::InvalidInput(
"CENC IV counter overflow (next_counter + sample_index)",
))?;
Ok(v.to_be_bytes().to_vec())
}
IvGen::Explicit(ivs) => {
let iv = ivs.get(idx).ok_or(Error::InvalidInput(
"IvGen::Explicit must supply exactly one IV per sample of the whole Media",
))?;
if !VALID_EXPLICIT_IV_LENS.contains(&iv.len()) {
return Err(Error::InvalidInput(
"CENC per-sample IV must be 8 or 16 bytes",
));
}
Ok(iv.clone())
}
IvGen::Constant(_) => Ok(Vec::new()),
}
}
}
fn assert_ivs_unique(plan: &[Vec<Vec<u8>>]) -> Result<()> {
let mut seen: BTreeSet<&[u8]> = BTreeSet::new();
for track_ivs in plan {
for iv in track_ivs {
if iv.is_empty() {
continue;
}
if !seen.insert(iv.as_slice()) {
return Err(Error::InvalidInput(
"duplicate CENC per-sample IV planned across the Media's tracks: an IV must \
be unique per content key (ISO/IEC 23001-7 §9.2), and one CencEncryptor key \
covers every track — reuse under cenc (AES-CTR) is a two-time pad",
));
}
}
}
Ok(())
}
fn nal_codec_for(config: &CodecConfig) -> Option<NalCodec> {
match config {
CodecConfig::Avc { .. } => Some(NalCodec::Avc),
CodecConfig::Hevc { .. } => Some(NalCodec::Hevc),
CodecConfig::Vvc { .. } => Some(NalCodec::Vvc),
_ => None,
}
}
fn nal_subsamples(codec: NalCodec, data: &[u8]) -> Result<Vec<SubSampleEntry>> {
let header_len: usize = match codec {
NalCodec::Avc => 1,
NalCodec::Hevc | NalCodec::Vvc => 2,
};
let nals = iter_length_prefixed_nals(data)?;
let mut out = Vec::with_capacity(nals.len());
for nal in nals {
let clear_header = header_len.min(nal.len());
out.push(SubSampleEntry {
bytes_of_clear_data: (NAL_LENGTH_SIZE + clear_header) as u16,
bytes_of_protected_data: (nal.len() - clear_header) as u32,
});
}
let total: usize = out
.iter()
.map(|s| s.bytes_of_clear_data as usize + s.bytes_of_protected_data as usize)
.sum();
if total != data.len() {
return Err(Error::InvalidInput(
"NAL subsample map does not cover the whole sample",
));
}
Ok(out)
}
fn tenc_iv_fields(iv_gen: &IvGen) -> Result<(u8, Option<Vec<u8>>)> {
match iv_gen {
IvGen::Constant(iv) => Ok((0, Some(iv.to_vec()))),
IvGen::Counter => Ok((PER_SAMPLE_IV_SIZE, None)),
IvGen::Explicit(ivs) => {
let len = match ivs.first() {
Some(first) => {
if ivs.iter().any(|iv| iv.len() != first.len()) {
return Err(Error::InvalidInput(
"IvGen::Explicit IVs must all share one length (tenc.default_per_sample_iv_size is one value for the whole track)",
));
}
first.len()
}
None => PER_SAMPLE_IV_SIZE as usize,
};
if !VALID_EXPLICIT_IV_LENS.contains(&len) {
return Err(Error::InvalidInput(
"CENC per-sample IV must be 8 or 16 bytes",
));
}
Ok((len as u8, None))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use broadcast_common::Unpackage;
use bytes::Bytes;
use crate::ts_demux::TsDemux;
const KID: [u8; 16] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x00,
];
const KEY: [u8; 16] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10,
];
fn clear_media() -> Media {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264");
path.push("main.ts");
let bytes = std::fs::read(&path).unwrap_or_else(|e| panic!("read {path:?}: {e}"));
let mut demux = TsDemux::new();
let media = demux
.unpackage(bytes.as_slice())
.expect("demux fixtures/ts/h264/main.ts");
media
.select_tracks_by(|t| matches!(t.spec.config, CodecConfig::Avc { .. }))
.expect("AVC video track present")
}
fn multi_track_media() -> Media {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264_aac.ts");
let bytes = std::fs::read(&path).unwrap_or_else(|e| panic!("read {path:?}: {e}"));
let mut demux = TsDemux::new();
let media = demux
.unpackage(bytes.as_slice())
.expect("demux fixtures/ts/h264_aac.ts");
assert!(
media.tracks.len() > 1,
"h264_aac.ts must be a multi-track fixture (got {})",
media.tracks.len()
);
media
}
fn snapshot(media: &Media) -> Vec<Bytes> {
media.tracks[0]
.samples
.iter()
.map(|s| s.data.clone())
.collect()
}
fn snapshot_all(media: &Media) -> Vec<Bytes> {
media
.tracks
.iter()
.flat_map(|t| t.samples.iter().map(|s| s.data.clone()))
.collect()
}
fn distinct_ivs(n: usize, len: usize) -> Vec<Vec<u8>> {
(0..n)
.map(|i| {
let mut iv = alloc::vec![0xABu8; len];
iv[len - 1] = i as u8;
iv[len - 2] = (i >> 8) as u8;
iv
})
.collect()
}
#[test]
fn cenc_round_trip_reverses_byte_identical() {
let mut media = clear_media();
let original = snapshot(&media);
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Counter,
pattern: None,
subsample: SubsamplePolicy::Video,
};
CencEncryptor::resume(KEY, 7)
.encrypt(&mut media, &cfg)
.expect("encrypt");
let track = &mut media.tracks[0];
let enc = track.encryption.clone().expect("track.encryption Some");
assert_eq!(enc.scheme, CencScheme::Cenc);
assert_eq!(enc.tenc.default_kid, KID);
assert_eq!(enc.samples.len(), track.samples.len());
assert!(
track
.samples
.iter()
.zip(original.iter())
.any(|(s, o)| s.data != *o),
"encrypt must change protected bytes"
);
for (sample, entry) in track.samples.iter_mut().zip(enc.samples.iter()) {
cenc_crypto::rewrite_in_place(&mut sample.data, |buf| {
cenc_crypto::apply_ctr(&entry.initialization_vector, &KEY, &entry.subsamples, buf)
})
.expect("reverse apply_ctr");
}
let reversed: Vec<Bytes> = track.samples.iter().map(|s| s.data.clone()).collect();
assert_eq!(reversed, original, "cenc round trip must be byte-identical");
}
#[test]
fn cbcs_round_trip_reverses_byte_identical() {
let mut media = clear_media();
let original = snapshot(&media);
let cfg = EncryptConfig {
scheme: CencScheme::Cbcs,
kid: KID,
iv: IvGen::Counter,
pattern: Some((1, 9)),
subsample: SubsamplePolicy::Video,
};
CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.expect("encrypt");
let track = &mut media.tracks[0];
let enc = track.encryption.clone().expect("track.encryption Some");
assert_eq!(enc.scheme, CencScheme::Cbcs);
assert_eq!(enc.tenc.default_crypt_byte_block, 1);
assert_eq!(enc.tenc.default_skip_byte_block, 9);
assert_eq!(enc.samples.len(), track.samples.len());
assert!(
track
.samples
.iter()
.zip(original.iter())
.any(|(s, o)| s.data != *o),
"encrypt must change protected bytes"
);
for (sample, entry) in track.samples.iter_mut().zip(enc.samples.iter()) {
cenc_crypto::rewrite_in_place(&mut sample.data, |buf| {
cenc_crypto::cbcs_sample(&enc.tenc, entry, &KEY, buf, CbcsOp::Decrypt)
})
.expect("reverse cbcs_sample");
}
let reversed: Vec<Bytes> = track.samples.iter().map(|s| s.data.clone()).collect();
assert_eq!(reversed, original, "cbcs round trip must be byte-identical");
}
#[test]
fn whole_sample_policy_yields_empty_subsample_map() {
let mut media = clear_media();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::default(),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.expect("encrypt");
let enc = media.tracks[0].encryption.as_ref().expect("Some");
assert!(
enc.samples.iter().all(|e| e.subsamples.is_empty()),
"WholeSample policy must record an empty subsample map"
);
}
#[test]
fn explicit_iv_count_mismatch_errors() {
let mut media = clear_media();
let n = media.tracks[0].samples.len();
assert!(n > 1, "fixture must have more than one sample to bite");
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(alloc::vec![alloc::vec![0u8; 8]; n - 1]),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn explicit_iv_too_long_errors() {
let mut media = clear_media();
let n = media.tracks[0].samples.len();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(alloc::vec![alloc::vec![0u8; 17]; n]),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn explicit_iv_empty_errors() {
let mut media = clear_media();
let n = media.tracks[0].samples.len();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(alloc::vec![alloc::vec![]; n]),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn explicit_iv_wrong_uniform_length_errors() {
let mut media = clear_media();
let n = media.tracks[0].samples.len();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(alloc::vec![alloc::vec![0u8; 12]; n]),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn explicit_iv_valid_lengths_are_ok() {
for len in [8usize, 16] {
let mut media = clear_media();
let n = media.tracks[0].samples.len();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(distinct_ivs(n, len)),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_or_else(|e| panic!("{len}-byte explicit IV must be accepted: {e:?}"));
let enc = media.tracks[0].encryption.as_ref().expect("Some");
assert_eq!(
enc.tenc.default_per_sample_iv_size, len as u8,
"tenc.default_per_sample_iv_size must match the actual IV length used"
);
}
}
#[test]
fn cbcs_pattern_zero_crypt_nonzero_skip_errors() {
let mut media = clear_media();
let cfg = EncryptConfig {
scheme: CencScheme::Cbcs,
kid: KID,
iv: IvGen::Counter,
pattern: Some((0, 9)),
subsample: SubsamplePolicy::Video,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn cbcs_pattern_component_too_large_errors() {
let mut media = clear_media();
let cfg = EncryptConfig {
scheme: CencScheme::Cbcs,
kid: KID,
iv: IvGen::Counter,
pattern: Some((17, 9)),
subsample: SubsamplePolicy::Video,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
}
#[test]
fn multi_track_round_trip_reverses_byte_identical_both_schemes() {
for scheme in [CencScheme::Cenc, CencScheme::Cbcs] {
let mut media = multi_track_media();
let original = snapshot_all(&media);
let cfg = EncryptConfig {
scheme,
kid: KID,
iv: IvGen::Counter,
pattern: if scheme == CencScheme::Cbcs {
Some((1, 9))
} else {
None
},
subsample: SubsamplePolicy::Video,
};
CencEncryptor::resume(KEY, 3)
.encrypt(&mut media, &cfg)
.unwrap_or_else(|e| panic!("{}: encrypt: {e:?}", scheme.name()));
assert_ne!(
snapshot_all(&media),
original,
"{}: encrypt must change protected bytes",
scheme.name()
);
for track in &mut media.tracks {
let enc = track.encryption.clone().expect("track.encryption Some");
assert_eq!(enc.samples.len(), track.samples.len());
for (sample, entry) in track.samples.iter_mut().zip(enc.samples.iter()) {
cenc_crypto::rewrite_in_place(&mut sample.data, |buf| match scheme {
CencScheme::Cenc => cenc_crypto::apply_ctr(
&entry.initialization_vector,
&KEY,
&entry.subsamples,
buf,
),
CencScheme::Cbcs => {
cenc_crypto::cbcs_sample(&enc.tenc, entry, &KEY, buf, CbcsOp::Decrypt)
}
})
.unwrap_or_else(|e| panic!("{}: reverse: {e:?}", scheme.name()));
}
}
assert_eq!(
snapshot_all(&media),
original,
"{}: multi-track round trip must be byte-identical",
scheme.name()
);
}
}
#[test]
fn counter_iv_runs_continuously_across_tracks() {
const BASE: u64 = 0x0102_0304_0506_0708;
let mut media = multi_track_media();
let first_track_len = media.tracks[0].samples.len();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Counter,
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
CencEncryptor::resume(KEY, BASE)
.encrypt(&mut media, &cfg)
.expect("encrypt");
let ivs: Vec<Vec<u8>> = media
.tracks
.iter()
.flat_map(|t| {
t.encryption
.as_ref()
.expect("Some")
.samples
.iter()
.map(|e| e.initialization_vector.clone())
})
.collect();
for (i, iv) in ivs.iter().enumerate() {
assert_eq!(
iv.as_slice(),
&(BASE + i as u64).to_be_bytes()[..],
"IV {i} (Media-wide index) must be base + i"
);
}
assert_eq!(
ivs[first_track_len].as_slice(),
&(BASE + first_track_len as u64).to_be_bytes()[..],
"track 1's first IV must continue track 0's counter, not restart at base"
);
}
#[test]
fn constant_iv_under_cenc_errors_and_leaves_media_untouched() {
let mut media = multi_track_media();
let original = snapshot_all(&media);
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Constant([0x5Au8; KEY_LEN]),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
assert_eq!(snapshot_all(&media), original, "no sample may be ciphered");
assert!(media.tracks.iter().all(|t| t.encryption.is_none()));
}
#[test]
fn rejected_explicit_iv_list_leaves_media_untouched() {
let mut media = multi_track_media();
let original = snapshot_all(&media);
let first_track_len = media.tracks[0].samples.len();
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(distinct_ivs(first_track_len, 8)),
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
assert_eq!(snapshot_all(&media), original, "no sample may be ciphered");
assert!(media.tracks.iter().all(|t| t.encryption.is_none()));
}
#[test]
fn planned_duplicate_ivs_are_rejected_before_ciphering() {
let dup = alloc::vec![0u8, 0, 0, 0, 0, 0, 0, 1];
let plan: Vec<Vec<Vec<u8>>> = alloc::vec![
alloc::vec![dup.clone(), alloc::vec![0u8, 0, 0, 0, 0, 0, 0, 2]],
alloc::vec![dup],
];
let err = assert_ivs_unique(&plan).unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
let good_plan: Vec<Vec<Vec<u8>>> = alloc::vec![
alloc::vec![
alloc::vec![0u8, 0, 0, 0, 0, 0, 0, 0],
alloc::vec![0u8, 0, 0, 0, 0, 0, 0, 1],
],
alloc::vec![alloc::vec![0u8, 0, 0, 0, 0, 0, 0, 2]],
];
assert!(
assert_ivs_unique(&good_plan).is_ok(),
"a correctly Media-wide-continuous plan must pass the backstop"
);
}
#[test]
fn planned_ivs_are_validated_before_any_sample_is_touched() {
let mut media = multi_track_media();
let original = snapshot_all(&media);
let n = media.tracks[0].samples.len();
assert!(n > 1, "fixture must have more than one sample to bite");
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Explicit(distinct_ivs(n - 1, 8)), pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let err = CencEncryptor::new(KEY)
.encrypt(&mut media, &cfg)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)));
assert_eq!(
snapshot_all(&media),
original,
"a rejected plan must leave every track's samples byte-identical"
);
assert!(media.tracks.iter().all(|t| t.encryption.is_none()));
}
#[test]
fn successive_encrypt_calls_on_one_instance_produce_disjoint_ivs() {
let cfg = EncryptConfig {
scheme: CencScheme::Cenc,
kid: KID,
iv: IvGen::Counter,
pattern: None,
subsample: SubsamplePolicy::WholeSample,
};
let mut enc = CencEncryptor::new(KEY);
let mut media_a = clear_media();
enc.encrypt(&mut media_a, &cfg).expect("first encrypt");
let ivs_a: alloc::collections::BTreeSet<Vec<u8>> = media_a.tracks[0]
.encryption
.as_ref()
.expect("Some")
.samples
.iter()
.map(|e| e.initialization_vector.clone())
.collect();
let mut media_b = clear_media();
enc.encrypt(&mut media_b, &cfg)
.expect("second encrypt, same instance/key");
let ivs_b: alloc::collections::BTreeSet<Vec<u8>> = media_b.tracks[0]
.encryption
.as_ref()
.expect("Some")
.samples
.iter()
.map(|e| e.initialization_vector.clone())
.collect();
assert!(
ivs_a.is_disjoint(&ivs_b),
"two encrypt() calls sharing one CencEncryptor (hence one key) must never reuse an \
IV — a shared IV under one key is a two-time pad (ISO/IEC 23001-7 §9.2)"
);
let mut media_c = clear_media();
CencEncryptor::new(KEY)
.encrypt(&mut media_c, &cfg)
.expect("fresh instance, third encrypt");
let ivs_c: alloc::collections::BTreeSet<Vec<u8>> = media_c.tracks[0]
.encryption
.as_ref()
.expect("Some")
.samples
.iter()
.map(|e| e.initialization_vector.clone())
.collect();
assert_eq!(
ivs_a, ivs_c,
"a fresh CencEncryptor::new(KEY) must reproduce media_a's exact IV set: \
CencEncryptor::new always starts the counter at 0, so reusing the SAME key across \
SEPARATE fresh instances is the one collision this type cannot structurally prevent \
— documented as the caller's obligation"
);
}
#[test]
fn real_fixture_samples_take_the_zero_copy_fast_path() {
let mut media = clear_media();
let track = &mut media.tracks[0];
assert!(track.samples.len() > 1, "fixture must carry samples");
for sample in &mut track.samples {
let took_fast_path =
cenc_crypto::rewrite_in_place(&mut sample.data, |_buf| Ok(())).expect("no-op ok");
assert!(
took_fast_path,
"a freshly-demuxed, not-yet-fanned-out sample must take the zero-copy path"
);
}
}
#[test]
fn fanned_out_sample_forces_the_copy_fallback() {
let mut media = clear_media();
let sample = &mut media.tracks[0].samples[0];
let fanned_out_consumer = sample.data.clone(); let took_fast_path = cenc_crypto::rewrite_in_place(&mut sample.data, |buf| {
buf[0] ^= 0xFF;
Ok(())
})
.expect("rewrite ok");
assert!(
!took_fast_path,
"a sample already fanned out to another consumer must not take the fast path"
);
assert_ne!(
sample.data[0], fanned_out_consumer[0],
"the rewritten handle's first byte must differ from the untouched consumer's"
);
}
}