Skip to main content

zpdf_parser/
crypt.rs

1//! PDF **Standard Security Handler** decryption.
2//!
3//! Authenticates a user or owner password (empty by default) and decrypts the
4//! Standard security handler:
5//! - RC4 (V1/V2, R2–R4) with MD5 key derivation (PDF 1.7 §7.6.3, Algorithms 2 & 1)
6//! - AES-128-CBC (V4/R4, crypt filter `AESV2`) — per-object key = MD5(file key
7//!   ‖ objnum ‖ gen ‖ `sAlT`); the first 16 bytes of each payload are the IV
8//! - AES-256-CBC (V5, R5/R6, crypt filter `AESV3`) — file key recovered from
9//!   `/UE` (or `/OE`) per ISO 32000-2 Algorithm 2.A, with the R6 hardened hash
10//!   (Algorithm 2.B); no per-object key derivation
11//!
12//! MD5 and RC4 are implemented inline; AES-CBC and SHA-2 come from the
13//! pure-Rust RustCrypto crates (`aes`, `cbc`, `sha2`). Zero C/C++ deps.
14//!
15//! ## How it plugs in
16//! [`Decryptor`] is built once at file-open time from the `/Encrypt` dictionary
17//! and the first element of the trailer `/ID`. Every top-level object parsed
18//! straight from the file (xref `InUse` entries) is then walked with
19//! [`Decryptor::decrypt_object`], which decrypts every string and stream in
20//! place (streams with the `/StmF` cipher, strings with the `/StrF` cipher —
21//! either may be `Identity`). Objects pulled out of a `/Type /ObjStm`
22//! compressed stream are **not** decrypted individually (the container stream
23//! is), and the `/Encrypt` dictionary itself is never decrypted.
24
25use aes::cipher::{generic_array::GenericArray, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
26use sha2::Digest;
27use std::sync::Arc;
28use zpdf_core::{ObjectId, PdfDict, PdfObject, PdfString};
29
30/// The 32-byte password-padding string from PDF 1.7 §7.6.3.3 (Algorithm 2).
31const PAD: [u8; 32] = [
32    0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
33    0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
34];
35
36#[derive(Clone, Copy, PartialEq, Eq, Debug)]
37enum Algo {
38    /// No encryption for this class (`/StmF` or `/StrF` is `Identity`, or the
39    /// crypt filter's `/CFM` is `None`).
40    Identity,
41    /// RC4 (V1/V2, or a V4 crypt filter with `/CFM /V2`).
42    Rc4,
43    /// AES-128 CBC (V4, crypt filter `AESV2`).
44    AesV2,
45    /// AES-256 CBC (V5 R5/R6, crypt filter `AESV3`).
46    AesV3,
47}
48
49/// Outcome of attempting to build a [`Decryptor`] for a document.
50pub enum BuildResult {
51    /// A usable decryptor (the password authenticated, or the empty-password
52    /// best-effort path was taken for an unvalidated RC4 document).
53    Decryptor(Decryptor),
54    /// No decryption: an unsupported security handler, or a V5 document whose
55    /// empty password did not validate. The document opens undecrypted.
56    Degrade,
57    /// A non-empty password was supplied but authenticated as neither the user
58    /// nor the owner password.
59    WrongPassword,
60}
61
62/// A built Standard-security-handler decryptor for one file.
63pub struct Decryptor {
64    /// File-level encryption key (Algorithm 2 or 2.A output), `n` bytes.
65    key: Vec<u8>,
66    /// Cipher for stream payloads (`/StmF` crypt filter).
67    stm_algo: Algo,
68    /// Cipher for strings (`/StrF` crypt filter).
69    str_algo: Algo,
70    /// The `/Encrypt` dictionary's own object id, which must never be
71    /// decrypted (its strings are stored in the clear). `None` when the
72    /// trailer carries a direct (non-reference) `/Encrypt` dict.
73    encrypt_id: Option<ObjectId>,
74    /// `/EncryptMetadata` (default true). When false, the document-level
75    /// `/Type /Metadata` stream payload is stored in the clear and must not be
76    /// "decrypted" (which would corrupt it).
77    encrypt_metadata: bool,
78}
79
80impl Decryptor {
81    /// Build a decryptor from the `/Encrypt` dictionary, the first element of the
82    /// trailer `/ID`, and a user/owner password (empty for the default open).
83    ///
84    /// The password is authenticated against `/U` (user) and `/O` (owner). A
85    /// non-empty password that matches neither yields [`BuildResult::WrongPassword`].
86    /// The empty-password default preserves the lenient behavior: an RC4 document
87    /// whose `/U` does not validate still opens (best-effort, with a warning),
88    /// since malformed-but-empty-password files are common.
89    pub fn from_encrypt_dict(
90        dict: &PdfDict,
91        id_first: &[u8],
92        encrypt_id: Option<ObjectId>,
93        password: &[u8],
94    ) -> BuildResult {
95        let filter = dict.get_name("Filter").unwrap_or("");
96        if filter != "Standard" {
97            tracing::warn!(
98                "unsupported security handler /Filter {filter}; document will not decrypt"
99            );
100            return BuildResult::Degrade;
101        }
102
103        let v = dict.get_i64("V").unwrap_or(0);
104        let r = dict.get_i64("R").unwrap_or(0);
105
106        // Per-class ciphers. V<4: one document-wide RC4 cipher for everything.
107        // V4/V5: `/CF` crypt filters selected by `/StmF` (streams) and `/StrF`
108        // (strings); per spec the default for each is `Identity` (no
109        // encryption for that class).
110        let (stm_algo, str_algo) = if v >= 4 {
111            (
112                algo_for_filter(dict, dict.get_name("StmF").unwrap_or("Identity")),
113                algo_for_filter(dict, dict.get_name("StrF").unwrap_or("Identity")),
114            )
115        } else {
116            (Algo::Rc4, Algo::Rc4)
117        };
118
119        // Default true (R<4 has no such key and always encrypts metadata).
120        let encrypt_metadata = match dict.get("EncryptMetadata") {
121            Some(PdfObject::Bool(b)) => *b,
122            _ => true,
123        };
124
125        let key = if v >= 5 {
126            // V5 (AESV3): ISO 32000-2 Algorithm 2.A. Validates the password and
127            // recovers the 32-byte file key from /UE or /OE. No best-effort path —
128            // the key can only come from a correct password.
129            match compute_key_v5(dict, r, password) {
130                Some(k) => k,
131                None if password.is_empty() => return BuildResult::Degrade,
132                None => return BuildResult::WrongPassword,
133            }
134        } else {
135            let o = string_bytes(dict, "O");
136            let u = string_bytes(dict, "U");
137            // /P is an integer bit-field, but some producers write it as a Real.
138            let p = match dict.get("P") {
139                Some(PdfObject::Integer(n)) => *n as i32,
140                Some(PdfObject::Real(n)) => *n as i32,
141                _ => 0,
142            };
143            // AESV2 always uses a 128-bit key regardless of what /Length says.
144            let length_bits = if stm_algo == Algo::AesV2 || str_algo == Algo::AesV2 {
145                128
146            } else {
147                key_length_bits(dict, v)
148            };
149
150            match authenticate_rc4(
151                password,
152                &o,
153                &u,
154                p,
155                id_first,
156                r,
157                length_bits,
158                encrypt_metadata,
159            ) {
160                Ok(key) => key,
161                // Authentication failed. Refuse only when a non-empty password
162                // was supplied AND there was a /U to check it against — that is
163                // an unambiguously wrong password. Otherwise open best-effort
164                // with the derived key (the lenient empty-password default, or a
165                // malformed document with no /U), preserving prior behavior.
166                Err(_) if !password.is_empty() && !u.is_empty() => {
167                    return BuildResult::WrongPassword;
168                }
169                Err(best_effort_key) => {
170                    if u.is_empty() && !password.is_empty() {
171                        tracing::warn!(
172                            "encrypted document has no /U to authenticate the password against \
173                             (V={v} R={r}); proceeding with the supplied password unverified"
174                        );
175                    } else {
176                        tracing::warn!(
177                            "encryption key did not validate against /U (V={v} R={r}); the PDF \
178                             may require a password — decrypted content may be garbage"
179                        );
180                    }
181                    best_effort_key
182                }
183            }
184        };
185
186        BuildResult::Decryptor(Self {
187            key,
188            stm_algo,
189            str_algo,
190            encrypt_id,
191            encrypt_metadata,
192        })
193    }
194
195    /// Recursively decrypt every string and stream contained in `obj`, in place,
196    /// using the per-object key derived from `id`. No-op for the `/Encrypt`
197    /// object itself.
198    pub fn decrypt_object(&self, obj: &mut PdfObject, id: ObjectId) {
199        if Some(id) == self.encrypt_id {
200            return;
201        }
202        self.walk(obj, id);
203    }
204
205    /// Decrypt a raw stream byte buffer with the per-object key for `id`. Used
206    /// for the `/Type /ObjStm` container, which is decrypted directly (not via
207    /// [`decrypt_object`](Self::decrypt_object)) before its filter pipeline.
208    pub fn decrypt_stream_bytes(&self, id: ObjectId, data: &[u8]) -> Vec<u8> {
209        self.decrypt(id, data, self.stm_algo)
210    }
211
212    /// **Encrypt** a stream payload for object `id` (the write-side inverse of
213    /// [`Self::decrypt_stream_bytes`]). Used by the incremental writer to add
214    /// objects to an already-encrypted document with its existing key.
215    pub fn encrypt_stream_bytes(&self, id: ObjectId, data: &[u8]) -> Vec<u8> {
216        self.encrypt(id, data, self.stm_algo)
217    }
218
219    /// **Encrypt** a string payload for object `id`.
220    pub fn encrypt_string_bytes(&self, id: ObjectId, data: &[u8]) -> Vec<u8> {
221        self.encrypt(id, data, self.str_algo)
222    }
223
224    /// Recursively encrypt every string in `obj` in place with the per-object
225    /// key for `id`. Stream payloads are NOT touched (they are encrypted
226    /// separately via [`Self::encrypt_stream_bytes`], since writers carry the
227    /// payload out-of-band).
228    pub fn encrypt_object_strings(&self, obj: &mut PdfObject, id: ObjectId) {
229        match obj {
230            PdfObject::String(s) if self.str_algo != Algo::Identity => {
231                *s = PdfString(self.encrypt(id, &s.0, self.str_algo));
232            }
233            PdfObject::String(_) => {}
234            PdfObject::Array(a) => {
235                for o in a.iter_mut() {
236                    self.encrypt_object_strings(o, id);
237                }
238            }
239            PdfObject::Dict(d) => {
240                for v in d.0.values_mut() {
241                    self.encrypt_object_strings(v, id);
242                }
243            }
244            PdfObject::Stream(s) => {
245                for v in s.dict.0.values_mut() {
246                    self.encrypt_object_strings(v, id);
247                }
248            }
249            _ => {}
250        }
251    }
252
253    /// Per-object key derivation + cipher application, encrypt direction.
254    fn encrypt(&self, id: ObjectId, data: &[u8], algo: Algo) -> Vec<u8> {
255        match algo {
256            Algo::Identity => data.to_vec(),
257            // RC4 is symmetric.
258            Algo::Rc4 => rc4(&self.object_key(id, algo), data),
259            Algo::AesV2 | Algo::AesV3 => aes_cbc_encrypt(&self.object_key(id, algo), data),
260        }
261    }
262
263    fn walk(&self, obj: &mut PdfObject, id: ObjectId) {
264        match obj {
265            PdfObject::String(s) if self.str_algo != Algo::Identity => {
266                *s = PdfString(self.decrypt(id, &s.0, self.str_algo));
267            }
268            PdfObject::String(_) => {}
269            PdfObject::Array(a) => {
270                for o in a.iter_mut() {
271                    self.walk(o, id);
272                }
273            }
274            PdfObject::Dict(d) => {
275                for v in d.0.values_mut() {
276                    self.walk(v, id);
277                }
278            }
279            PdfObject::Stream(s) => {
280                let typ = s.dict.get_name("Type").unwrap_or("");
281                // Cross-reference streams are never encrypted (PDF 1.7 §7.6.1).
282                let is_xref = typ == "XRef";
283                // /EncryptMetadata false: metadata stream payloads are stored
284                // in the clear; "decrypting" them would corrupt the XMP.
285                // Limitation: only streams self-identifying as /Type /Metadata
286                // are detectable here (we don't know if this object is the
287                // catalog's /Metadata target) — that covers conforming files.
288                let plain_meta = !self.encrypt_metadata && typ == "Metadata";
289                if !is_xref && !plain_meta && self.stm_algo != Algo::Identity {
290                    let dec = self.decrypt(id, &s.data, self.stm_algo);
291                    s.data = Arc::from(dec);
292                }
293                for v in s.dict.0.values_mut() {
294                    self.walk(v, id);
295                }
296            }
297            // Refs are followed (and decrypted) when resolved; scalars are plain.
298            _ => {}
299        }
300    }
301
302    /// Per-object key derivation (Algorithm 1) + cipher application.
303    fn decrypt(&self, id: ObjectId, data: &[u8], algo: Algo) -> Vec<u8> {
304        match algo {
305            Algo::Identity => data.to_vec(),
306            Algo::Rc4 => rc4(&self.object_key(id, algo), data),
307            Algo::AesV2 | Algo::AesV3 => aes_cbc_decrypt(&self.object_key(id, algo), data),
308        }
309    }
310
311    /// Algorithm 1: object key = MD5(file_key || obj_num[3 LE] || gen[2 LE]
312    /// [|| "sAlT" for AESV2]), truncated to min(n+5, 16) bytes. AESV3 (V5) has
313    /// no per-object derivation — the 32-byte file key is used directly.
314    fn object_key(&self, id: ObjectId, algo: Algo) -> Vec<u8> {
315        if algo == Algo::AesV3 {
316            return self.key.clone();
317        }
318        let mut input = Vec::with_capacity(self.key.len() + 9);
319        input.extend_from_slice(&self.key);
320        let num = id.0.to_le_bytes();
321        input.extend_from_slice(&num[..3]);
322        let gen = id.1.to_le_bytes();
323        input.extend_from_slice(&gen[..2]);
324        if algo == Algo::AesV2 {
325            input.extend_from_slice(b"sAlT");
326        }
327        let hash = md5(&input);
328        let n = (self.key.len() + 5).min(16);
329        hash[..n].to_vec()
330    }
331}
332
333/// Effective file-key length, in bits. For V≥4 the key size of an RC4 crypt
334/// filter lives in `/CF/<StmF>/Length` and is expressed in **bytes** (ISO 32000
335/// §7.6.5), distinct from the document-level `/Encrypt /Length` (in **bits**,
336/// §7.6.3). Prefer the crypt-filter length; fall back to the document length,
337/// then to 40.
338fn key_length_bits(dict: &PdfDict, v: i64) -> i64 {
339    if v >= 4 {
340        let stmf = dict.get_name("StmF").unwrap_or("Identity");
341        if stmf != "Identity" {
342            if let Some(len) = dict
343                .get_dict("CF")
344                .ok()
345                .and_then(|cf| cf.get_dict(stmf).ok())
346                .and_then(|f| f.get_i64("Length").ok())
347            {
348                // Spec says bytes (5..=16); a value clearly too large for bytes is
349                // a non-conforming producer that wrote bits — accept either.
350                return if len <= 32 { len * 8 } else { len };
351            }
352        }
353    }
354    dict.get_i64("Length").unwrap_or(40)
355}
356
357/// Validate the derived file key against `/U` (Algorithm 6).
358/// R2: `/U` == RC4(key, PAD) (Algorithm 4). R≥3: the first 16 bytes of `/U`
359/// match the Algorithm 5 computation. Returns `false` when `/U` is absent —
360/// there is nothing to authenticate against, which the caller handles as a
361/// best-effort open rather than a confirmed match.
362fn validate_user_password(key: &[u8], u: &[u8], id_first: &[u8], r: i64) -> bool {
363    if u.is_empty() {
364        return false;
365    }
366    if r == 2 {
367        return rc4(key, &PAD) == u;
368    }
369    // R≥3 (Algorithm 5): MD5(PAD || ID), RC4 with the key, then 19 more RC4
370    // passes whose key is the file key XORed with the 1-based iteration index.
371    let mut input = Vec::with_capacity(PAD.len() + id_first.len());
372    input.extend_from_slice(&PAD);
373    input.extend_from_slice(id_first);
374    let mut x = rc4(key, &md5(&input));
375    for i in 1u8..=19 {
376        let step_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
377        x = rc4(&step_key, &x);
378    }
379    // Only the first 16 bytes of /U are deterministic; the rest is padding.
380    u.len() >= 16 && x.len() >= 16 && x[..16] == u[..16]
381}
382
383/// Map a `/StmF`-or-`/StrF` crypt-filter name to a cipher. `Identity` (also the
384/// spec default) means no encryption for that class. Otherwise the named filter
385/// in `/CF` declares its method via `/CFM`: `V2` (RC4), `AESV2`, `AESV3`, or
386/// `None`.
387fn algo_for_filter(dict: &PdfDict, filter_name: &str) -> Algo {
388    if filter_name == "Identity" {
389        return Algo::Identity;
390    }
391    let cfm = dict
392        .get_dict("CF")
393        .ok()
394        .and_then(|cf| cf.get_dict(filter_name).ok())
395        .and_then(|f| f.get_name("CFM").ok())
396        .unwrap_or("V2");
397    match cfm {
398        "AESV2" => Algo::AesV2,
399        "AESV3" => Algo::AesV3,
400        "None" => Algo::Identity,
401        _ => Algo::Rc4,
402    }
403}
404
405/// Pad a password to 32 bytes per Algorithm 2 step (a): the first ≤32 password
406/// bytes followed by the standard 32-byte PAD, truncated to 32.
407fn pad_password(password: &[u8]) -> [u8; 32] {
408    let mut out = [0u8; 32];
409    let take = password.len().min(32);
410    out[..take].copy_from_slice(&password[..take]);
411    out[take..].copy_from_slice(&PAD[..32 - take]);
412    out
413}
414
415/// The RC4/AES-128 key-derivation byte length `n` for the given revision.
416fn rc4_key_len(r: i64, length_bits: i64) -> usize {
417    if r == 2 {
418        5
419    } else {
420        (length_bits / 8).clamp(5, 16) as usize
421    }
422}
423
424/// Authenticate `password` for an RC4/AES-128 document: try it as the user
425/// password (Algorithm 6), then as the owner password (Algorithm 7, which
426/// recovers the user password from `/O`). `Ok(key)` is a validated key; `Err(key)`
427/// carries the user-password-derived key as a best-effort fallback (for the
428/// lenient empty-password open, or a malformed document with no `/U` to check).
429#[allow(clippy::too_many_arguments)]
430fn authenticate_rc4(
431    password: &[u8],
432    o: &[u8],
433    u: &[u8],
434    p: i32,
435    id_first: &[u8],
436    r: i64,
437    length_bits: i64,
438    encrypt_metadata: bool,
439) -> std::result::Result<Vec<u8>, Vec<u8>> {
440    // User password (Algorithm 6).
441    let key = compute_key_rc4(password, o, p, id_first, r, length_bits, encrypt_metadata);
442    if validate_user_password(&key, u, id_first, r) {
443        return Ok(key);
444    }
445    // Owner password (Algorithm 7): recover the user password from /O, then
446    // run Algorithm 2 with it.
447    let recovered = recover_user_password_rc4(password, o, r, length_bits);
448    let owner_key = compute_key_rc4(&recovered, o, p, id_first, r, length_bits, encrypt_metadata);
449    if validate_user_password(&owner_key, u, id_first, r) {
450        return Ok(owner_key);
451    }
452    Err(key)
453}
454
455/// Algorithm 7: recover the (padded) user password from `/O` using the supplied
456/// owner password. The owner key is derived as in Algorithm 3, then `/O` is
457/// RC4-decrypted (a single pass for R2, 20 reverse-counter passes for R≥3).
458fn recover_user_password_rc4(owner_password: &[u8], o: &[u8], r: i64, length_bits: i64) -> Vec<u8> {
459    let n = rc4_key_len(r, length_bits);
460    let mut hash = md5(&pad_password(owner_password));
461    if r >= 3 {
462        for _ in 0..50 {
463            hash = md5(&hash[..n]);
464        }
465    }
466    let owner_key = &hash[..n];
467
468    let mut user = o.to_vec();
469    if r == 2 {
470        user = rc4(owner_key, &user);
471    } else {
472        for i in (0..=19u8).rev() {
473            let step_key: Vec<u8> = owner_key.iter().map(|b| b ^ i).collect();
474            user = rc4(&step_key, &user);
475        }
476    }
477    user
478}
479
480/// Algorithm 2 (RC4/AES-128 key, revisions 2–4): derive the file encryption key
481/// from the (padded) user password.
482fn compute_key_rc4(
483    password: &[u8],
484    o: &[u8],
485    p: i32,
486    id_first: &[u8],
487    r: i64,
488    length_bits: i64,
489    encrypt_metadata: bool,
490) -> Vec<u8> {
491    let n = rc4_key_len(r, length_bits);
492
493    let mut input = Vec::with_capacity(32 + 32 + 4 + id_first.len() + 4);
494    // Step (a): the padded user password.
495    input.extend_from_slice(&pad_password(password));
496    // Step (b): the /O entry, padded/truncated to 32 bytes.
497    let mut o32 = [0u8; 32];
498    let take = o.len().min(32);
499    o32[..take].copy_from_slice(&o[..take]);
500    input.extend_from_slice(&o32);
501    // Step (c): /P as 4 bytes, low-order byte first.
502    input.extend_from_slice(&(p as u32).to_le_bytes());
503    // Step (d): the first file identifier.
504    input.extend_from_slice(id_first);
505    // Step (e): R≥4 with EncryptMetadata=false appends 0xFFFFFFFF.
506    if r >= 4 && !encrypt_metadata {
507        input.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
508    }
509
510    let mut hash = md5(&input);
511    // Step (f), R≥3: 50 extra MD5 passes over the first n bytes.
512    if r >= 3 {
513        for _ in 0..50 {
514            hash = md5(&hash[..n]);
515        }
516    }
517    hash[..n].to_vec()
518}
519
520// ----------------------------------------------------------------------------
521// V5 (AES-256) key derivation — ISO 32000-2 §7.6.4.3.3/4, Algorithms 2.A & 2.B
522// ----------------------------------------------------------------------------
523
524/// Algorithm 2.A: validate `password` as the user password against `/U` and
525/// recover the 32-byte file key from `/UE`; fall back to the owner password
526/// (`/O` with the first 48 bytes of `/U` appended to the hash input) and `/OE`.
527/// Returns `None` (with a warning) when neither validates — a wrong/missing
528/// password.
529fn compute_key_v5(dict: &PdfDict, r: i64, password: &[u8]) -> Option<Vec<u8>> {
530    let o = string_bytes(dict, "O");
531    let u = string_bytes(dict, "U");
532    let oe = string_bytes(dict, "OE");
533    let ue = string_bytes(dict, "UE");
534    // ISO 32000-2 §7.6.4.3.3: the V5 password is UTF-8, SASLprep-normalized, and
535    // truncated to at most 127 bytes before hashing. We apply the byte cap (the
536    // common interop case); SASLprep normalization of non-ASCII passwords is not
537    // performed (it would need a stringprep table — out of scope for now).
538    let password = &password[..password.len().min(127)];
539
540    // Algorithm 11 (user): /U = hash[32] || validation-salt[8] || key-salt[8].
541    // On a validation hit but a broken /UE, fall through to the owner path.
542    if u.len() >= 48 {
543        let (vsalt, ksalt) = (&u[32..40], &u[40..48]);
544        if hash_v5(r, password, vsalt, &[])[..] == u[..32] {
545            let ik = hash_v5(r, password, ksalt, &[]);
546            if let Some(key) = decrypt_file_key(&ik, &ue, "UE") {
547                return Some(key);
548            }
549        }
550    }
551    // Algorithm 12 (owner): same layout, with U[0..48] appended to the input.
552    if o.len() >= 48 && u.len() >= 48 {
553        let u48 = &u[..48];
554        let (vsalt, ksalt) = (&o[32..40], &o[40..48]);
555        if hash_v5(r, password, vsalt, u48)[..] == o[..32] {
556            let ik = hash_v5(r, password, ksalt, u48);
557            if let Some(key) = decrypt_file_key(&ik, &oe, "OE") {
558                return Some(key);
559            }
560        }
561    }
562    tracing::warn!(
563        "V5/R{r} password validation failed (the PDF likely requires a password); \
564         document will not decrypt"
565    );
566    None
567}
568
569/// Decrypt the 32-byte file key from `/UE` or `/OE`: AES-256-CBC with the
570/// intermediate key, a zero IV, and no padding.
571fn decrypt_file_key(intermediate: &[u8; 32], encrypted: &[u8], which: &str) -> Option<Vec<u8>> {
572    if encrypted.len() != 32 {
573        tracing::warn!(
574            "/{which} must be 32 bytes, got {}; document will not decrypt",
575            encrypted.len()
576        );
577        return None;
578    }
579    let mut buf = encrypted.to_vec();
580    if !cbc_decrypt_in_place(intermediate, &[0u8; 16], &mut buf) {
581        return None;
582    }
583    Some(buf)
584}
585
586/// The V5 password hash: SHA-256(password ‖ salt ‖ udata), hardened with
587/// Algorithm 2.B for R6.
588fn hash_v5(r: i64, password: &[u8], salt: &[u8], udata: &[u8]) -> [u8; 32] {
589    let mut input = Vec::with_capacity(password.len() + salt.len() + udata.len());
590    input.extend_from_slice(password);
591    input.extend_from_slice(salt);
592    input.extend_from_slice(udata);
593    let initial: [u8; 32] = sha2::Sha256::digest(&input).into();
594    if r >= 6 {
595        hash_r6(initial, password, udata)
596    } else {
597        initial
598    }
599}
600
601/// Algorithm 2.B (R6 hardened hash): iterate AES-128-CBC over 64 repetitions of
602/// (password ‖ K ‖ udata), re-hashing K with SHA-256/384/512 chosen by the
603/// first 16 bytes of the ciphertext mod 3. At least 64 rounds; stop once the
604/// last ciphertext byte is ≤ (round − 32).
605fn hash_r6(initial: [u8; 32], password: &[u8], udata: &[u8]) -> [u8; 32] {
606    let mut k: Vec<u8> = initial.to_vec();
607    let mut e_last: u8 = 0;
608    let mut round: i64 = 0;
609    while round < 64 || i64::from(e_last) > round - 32 {
610        // K1 = 64 repetitions of (password || K || udata). Its length is always
611        // a multiple of 16 (any unit length × 64 is a multiple of 64).
612        let mut k1 = Vec::with_capacity(64 * (password.len() + k.len() + udata.len()));
613        for _ in 0..64 {
614            k1.extend_from_slice(password);
615            k1.extend_from_slice(&k);
616            k1.extend_from_slice(udata);
617        }
618        let e = aes128_cbc_encrypt_nopad(&k[..16], &k[16..32], &k1);
619        e_last = *e.last().unwrap_or(&0);
620        let m = e[..16].iter().map(|&b| u32::from(b)).sum::<u32>() % 3;
621        k = match m {
622            0 => sha2::Sha256::digest(&e).to_vec(),
623            1 => sha2::Sha384::digest(&e).to_vec(),
624            _ => sha2::Sha512::digest(&e).to_vec(),
625        };
626        round += 1;
627    }
628    let mut out = [0u8; 32];
629    out.copy_from_slice(&k[..32]);
630    out
631}
632
633/// Read a PDF string entry's raw bytes from a dict (empty if absent/non-string).
634fn string_bytes(dict: &PdfDict, key: &str) -> Vec<u8> {
635    match dict.get(key) {
636        Some(PdfObject::String(s)) => s.0.clone(),
637        _ => Vec::new(),
638    }
639}
640
641// ----------------------------------------------------------------------------
642// AES-CBC (pure-Rust RustCrypto `aes` + `cbc`)
643// ----------------------------------------------------------------------------
644
645/// Decrypt an AES-CBC payload as stored in a PDF: the first 16 bytes are the
646/// IV, the rest is ciphertext with PKCS#5 padding. The padding is stripped
647/// defensively — on invalid padding the unpadded plaintext is kept (with a
648/// warning) rather than truncated arbitrarily. A structurally impossible
649/// payload (length not 16+16k) or a bad key length returns the input unchanged.
650fn aes_cbc_decrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
651    if data.is_empty() {
652        return Vec::new();
653    }
654    if data.len() < 16 || !(data.len() - 16).is_multiple_of(16) {
655        tracing::warn!(
656            "AES-CBC payload length {} is not 16+16k; leaving data unmodified",
657            data.len()
658        );
659        return data.to_vec();
660    }
661    let (iv, ct) = data.split_at(16);
662    let mut buf = ct.to_vec();
663    if !cbc_decrypt_in_place(key, iv, &mut buf) {
664        tracing::warn!(
665            "invalid AES key length {}; leaving data unmodified",
666            key.len()
667        );
668        return data.to_vec();
669    }
670    strip_pkcs5_padding(buf)
671}
672
673/// Strip PKCS#5/7 padding in place; on malformed padding keep the data and warn.
674fn strip_pkcs5_padding(mut buf: Vec<u8>) -> Vec<u8> {
675    let Some(&last) = buf.last() else { return buf };
676    let pad = last as usize;
677    if (1..=16).contains(&pad)
678        && pad <= buf.len()
679        && buf[buf.len() - pad..].iter().all(|&b| b == last)
680    {
681        buf.truncate(buf.len() - pad);
682    } else {
683        tracing::warn!("invalid PKCS#5 padding byte {last}; keeping unpadded data");
684    }
685    buf
686}
687
688/// AES-CBC decrypt `buf` in place with no padding handling. Key length selects
689/// AES-128 vs AES-256. Returns `false` for an unsupported key or IV length.
690fn cbc_decrypt_in_place(key: &[u8], iv: &[u8], buf: &mut [u8]) -> bool {
691    debug_assert_eq!(buf.len() % 16, 0);
692    match key.len() {
693        16 => {
694            let Ok(mut dec) = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv) else {
695                return false;
696            };
697            for block in buf.chunks_exact_mut(16) {
698                dec.decrypt_block_mut(GenericArray::from_mut_slice(block));
699            }
700            true
701        }
702        32 => {
703            let Ok(mut dec) = cbc::Decryptor::<aes::Aes256>::new_from_slices(key, iv) else {
704                return false;
705            };
706            for block in buf.chunks_exact_mut(16) {
707                dec.decrypt_block_mut(GenericArray::from_mut_slice(block));
708            }
709            true
710        }
711        _ => false,
712    }
713}
714
715/// AES-128-CBC **encrypt** with no padding (input length must be a multiple of
716/// 16). Used only by the R6 hardened hash (Algorithm 2.B).
717fn aes128_cbc_encrypt_nopad(key: &[u8], iv: &[u8], data: &[u8]) -> Vec<u8> {
718    debug_assert_eq!(data.len() % 16, 0);
719    let mut buf = data.to_vec();
720    let Ok(mut enc) = cbc::Encryptor::<aes::Aes128>::new_from_slices(key, iv) else {
721        return buf; // unreachable: callers always pass 16-byte key/iv slices
722    };
723    for block in buf.chunks_exact_mut(16) {
724        enc.encrypt_block_mut(GenericArray::from_mut_slice(block));
725    }
726    buf
727}
728
729/// AES-CBC **encrypt** in the PDF payload format: random IV || ciphertext with
730/// PKCS#5 padding. Key length selects AES-128 (AESV2) vs AES-256 (AESV3). The
731/// write-side inverse of [`aes_cbc_decrypt`]; used when adding objects to an
732/// encrypted document.
733fn aes_cbc_encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
734    let mut iv = [0u8; 16];
735    // The IV must be unpredictable but need not be secret. getrandom is not a
736    // parser dependency, so derive it from entropy we have: MD5 over the data
737    // plus a process-unique counter. (Writers with stronger requirements
738    // encrypt via zpdf-writer's Encryptor, which uses the system RNG.)
739    use std::sync::atomic::{AtomicU64, Ordering};
740    static COUNTER: AtomicU64 = AtomicU64::new(0);
741    let nonce = COUNTER.fetch_add(1, Ordering::Relaxed);
742    let mut seed = Vec::with_capacity(data.len().min(256) + 16);
743    seed.extend_from_slice(&nonce.to_le_bytes());
744    seed.extend_from_slice(&(data.len() as u64).to_le_bytes());
745    seed.extend_from_slice(&data[..data.len().min(240)]);
746    iv.copy_from_slice(&md5(&seed));
747
748    let pad = 16 - (data.len() % 16);
749    let mut buf = Vec::with_capacity(data.len() + pad);
750    buf.extend_from_slice(data);
751    buf.extend(std::iter::repeat_n(pad as u8, pad));
752
753    let ok = match key.len() {
754        16 => {
755            if let Ok(mut enc) = cbc::Encryptor::<aes::Aes128>::new_from_slices(key, &iv) {
756                for block in buf.chunks_exact_mut(16) {
757                    enc.encrypt_block_mut(GenericArray::from_mut_slice(block));
758                }
759                true
760            } else {
761                false
762            }
763        }
764        32 => {
765            if let Ok(mut enc) = cbc::Encryptor::<aes::Aes256>::new_from_slices(key, &iv) {
766                for block in buf.chunks_exact_mut(16) {
767                    enc.encrypt_block_mut(GenericArray::from_mut_slice(block));
768                }
769                true
770            } else {
771                false
772            }
773        }
774        _ => false,
775    };
776    if !ok {
777        tracing::warn!(
778            "invalid AES key length {}; data left unencrypted",
779            key.len()
780        );
781        return data.to_vec();
782    }
783
784    let mut out = Vec::with_capacity(16 + buf.len());
785    out.extend_from_slice(&iv);
786    out.extend_from_slice(&buf);
787    out
788}
789
790// ----------------------------------------------------------------------------
791// RC4 stream cipher
792// ----------------------------------------------------------------------------
793
794/// RC4 encrypt/decrypt (symmetric). Returns the input unchanged if `key` is
795/// empty (degenerate, should not happen for a valid handler).
796fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
797    if key.is_empty() {
798        return data.to_vec();
799    }
800    let mut s: [u8; 256] = [0; 256];
801    for (i, b) in s.iter_mut().enumerate() {
802        *b = i as u8;
803    }
804    let mut j: u8 = 0;
805    for i in 0..256 {
806        j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
807        s.swap(i, j as usize);
808    }
809
810    let mut out = Vec::with_capacity(data.len());
811    let mut i: u8 = 0;
812    let mut j: u8 = 0;
813    for &byte in data {
814        i = i.wrapping_add(1);
815        j = j.wrapping_add(s[i as usize]);
816        s.swap(i as usize, j as usize);
817        let k = s[(s[i as usize].wrapping_add(s[j as usize])) as usize];
818        out.push(byte ^ k);
819    }
820    out
821}
822
823// ----------------------------------------------------------------------------
824// MD5 (RFC 1321) — one-shot
825// ----------------------------------------------------------------------------
826
827/// Per-round left-rotation amounts.
828const MD5_S: [u32; 64] = [
829    7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
830    14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
831    21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
832];
833
834/// Precomputed `floor(2^32 * abs(sin(i+1)))` constants.
835const MD5_K: [u32; 64] = [
836    0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
837    0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
838    0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
839    0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
840    0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
841    0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
842    0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
843    0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
844];
845
846/// Compute the MD5 digest of `data`.
847pub fn md5(data: &[u8]) -> [u8; 16] {
848    let mut a0: u32 = 0x67452301;
849    let mut b0: u32 = 0xefcdab89;
850    let mut c0: u32 = 0x98badcfe;
851    let mut d0: u32 = 0x10325476;
852
853    // Pad: append 0x80, then zeros, then the 64-bit little-endian bit length.
854    let bit_len = (data.len() as u64).wrapping_mul(8);
855    let mut msg = data.to_vec();
856    msg.push(0x80);
857    while msg.len() % 64 != 56 {
858        msg.push(0);
859    }
860    msg.extend_from_slice(&bit_len.to_le_bytes());
861
862    for chunk in msg.chunks_exact(64) {
863        let mut m = [0u32; 16];
864        for (i, word) in m.iter_mut().enumerate() {
865            *word = u32::from_le_bytes([
866                chunk[i * 4],
867                chunk[i * 4 + 1],
868                chunk[i * 4 + 2],
869                chunk[i * 4 + 3],
870            ]);
871        }
872
873        let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
874        for i in 0..64 {
875            let (f, g) = match i {
876                0..=15 => ((b & c) | (!b & d), i),
877                16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
878                32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
879                _ => (c ^ (b | !d), (7 * i) % 16),
880            };
881            let f = f.wrapping_add(a).wrapping_add(MD5_K[i]).wrapping_add(m[g]);
882            a = d;
883            d = c;
884            c = b;
885            b = b.wrapping_add(f.rotate_left(MD5_S[i]));
886        }
887
888        a0 = a0.wrapping_add(a);
889        b0 = b0.wrapping_add(b);
890        c0 = c0.wrapping_add(c);
891        d0 = d0.wrapping_add(d);
892    }
893
894    let mut out = [0u8; 16];
895    out[0..4].copy_from_slice(&a0.to_le_bytes());
896    out[4..8].copy_from_slice(&b0.to_le_bytes());
897    out[8..12].copy_from_slice(&c0.to_le_bytes());
898    out[12..16].copy_from_slice(&d0.to_le_bytes());
899    out
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use zpdf_core::{PdfName, PdfStream};
906
907    fn hex(bytes: &[u8]) -> String {
908        bytes.iter().map(|b| format!("{b:02x}")).collect()
909    }
910
911    fn unhex(s: &str) -> Vec<u8> {
912        (0..s.len())
913            .step_by(2)
914            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
915            .collect()
916    }
917
918    /// AES-256-CBC encrypt with no padding — test-only helper for building V5
919    /// fixtures (the production code only ever decrypts with AES-256).
920    fn aes256_cbc_encrypt_nopad(key: &[u8], iv: &[u8], data: &[u8]) -> Vec<u8> {
921        let mut buf = data.to_vec();
922        let mut enc = cbc::Encryptor::<aes::Aes256>::new_from_slices(key, iv).unwrap();
923        for block in buf.chunks_exact_mut(16) {
924            enc.encrypt_block_mut(GenericArray::from_mut_slice(block));
925        }
926        buf
927    }
928
929    /// Navigate Root → Pages → Kids[0] → Contents and decode the stream.
930    fn content_stream_bytes(file: &crate::PdfFile) -> Vec<u8> {
931        let root = file.trailer.get_ref("Root").expect("trailer /Root");
932        let cat = file.resolve(root).expect("resolve catalog");
933        let pages_ref = cat.as_dict().unwrap().get_ref("Pages").unwrap();
934        let pages = file.resolve(pages_ref).expect("resolve pages");
935        let kids = pages.as_dict().unwrap().get_array("Kids").unwrap().to_vec();
936        let PdfObject::Ref(page_ref) = kids[0] else {
937            panic!("Kids[0] is not a reference")
938        };
939        let page = file.resolve(page_ref).expect("resolve page");
940        let contents_ref = page.as_dict().unwrap().get_ref("Contents").unwrap();
941        file.resolve_stream_data(contents_ref)
942            .expect("decode content stream")
943    }
944
945    /// Validates the full RC4-40 key-derivation pipeline against reference
946    /// values computed independently from tests/test4/1.pdf (/V 1 /R 2
947    /// /Length 40, empty user password). Self-contained: no file needed.
948    #[test]
949    fn test4_rc4_key_derivation_oracle() {
950        let o = unhex("c5e5cd078ac4b56637f8a5d03a1ecd261ecf59fdcd8b50944ba1bb0e9e95ebfb");
951        let u = unhex("ffe4a8e86d2951800946f19d21089e1a71ca3d813608e586339bab72aa28206a");
952        let id0 = unhex("1a6dd6c3b3c1957a915bb98dbf691ce0");
953        let p: i32 = -64;
954
955        // Algorithm 2 → file encryption key.
956        let key = compute_key_rc4(b"", &o, p, &id0, 2, 40, true);
957        assert_eq!(hex(&key), "b374aaeaf4", "file key (Algorithm 2)");
958
959        // Algorithm 4 (R2): RC4(file_key, PAD) must equal stored /U.
960        assert_eq!(rc4(&key, &PAD), u, "user-password validation (Algorithm 4)");
961        assert!(
962            validate_user_password(&key, &u, &id0, 2),
963            "validate_user_password should accept the correct R2 key"
964        );
965        // A wrong key (empty /ID) must NOT validate.
966        let wrong = compute_key_rc4(b"", &o, p, &[], 2, 40, true);
967        assert!(
968            !validate_user_password(&wrong, &u, &id0, 2),
969            "validate_user_password should reject a wrong key"
970        );
971
972        // Algorithm 1 → per-object key for the page-1 content stream (1652, 0).
973        let dec = Decryptor {
974            key,
975            stm_algo: Algo::Rc4,
976            str_algo: Algo::Rc4,
977            encrypt_id: None,
978            encrypt_metadata: true,
979        };
980        let objkey = dec.object_key(ObjectId(1652, 0), Algo::Rc4);
981        assert_eq!(
982            hex(&objkey),
983            "30dadd6463d5f9765abc",
984            "per-object key (Algorithm 1)"
985        );
986    }
987
988    #[test]
989    fn md5_known_answers() {
990        // RFC 1321 test suite.
991        assert_eq!(hex(&md5(b"")), "d41d8cd98f00b204e9800998ecf8427e");
992        assert_eq!(hex(&md5(b"a")), "0cc175b9c0f1b6a831c399e269772661");
993        assert_eq!(hex(&md5(b"abc")), "900150983cd24fb0d6963f7d28e17f72");
994        assert_eq!(
995            hex(&md5(b"message digest")),
996            "f96b697d7cb7938d525a2f31aaf161d0"
997        );
998        assert_eq!(
999            hex(&md5(b"abcdefghijklmnopqrstuvwxyz")),
1000            "c3fcd3d76192e4007dfb496cca67e13b"
1001        );
1002        // Exercises the multi-block (>56 byte) padding path.
1003        assert_eq!(
1004            hex(&md5(
1005                b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
1006            )),
1007            "d174ab98d277d9f5a5611c2c9f419d9f"
1008        );
1009    }
1010
1011    #[test]
1012    fn v4_rc4_key_length_from_crypt_filter() {
1013        // A /V 4 /R 4 RC4-128 handler that records its key size only in the
1014        // crypt filter (/CF/StdCF/Length 16 bytes), with no top-level /Length.
1015        let mut stdcf = PdfDict::new();
1016        stdcf.insert(PdfName::new("CFM"), PdfObject::Name(PdfName::new("V2")));
1017        stdcf.insert(PdfName::new("Length"), PdfObject::Integer(16)); // bytes
1018        let mut cf = PdfDict::new();
1019        cf.insert(PdfName::new("StdCF"), PdfObject::Dict(stdcf));
1020        let mut dict = PdfDict::new();
1021        dict.insert(PdfName::new("CF"), PdfObject::Dict(cf));
1022        dict.insert(PdfName::new("StmF"), PdfObject::Name(PdfName::new("StdCF")));
1023        dict.insert(PdfName::new("V"), PdfObject::Integer(4));
1024
1025        // Must read 16 bytes → 128 bits from the crypt filter, not default to 40.
1026        assert_eq!(key_length_bits(&dict, 4), 128);
1027        // And the cipher must classify as RC4 (CFM == V2).
1028        assert_eq!(algo_for_filter(&dict, "StdCF"), Algo::Rc4);
1029        // /Identity and /CFM /None mean "no encryption for this class".
1030        assert_eq!(algo_for_filter(&dict, "Identity"), Algo::Identity);
1031    }
1032
1033    #[test]
1034    fn rc4_known_answers() {
1035        // Classic RC4 test vectors (key "Key", plaintext "Plaintext").
1036        let ct = rc4(b"Key", b"Plaintext");
1037        assert_eq!(hex(&ct), "bbf316e8d940af0ad3");
1038        // Symmetry: decrypting the ciphertext recovers the plaintext.
1039        assert_eq!(rc4(b"Key", &ct), b"Plaintext");
1040
1041        let ct = rc4(b"Wiki", b"pedia");
1042        assert_eq!(hex(&ct), "1021bf0420");
1043
1044        let ct = rc4(b"Secret", b"Attack at dawn");
1045        assert_eq!(hex(&ct), "45a01f645fc35b383552544b9bf5");
1046    }
1047
1048    #[test]
1049    fn aes_cbc_known_answers() {
1050        // NIST SP 800-38A F.2.1 (CBC-AES128.Encrypt), first block.
1051        let key = unhex("2b7e151628aed2a6abf7158809cf4f3c");
1052        let iv = unhex("000102030405060708090a0b0c0d0e0f");
1053        let pt = unhex("6bc1bee22e409f96e93d7e117393172a");
1054        let ct = aes128_cbc_encrypt_nopad(&key, &iv, &pt);
1055        assert_eq!(hex(&ct), "7649abac8119b246cee98e9b12e9197d");
1056        // Decrypt round-trips.
1057        let mut buf = ct.clone();
1058        assert!(cbc_decrypt_in_place(&key, &iv, &mut buf));
1059        assert_eq!(buf, pt);
1060
1061        // NIST SP 800-38A F.2.5 (CBC-AES256.Encrypt), first block.
1062        let key = unhex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4");
1063        let ct256 = aes256_cbc_encrypt_nopad(&key, &iv, &pt);
1064        assert_eq!(hex(&ct256), "f58c4c04d6e5f1ba779eabfb5f7bfbd6");
1065        let mut buf = ct256.clone();
1066        assert!(cbc_decrypt_in_place(&key, &iv, &mut buf));
1067        assert_eq!(buf, pt);
1068    }
1069
1070    #[test]
1071    fn aes_iv_prefix_and_padding() {
1072        let key = unhex("000102030405060708090a0b0c0d0e0f");
1073        let iv = [0x42u8; 16];
1074        let plaintext = b"attack at dawn".to_vec(); // 14 bytes → 2 bytes padding
1075
1076        // Build a PDF-style payload: IV || AES-CBC(plaintext + PKCS#5 pad).
1077        let mut padded = plaintext.clone();
1078        padded.extend_from_slice(&[2, 2]);
1079        let mut payload = iv.to_vec();
1080        payload.extend_from_slice(&aes128_cbc_encrypt_nopad(&key, &iv, &padded));
1081        assert_eq!(aes_cbc_decrypt(&key, &payload), plaintext);
1082
1083        // Invalid padding (last byte 0 / out of range): keep the data, warn.
1084        let mut bad = plaintext.clone();
1085        bad.extend_from_slice(&[2, 0]);
1086        let mut payload = iv.to_vec();
1087        payload.extend_from_slice(&aes128_cbc_encrypt_nopad(&key, &iv, &bad));
1088        assert_eq!(aes_cbc_decrypt(&key, &payload), bad);
1089
1090        // Structurally impossible lengths are returned unmodified.
1091        assert_eq!(aes_cbc_decrypt(&key, &[1, 2, 3]), vec![1, 2, 3]);
1092        assert_eq!(
1093            aes_cbc_decrypt(&key, &payload[..17]),
1094            payload[..17].to_vec()
1095        );
1096        assert_eq!(aes_cbc_decrypt(&key, b""), Vec::<u8>::new());
1097        // An empty ciphertext (IV only) decodes to an empty plaintext.
1098        assert_eq!(aes_cbc_decrypt(&key, &iv), Vec::<u8>::new());
1099    }
1100
1101    /// Round-trip ISO 32000-2 Algorithm 2.A against a synthetic /Encrypt dict
1102    /// built with the writer-side algorithms (8 & 9): both the user (/U + /UE)
1103    /// and owner (/O + /OE) paths must recover the same file key.
1104    #[test]
1105    fn v5_key_derivation_roundtrip() {
1106        for r in [5i64, 6] {
1107            let file_key = [0xA5u8; 32];
1108            let (uvsalt, uksalt) = ([0x11u8; 8], [0x22u8; 8]);
1109
1110            // Algorithm 8: /U and /UE for the empty user password.
1111            let mut u = hash_v5(r, b"", &uvsalt, &[]).to_vec();
1112            u.extend_from_slice(&uvsalt);
1113            u.extend_from_slice(&uksalt);
1114            let ik = hash_v5(r, b"", &uksalt, &[]);
1115            let ue = aes256_cbc_encrypt_nopad(&ik, &[0u8; 16], &file_key);
1116
1117            // Algorithm 9: /O and /OE for the empty owner password (over U).
1118            let (ovsalt, oksalt) = ([0x33u8; 8], [0x44u8; 8]);
1119            let mut o = hash_v5(r, b"", &ovsalt, &u[..48]).to_vec();
1120            o.extend_from_slice(&ovsalt);
1121            o.extend_from_slice(&oksalt);
1122            let oik = hash_v5(r, b"", &oksalt, &u[..48]);
1123            let oe = aes256_cbc_encrypt_nopad(&oik, &[0u8; 16], &file_key);
1124
1125            let mut dict = PdfDict::new();
1126            dict.insert(PdfName::new("U"), PdfObject::String(PdfString(u.clone())));
1127            dict.insert(PdfName::new("UE"), PdfObject::String(PdfString(ue)));
1128            dict.insert(PdfName::new("O"), PdfObject::String(PdfString(o.clone())));
1129            dict.insert(PdfName::new("OE"), PdfObject::String(PdfString(oe.clone())));
1130            assert_eq!(
1131                compute_key_v5(&dict, r, b"").as_deref(),
1132                Some(&file_key[..]),
1133                "user-password path, R{r}"
1134            );
1135
1136            // Omit /UE: the user hash still validates but the file key cannot
1137            // be recovered from it, so the owner (/O + /OE) path must take
1138            // over. (The owner hashes bind to the original /U, so /U itself
1139            // must stay intact.)
1140            let mut dict2 = PdfDict::new();
1141            dict2.insert(PdfName::new("U"), PdfObject::String(PdfString(u)));
1142            dict2.insert(PdfName::new("O"), PdfObject::String(PdfString(o)));
1143            dict2.insert(PdfName::new("OE"), PdfObject::String(PdfString(oe)));
1144            assert_eq!(
1145                compute_key_v5(&dict2, r, b"").as_deref(),
1146                Some(&file_key[..]),
1147                "owner-password fallback, R{r}"
1148            );
1149        }
1150    }
1151
1152    /// /StmF Identity + /StrF StdCF(RC4): strings decrypt, streams pass through.
1153    #[test]
1154    fn v4_identity_stream_filter_leaves_streams_alone() {
1155        let mut stdcf = PdfDict::new();
1156        stdcf.insert(PdfName::new("CFM"), PdfObject::Name(PdfName::new("V2")));
1157        stdcf.insert(PdfName::new("Length"), PdfObject::Integer(16));
1158        let mut cf = PdfDict::new();
1159        cf.insert(PdfName::new("StdCF"), PdfObject::Dict(stdcf));
1160        let mut dict = PdfDict::new();
1161        dict.insert(
1162            PdfName::new("Filter"),
1163            PdfObject::Name(PdfName::new("Standard")),
1164        );
1165        dict.insert(PdfName::new("V"), PdfObject::Integer(4));
1166        dict.insert(PdfName::new("R"), PdfObject::Integer(4));
1167        dict.insert(PdfName::new("CF"), PdfObject::Dict(cf));
1168        dict.insert(
1169            PdfName::new("StmF"),
1170            PdfObject::Name(PdfName::new("Identity")),
1171        );
1172        dict.insert(PdfName::new("StrF"), PdfObject::Name(PdfName::new("StdCF")));
1173
1174        let dec = match Decryptor::from_encrypt_dict(&dict, &[], None, b"") {
1175            BuildResult::Decryptor(d) => d,
1176            _ => panic!("decryptor"),
1177        };
1178        assert_eq!(dec.stm_algo, Algo::Identity);
1179        assert_eq!(dec.str_algo, Algo::Rc4);
1180
1181        let stream_data = b"stream payload".to_vec();
1182        let string_data = b"string payload".to_vec();
1183        let mut arr = PdfObject::Array(vec![
1184            PdfObject::Stream(PdfStream::new(PdfDict::new(), stream_data.clone())),
1185            PdfObject::String(PdfString(string_data.clone())),
1186        ]);
1187        dec.decrypt_object(&mut arr, ObjectId(9, 0));
1188        let PdfObject::Array(items) = &arr else {
1189            unreachable!()
1190        };
1191        let PdfObject::Stream(s) = &items[0] else {
1192            unreachable!()
1193        };
1194        assert_eq!(
1195            &s.data[..],
1196            &stream_data[..],
1197            "Identity /StmF must not touch streams"
1198        );
1199        let PdfObject::String(st) = &items[1] else {
1200            unreachable!()
1201        };
1202        assert_ne!(st.0, string_data, "/StrF StdCF must decrypt strings");
1203        // ObjStm container bytes go through the stream filter → untouched too.
1204        assert_eq!(
1205            dec.decrypt_stream_bytes(ObjectId(9, 0), b"objstm"),
1206            b"objstm"
1207        );
1208    }
1209
1210    /// /EncryptMetadata false leaves /Type /Metadata stream payloads alone.
1211    #[test]
1212    fn encrypt_metadata_false_skips_metadata_stream() {
1213        let dec = Decryptor {
1214            key: vec![1, 2, 3, 4, 5],
1215            stm_algo: Algo::Rc4,
1216            str_algo: Algo::Rc4,
1217            encrypt_id: None,
1218            encrypt_metadata: false,
1219        };
1220        let xmp = b"<x:xmpmeta/>".to_vec();
1221        let mut meta_dict = PdfDict::new();
1222        meta_dict.insert(
1223            PdfName::new("Type"),
1224            PdfObject::Name(PdfName::new("Metadata")),
1225        );
1226        let mut meta = PdfObject::Stream(PdfStream::new(meta_dict, xmp.clone()));
1227        dec.decrypt_object(&mut meta, ObjectId(7, 0));
1228        let PdfObject::Stream(s) = &meta else {
1229            unreachable!()
1230        };
1231        assert_eq!(
1232            &s.data[..],
1233            &xmp[..],
1234            "plaintext metadata must not be corrupted"
1235        );
1236
1237        // A regular stream under the same decryptor IS decrypted.
1238        let mut other = PdfObject::Stream(PdfStream::new(PdfDict::new(), xmp.clone()));
1239        dec.decrypt_object(&mut other, ObjectId(7, 0));
1240        let PdfObject::Stream(s) = &other else {
1241            unreachable!()
1242        };
1243        assert_ne!(&s.data[..], &xmp[..]);
1244    }
1245
1246    // ------------------------------------------------------------------
1247    // End-to-end fixtures (generated by target/crypto_fixtures/make_fixtures.py
1248    // via pypdf, empty user password). See tests/fixtures/.
1249    // ------------------------------------------------------------------
1250
1251    const AES_MARKER: &[u8] = b"(Hello AES zpdf fixture) Tj";
1252
1253    fn assert_fixture_decrypts(bytes: &[u8]) {
1254        let file = crate::PdfFile::parse(bytes.to_vec()).expect("parse encrypted fixture");
1255        let content = content_stream_bytes(&file);
1256        assert!(
1257            content.windows(AES_MARKER.len()).any(|w| w == AES_MARKER),
1258            "decrypted content stream should contain the known marker, got: {:?}",
1259            String::from_utf8_lossy(&content)
1260        );
1261    }
1262
1263    /// V4/R4 crypt filter AESV2 (AES-128-CBC), empty user password.
1264    #[test]
1265    fn aesv2_r4_decrypts_end_to_end() {
1266        assert_fixture_decrypts(include_bytes!("../tests/fixtures/aesv2_r4.pdf"));
1267    }
1268
1269    /// V5/R5 crypt filter AESV3 (AES-256-CBC, plain SHA-256 hash).
1270    #[test]
1271    fn aesv3_r5_decrypts_end_to_end() {
1272        assert_fixture_decrypts(include_bytes!("../tests/fixtures/aesv3_r5.pdf"));
1273    }
1274
1275    /// V5/R6 crypt filter AESV3 (AES-256-CBC, Algorithm 2.B hardened hash).
1276    #[test]
1277    fn aesv3_r6_decrypts_end_to_end() {
1278        assert_fixture_decrypts(include_bytes!("../tests/fixtures/aesv3_r6.pdf"));
1279    }
1280
1281    // ------------------------------------------------------------------
1282    // Direct (non-reference) /Encrypt dict in the trailer + RC4 regression
1283    // ------------------------------------------------------------------
1284
1285    fn hexstr(b: &[u8]) -> String {
1286        b.iter().map(|x| format!("{x:02x}")).collect()
1287    }
1288
1289    /// Hand-build a tiny V1/R2 RC4-40 PDF whose trailer carries the /Encrypt
1290    /// dictionary **directly** (not as an indirect reference).
1291    fn build_rc4_direct_encrypt_pdf(content_plain: &[u8]) -> Vec<u8> {
1292        // Algorithm 3 (R2): /O = RC4(MD5(padded owner pwd)[..5], padded user pwd),
1293        // both passwords empty → both pads.
1294        let okey = md5(&PAD);
1295        let o = rc4(&okey[..5], &PAD);
1296        let id0: Vec<u8> = (0u8..16).collect();
1297        let p: i32 = -1;
1298        let key = compute_key_rc4(b"", &o, p, &id0, 2, 40, true);
1299        let u = rc4(&key, &PAD); // Algorithm 4
1300
1301        // RC4 is symmetric: "decrypting" the plaintext produces the ciphertext.
1302        let enc = Decryptor {
1303            key,
1304            stm_algo: Algo::Rc4,
1305            str_algo: Algo::Rc4,
1306            encrypt_id: None,
1307            encrypt_metadata: true,
1308        };
1309        let content_enc = enc.decrypt_stream_bytes(ObjectId(5, 0), content_plain);
1310
1311        let mut stream_obj = format!("<< /Length {} >>\nstream\n", content_enc.len()).into_bytes();
1312        stream_obj.extend_from_slice(&content_enc);
1313        stream_obj.extend_from_slice(b"\nendstream");
1314        let bodies: Vec<Vec<u8>> = vec![
1315            b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1316            b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1317            b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>".to_vec(),
1318            b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
1319            stream_obj,
1320        ];
1321
1322        let mut out: Vec<u8> = b"%PDF-1.4\n".to_vec();
1323        let mut offsets = Vec::new();
1324        for (i, body) in bodies.iter().enumerate() {
1325            offsets.push(out.len());
1326            out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
1327            out.extend_from_slice(body);
1328            out.extend_from_slice(b"\nendobj\n");
1329        }
1330        let xref_pos = out.len();
1331        out.extend_from_slice(b"xref\n0 6\n0000000000 65535 f \n");
1332        for off in &offsets {
1333            out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
1334        }
1335        out.extend_from_slice(
1336            format!(
1337                "trailer\n<< /Size 6 /Root 1 0 R /ID [<{id}> <{id}>] /Encrypt << /Filter \
1338                 /Standard /V 1 /R 2 /Length 40 /O <{o}> /U <{u}> /P {p} >> >>\nstartxref\n\
1339                 {xref_pos}\n%%EOF\n",
1340                id = hexstr(&id0),
1341                o = hexstr(&o),
1342                u = hexstr(&u),
1343            )
1344            .as_bytes(),
1345        );
1346        out
1347    }
1348
1349    /// A direct /Encrypt dict must still enable decryption (regression: it used
1350    /// to silently disable it), and the plain RC4 path must keep working.
1351    #[test]
1352    fn rc4_direct_encrypt_dict_in_trailer() {
1353        let plain = b"BT /F1 12 Tf (direct encrypt dict) Tj ET";
1354        let pdf = build_rc4_direct_encrypt_pdf(plain);
1355        let file = crate::PdfFile::parse(pdf).expect("parse hand-built encrypted PDF");
1356        assert_eq!(content_stream_bytes(&file), plain);
1357    }
1358
1359    // ------------------------------------------------------------------
1360    // Non-empty-password (RC4 V2/R3, 128-bit) authentication
1361    // ------------------------------------------------------------------
1362
1363    /// Hand-build a V2/R3 RC4-128 PDF encrypted with distinct user and owner
1364    /// passwords (Algorithms 2/3/5 on the encrypt side). `omit_u` drops `/U` to
1365    /// model a malformed document with nothing to authenticate against.
1366    fn build_rc4_password_pdf(
1367        user_pw: &[u8],
1368        owner_pw: &[u8],
1369        content_plain: &[u8],
1370        omit_u: bool,
1371    ) -> Vec<u8> {
1372        let (r, bits, n) = (3i64, 128i64, 16usize);
1373        let id0: Vec<u8> = (0u8..16).collect();
1374        let p: i32 = -44;
1375
1376        // Algorithm 3: /O = encrypt(padded user pwd) under the owner key.
1377        let mut okey = md5(&pad_password(owner_pw));
1378        for _ in 0..50 {
1379            okey = md5(&okey[..n]);
1380        }
1381        let owner_key = &okey[..n];
1382        let mut o = pad_password(user_pw).to_vec();
1383        for i in 0..=19u8 {
1384            let step_key: Vec<u8> = owner_key.iter().map(|b| b ^ i).collect();
1385            o = rc4(&step_key, &o);
1386        }
1387
1388        // Algorithm 2: file key from the user password + /O.
1389        let key = compute_key_rc4(user_pw, &o, p, &id0, r, bits, true);
1390
1391        // Algorithm 5 (R≥3): /U = first 16 bytes of the iterated RC4 of MD5(PAD‖ID),
1392        // padded out to 32 bytes.
1393        let mut u_input = Vec::new();
1394        u_input.extend_from_slice(&PAD);
1395        u_input.extend_from_slice(&id0);
1396        let mut x = rc4(&key, &md5(&u_input));
1397        for i in 1..=19u8 {
1398            let step_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1399            x = rc4(&step_key, &x);
1400        }
1401        let mut u = x;
1402        u.extend_from_slice(&[0u8; 16]);
1403
1404        let enc = Decryptor {
1405            key,
1406            stm_algo: Algo::Rc4,
1407            str_algo: Algo::Rc4,
1408            encrypt_id: None,
1409            encrypt_metadata: true,
1410        };
1411        let content_enc = enc.decrypt_stream_bytes(ObjectId(5, 0), content_plain);
1412
1413        let mut stream_obj = format!("<< /Length {} >>\nstream\n", content_enc.len()).into_bytes();
1414        stream_obj.extend_from_slice(&content_enc);
1415        stream_obj.extend_from_slice(b"\nendstream");
1416        let bodies: Vec<Vec<u8>> = vec![
1417            b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1418            b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1419            b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>".to_vec(),
1420            b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
1421            stream_obj,
1422        ];
1423
1424        let mut out: Vec<u8> = b"%PDF-1.6\n".to_vec();
1425        let mut offsets = Vec::new();
1426        for (i, body) in bodies.iter().enumerate() {
1427            offsets.push(out.len());
1428            out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
1429            out.extend_from_slice(body);
1430            out.extend_from_slice(b"\nendobj\n");
1431        }
1432        let xref_pos = out.len();
1433        out.extend_from_slice(b"xref\n0 6\n0000000000 65535 f \n");
1434        for off in &offsets {
1435            out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
1436        }
1437        let u_entry = if omit_u {
1438            String::new()
1439        } else {
1440            format!("/U <{}> ", hexstr(&u))
1441        };
1442        out.extend_from_slice(
1443            format!(
1444                "trailer\n<< /Size 6 /Root 1 0 R /ID [<{id}> <{id}>] /Encrypt << /Filter \
1445                 /Standard /V 2 /R 3 /Length 128 /O <{o}> {u_entry}/P {p} >> >>\nstartxref\n\
1446                 {xref_pos}\n%%EOF\n",
1447                id = hexstr(&id0),
1448                o = hexstr(&o),
1449            )
1450            .as_bytes(),
1451        );
1452        out
1453    }
1454
1455    #[test]
1456    fn user_password_decrypts() {
1457        let plain = b"BT (user password works) Tj ET";
1458        let pdf = build_rc4_password_pdf(b"secret", b"master", plain, false);
1459        let file =
1460            crate::PdfFile::parse_with_password(pdf, b"secret").expect("user password opens");
1461        assert_eq!(content_stream_bytes(&file), plain);
1462    }
1463
1464    #[test]
1465    fn owner_password_decrypts_via_recovery() {
1466        // The owner password authenticates by recovering the user password from
1467        // /O (Algorithm 7), then deriving the same file key.
1468        let plain = b"BT (owner password works) Tj ET";
1469        let pdf = build_rc4_password_pdf(b"secret", b"master", plain, false);
1470        let file =
1471            crate::PdfFile::parse_with_password(pdf, b"master").expect("owner password opens");
1472        assert_eq!(content_stream_bytes(&file), plain);
1473    }
1474
1475    #[test]
1476    fn wrong_password_is_rejected() {
1477        let pdf = build_rc4_password_pdf(b"secret", b"master", b"BT (x) Tj ET", false);
1478        match crate::PdfFile::parse_with_password(pdf, b"nope") {
1479            Err(zpdf_core::Error::WrongPassword) => {}
1480            Err(e) => panic!("expected WrongPassword, got error {e:?}"),
1481            Ok(_) => panic!("expected WrongPassword, but the document opened"),
1482        }
1483    }
1484
1485    #[test]
1486    fn empty_password_open_degrades_without_erroring() {
1487        // The default open (empty password) must NOT error on a password-needing
1488        // document — it opens best-effort, but the content does not decrypt to
1489        // the plaintext.
1490        let plain = b"BT (needs a password) Tj ET";
1491        let pdf = build_rc4_password_pdf(b"secret", b"master", plain, false);
1492        let file = crate::PdfFile::parse(pdf).expect("default open still succeeds");
1493        assert!(file.is_encrypted());
1494        assert_ne!(content_stream_bytes(&file), plain);
1495    }
1496
1497    #[test]
1498    fn missing_u_opens_best_effort_not_wrong_password() {
1499        // A malformed document with no /U cannot be authenticated, so a supplied
1500        // password is used unverified (best-effort) rather than reported wrong.
1501        // The correct password still yields the right key and decrypts.
1502        let plain = b"BT (no /U to check) Tj ET";
1503        let pdf = build_rc4_password_pdf(b"secret", b"master", plain, true);
1504        let file =
1505            crate::PdfFile::parse_with_password(pdf, b"secret").expect("correct password opens");
1506        assert_eq!(content_stream_bytes(&file), plain);
1507
1508        // A wrong password also opens (garbage), but never WrongPassword.
1509        let pdf = build_rc4_password_pdf(b"secret", b"master", plain, true);
1510        let file = crate::PdfFile::parse_with_password(pdf, b"nope")
1511            .expect("wrong password still opens best-effort (no /U to reject against)");
1512        assert_ne!(content_stream_bytes(&file), plain);
1513    }
1514}