Skip to main content

sheathe_crypto/
lib.rs

1//! Common Encryption (ISO/IEC 23001-7) for **sheathe**.
2//!
3//! Mirrors Shaka Packager's `media/crypto`: the CENC protection schemes plus a
4//! sample [`Encryptor`] that applies them. Two schemes are implemented on top of
5//! a pure-Rust AES-128 block cipher:
6//!
7//! - **`cenc`** — AES-128 **CTR**. The keystream runs continuously across a
8//!   sample's protected byte ranges (clear ranges do not advance the counter).
9//! - **`cbcs`** — AES-128 **CBC** with pattern encryption (crypt 1 block, skip
10//!   9), CBC chaining reset to the constant IV at the start of each subsample;
11//!   a trailing partial block (< 16 bytes) is left in the clear.
12//!
13//! Encryption operates on a list of [`Subsample`] (clear/protected byte runs),
14//! so the caller (the MP4 muxer) decides the NAL-aware clear/protected split;
15//! this crate stays format-agnostic.
16
17use aes::cipher::generic_array::GenericArray;
18use aes::cipher::{BlockEncrypt, KeyInit};
19use aes::Aes128;
20use sheathe_core::{Error, Result};
21
22/// A CENC protection scheme (the `schm` `scheme_type`).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Scheme {
25    /// `cenc` — AES-128 CTR, (sub)sample encryption.
26    Cenc,
27    /// `cbcs` — AES-128 CBC, pattern encryption (Apple FairPlay friendly).
28    Cbcs,
29}
30
31impl Scheme {
32    /// The four-character scheme type written into the `schm` box.
33    pub fn scheme_type(self) -> [u8; 4] {
34        match self {
35            Scheme::Cenc => *b"cenc",
36            Scheme::Cbcs => *b"cbcs",
37        }
38    }
39}
40
41/// A content key plus its 16-byte Key ID (`KID`).
42#[derive(Debug, Clone)]
43pub struct ContentKey {
44    /// The 16-byte key identifier referenced by `tenc`/`pssh`.
45    pub kid: [u8; 16],
46    /// The 16-byte AES content key.
47    pub key: [u8; 16],
48}
49
50/// A contiguous run within a sample: `clear` plaintext bytes followed by
51/// `protected` bytes to encrypt.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Subsample {
54    /// Number of leading clear (unencrypted) bytes.
55    pub clear: u32,
56    /// Number of following protected (encrypted) bytes.
57    pub protected: u32,
58}
59
60/// `cbcs` pattern: encrypt 1 of every 10 sixteen-byte blocks.
61const CBCS_CRYPT_BLOCKS: usize = 1;
62const CBCS_PATTERN_BLOCKS: usize = 10;
63
64/// An AES-128 sample encryptor bound to one content key.
65pub struct Encryptor {
66    cipher: Aes128,
67}
68
69impl Encryptor {
70    /// Build an encryptor for a 16-byte AES-128 key.
71    pub fn new(key: &[u8; 16]) -> Self {
72        Self {
73            cipher: Aes128::new(GenericArray::from_slice(key)),
74        }
75    }
76
77    /// Encrypt `data` in place under `scheme`, treating it as the given
78    /// subsample layout. `iv` is the 16-byte per-sample initialization vector.
79    pub fn encrypt(
80        &self,
81        scheme: Scheme,
82        iv: &[u8; 16],
83        data: &mut [u8],
84        subsamples: &[Subsample],
85    ) -> Result<()> {
86        // Validate the layout covers exactly `data`.
87        let total: u64 = subsamples
88            .iter()
89            .map(|s| u64::from(s.clear) + u64::from(s.protected))
90            .sum();
91        if total != data.len() as u64 {
92            return Err(Error::malformed("subsample layout does not cover sample"));
93        }
94        match scheme {
95            Scheme::Cenc => self.cenc(iv, data, subsamples),
96            Scheme::Cbcs => self.cbcs(iv, data, subsamples),
97        }
98        Ok(())
99    }
100
101    /// AES-128-CTR with a counter continuous across protected bytes.
102    fn cenc(&self, iv: &[u8; 16], data: &mut [u8], subsamples: &[Subsample]) {
103        let mut counter = *iv;
104        let mut keystream = [0u8; 16];
105        let mut ks_pos = 16usize; // force a fresh block on first use
106        let mut off = 0usize;
107
108        for s in subsamples {
109            off += s.clear as usize;
110            let end = off + s.protected as usize;
111            while off < end {
112                if ks_pos == 16 {
113                    keystream = counter;
114                    self.encrypt_block(&mut keystream);
115                    incr_be(&mut counter);
116                    ks_pos = 0;
117                }
118                data[off] ^= keystream[ks_pos];
119                ks_pos += 1;
120                off += 1;
121            }
122        }
123    }
124
125    /// AES-128-CBC with 1:9 pattern encryption per subsample.
126    fn cbcs(&self, iv: &[u8; 16], data: &mut [u8], subsamples: &[Subsample]) {
127        let mut off = 0usize;
128        for s in subsamples {
129            off += s.clear as usize;
130            let mut remaining = s.protected as usize;
131            let mut chain = *iv;
132            let mut block_index = 0usize;
133            while remaining >= 16 {
134                if block_index % CBCS_PATTERN_BLOCKS < CBCS_CRYPT_BLOCKS {
135                    let mut block = [0u8; 16];
136                    block.copy_from_slice(&data[off..off + 16]);
137                    for (b, c) in block.iter_mut().zip(chain.iter()) {
138                        *b ^= *c;
139                    }
140                    self.encrypt_block(&mut block);
141                    data[off..off + 16].copy_from_slice(&block);
142                    chain = block;
143                }
144                off += 16;
145                remaining -= 16;
146                block_index += 1;
147            }
148            off += remaining; // trailing partial block stays clear
149        }
150    }
151
152    /// Encrypt one 16-byte block in place (AES-128-ECB primitive).
153    fn encrypt_block(&self, block: &mut [u8; 16]) {
154        let mut ga = GenericArray::clone_from_slice(block);
155        self.cipher.encrypt_block(&mut ga);
156        block.copy_from_slice(&ga);
157    }
158}
159
160/// Increment a 16-byte big-endian counter by one (wrapping).
161fn incr_be(counter: &mut [u8; 16]) {
162    for byte in counter.iter_mut().rev() {
163        let (v, carry) = byte.overflowing_add(1);
164        *byte = v;
165        if !carry {
166            break;
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    const KEY: [u8; 16] = [
176        0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f,
177        0x3c,
178    ];
179
180    fn hex(s: &str) -> Vec<u8> {
181        (0..s.len())
182            .step_by(2)
183            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
184            .collect()
185    }
186
187    #[test]
188    fn cenc_matches_nist_ctr_vector() {
189        // NIST SP800-38A, F.5.1 (CTR-AES128.Encrypt), first block.
190        let iv = hex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
191        let mut data = hex("6bc1bee22e409f96e93d7e117393172a");
192        let enc = Encryptor::new(&KEY);
193        let subs = [Subsample {
194            clear: 0,
195            protected: 16,
196        }];
197        enc.encrypt(Scheme::Cenc, iv[..].try_into().unwrap(), &mut data, &subs)
198            .unwrap();
199        assert_eq!(data, hex("874d6191b620e3261bef6864990db6ce"));
200    }
201
202    #[test]
203    fn cbcs_first_block_matches_nist_cbc_vector() {
204        // NIST SP800-38A, F.2.1 (CBC-AES128.Encrypt), first block.
205        let iv = hex("000102030405060708090a0b0c0d0e0f");
206        let mut data = hex("6bc1bee22e409f96e93d7e117393172a");
207        let enc = Encryptor::new(&KEY);
208        let subs = [Subsample {
209            clear: 0,
210            protected: 16,
211        }];
212        enc.encrypt(Scheme::Cbcs, iv[..].try_into().unwrap(), &mut data, &subs)
213            .unwrap();
214        assert_eq!(data, hex("7649abac8119b246cee98e9b12e9197d"));
215    }
216
217    #[test]
218    fn cenc_leaves_clear_bytes_untouched() {
219        let iv = [0u8; 16];
220        let mut data = vec![0xAAu8; 32];
221        let enc = Encryptor::new(&KEY);
222        // 8 clear, 24 protected (8+24=32).
223        enc.encrypt(
224            Scheme::Cenc,
225            &iv,
226            &mut data,
227            &[Subsample {
228                clear: 8,
229                protected: 24,
230            }],
231        )
232        .unwrap();
233        assert!(
234            data[..8].iter().all(|&b| b == 0xAA),
235            "clear prefix must be untouched"
236        );
237        assert!(
238            data[8..].iter().any(|&b| b != 0xAA),
239            "protected region must change"
240        );
241    }
242
243    #[test]
244    fn cbcs_pattern_skips_blocks() {
245        let iv = [0u8; 16];
246        // 10 blocks: only block 0 is encrypted, blocks 1..9 skipped (clear).
247        let mut data = vec![0x11u8; 160];
248        let original = data.clone();
249        let enc = Encryptor::new(&KEY);
250        enc.encrypt(
251            Scheme::Cbcs,
252            &iv,
253            &mut data,
254            &[Subsample {
255                clear: 0,
256                protected: 160,
257            }],
258        )
259        .unwrap();
260        assert_ne!(data[..16], original[..16], "first block encrypted");
261        assert_eq!(data[16..], original[16..], "blocks 1..9 skipped");
262    }
263
264    #[test]
265    fn rejects_mismatched_layout() {
266        let enc = Encryptor::new(&KEY);
267        let mut data = vec![0u8; 10];
268        let err = enc.encrypt(
269            Scheme::Cenc,
270            &[0u8; 16],
271            &mut data,
272            &[Subsample {
273                clear: 0,
274                protected: 9,
275            }],
276        );
277        assert!(err.is_err());
278    }
279
280    #[test]
281    fn cenc_round_trips_across_subsamples() {
282        // CTR is symmetric: encrypting the ciphertext again recovers the input,
283        // exercising the continuous counter across multiple subsamples and the
284        // preservation of clear regions.
285        let enc = Encryptor::new(&KEY);
286        let iv = [3u8; 16];
287        let subs = [
288            Subsample {
289                clear: 5,
290                protected: 20,
291            },
292            Subsample {
293                clear: 10,
294                protected: 65,
295            },
296        ];
297        let original: Vec<u8> = (0..100u8).collect();
298        let mut data = original.clone();
299
300        enc.encrypt(Scheme::Cenc, &iv, &mut data, &subs).unwrap();
301        assert_ne!(data, original, "ciphertext must differ");
302        assert_eq!(&data[..5], &original[..5], "leading clear bytes preserved");
303
304        enc.encrypt(Scheme::Cenc, &iv, &mut data, &subs).unwrap();
305        assert_eq!(data, original, "CTR round-trip restores plaintext");
306    }
307}