Skip to main content

vsd_mp4/decrypt/
cenc.rs

1use crate::{
2    Mp4Parser,
3    boxes::{SchmBox, SencBox, TencBox, TrunBox},
4    data,
5    decrypt::{cipher::CencProcessor, reader::Mp4Reader},
6    error::{Error, Result},
7    parser,
8};
9use std::io::{Read, Write};
10
11#[derive(Clone, Default)]
12struct Tenc {
13    scheme_type: u32,
14    per_sample_iv_size: u8,
15    constant_iv: Option<[u8; 16]>,
16    crypt_byte_block: u8,
17    skip_byte_block: u8,
18}
19
20/// A decrypter for Common Encryption (CENC) protected ISO Base Media File Format (ISOBMFF) streams.
21///
22/// `CencDecrypter` provides utilities to decrypt fragmented MP4 files or individual fragments
23/// encrypted using common encryption schemes (e.g., `cenc`, `cbcs`, `cens`, `cbc1`).
24#[derive(Clone)]
25pub struct CencDecrypter {
26    key: [u8; 16],
27    tenc: Option<Tenc>,
28}
29
30impl CencDecrypter {
31    /// Creates a new `CencDecrypter` with a 16-byte decryption key.
32    ///
33    /// The `key` must be a hexadecimal string representing the 16-byte decryption key.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the hex string is invalid or if the decoded key is not exactly 16 bytes.
38    pub fn new(key: &str) -> Result<Self> {
39        Ok(Self {
40            key: hex::decode(key.to_ascii_lowercase().replace('-', ""))?
41                .try_into()
42                .map_err(|v: Vec<u8>| Error::InvalidKeySize(v.len()))?,
43            tenc: None,
44        })
45    }
46
47    /// Creates a new `CencDecrypter` with a decryption key and pre-parse initialization data.
48    ///
49    /// The initialization data (`init`) represents the MP4 metadata/header (e.g., `moov` box),
50    /// which is parsed to extract the track encryption (`tenc`) and scheme type (`schm`) parameters.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the key is invalid or if parsing the initialization data fails.
55    pub fn with_init(key: &str, init: &[u8]) -> Result<Self> {
56        let mut decrypter = Self::new(key)?;
57        decrypter.tenc = Some(Self::parse_init(init)?);
58        Ok(decrypter)
59    }
60
61    fn parse_init(init: &[u8]) -> Result<Tenc> {
62        let tenc = data!(Tenc::default());
63
64        Mp4Parser::new()
65            .base_box("enca", parser::audio_sample_entry)
66            .base_box("encv", parser::visual_sample_entry)
67            .base_box("mdia", parser::children)
68            .base_box("minf", parser::children)
69            .base_box("moov", parser::children)
70            .base_box("schi", parser::children)
71            .base_box("sinf", parser::children)
72            .base_box("stbl", parser::children)
73            .full_box("stsd", parser::sample_description)
74            .base_box("trak", parser::children)
75            .full_box("schm", {
76                let tenc = tenc.clone();
77                move |mut box_| {
78                    tenc.borrow_mut().scheme_type = SchmBox::new(&mut box_)?.scheme_type;
79                    Ok(())
80                }
81            })
82            .full_box("tenc", {
83                let tenc = tenc.clone();
84                move |mut box_| {
85                    let tenc_box = TencBox::new(&mut box_)?;
86                    let t = &mut *tenc.borrow_mut();
87                    t.per_sample_iv_size = tenc_box.per_sample_iv_size;
88                    t.constant_iv = tenc_box.constant_iv;
89                    t.crypt_byte_block = tenc_box.crypt_byte_block;
90                    t.skip_byte_block = tenc_box.skip_byte_block;
91                    box_.parser.stop();
92                    Ok(())
93                }
94            })
95            .parse(init, true, true)?;
96
97        Ok(tenc.take())
98    }
99
100    /// Decrypts a single MP4 fragment in-place and returns the decrypted fragment.
101    ///
102    /// A fragment typically consists of a movie fragment box (`moof`) followed by a media data box (`mdat`).
103    ///
104    /// If `init` is provided, its track encryption parameters are parsed and used for decryption.
105    /// Otherwise, cached parameters are used. If both are missing, it attempts to parse
106    /// the track encryption parameters directly from the `input` fragment.
107    ///
108    /// # Limitations
109    ///
110    /// * **Single-Track Assumption**: The function assumes that the fragment contains media data
111    ///   for a single track, or that all encrypted tracks share the same encryption parameters.
112    ///   It does not differentiate between tracks using track IDs (e.g., `track_id` in `tenc`, `traf`,
113    ///   or `tfhd` boxes is ignored), and applies the parsed track encryption parameters to all samples
114    ///   sequentially.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if parsing the fragment, parsing the initialization data, or decryption fails.
119    pub fn decrypt_fragment(&self, mut input: Vec<u8>, init: Option<&[u8]>) -> Result<Vec<u8>> {
120        if input.is_empty() {
121            return Ok(input);
122        }
123
124        let tenc_own;
125        let tenc = if let Some(init) = init {
126            tenc_own = Self::parse_init(init)?;
127            &tenc_own
128        } else if let Some(cached) = &self.tenc {
129            cached
130        } else {
131            tenc_own = Self::parse_init(&input)?;
132            &tenc_own
133        };
134
135        if tenc.scheme_type == 0 {
136            return Ok(input);
137        }
138
139        #[derive(Default)]
140        struct State {
141            start: u64,
142            senc: Option<SencBox>,
143            trun: Option<TrunBox>,
144        }
145        let state = data!(State::default());
146        let iv_size = tenc.per_sample_iv_size;
147        let constant_iv = tenc.constant_iv;
148
149        Mp4Parser::new()
150            .base_box("traf", parser::children)
151            .base_box("moof", {
152                let state = state.clone();
153                move |box_| {
154                    state.borrow_mut().start = box_.start;
155                    parser::children(box_)
156                }
157            })
158            .full_box("senc", {
159                let state = state.clone();
160                move |mut box_| {
161                    state.borrow_mut().senc =
162                        Some(SencBox::new(&mut box_, iv_size, constant_iv.as_ref())?);
163                    Ok(())
164                }
165            })
166            .full_box("trun", {
167                let state = state.clone();
168                move |mut box_| {
169                    state.borrow_mut().trun = Some(TrunBox::new(&mut box_)?);
170                    Ok(())
171                }
172            })
173            .parse(&input, true, true)?;
174
175        let state = state.take();
176        let (Some(trun), Some(senc)) = (state.trun, state.senc) else {
177            return Ok(input);
178        };
179        let mut processor = CencProcessor::new(
180            &self.key,
181            tenc.crypt_byte_block,
182            tenc.skip_byte_block,
183            tenc.scheme_type,
184        );
185        let mut offset = (state.start + trun.data_offset.unwrap_or(0) as u64) as usize;
186        let output_len = input.len();
187
188        for (trun_sample, senc_sample) in trun.sample_data.iter().zip(senc.samples.iter()) {
189            let size = trun_sample.sample_size.unwrap_or_default() as usize;
190            if size == 0 {
191                continue;
192            }
193
194            let end = offset + size;
195            if end > output_len {
196                break;
197            }
198
199            processor.decrypt_sample_inplace(&mut input[offset..end], senc_sample);
200            offset = end;
201        }
202
203        Ok(input)
204    }
205
206    /// Decrypts a fragmented MP4 stream from a reader and writes the decrypted output to a writer.
207    ///
208    /// If `init` is provided, it is parsed for track encryption parameters. Otherwise, the decrypter
209    /// will read the initialization data from the beginning of the stream.
210    ///
211    /// # Limitations
212    ///
213    /// * **Fragment Box Order**: It expects each fragment to consist of a `moof` box followed
214    ///   eventually by an `mdat` box. While typical for DASH/HLS fragmented streams, streams with
215    ///   complex interleaving or out-of-order boxes might not be handled correctly.
216    /// * **Unfragmented (Progressive) Streams**: Only fragmented MP4 streams or individual fragments
217    ///   are decrypted. If an unfragmented MP4 stream is processed (indicated by the absence of a `moof`
218    ///   box), it is written to the output unmodified without attempting decryption.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if reading from the stream, parsing metadata, decrypting, or writing fails.
223    pub fn decrypt_stream<R: Read, W: Write>(
224        &mut self,
225        reader: &mut R,
226        writer: &mut W,
227        init: Option<&[u8]>,
228    ) -> Result<()> {
229        let mut next = if let Some(init) = init {
230            self.tenc = Some(Self::parse_init(init)?);
231            Mp4Reader::header(reader)?
232        } else {
233            let (init, moof) = Mp4Reader::init(reader)?;
234            writer.write_all(&init)?;
235
236            if moof.is_none() {
237                std::io::copy(reader, writer)?;
238                return Ok(());
239            }
240
241            self.tenc = Some(Self::parse_init(&init)?);
242            moof
243        };
244
245        while let Some(header) = next {
246            if &header.box_type == b"moof" {
247                let mut fragment = header.data(reader)?;
248
249                while let Some(next) = Mp4Reader::header(reader)? {
250                    fragment.append(&mut next.data(reader)?);
251
252                    if &next.box_type == b"mdat" {
253                        break;
254                    }
255                }
256
257                let decrypted = self.decrypt_fragment(fragment, None)?;
258                writer.write_all(&decrypted)?;
259            } else {
260                writer.write_all(&header.data(reader)?)?;
261            }
262
263            next = Mp4Reader::header(reader)?;
264        }
265
266        writer.flush()?;
267        Ok(())
268    }
269}