Skip to main content

pdfrum_crypt/
object.rs

1//! Per-object keys and payload encryption/decryption (ISO 32000 §7.6.2,
2//! Algorithm 1).
3//!
4//! Every string and stream in a document is enciphered under a key derived
5//! from the file key and the *enclosing indirect object's* number and
6//! generation — not the number of whatever nested object the string sits in.
7//! AESV3 is the exception: at a 32-byte key the file key is used verbatim,
8//! with no per-object derivation at all.
9//!
10//! # The two directions are not mirror images
11//!
12//! The **object key** derivation is shared: encrypt and decrypt call the same
13//! three functions, which is what makes a round trip work at all.
14//!
15//! The **cipher** is where they part. RC4 is its own inverse, so
16//! [`encrypt_rc4`] and [`decrypt_rc4`] are literally the same call. AES is
17//! not: the decrypt side reproduces PDFium's streaming decoder, whose
18//! one-block lag and unvalidated padding are quirks of *reading* a file
19//! someone else wrote (Divergence D6). Writing one has no such history to
20//! honour — we emit a fresh random IV and standard PKCS#7, which the quirky
21//! reader accepts because a well-formed PKCS#7 tail is exactly the case its
22//! rules were built around. See [`encrypt_aes_cbc`].
23
24use pdfrum_object::ObjRef;
25
26use crate::key::SmallKey;
27use crate::primitives::{BLOCK, aes_cbc_decrypt, aes_cbc_encrypt, md5};
28use crate::rc4::rc4;
29
30/// Which crypt filter class a payload belongs to.
31///
32/// `Stream` and `String` resolve to one cipher because PDFium refuses a
33/// document whose `/StmF` and `/StrF` differ (Divergence D1). `Embedded` is
34/// the one class that can genuinely name another cipher — see
35/// [`crate::SecurityHandler::embedded_cipher`] and the `[oracle-bug]` note on
36/// `standard::embedded_cipher`, which records that the oracle reads `/EFF`
37/// nowhere. Only the *cipher* differs: ISO 32000-1 §7.6.5 gives every `/CF`
38/// entry the same file encryption key.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum CryptClass {
41    /// A stream's data, governed by `/StmF`.
42    Stream,
43    /// A string object, governed by `/StrF`.
44    String,
45    /// An embedded file stream, nominally governed by `/EFF`.
46    Embedded,
47}
48
49/// One AES initialisation vector, supplied by the caller of
50/// [`crate::SecurityHandler::encrypt`].
51///
52/// A named type rather than a bare `[u8; 16]` because the encrypt call
53/// already carries an object reference and a payload, and a bare array beside
54/// those is one `&[u8]` away from being passed the payload by mistake. It
55/// also gives the "where does this come from?" question somewhere to be
56/// answered: nowhere in this crate, is the answer — see the crate docs.
57///
58/// The RC4 handlers ignore it entirely.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct Iv(pub [u8; BLOCK]);
61
62impl Iv {
63    /// The vector's bytes.
64    #[must_use]
65    pub const fn bytes(&self) -> &[u8; BLOCK] {
66        &self.0
67    }
68}
69
70/// The four ASCII bytes AESV2 appends before hashing the object key.
71const AES_SALT: [u8; 4] = *b"sAlT";
72
73/// Algorithm 1's scratch buffer: the file key, then three bytes of object
74/// number and two of generation, all little-endian and all truncated.
75///
76/// The truncation is real behavior, not an oversight to guard against: an
77/// object number of 2^24 or more wraps, so two such objects share a key.
78fn salted(key: &SmallKey, obj: ObjRef) -> ([u8; 48], usize) {
79    let mut scratch = [0u8; 48];
80    let key_len = key.len().min(scratch.len());
81    if let (Some(head), Some(from)) = (scratch.get_mut(..key_len), key.bytes().get(..key_len)) {
82        head.copy_from_slice(from);
83    }
84    let num = obj.num.to_le_bytes();
85    let generation = obj.generation.to_le_bytes();
86    for (offset, byte) in num
87        .iter()
88        .take(3)
89        .chain(generation.iter().take(2))
90        .enumerate()
91    {
92        if let Some(slot) = scratch.get_mut(key_len + offset) {
93            *slot = *byte;
94        }
95    }
96    (scratch, key_len)
97}
98
99/// The RC4 object key: MD5 of the salted scratch, capped at sixteen bytes.
100///
101/// The cap is why a 16-byte file key yields a 16-byte object key rather than
102/// the 21 bytes the scratch length would suggest.
103#[must_use]
104fn rc4_object_key(key: &SmallKey, obj: ObjRef) -> Vec<u8> {
105    let (scratch, key_len) = salted(key, obj);
106    let digest = md5(scratch.get(..key_len + 5).unwrap_or(&scratch));
107    let len = (key_len + 5).min(digest.len());
108    digest.get(..len).unwrap_or(&digest).to_vec()
109}
110
111/// The AESV2 object key: MD5 of the salted scratch plus `sAlT`, all sixteen
112/// bytes of it.
113///
114/// The C++'s encrypt path truncates this to the file key length while its
115/// decrypt path does not; since AESV2 is always a 16-byte key in practice the
116/// two agree, and we implement the decrypt reading.
117#[must_use]
118fn aes_v4_object_key(key: &SmallKey, obj: ObjRef) -> [u8; 16] {
119    let (mut scratch, key_len) = salted(key, obj);
120    for (offset, byte) in AES_SALT.iter().enumerate() {
121        if let Some(slot) = scratch.get_mut(key_len + 5 + offset) {
122            *slot = *byte;
123        }
124    }
125    md5(scratch.get(..key_len + 9).unwrap_or(&scratch))
126}
127
128/// Decrypt an RC4 payload. Symmetric, so this is just the cipher.
129#[must_use]
130pub(crate) fn decrypt_rc4(key: &SmallKey, obj: ObjRef, data: &[u8]) -> Vec<u8> {
131    rc4(&rc4_object_key(key, obj), data)
132}
133
134/// Decrypt an AESV2 payload under a per-object key.
135#[must_use]
136pub(crate) fn decrypt_aes_v4(key: &SmallKey, obj: ObjRef, data: &[u8]) -> Vec<u8> {
137    decrypt_aes_cbc(&aes_v4_object_key(key, obj), data)
138}
139
140/// Decrypt an AESV3 payload under the file key itself.
141#[must_use]
142pub(crate) fn decrypt_aes_v5(key: &[u8; 32], data: &[u8]) -> Vec<u8> {
143    decrypt_aes_cbc(key, data)
144}
145
146/// Encrypt an RC4 payload. Symmetric, so this is the same call as
147/// [`decrypt_rc4`] — the two names exist so the call sites read in the
148/// direction they mean.
149#[must_use]
150pub(crate) fn encrypt_rc4(key: &SmallKey, obj: ObjRef, data: &[u8]) -> Vec<u8> {
151    rc4(&rc4_object_key(key, obj), data)
152}
153
154/// Encrypt an AESV2 payload under a per-object key, prefixing `iv`.
155///
156/// The object key is [`aes_v4_object_key`]'s full sixteen bytes — the same
157/// derivation the decrypt side uses. The C++'s encrypt path truncates it to
158/// the *file* key's length instead (`EncryptContent` calls `CRYPT_AESSetKey`
159/// with `realkey.first(key_len_)`), which differs whenever the file key is
160/// not sixteen bytes. It never is: AESV2 is 128-bit by definition, so the two
161/// readings coincide on every real document, and at a 24-byte file key the
162/// C++ would ask a 16-byte array for 24 bytes and abort. Sharing one
163/// derivation is what makes encrypt-then-decrypt exact.
164#[must_use]
165pub(crate) fn encrypt_aes_v4(
166    key: &SmallKey,
167    obj: ObjRef,
168    iv: &[u8; BLOCK],
169    data: &[u8],
170) -> Vec<u8> {
171    encrypt_aes_cbc(&aes_v4_object_key(key, obj), iv, data)
172}
173
174/// Encrypt an AESV3 payload under the file key itself, prefixing `iv`.
175#[must_use]
176pub(crate) fn encrypt_aes_v5(key: &[u8; 32], iv: &[u8; BLOCK], data: &[u8]) -> Vec<u8> {
177    encrypt_aes_cbc(key, iv, data)
178}
179
180/// Emit `iv` followed by the PKCS#7-padded, CBC-encrypted plaintext.
181///
182/// Standard PKCS#7 throughout: the padding is always added, so a plaintext
183/// whose length is already a multiple of sixteen grows by a whole block of
184/// `0x10` bytes, and the output is always `16 + 16 * ceil((n + 1) / 16)`
185/// bytes. That is what makes the round trip exact against the quirky decoder
186/// on the other side — its "last plaintext byte under sixteen strips that
187/// many" rule and PKCS#7 agree on every value 1 through 16, and the extra
188/// block is what keeps a length-16 payload from being read as a length-0 one.
189///
190/// A key AES cannot accept yields empty output rather than an error, matching
191/// the decrypt side's contract: this crate never fails a cipher call.
192#[must_use]
193fn encrypt_aes_cbc(key: &[u8], iv: &[u8; BLOCK], data: &[u8]) -> Vec<u8> {
194    let pad = BLOCK - data.len() % BLOCK;
195    let mut body = data.to_vec();
196    // `pad` is 1..=16, so the conversion never saturates.
197    body.extend(std::iter::repeat_n(u8::try_from(pad).unwrap_or(0), pad));
198    if aes_cbc_encrypt(key, iv, &mut body).is_err() {
199        return Vec::new();
200    }
201    let mut out = Vec::with_capacity(BLOCK.saturating_add(body.len()));
202    out.extend_from_slice(iv);
203    out.append(&mut body);
204    out
205}
206
207/// Strip the leading initialisation vector and CBC-decrypt the rest,
208/// reproducing PDFium's buffering rules exactly.
209///
210/// The rules are quirks of a streaming decoder that buffers one block behind
211/// its input, restated here as an output rule (Divergence D6):
212///
213/// - the first sixteen bytes are the IV and are consumed, never emitted, so
214///   fewer than seventeen bytes of input produce nothing at all;
215/// - a block is emitted only once more input has arrived after it, which
216///   leaves the final block for the padding step;
217/// - that final block is emitted minus its last plaintext byte's worth of
218///   padding when that byte is under sixteen, and dropped entirely otherwise.
219///   Nothing checks that the padding bytes agree with the count, and a last
220///   byte of zero keeps the whole block — neither of which strict PKCS#7
221///   would do;
222/// - a trailing partial block never fills, so it is discarded — but the full
223///   block before it *was* followed by input and so is emitted.
224///
225/// A key AES cannot accept, or a chaining failure, yields empty output rather
226/// than an error: PDFium never fails a decrypt, it produces a best-effort
227/// result and lets the consumer see a short or empty payload.
228#[must_use]
229fn decrypt_aes_cbc(key: &[u8], data: &[u8]) -> Vec<u8> {
230    let Some(iv) = data
231        .get(..BLOCK)
232        .and_then(|s| <[u8; BLOCK]>::try_from(s).ok())
233    else {
234        return Vec::new();
235    };
236    let body = data.get(BLOCK..).unwrap_or_default();
237    let whole = body.len() - body.len() % BLOCK;
238    let Some(mut out) = body.get(..whole).map(<[u8]>::to_vec) else {
239        return Vec::new();
240    };
241    if aes_cbc_decrypt(key, &iv, &mut out).is_err() {
242        return Vec::new();
243    }
244
245    if whole < body.len() {
246        // A partial tail arrived after the last full block, so that block was
247        // emitted and only the tail is lost.
248        return out;
249    }
250    // No tail: the last block is still in the lag buffer and gets the
251    // padding treatment.
252    let Some(pad) = out.last().copied() else {
253        return out;
254    };
255    if usize::from(pad) >= BLOCK {
256        out.truncate(out.len() - BLOCK);
257    } else {
258        out.truncate(out.len() - usize::from(pad));
259    }
260    out
261}
262
263#[cfg(test)]
264mod tests {
265    use super::{
266        BLOCK, CryptClass, Iv, aes_v4_object_key, decrypt_aes_cbc, decrypt_aes_v4, decrypt_aes_v5,
267        decrypt_rc4, encrypt_aes_cbc, encrypt_aes_v4, encrypt_aes_v5, encrypt_rc4, rc4_object_key,
268    };
269    use crate::key::SmallKey;
270    use crate::primitives::aes_cbc_encrypt;
271    use pdfrum_object::ObjRef;
272
273    fn key(len: usize) -> SmallKey {
274        SmallKey::from_prefix(&(0..32u8).collect::<Vec<_>>(), len)
275    }
276
277    // T17 — the RC4 object-key length is min(key_len + 5, 16), so a 16-byte
278    // file key produces a 16-byte object key, not a 21-byte one.
279    #[test]
280    fn rc4_object_key_length_is_capped_at_sixteen() {
281        assert_eq!(rc4_object_key(&key(5), ObjRef::new(1, 0)).len(), 10);
282        assert_eq!(rc4_object_key(&key(10), ObjRef::new(1, 0)).len(), 15);
283        assert_eq!(rc4_object_key(&key(16), ObjRef::new(1, 0)).len(), 16);
284    }
285
286    // T17 — only three bytes of object number and two of generation take
287    // part, so numbers 2^24 apart collide.
288    #[test]
289    fn object_numbers_contribute_three_bytes() {
290        let k = key(16);
291        assert_eq!(
292            rc4_object_key(&k, ObjRef::new(1, 0)),
293            rc4_object_key(&k, ObjRef::new(0x0100_0001, 0))
294        );
295        assert_ne!(
296            rc4_object_key(&k, ObjRef::new(1, 0)),
297            rc4_object_key(&k, ObjRef::new(2, 0))
298        );
299        assert_ne!(
300            rc4_object_key(&k, ObjRef::new(1, 0)),
301            rc4_object_key(&k, ObjRef::new(1, 1))
302        );
303    }
304
305    #[test]
306    fn generation_contributes_two_bytes() {
307        let k = key(16);
308        assert_ne!(
309            rc4_object_key(&k, ObjRef::new(1, 0x0100)),
310            rc4_object_key(&k, ObjRef::new(1, 0))
311        );
312    }
313
314    // T17 — AESV2 hashes the scratch plus "sAlT" and keeps all sixteen bytes.
315    #[test]
316    fn aes_v4_object_key_is_a_full_digest() {
317        let derived = aes_v4_object_key(&key(16), ObjRef::new(1, 0));
318        assert_eq!(derived.len(), 16);
319        // Hand-computed: the scratch is key || 01 00 00 00 00 || 73 41 6C 54,
320        // hashed over its first key_len + 9 = 25 bytes.
321        let mut scratch = Vec::new();
322        scratch.extend_from_slice(key(16).bytes());
323        scratch.extend_from_slice(&[1, 0, 0, 0, 0]);
324        scratch.extend_from_slice(b"sAlT");
325        assert_eq!(scratch.len(), 25);
326        assert_eq!(derived, crate::primitives::md5(&scratch));
327    }
328
329    // T17 — AESV3 ignores the object entirely.
330    #[test]
331    fn aes_v5_uses_the_file_key_verbatim() {
332        let file_key = [7u8; 32];
333        let payload = encrypted(&file_key, b"hello");
334        let first = decrypt_aes_v5(&file_key, &payload);
335        assert_eq!(first, b"hello");
336        // Not keyed by an object at all: the same bytes decrypt the same way
337        // whichever object they came from.
338        assert_eq!(decrypt_aes_v5(&file_key, &payload), first);
339    }
340
341    /// Build an IV-prefixed, PKCS#7-padded ciphertext the way a writer would.
342    fn encrypted(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
343        let iv = [0x5Au8; BLOCK];
344        let pad = BLOCK - plaintext.len() % BLOCK;
345        let mut body = plaintext.to_vec();
346        body.extend(std::iter::repeat_n(u8::try_from(pad).unwrap_or(0), pad));
347        aes_cbc_encrypt(key, &iv, &mut body).expect("valid key");
348        let mut out = iv.to_vec();
349        out.extend_from_slice(&body);
350        out
351    }
352
353    // T18 — the acceptance table for the buffering rules.
354    #[test]
355    fn aes_lengths_under_seventeen_bytes_yield_nothing() {
356        let k = [0u8; 16];
357        for len in 0..=BLOCK {
358            assert!(
359                decrypt_aes_cbc(&k, &vec![0xAA; len]).is_empty(),
360                "{len} bytes"
361            );
362        }
363    }
364
365    #[test]
366    fn a_round_trip_recovers_the_plaintext() {
367        let k = [3u8; 16];
368        for len in [0usize, 1, 15, 16, 17, 31, 32, 100] {
369            let plaintext: Vec<u8> = (0..len)
370                .map(|i| u8::try_from(i % 251).unwrap_or(0))
371                .collect();
372            assert_eq!(
373                decrypt_aes_cbc(&k, &encrypted(&k, &plaintext)),
374                plaintext,
375                "{len} bytes"
376            );
377        }
378    }
379
380    /// Encrypt one block of chosen plaintext so its last byte can be dialled.
381    fn one_block_with_last(key: &[u8], last: u8) -> Vec<u8> {
382        let iv = [0u8; BLOCK];
383        let mut block = [0u8; BLOCK];
384        if let Some(slot) = block.last_mut() {
385            *slot = last;
386        }
387        let mut body = block.to_vec();
388        aes_cbc_encrypt(key, &iv, &mut body).expect("valid key");
389        let mut out = iv.to_vec();
390        out.extend_from_slice(&body);
391        out
392    }
393
394    // A final byte of 16 or more drops the whole block; a byte of zero keeps
395    // it, which strict PKCS#7 would reject.
396    #[test]
397    fn the_final_block_pad_byte_decides_how_much_survives() {
398        let k = [9u8; 16];
399        assert!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 0x10)).is_empty());
400        assert!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 0xFF)).is_empty());
401        assert_eq!(
402            decrypt_aes_cbc(&k, &one_block_with_last(&k, 0)).len(),
403            BLOCK
404        );
405        assert_eq!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 1)).len(), 15);
406        assert_eq!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 15)).len(), 1);
407    }
408
409    // Nothing validates that the padding bytes agree with the count.
410    #[test]
411    fn inconsistent_padding_is_accepted() {
412        let k = [9u8; 16];
413        // A block ending in 4 but whose preceding bytes are not 4s.
414        assert_eq!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 4)).len(), 12);
415    }
416
417    // T18 — a partial tail is discarded, but the full block before it was
418    // followed by input and so survives, unstripped.
419    #[test]
420    fn a_partial_tail_is_dropped_and_the_block_before_it_is_kept() {
421        let k = [4u8; 16];
422        let mut payload = one_block_with_last(&k, 3);
423        assert_eq!(decrypt_aes_cbc(&k, &payload).len(), 13);
424        payload.extend_from_slice(&[0xEE; 5]);
425        // 16 IV + 16 data + 5 tail: the data block is emitted whole.
426        assert_eq!(decrypt_aes_cbc(&k, &payload).len(), BLOCK);
427    }
428
429    #[test]
430    fn two_blocks_plus_a_tail_keep_both_blocks() {
431        let k = [4u8; 16];
432        let iv = [0u8; BLOCK];
433        let mut body = vec![0u8; 2 * BLOCK];
434        aes_cbc_encrypt(&k, &iv, &mut body).expect("valid key");
435        let mut payload = iv.to_vec();
436        payload.extend_from_slice(&body);
437        payload.extend_from_slice(&[0x11; 7]);
438        assert_eq!(decrypt_aes_cbc(&k, &payload).len(), 2 * BLOCK);
439    }
440
441    // T19 — RC4 has no length transformation at all.
442    #[test]
443    fn rc4_preserves_length_and_round_trips() {
444        let k = key(16);
445        let obj = ObjRef::new(5, 0);
446        assert!(decrypt_rc4(&k, obj, &[]).is_empty());
447        let data: Vec<u8> = (0..77u8).collect();
448        let once = decrypt_rc4(&k, obj, &data);
449        assert_eq!(once.len(), data.len());
450        assert_eq!(decrypt_rc4(&k, obj, &once), data);
451    }
452
453    #[test]
454    fn a_key_aes_cannot_accept_yields_empty_output_rather_than_a_panic() {
455        for len in [0usize, 1, 15, 17, 31, 33] {
456            let bad = vec![0u8; len];
457            assert!(
458                decrypt_aes_cbc(&bad, &[0xAA; 48]).is_empty(),
459                "{len}-byte key"
460            );
461        }
462    }
463
464    // ---- : the encrypt direction ----
465
466    // The length law, which is what the writer's `/Length` depends on: a
467    // vector plus a PKCS#7 pad that is *always* added.
468    #[test]
469    fn aes_encryption_grows_a_payload_by_a_vector_and_a_pad() {
470        let k = [0x2Bu8; 16];
471        for (plain, expected) in [
472            (0usize, 32),
473            (1, 32),
474            (15, 32),
475            (16, 48),
476            (17, 48),
477            (31, 48),
478        ] {
479            let out = encrypt_aes_cbc(&k, &[0u8; BLOCK], &vec![0xA5; plain]);
480            assert_eq!(out.len(), expected, "{plain} bytes of plaintext");
481        }
482    }
483
484    // The whole point: our own decoder — quirks and all — reads back exactly
485    // what our encoder wrote, at every length.
486    #[test]
487    fn aes_round_trips_through_the_quirky_decoder_at_every_length() {
488        let k = [0x3Cu8; 16];
489        for len in 0..96usize {
490            let plaintext: Vec<u8> = (0..len)
491                .map(|i| u8::try_from(i % 251).unwrap_or(0))
492                .collect();
493            let iv = [u8::try_from(len % 256).unwrap_or(0); BLOCK];
494            let sealed = encrypt_aes_cbc(&k, &iv, &plaintext);
495            assert_eq!(
496                decrypt_aes_cbc(&k, &sealed),
497                plaintext,
498                "{len} bytes did not survive"
499            );
500        }
501    }
502
503    // The vector really is the first sixteen bytes, and really does change
504    // the ciphertext: two vectors over one plaintext share no block.
505    #[test]
506    fn the_vector_is_the_prefix_and_changes_every_block() {
507        let k = [0x11u8; 32];
508        let plaintext = vec![0u8; 3 * BLOCK];
509        let first = encrypt_aes_cbc(&k, &[1u8; BLOCK], &plaintext);
510        let second = encrypt_aes_cbc(&k, &[2u8; BLOCK], &plaintext);
511        assert_eq!(first.get(..BLOCK), Some(&[1u8; BLOCK][..]));
512        assert_eq!(second.get(..BLOCK), Some(&[2u8; BLOCK][..]));
513        assert_ne!(first.get(BLOCK..), second.get(BLOCK..));
514    }
515
516    // Per-object keys really are per object, in both directions and by the
517    // same derivation — which is what makes the round trip object-keyed.
518    #[test]
519    fn the_object_keyed_ciphers_round_trip_under_the_same_reference() {
520        let k = key(16);
521        let obj = ObjRef::new(12, 3);
522        let payload: Vec<u8> = (0..70u8).collect();
523
524        assert_eq!(
525            decrypt_rc4(&k, obj, &encrypt_rc4(&k, obj, &payload)),
526            payload
527        );
528        let sealed = encrypt_aes_v4(&k, obj, &Iv([9; BLOCK]).0, &payload);
529        assert_eq!(decrypt_aes_v4(&k, obj, &sealed), payload);
530        // A different object cannot read it.
531        assert_ne!(decrypt_aes_v4(&k, ObjRef::new(13, 3), &sealed), payload);
532
533        let file_key = [0x5Au8; 32];
534        let sealed = encrypt_aes_v5(&file_key, &[3; BLOCK], &payload);
535        assert_eq!(decrypt_aes_v5(&file_key, &sealed), payload);
536    }
537
538    // RC4 encrypt and decrypt are the same call, so applying either twice is
539    // the identity.
540    #[test]
541    fn rc4_encryption_is_its_own_inverse() {
542        let k = key(10);
543        let obj = ObjRef::new(3, 0);
544        let payload: Vec<u8> = (0..40u8).map(|i| i.wrapping_mul(7)).collect();
545        assert_eq!(
546            encrypt_rc4(&k, obj, &payload),
547            decrypt_rc4(&k, obj, &payload)
548        );
549        assert!(encrypt_rc4(&k, obj, &[]).is_empty());
550    }
551
552    #[test]
553    fn an_impossible_key_encrypts_to_nothing_rather_than_panicking() {
554        for len in [0usize, 1, 15, 17, 31, 33] {
555            assert!(encrypt_aes_cbc(&vec![0u8; len], &[0; BLOCK], b"payload").is_empty());
556        }
557    }
558
559    #[test]
560    fn crypt_classes_are_distinct_values() {
561        assert_ne!(CryptClass::Stream, CryptClass::String);
562        assert_ne!(CryptClass::String, CryptClass::Embedded);
563    }
564}