Skip to main content

pdfrum_crypt/
primitives.rs

1//! The hash and block-cipher primitives the security handler is built from.
2//!
3//! Thin, typed wrappers over RustCrypto: fixed-size digest arrays instead of
4//! streaming contexts, and an AES-CBC value that owns its chaining state so a
5//! multi-block stream decrypts as one call. The C++ aborts the process when a
6//! key length or buffer length is wrong; every such case is a
7//! [`CipherError`] here, because all of them are reachable from bytes an
8//! untrusted file chose.
9
10use aes::{Aes128, Aes192, Aes256};
11use cbc::{Decryptor, Encryptor};
12use cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit};
13use md5::{Digest, Md5};
14use sha1::Sha1;
15use sha2::{Sha256, Sha384, Sha512};
16
17/// One AES block, in bytes.
18pub(crate) const BLOCK: usize = 16;
19
20/// Byte equality that does not return on the first mismatch.
21///
22/// Length mismatch still returns immediately: the revision-2/3 `/U` compare
23/// and the AES-256 validation hash are fixed-size, and a truncated `/U` is
24/// already rejected before we get here. This is hygiene against a
25/// byte-by-byte leak of a stored hash, not a claim about the password
26/// check's overall timing.
27pub(crate) fn ct_eq(a: &[u8], b: &[u8]) -> bool {
28    if a.len() != b.len() {
29        return false;
30    }
31    let mut diff = 0u8;
32    for (x, y) in a.iter().zip(b) {
33        diff |= x ^ y;
34    }
35    diff == 0
36}
37
38/// A key or buffer length AES cannot accept.
39///
40/// The C++ `CHECK`s these and aborts; we report them (Divergence D4).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
42pub(crate) enum CipherError {
43    /// AES accepts 16-, 24- and 32-byte keys only.
44    #[error("AES key length {0} bytes is not 16, 24 or 32")]
45    KeyLength(usize),
46    /// CBC consumes whole blocks.
47    #[error("CBC input of {0} bytes is not a multiple of 16")]
48    NotBlockAligned(usize),
49}
50
51/// MD5 of one buffer (RFC 1321).
52#[must_use]
53pub fn md5(data: &[u8]) -> [u8; 16] {
54    Md5::digest(data).into()
55}
56
57/// MD5 of several buffers hashed as one stream.
58///
59/// Key derivation feeds an MD5 in a precise order with optional pieces; this
60/// keeps the order visible at the call site instead of building a scratch
61/// buffer.
62#[must_use]
63pub(crate) fn md5_parts(parts: &[&[u8]]) -> [u8; 16] {
64    let mut hasher = Md5::new();
65    for part in parts {
66        hasher.update(part);
67    }
68    hasher.finalize().into()
69}
70
71/// SHA-1 of one buffer (FIPS 180-2).
72#[must_use]
73pub fn sha1(data: &[u8]) -> [u8; 20] {
74    Sha1::digest(data).into()
75}
76
77/// SHA-256 of one buffer.
78#[must_use]
79pub(crate) fn sha256(data: &[u8]) -> [u8; 32] {
80    Sha256::digest(data).into()
81}
82
83/// SHA-256 of several buffers hashed as one stream.
84#[must_use]
85pub(crate) fn sha256_parts(parts: &[&[u8]]) -> [u8; 32] {
86    let mut hasher = Sha256::new();
87    for part in parts {
88        hasher.update(part);
89    }
90    hasher.finalize().into()
91}
92
93/// SHA-384 of one buffer.
94#[must_use]
95pub(crate) fn sha384(data: &[u8]) -> [u8; 48] {
96    Sha384::digest(data).into()
97}
98
99/// SHA-512 of one buffer.
100#[must_use]
101pub(crate) fn sha512(data: &[u8]) -> [u8; 64] {
102    Sha512::digest(data).into()
103}
104
105/// AES-CBC encrypt `data` in place under `key` and `iv`.
106///
107/// Used only by the revision 6 hardened hash, which encrypts a buffer that is
108/// a multiple of 64 bytes by construction.
109pub(crate) fn aes_cbc_encrypt(
110    key: &[u8],
111    iv: &[u8; BLOCK],
112    data: &mut [u8],
113) -> Result<(), CipherError> {
114    let mut blocks = split_blocks(data)?;
115    match key.len() {
116        16 => {
117            Encryptor::<Aes128>::new_from_slices(key, iv).map(|mut c| c.encrypt_blocks(&mut blocks))
118        }
119        24 => {
120            Encryptor::<Aes192>::new_from_slices(key, iv).map(|mut c| c.encrypt_blocks(&mut blocks))
121        }
122        32 => {
123            Encryptor::<Aes256>::new_from_slices(key, iv).map(|mut c| c.encrypt_blocks(&mut blocks))
124        }
125        other => return Err(CipherError::KeyLength(other)),
126    }
127    .map_err(|_| CipherError::KeyLength(key.len()))?;
128    join_blocks(&blocks, data);
129    Ok(())
130}
131
132/// Split a block-aligned buffer into cipher blocks.
133fn split_blocks(data: &[u8]) -> Result<Vec<cipher::Block<Aes128>>, CipherError> {
134    if !data.len().is_multiple_of(BLOCK) {
135        return Err(CipherError::NotBlockAligned(data.len()));
136    }
137    // A block-aligned buffer splits into whole blocks, so the remainder the
138    // split also yields is empty and no block is dropped.
139    Ok(data
140        .as_chunks::<BLOCK>()
141        .0
142        .iter()
143        .map(|block| (*block).into())
144        .collect())
145}
146
147/// Write processed blocks back over the buffer they came from.
148fn join_blocks(blocks: &[cipher::Block<Aes128>], data: &mut [u8]) {
149    for (chunk, block) in data.as_chunks_mut::<BLOCK>().0.iter_mut().zip(blocks) {
150        chunk.copy_from_slice(block);
151    }
152}
153
154/// AES-CBC decrypt `data` in place under `key` and `iv`.
155///
156/// No padding is stripped: the callers here decrypt either a bare 32-byte
157/// file key, a single `/Perms` block, or a document stream whose padding rule
158/// is PDFium's own rather than PKCS#7's.
159pub(crate) fn aes_cbc_decrypt(
160    key: &[u8],
161    iv: &[u8; BLOCK],
162    data: &mut [u8],
163) -> Result<(), CipherError> {
164    let mut blocks = split_blocks(data)?;
165    match key.len() {
166        16 => {
167            Decryptor::<Aes128>::new_from_slices(key, iv).map(|mut c| c.decrypt_blocks(&mut blocks))
168        }
169        24 => {
170            Decryptor::<Aes192>::new_from_slices(key, iv).map(|mut c| c.decrypt_blocks(&mut blocks))
171        }
172        32 => {
173            Decryptor::<Aes256>::new_from_slices(key, iv).map(|mut c| c.decrypt_blocks(&mut blocks))
174        }
175        other => return Err(CipherError::KeyLength(other)),
176    }
177    .map_err(|_| CipherError::KeyLength(key.len()))?;
178    join_blocks(&blocks, data);
179    Ok(())
180}
181
182#[cfg(test)]
183mod tests {
184    use super::{
185        BLOCK, CipherError, aes_cbc_decrypt, aes_cbc_encrypt, ct_eq, md5, md5_parts, sha1, sha256,
186        sha384, sha512,
187    };
188
189    fn hex(bytes: &[u8]) -> String {
190        use std::fmt::Write as _;
191        bytes.iter().fold(String::new(), |mut out, b| {
192            let _ = write!(out, "{b:02x}");
193            out
194        })
195    }
196
197    fn unhex(s: &str) -> Vec<u8> {
198        s.as_bytes()
199            .chunks(2)
200            .filter_map(|c| std::str::from_utf8(c).ok())
201            .filter_map(|c| u8::from_str_radix(c, 16).ok())
202            .collect()
203    }
204
205    // T1 — RFC 1321 A.5, ported from fx_crypt_unittest.cpp:51-192.
206    #[test]
207    fn md5_rfc1321_suite() {
208        let cases: [(&[u8], &str); 7] = [
209            (b"", "d41d8cd98f00b204e9800998ecf8427e"),
210            (b"a", "0cc175b9c0f1b6a831c399e269772661"),
211            (b"abc", "900150983cd24fb0d6963f7d28e17f72"),
212            (b"message digest", "f96b697d7cb7938d525a2f31aaf161d0"),
213            (
214                b"abcdefghijklmnopqrstuvwxyz",
215                "c3fcd3d76192e4007dfb496cca67e13b",
216            ),
217            (
218                b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
219                "d174ab98d277d9f5a5611c2c9f419d9f",
220            ),
221            (
222                b"1234567890123456789012345678901234567890\
223                  1234567890123456789012345678901234567890",
224                "57edf4a22be3c955ac49da2e2107b67a",
225            ),
226        ];
227        for (input, expected) in cases {
228            assert_eq!(hex(&md5(input)), expected, "md5 of {input:?}");
229        }
230    }
231
232    // From fx_crypt_unittest.cpp:79-130: ten megabytes of a byte ramp, fed in
233    // 4097-byte chunks so a non-power-of-two update boundary is exercised.
234    #[test]
235    fn md5_over_ten_megabytes_in_odd_chunks() {
236        let data: Vec<u8> = (0..=10 * 1024 * 1024_usize)
237            .map(|i| u8::try_from(i & 0xFF).unwrap_or(0))
238            .collect();
239        assert_eq!(hex(&md5(&data)), "90bd6ad90acef5adaa92203e21c7a13e");
240        let chunks: Vec<&[u8]> = data.chunks(4097).collect();
241        assert_eq!(hex(&md5_parts(&chunks)), "90bd6ad90acef5adaa92203e21c7a13e");
242    }
243
244    #[test]
245    fn md5_parts_hashes_the_concatenation() {
246        assert_eq!(md5_parts(&[b"ab", b"c"]), md5(b"abc"));
247        assert_eq!(md5_parts(&[]), md5(b""));
248    }
249
250    // T2 — SHA, from fx_crypt_unittest.cpp:194-260 and :506-600.
251    #[test]
252    fn sha1_vectors() {
253        assert_eq!(hex(&sha1(b"")), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
254        assert_eq!(
255            hex(&sha1(b"abc")),
256            "a9993e364706816aba3e25717850c26c9cd0d89d"
257        );
258        // FIPS 180-2 A.2: two blocks.
259        assert_eq!(
260            hex(&sha1(
261                b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
262            )),
263            "84983e441c3bd26ebaae4aa1f95129e5e54670f1"
264        );
265    }
266
267    #[test]
268    fn sha256_vectors() {
269        assert_eq!(
270            hex(&sha256(b"")),
271            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
272        );
273        assert_eq!(
274            hex(&sha256(b"abc")),
275            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
276        );
277        // FIPS 180-2 B.2.
278        assert_eq!(
279            hex(&sha256(
280                b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
281            )),
282            "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
283        );
284    }
285
286    #[test]
287    fn sha384_vectors() {
288        assert_eq!(
289            hex(&sha384(b"")),
290            "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da\
291             274edebfe76f65fbd51ad2f14898b95b"
292                .replace(char::is_whitespace, "")
293        );
294        assert_eq!(
295            hex(&sha384(
296                b"This is a simple test. To see whether it is getting correct value."
297            )),
298            "9554ffd389f0d642e933fe4c078119cacbb31446d8bda4f412d554037928e5dc\
299             12a51be9fe59253c92305ee50e035807"
300                .replace(char::is_whitespace, "")
301        );
302    }
303
304    // The 112-byte cases straddle the 1024-bit variants' 112-vs-128 padding
305    // rule (fx_crypt_unittest.cpp:534-548, :583-600).
306    #[test]
307    fn sha384_and_sha512_at_the_padding_boundary() {
308        let input = [b'a'; 112];
309        assert_eq!(
310            hex(&sha384(&input)),
311            "187d4e07cb306103c69967bf544d0dfbe904257759 9c73c330abc0cb64c61236\
312             d5ed565ee19119d8c31779a38f791fcd"
313                .replace(char::is_whitespace, "")
314        );
315        assert_eq!(
316            hex(&sha512(&input)),
317            "c01d080efd492776a1c43bd23dd99d0a2e626d481e16782e75d54c2503b5dc32\
318             bd05f0f1ba33e568b88fd2d970929b719ecbb152f58f130a407c8830604b70ca"
319                .replace(char::is_whitespace, "")
320        );
321    }
322
323    #[test]
324    fn sha512_vectors() {
325        assert_eq!(
326            hex(&sha512(b"")),
327            "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
328             47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
329                .replace(char::is_whitespace, "")
330        );
331        assert_eq!(
332            hex(&sha512(
333                b"This is a simple test. To see whether it is getting correct value."
334            )),
335            "86b50563a26fd6faeb9bc3bb9eb70382b650556b9069d0a7530a34ddea11cc91\
336             5cc793caae30d196bed035214ac642560ca300694477cc3ed4d61031c6c058cf"
337                .replace(char::is_whitespace, "")
338        );
339    }
340
341    /// T3 — the `BoringSSL` NIST SP 800-38A vectors, chained so each IV is
342    /// the previous ciphertext. Every case is a round trip.
343    fn aes_chain(key_hex: &str, ciphertexts: [&str; 4]) {
344        const PLAINTEXTS: [&str; 4] = [
345            "6bc1bee22e409f96e93d7e117393172a",
346            "ae2d8a571e03ac9c9eb76fac45af8e51",
347            "30c81c46a35ce411e5fbc1191a0a52ef",
348            "f69f2445df4f9b17ad2b417be66c3710",
349        ];
350        let key = unhex(key_hex);
351        let mut iv: [u8; BLOCK] = unhex("000102030405060708090a0b0c0d0e0f")
352            .try_into()
353            .expect("16 bytes");
354        for (plaintext, expected) in PLAINTEXTS.iter().zip(ciphertexts) {
355            let mut buf = unhex(plaintext);
356            aes_cbc_encrypt(&key, &iv, &mut buf).expect("valid key and length");
357            assert_eq!(hex(&buf), expected, "key {key_hex} iv {}", hex(&iv));
358
359            let mut back = buf.clone();
360            aes_cbc_decrypt(&key, &iv, &mut back).expect("valid key and length");
361            assert_eq!(hex(&back), *plaintext);
362
363            iv = buf.try_into().expect("16 bytes");
364        }
365    }
366
367    #[test]
368    fn aes128_cbc_chain() {
369        aes_chain(
370            "2b7e151628aed2a6abf7158809cf4f3c",
371            [
372                "7649abac8119b246cee98e9b12e9197d",
373                "5086cb9b507219ee95db113a917678b2",
374                "73bed6b8e3c1743b7116e69e22229516",
375                "3ff1caa1681fac09120eca307586e1a7",
376            ],
377        );
378    }
379
380    #[test]
381    fn aes192_cbc_chain() {
382        aes_chain(
383            "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b",
384            [
385                "4f021db243bc633d7178183a9fa071e8",
386                "b4d9ada9ad7dedf4e5e738763f69145a",
387                "571b242012fb7ae07fa9baac3df102e0",
388                "08b0e27988598881d920a9e64f5615cd",
389            ],
390        );
391    }
392
393    #[test]
394    fn aes256_cbc_chain() {
395        aes_chain(
396            "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4",
397            [
398                "f58c4c04d6e5f1ba779eabfb5f7bfbd6",
399                "9cfc4e967edb808d679f777bc6702c7d",
400                "39f23369a9d9bacfa530e26304231461",
401                "b2eb05e2c39be9fcda6c19078c6a9d1b",
402            ],
403        );
404    }
405
406    /// A multi-block call must chain, not repeat the IV per block.
407    #[test]
408    fn cbc_chains_across_blocks_within_one_call() {
409        let key = unhex("2b7e151628aed2a6abf7158809cf4f3c");
410        let iv: [u8; BLOCK] = unhex("000102030405060708090a0b0c0d0e0f")
411            .try_into()
412            .expect("16 bytes");
413        let mut buf = unhex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51");
414        aes_cbc_encrypt(&key, &iv, &mut buf).expect("valid");
415        assert_eq!(
416            hex(&buf),
417            "7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2"
418        );
419    }
420
421    // D4 — every C++ `CHECK` becomes an error return.
422    #[test]
423    fn bad_key_and_buffer_lengths_report_rather_than_abort() {
424        let iv = [0u8; BLOCK];
425        let mut block = [0u8; BLOCK];
426        for len in [0usize, 1, 15, 17, 31, 33, 64] {
427            let key = vec![0u8; len];
428            assert_eq!(
429                aes_cbc_decrypt(&key, &iv, &mut block),
430                Err(CipherError::KeyLength(len))
431            );
432        }
433        let key = [0u8; 16];
434        for len in [1usize, 15, 17, 31] {
435            let mut buf = vec![0u8; len];
436            assert_eq!(
437                aes_cbc_encrypt(&key, &iv, &mut buf),
438                Err(CipherError::NotBlockAligned(len))
439            );
440        }
441        // An empty buffer is block-aligned and is a no-op.
442        assert_eq!(aes_cbc_decrypt(&key, &iv, &mut []), Ok(()));
443    }
444
445    #[test]
446    fn ct_eq_is_length_sensitive_and_agrees_with_eq() {
447        assert!(ct_eq(b"abcd", b"abcd"));
448        assert!(!ct_eq(b"abcd", b"abce"));
449        assert!(!ct_eq(b"abc", b"abcd"));
450        assert!(ct_eq(&[], &[]));
451    }
452}