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 four CENC protection schemes
4//! plus a sample [`Encryptor`] that applies them, all on top of a pure-Rust
5//! AES-128 block cipher.
6//!
7//! - **`cenc`** — AES-128 **CTR**, full-region. The keystream runs continuously
8//!   across a sample's protected byte ranges (clear ranges do not advance the
9//!   counter).
10//! - **`cens`** — AES-128 **CTR** with [pattern](Pattern) encryption: only the
11//!   crypt-phase blocks consume keystream; skipped blocks pass through clear,
12//!   the counter continuing across them.
13//! - **`cbc1`** — AES-128 **CBC**, full-region. CBC chaining runs continuously
14//!   across the sample (clear ranges are skipped); a trailing partial block
15//!   (< 16 bytes) of each protected range is left in the clear.
16//! - **`cbcs`** — AES-128 **CBC** with pattern encryption, chaining reset to the
17//!   constant IV at the start of each subsample; trailing partial blocks clear.
18//!
19//! Pattern encryption (`cens`/`cbcs`) is, by convention, applied to video only;
20//! audio uses [`Pattern::NONE`] (full-region) even under those schemes. The
21//! caller decides the per-track pattern and the NAL-aware clear/protected split
22//! via the [`Subsample`] list, so this crate stays format-agnostic.
23
24use aes::cipher::{BlockCipherEncrypt, KeyInit};
25use aes::{Aes128, Aes256};
26use sheathe_core::{Error, Result};
27
28mod pssh;
29pub use pssh::ProtectionSystem;
30
31/// A CENC protection scheme (the `schm` `scheme_type`).
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Scheme {
34    /// `cenc` — AES-128 CTR, (sub)sample encryption.
35    Cenc,
36    /// `cens` — AES-128 CTR with pattern encryption.
37    Cens,
38    /// `cbc1` — AES-128 CBC, full-region (sub)sample encryption.
39    Cbc1,
40    /// `cbcs` — AES-128 CBC, pattern encryption (Apple FairPlay friendly).
41    Cbcs,
42}
43
44impl Scheme {
45    /// The four-character scheme type written into the `schm` box.
46    pub fn scheme_type(self) -> [u8; 4] {
47        match self {
48            Scheme::Cenc => *b"cenc",
49            Scheme::Cens => *b"cens",
50            Scheme::Cbc1 => *b"cbc1",
51            Scheme::Cbcs => *b"cbcs",
52        }
53    }
54
55    /// CBC-based schemes (`cbc1`, `cbcs`); the others are CTR (`cenc`, `cens`).
56    /// Non-pattern CBC requires 16-byte-aligned protected subsample ranges.
57    pub fn is_cbc(self) -> bool {
58        matches!(self, Scheme::Cbc1 | Scheme::Cbcs)
59    }
60
61    /// Pattern-capable schemes (`cens`, `cbcs`) — written with a version-1
62    /// `tenc` carrying the crypt/skip block counts.
63    pub fn is_pattern(self) -> bool {
64        matches!(self, Scheme::Cens | Scheme::Cbcs)
65    }
66
67    /// `cbcs` reuses one constant IV for every sample; the others derive a
68    /// unique per-sample IV.
69    pub fn uses_constant_iv(self) -> bool {
70        matches!(self, Scheme::Cbcs)
71    }
72}
73
74/// A crypt/skip block pattern (ISO/IEC 23001-7 §9.6): encrypt `crypt_blocks`
75/// 16-byte blocks, then leave `skip_blocks` blocks clear, repeating across a
76/// protected range. [`Pattern::NONE`] (`crypt_blocks == 0`) means full-region
77/// encryption — used by `cenc`/`cbc1` and for audio under `cens`/`cbcs`.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct Pattern {
80    /// Number of 16-byte blocks encrypted per pattern cycle.
81    pub crypt_blocks: u8,
82    /// Number of 16-byte blocks skipped (left clear) per cycle.
83    pub skip_blocks: u8,
84}
85
86impl Pattern {
87    /// No pattern: encrypt the whole protected range.
88    pub const NONE: Pattern = Pattern { crypt_blocks: 0, skip_blocks: 0 };
89    /// The standard CMAF video pattern: encrypt 1 block, skip 9.
90    pub const VIDEO: Pattern = Pattern { crypt_blocks: 1, skip_blocks: 9 };
91
92    /// Build a pattern from Shaka-style `--crypt_byte_block` / `--skip_byte_block`.
93    /// `(0, 0)` is treated as [`Pattern::NONE`].
94    pub const fn from_blocks(crypt: u8, skip: u8) -> Pattern {
95        Pattern { crypt_blocks: crypt, skip_blocks: skip }
96    }
97
98    /// Whether this pattern leaves any blocks clear (i.e. is a real pattern).
99    fn is_patterned(self) -> bool {
100        self.crypt_blocks != 0
101    }
102}
103
104/// A content key plus its 16-byte Key ID (`KID`).
105#[derive(Debug, Clone)]
106pub struct ContentKey {
107    /// The 16-byte key identifier referenced by `tenc`/`pssh`.
108    pub kid: [u8; 16],
109    /// The 16-byte AES content key.
110    pub key: [u8; 16],
111}
112
113impl ContentKey {
114    /// The key and KID for crypto period `period`, derived by left-rotating both
115    /// by `period % 16` bytes — the naive scheme Shaka Packager uses for raw-key
116    /// rotation. Period 0 returns the key unchanged.
117    pub fn rotated(&self, period: u32) -> ContentKey {
118        let n = (period % 16) as usize;
119        let mut kid = [0u8; 16];
120        let mut key = [0u8; 16];
121        for i in 0..16 {
122            kid[i] = self.kid[(i + n) % 16];
123            key[i] = self.key[(i + n) % 16];
124        }
125        ContentKey { kid, key }
126    }
127}
128
129/// A contiguous run within a sample: `clear` plaintext bytes followed by
130/// `protected` bytes to encrypt.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct Subsample {
133    /// Number of leading clear (unencrypted) bytes.
134    pub clear: u32,
135    /// Number of following protected (encrypted) bytes.
136    pub protected: u32,
137}
138
139/// An AES-128 sample encryptor bound to one content key.
140pub struct Encryptor {
141    cipher: Aes128,
142}
143
144impl Encryptor {
145    /// Build an encryptor for a 16-byte AES-128 key.
146    pub fn new(key: &[u8; 16]) -> Self {
147        Self { cipher: Aes128::new_from_slice(key).expect("AES-128 key is 16 bytes") }
148    }
149
150    /// Encrypt `data` in place under `scheme` with the given `pattern`, treating
151    /// it as the given subsample layout. `iv` is the 16-byte initialization
152    /// vector (per-sample, or the constant IV for `cbcs`). `pattern` must be
153    /// [`Pattern::NONE`] for the non-pattern schemes `cenc`/`cbc1`.
154    pub fn encrypt(
155        &self,
156        scheme: Scheme,
157        pattern: Pattern,
158        iv: &[u8; 16],
159        data: &mut [u8],
160        subsamples: &[Subsample],
161    ) -> Result<()> {
162        // Validate the layout covers exactly `data`.
163        let total: u64 =
164            subsamples.iter().map(|s| u64::from(s.clear) + u64::from(s.protected)).sum();
165        if total != data.len() as u64 {
166            return Err(Error::malformed("subsample layout does not cover sample"));
167        }
168        if pattern.is_patterned() && !scheme.is_pattern() {
169            return Err(Error::malformed("pattern set on a non-pattern scheme"));
170        }
171        if scheme.is_cbc() {
172            self.cbc(pattern, iv, data, subsamples);
173        } else {
174            self.ctr(pattern, iv, data, subsamples);
175        }
176        Ok(())
177    }
178
179    /// AES-128-CTR (`cenc`/`cens`). The counter runs continuously over the
180    /// encrypted byte ranges of the whole sample; clear bytes and skipped
181    /// pattern blocks do not advance it. For [`Pattern::NONE`] the encrypted
182    /// ranges are the full protected ranges (`cenc`); otherwise they are the
183    /// crypt-phase blocks of each range (`cens`).
184    fn ctr(&self, pattern: Pattern, iv: &[u8; 16], data: &mut [u8], subsamples: &[Subsample]) {
185        let mut counter = *iv;
186        let mut keystream = [0u8; 16];
187        let mut ks_pos = 16usize; // force a fresh block on first use
188
189        for_each_crypt_range(pattern, subsamples, |start, len| {
190            for byte in &mut data[start..start + len] {
191                if ks_pos == 16 {
192                    keystream = counter;
193                    self.encrypt_block(&mut keystream);
194                    incr_be(&mut counter);
195                    ks_pos = 0;
196                }
197                *byte ^= keystream[ks_pos];
198                ks_pos += 1;
199            }
200        });
201    }
202
203    /// AES-128-CBC (`cbc1`/`cbcs`). For [`Pattern::NONE`] (`cbc1`) the CBC chain
204    /// runs continuously across the sample's protected ranges, seeded from `iv`;
205    /// each range's trailing partial block (< 16 bytes) is left clear. For a
206    /// pattern (`cbcs`) the chain resets to the constant `iv` at the start of
207    /// each subsample and advances only over crypt-phase blocks.
208    fn cbc(&self, pattern: Pattern, iv: &[u8; 16], data: &mut [u8], subsamples: &[Subsample]) {
209        let mut off = 0usize;
210        // `cbc1` chains across subsamples; `cbcs` reuses the constant IV per
211        // subsample. A single running chain handles both: it is only reset per
212        // subsample when a pattern is in effect.
213        let mut chain = *iv;
214        for s in subsamples {
215            off += s.clear as usize;
216            if pattern.is_patterned() {
217                chain = *iv;
218            }
219            let mut remaining = s.protected as usize;
220            let mut block_index = 0usize;
221            let cycle = pattern.crypt_blocks as usize + pattern.skip_blocks as usize;
222            while remaining >= 16 {
223                let encrypt =
224                    !pattern.is_patterned() || block_index % cycle < pattern.crypt_blocks as usize;
225                if encrypt {
226                    let mut block = [0u8; 16];
227                    block.copy_from_slice(&data[off..off + 16]);
228                    for (b, c) in block.iter_mut().zip(chain.iter()) {
229                        *b ^= *c;
230                    }
231                    self.encrypt_block(&mut block);
232                    data[off..off + 16].copy_from_slice(&block);
233                    chain = block;
234                }
235                off += 16;
236                remaining -= 16;
237                block_index += 1;
238            }
239            off += remaining; // trailing partial block stays clear
240        }
241    }
242
243    /// Encrypt one 16-byte block in place (AES-128-ECB primitive).
244    fn encrypt_block(&self, block: &mut [u8; 16]) {
245        let mut ga = (*block).into();
246        self.cipher.encrypt_block(&mut ga);
247        block.copy_from_slice(&ga);
248    }
249}
250
251/// Invoke `f(start, len)` for each contiguous byte range that gets encrypted,
252/// in order, given a subsample layout and pattern. With [`Pattern::NONE`] this
253/// is each protected range whole; with a crypt/skip pattern it is the
254/// crypt-phase blocks of each protected range. Per ISO/IEC 23001-7 §9.6, a
255/// trailing partial block (< 16 bytes) of a protected range is left in the
256/// clear under pattern encryption, so the crypt phase only covers whole blocks.
257fn for_each_crypt_range(
258    pattern: Pattern,
259    subsamples: &[Subsample],
260    mut f: impl FnMut(usize, usize),
261) {
262    let mut off = 0usize;
263    for s in subsamples {
264        off += s.clear as usize;
265        let protected = s.protected as usize;
266        if !pattern.is_patterned() {
267            if protected > 0 {
268                f(off, protected);
269            }
270            off += protected;
271            continue;
272        }
273        let crypt = pattern.crypt_blocks as usize * 16;
274        let skip = pattern.skip_blocks as usize * 16;
275        let mut pos = 0usize;
276        while pos < protected {
277            let phase = crypt.min(protected - pos);
278            // Encrypt only whole 16-byte blocks; any partial block at the very
279            // end of the range stays clear.
280            let whole = phase - phase % 16;
281            if whole > 0 {
282                f(off + pos, whole);
283            }
284            pos += phase;
285            pos += skip.min(protected - pos);
286        }
287        off += protected;
288    }
289}
290
291/// Increment a 16-byte big-endian counter by one (wrapping).
292fn incr_be(counter: &mut [u8; 16]) {
293    for byte in counter.iter_mut().rev() {
294        let (v, carry) = byte.overflowing_add(1);
295        *byte = v;
296        if !carry {
297            break;
298        }
299    }
300}
301
302/// AES-CBC encrypt `data` with PKCS#7 padding. `key` is 16 (AES-128) or 32
303/// (AES-256) bytes; `iv` is 16 bytes. Used for HLS `METHOD=AES-128` full-segment
304/// encryption and Widevine AES request signing (SHA-1 hash → AES-CBC).
305pub fn aes_cbc_pkcs7_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
306    if iv.len() != 16 {
307        return Err(Error::malformed("CBC IV must be 16 bytes"));
308    }
309    let padded = pkcs7_pad(data, 16);
310    aes_cbc_raw(key, iv, &padded)
311}
312
313/// AES-CBC decrypt PKCS#7-padded ciphertext. Inverse of [`aes_cbc_pkcs7_encrypt`].
314pub fn aes_cbc_pkcs7_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
315    if iv.len() != 16 {
316        return Err(Error::malformed("CBC IV must be 16 bytes"));
317    }
318    if data.len() % 16 != 0 || data.is_empty() {
319        return Err(Error::malformed("CBC ciphertext must be a non-empty multiple of 16"));
320    }
321    let plain = aes_cbc_raw_decrypt(key, iv, data)?;
322    pkcs7_unpad(&plain)
323}
324
325fn pkcs7_pad(data: &[u8], block: usize) -> Vec<u8> {
326    let n = block - (data.len() % block);
327    let mut v = data.to_vec();
328    v.extend(std::iter::repeat_n(n as u8, n));
329    v
330}
331
332fn pkcs7_unpad(data: &[u8]) -> Result<Vec<u8>> {
333    let n = *data.last().ok_or_else(|| Error::malformed("empty PKCS7 buffer"))? as usize;
334    if n == 0
335        || n > 16
336        || n > data.len()
337        || !data[data.len() - n..].iter().all(|&b| b as usize == n)
338    {
339        return Err(Error::malformed("invalid PKCS7 padding"));
340    }
341    Ok(data[..data.len() - n].to_vec())
342}
343
344fn aes_cbc_raw(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
345    let mut prev = [0u8; 16];
346    prev.copy_from_slice(iv);
347    let mut out = data.to_vec();
348    for chunk in out.chunks_mut(16) {
349        for (b, p) in chunk.iter_mut().zip(prev.iter()) {
350            *b ^= *p;
351        }
352        encrypt_block(key, chunk)?;
353        prev.copy_from_slice(chunk);
354    }
355    Ok(out)
356}
357
358fn aes_cbc_raw_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
359    let mut prev = [0u8; 16];
360    prev.copy_from_slice(iv);
361    let mut out = data.to_vec();
362    for chunk in out.chunks_mut(16) {
363        let saved = <[u8; 16]>::try_from(&chunk[..]).unwrap();
364        decrypt_block(key, chunk)?;
365        for (b, p) in chunk.iter_mut().zip(prev.iter()) {
366            *b ^= *p;
367        }
368        prev = saved;
369    }
370    Ok(out)
371}
372
373fn encrypt_block(key: &[u8], block: &mut [u8]) -> Result<()> {
374    match key.len() {
375        16 => {
376            let cipher = Aes128::new_from_slice(key).expect("16");
377            let mut b = [0u8; 16];
378            b.copy_from_slice(block);
379            cipher.encrypt_block((&mut b).into());
380            block.copy_from_slice(&b);
381            Ok(())
382        }
383        32 => {
384            let cipher = Aes256::new_from_slice(key).expect("32");
385            let mut b = [0u8; 16];
386            b.copy_from_slice(block);
387            cipher.encrypt_block((&mut b).into());
388            block.copy_from_slice(&b);
389            Ok(())
390        }
391        _ => Err(Error::malformed("AES key must be 16 or 32 bytes")),
392    }
393}
394
395fn decrypt_block(key: &[u8], block: &mut [u8]) -> Result<()> {
396    // AES decrypt = encrypt with inverse cipher. Aes128 implements BlockCipherDecrypt
397    // via the `cipher` crate when the `block-decrypt` feature is on; aes 0.9
398    // `Aes128` implements both encrypt and decrypt through `cipher::BlockDecrypt`.
399    use aes::cipher::BlockCipherDecrypt;
400    match key.len() {
401        16 => {
402            let cipher = Aes128::new_from_slice(key).expect("16");
403            let mut b = [0u8; 16];
404            b.copy_from_slice(block);
405            cipher.decrypt_block((&mut b).into());
406            block.copy_from_slice(&b);
407            Ok(())
408        }
409        32 => {
410            let cipher = Aes256::new_from_slice(key).expect("32");
411            let mut b = [0u8; 16];
412            b.copy_from_slice(block);
413            cipher.decrypt_block((&mut b).into());
414            block.copy_from_slice(&b);
415            Ok(())
416        }
417        _ => Err(Error::malformed("AES key must be 16 or 32 bytes")),
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    const KEY: [u8; 16] = [
426        0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f,
427        0x3c,
428    ];
429
430    fn hex(s: &str) -> Vec<u8> {
431        (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
432    }
433
434    #[test]
435    fn cenc_matches_nist_ctr_vector() {
436        // NIST SP800-38A, F.5.1 (CTR-AES128.Encrypt), first block.
437        let iv = hex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
438        let mut data = hex("6bc1bee22e409f96e93d7e117393172a");
439        let enc = Encryptor::new(&KEY);
440        let subs = [Subsample { clear: 0, protected: 16 }];
441        enc.encrypt(Scheme::Cenc, Pattern::NONE, iv[..].try_into().unwrap(), &mut data, &subs)
442            .unwrap();
443        assert_eq!(data, hex("874d6191b620e3261bef6864990db6ce"));
444    }
445
446    #[test]
447    fn cbc_schemes_match_nist_cbc_vector() {
448        // NIST SP800-38A, F.2.1 (CBC-AES128.Encrypt), first block. Both `cbc1`
449        // (no pattern) and `cbcs` (1:9, first block is in the crypt phase)
450        // encrypt the first block identically.
451        let iv = hex("000102030405060708090a0b0c0d0e0f");
452        let subs = [Subsample { clear: 0, protected: 16 }];
453        for (scheme, pattern) in [(Scheme::Cbc1, Pattern::NONE), (Scheme::Cbcs, Pattern::VIDEO)] {
454            let mut data = hex("6bc1bee22e409f96e93d7e117393172a");
455            let enc = Encryptor::new(&KEY);
456            enc.encrypt(scheme, pattern, iv[..].try_into().unwrap(), &mut data, &subs).unwrap();
457            assert_eq!(data, hex("7649abac8119b246cee98e9b12e9197d"), "{scheme:?}");
458        }
459    }
460
461    #[test]
462    fn cenc_leaves_clear_bytes_untouched() {
463        let iv = [0u8; 16];
464        let mut data = vec![0xAAu8; 32];
465        let enc = Encryptor::new(&KEY);
466        // 8 clear, 24 protected (8+24=32).
467        enc.encrypt(
468            Scheme::Cenc,
469            Pattern::NONE,
470            &iv,
471            &mut data,
472            &[Subsample { clear: 8, protected: 24 }],
473        )
474        .unwrap();
475        assert!(data[..8].iter().all(|&b| b == 0xAA), "clear prefix must be untouched");
476        assert!(data[8..].iter().any(|&b| b != 0xAA), "protected region must change");
477    }
478
479    #[test]
480    fn pattern_schemes_skip_blocks() {
481        // 10 blocks under a 1:9 pattern: only block 0 is encrypted; blocks 1..9
482        // are skipped (left clear). Holds for both `cbcs` (CBC) and `cens` (CTR).
483        let iv = [0u8; 16];
484        for scheme in [Scheme::Cbcs, Scheme::Cens] {
485            let mut data = vec![0x11u8; 160];
486            let original = data.clone();
487            let enc = Encryptor::new(&KEY);
488            enc.encrypt(
489                scheme,
490                Pattern::VIDEO,
491                &iv,
492                &mut data,
493                &[Subsample { clear: 0, protected: 160 }],
494            )
495            .unwrap();
496            assert_ne!(data[..16], original[..16], "{scheme:?}: first block encrypted");
497            assert_eq!(data[16..], original[16..], "{scheme:?}: blocks 1..9 skipped");
498        }
499    }
500
501    #[test]
502    fn rejects_mismatched_layout() {
503        let enc = Encryptor::new(&KEY);
504        let mut data = vec![0u8; 10];
505        let err = enc.encrypt(
506            Scheme::Cenc,
507            Pattern::NONE,
508            &[0u8; 16],
509            &mut data,
510            &[Subsample { clear: 0, protected: 9 }],
511        );
512        assert!(err.is_err());
513    }
514
515    #[test]
516    fn content_key_rotation_left_rotates_by_period() {
517        let base = ContentKey { kid: KEY, key: KEY };
518        assert_eq!(base.rotated(0).kid, KEY, "period 0 is unchanged");
519        // Period 1 left-rotates by one byte.
520        let mut expect = KEY;
521        expect.rotate_left(1);
522        assert_eq!(base.rotated(1).kid, expect);
523        assert_eq!(base.rotated(1).key, expect);
524        // Period 16 wraps back to the original.
525        assert_eq!(base.rotated(16).kid, KEY);
526    }
527
528    #[test]
529    fn rejects_pattern_on_non_pattern_scheme() {
530        let enc = Encryptor::new(&KEY);
531        let mut data = vec![0u8; 16];
532        let err = enc.encrypt(
533            Scheme::Cenc,
534            Pattern::VIDEO,
535            &[0u8; 16],
536            &mut data,
537            &[Subsample { clear: 0, protected: 16 }],
538        );
539        assert!(err.is_err());
540    }
541
542    /// Every scheme is symmetric here: CTR is self-inverse, and applying the CBC
543    /// path twice with these helpers is not — so we only round-trip the CTR
544    /// schemes via re-encryption, and check CBC via known structure elsewhere.
545    #[test]
546    fn ctr_schemes_round_trip_across_subsamples() {
547        let enc = Encryptor::new(&KEY);
548        let iv = [3u8; 16];
549        let subs = [Subsample { clear: 5, protected: 40 }, Subsample { clear: 10, protected: 65 }];
550        for (scheme, pattern) in [(Scheme::Cenc, Pattern::NONE), (Scheme::Cens, Pattern::VIDEO)] {
551            let original: Vec<u8> = (0..120u8).collect();
552            let mut data = original.clone();
553            enc.encrypt(scheme, pattern, &iv, &mut data, &subs).unwrap();
554            assert_ne!(data, original, "{scheme:?}: ciphertext must differ");
555            assert_eq!(&data[..5], &original[..5], "{scheme:?}: leading clear bytes preserved");
556            enc.encrypt(scheme, pattern, &iv, &mut data, &subs).unwrap();
557            assert_eq!(data, original, "{scheme:?}: CTR round-trip restores plaintext");
558        }
559    }
560
561    /// `cbc1` decrypts back to plaintext: decrypt the full-block portion of one
562    /// protected range and confirm the trailing partial block was left clear.
563    #[test]
564    fn cbc1_leaves_trailing_partial_clear() {
565        let enc = Encryptor::new(&KEY);
566        let iv = [7u8; 16];
567        // 37 protected bytes = 2 full blocks (32) + 5 trailing clear.
568        let original: Vec<u8> = (0..40u8).collect();
569        let mut data = original.clone();
570        enc.encrypt(
571            Scheme::Cbc1,
572            Pattern::NONE,
573            &iv,
574            &mut data,
575            &[Subsample { clear: 3, protected: 37 }],
576        )
577        .unwrap();
578        assert_eq!(&data[..3], &original[..3], "leading clear preserved");
579        assert_ne!(&data[3..35], &original[3..35], "full blocks encrypted");
580        assert_eq!(&data[35..], &original[35..], "trailing partial block left clear");
581    }
582}