Skip to main content

pdf_objects/
crypto.rs

1//! PDF Standard Security Handler (encryption) — decryption side only.
2//!
3//! This module implements enough of the PDF 1.7 / PDF 2.0 Standard Security
4//! Handler to decrypt documents produced by:
5//!
6//! - revisions 2 and 3 (V=1 or V=2, RC4 with a 40-bit or 128-bit key),
7//! - revision 4 with V=4 crypt filters naming `/V2` (RC4-128) or
8//!   `/AESV2` (AES-128-CBC) as the stream and string method,
9//! - revisions 5 and 6 (V=5, AES-256-CBC) via the `/AESV3` crypt filter —
10//!   R=5 uses a plain SHA-256 verifier (the vulnerable Extension Level 3
11//!   form) and R=6 uses the ISO 32000-2 iterative Algorithm 2.B hash.
12//!
13//! Authentication runs against either the user password or the owner
14//! password. The empty user password is accepted as a special case of
15//! the general user-password path.
16//!
17//! Public-key security handlers are not yet implemented and still fail
18//! up front with `PdfError::Unsupported`. They can be layered on top
19//! without changing this module's public surface.
20
21use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
22use aes::{Aes128, Aes256};
23use md5::{Digest, Md5};
24use sha2::{Sha256, Sha384, Sha512};
25
26use crate::error::{PdfError, PdfResult};
27use crate::types::{ObjectRef, PdfDictionary, PdfValue};
28
29/// Adobe's 32-byte password padding string (PDF 1.7, algorithm 2).
30const PASSWORD_PADDING: [u8; 32] = [
31    0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
32    0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
33];
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SecurityRevision {
37    R2,
38    R3,
39    R4,
40    R5,
41    R6,
42}
43
44/// Which crypt filter method applies to a given piece of ciphertext.
45///
46/// V=1/2 documents always use [`CryptMethod::V2`] (RC4) for everything.
47/// V=4 documents name a crypt filter per kind (`/StmF`, `/StrF`, `/EFF`);
48/// each may point at `/Identity` (no encryption), a V2 filter (RC4), or
49/// an AESV2 filter (AES-128-CBC).
50/// V=5 documents name the `/AESV3` filter (AES-256-CBC).
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum CryptMethod {
53    Identity,
54    V2,
55    AesV2,
56    AesV3,
57}
58
59/// Which slot the ciphertext belongs to. Drives the crypt-method choice
60/// (string vs stream) on V=4 documents and is a no-op on V=1/2.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum BytesKind {
63    String,
64    Stream,
65}
66
67#[derive(Debug, Clone)]
68pub struct StandardSecurityHandler {
69    file_key: Vec<u8>,
70    string_method: CryptMethod,
71    stream_method: CryptMethod,
72    /// `false` only for V=4 documents that explicitly set
73    /// `/EncryptMetadata false`; `true` everywhere else. When `false`,
74    /// streams with `/Type /Metadata` and `/Subtype /XML` skip
75    /// decryption.
76    encrypt_metadata: bool,
77}
78
79impl StandardSecurityHandler {
80    /// Builds a decryption handler from the `/Encrypt` dictionary and the
81    /// trailer's first `/ID` string, authenticating the supplied password.
82    /// Returns `None` if the password does not authenticate.
83    pub fn open(
84        encrypt_dict: &PdfDictionary,
85        id_first: &[u8],
86        password: &[u8],
87    ) -> PdfResult<Option<Self>> {
88        let filter = encrypt_dict
89            .get("Filter")
90            .and_then(PdfValue::as_name)
91            .unwrap_or("");
92        if filter != "Standard" {
93            return Err(PdfError::Unsupported(format!(
94                "encryption filter /{filter} is not supported"
95            )));
96        }
97        let v = encrypt_dict
98            .get("V")
99            .and_then(PdfValue::as_integer)
100            .unwrap_or(0);
101        let r = encrypt_dict
102            .get("R")
103            .and_then(PdfValue::as_integer)
104            .unwrap_or(0);
105        let revision = match r {
106            2 => SecurityRevision::R2,
107            3 => SecurityRevision::R3,
108            4 => SecurityRevision::R4,
109            5 => SecurityRevision::R5,
110            6 => SecurityRevision::R6,
111            other => {
112                return Err(PdfError::Unsupported(format!(
113                    "Standard security handler revision {other} is not supported (only R=2..R=6 handled)"
114                )));
115            }
116        };
117
118        // V=5 is a separate code path: the file key is stored encrypted
119        // in /OE /UE rather than derived algorithmically from /P + /ID.
120        if v == 5 {
121            return open_v5(encrypt_dict, revision, password);
122        }
123
124        let (string_method, stream_method, key_length_bytes) = match v {
125            1 | 2 => {
126                let bits = encrypt_dict
127                    .get("Length")
128                    .and_then(PdfValue::as_integer)
129                    .unwrap_or(40);
130                if bits % 8 != 0 || !(40..=128).contains(&bits) {
131                    return Err(PdfError::Corrupt(format!(
132                        "invalid /Length {bits} in Encrypt dictionary"
133                    )));
134                }
135                (CryptMethod::V2, CryptMethod::V2, (bits / 8) as usize)
136            }
137            4 => {
138                // V=4: crypt filters decide the method per slot. The file
139                // key is always 128-bit (16 bytes).
140                let (strf, stmf) = resolve_v4_crypt_filters(encrypt_dict)?;
141                (strf, stmf, 16)
142            }
143            other => {
144                return Err(PdfError::Unsupported(format!(
145                    "Standard security handler V={other} is not supported (only V=1, V=2, V=4, and V=5 handled)"
146                )));
147            }
148        };
149
150        // V=4's Algorithm 2 step 5: when /EncryptMetadata is explicitly
151        // false, 0xFFFFFFFF is appended before the 50-round rehash.
152        let encrypt_metadata = if matches!(revision, SecurityRevision::R4) {
153            encrypt_dict
154                .get("EncryptMetadata")
155                .and_then(PdfValue::as_bool)
156                .unwrap_or(true)
157        } else {
158            true
159        };
160
161        let o = pdf_string_bytes(encrypt_dict, "O")?;
162        let u = pdf_string_bytes(encrypt_dict, "U")?;
163        let p = encrypt_dict
164            .get("P")
165            .and_then(PdfValue::as_integer)
166            .ok_or_else(|| PdfError::Corrupt("Encrypt dictionary missing /P".to_string()))?;
167        if o.len() != 32 || u.len() != 32 {
168            return Err(PdfError::Corrupt(
169                "Encrypt /O and /U must each be 32 bytes".to_string(),
170            ));
171        }
172
173        // First try the supplied password as the user password.
174        let user_file_key = compute_file_key(
175            password,
176            &o,
177            p as i32,
178            id_first,
179            key_length_bytes,
180            revision,
181            encrypt_metadata,
182        );
183        if authenticate_user_password(&user_file_key, revision, &u, id_first) {
184            return Ok(Some(Self {
185                file_key: user_file_key,
186                string_method,
187                stream_method,
188                encrypt_metadata,
189            }));
190        }
191
192        // Then try it as the owner password: Algorithm 7 recovers the
193        // padded user password from /O, after which we redo the user-
194        // password authentication with that recovered value. The file key
195        // used for object decryption is always derived from the user
196        // password — the owner password is only a way of recovering it.
197        let recovered_user_password =
198            recover_user_password_from_owner(password, &o, revision, key_length_bytes);
199        let owner_file_key = compute_file_key(
200            &recovered_user_password,
201            &o,
202            p as i32,
203            id_first,
204            key_length_bytes,
205            revision,
206            encrypt_metadata,
207        );
208        if authenticate_user_password(&owner_file_key, revision, &u, id_first) {
209            return Ok(Some(Self {
210                file_key: owner_file_key,
211                string_method,
212                stream_method,
213                encrypt_metadata,
214            }));
215        }
216
217        Ok(None)
218    }
219
220    /// Builds a handler from an externally-derived file key and the
221    /// pre-resolved crypt methods. Used by credential paths that derive
222    /// the file key out-of-band — the public-key handler unwraps the
223    /// file key from a CMS recipient envelope and then constructs the
224    /// handler via this entry point rather than `open`.
225    pub fn from_file_key(
226        file_key: Vec<u8>,
227        string_method: CryptMethod,
228        stream_method: CryptMethod,
229        encrypt_metadata: bool,
230    ) -> Self {
231        Self {
232            file_key,
233            string_method,
234            stream_method,
235            encrypt_metadata,
236        }
237    }
238
239    /// Returns true when this handler was configured with
240    /// `/EncryptMetadata false`. Parser uses this to skip
241    /// `/Type /Metadata` streams.
242    pub fn encrypts_metadata(&self) -> bool {
243        self.encrypt_metadata
244    }
245
246    /// Decrypts `bytes` produced for the indirect object `(num, gen)`.
247    /// The crypt method is chosen per `kind` — strings use `/StrF`,
248    /// streams use `/StmF`. Returns the ciphertext unchanged for
249    /// `/Identity` filters; returns an error for malformed AES input
250    /// (wrong length, bad PKCS#7 padding).
251    pub fn decrypt_bytes(
252        &self,
253        bytes: &[u8],
254        object_ref: ObjectRef,
255        kind: BytesKind,
256    ) -> PdfResult<Vec<u8>> {
257        let method = match kind {
258            BytesKind::String => self.string_method,
259            BytesKind::Stream => self.stream_method,
260        };
261        match method {
262            CryptMethod::Identity => Ok(bytes.to_vec()),
263            CryptMethod::V2 => Ok(rc4(&self.object_key(object_ref, method), bytes)),
264            CryptMethod::AesV2 => aes_128_cbc_decrypt(&self.object_key(object_ref, method), bytes),
265            CryptMethod::AesV3 => {
266                // AES-256-CBC in V=5 uses the file key directly — there is
267                // no per-object key mixing, the `sAlT` suffix, or any
268                // object-number-derived material.
269                aes_256_cbc_decrypt(&self.file_key, bytes)
270            }
271        }
272    }
273
274    fn object_key(&self, object_ref: ObjectRef, method: CryptMethod) -> Vec<u8> {
275        // Algorithm 1 / 1a. Append the 4-byte ASCII suffix "sAlT" when
276        // the method is AES so keys derived for the same object under
277        // different methods never collide.
278        let suffix_len = if matches!(method, CryptMethod::AesV2) {
279            9
280        } else {
281            5
282        };
283        let mut material = Vec::with_capacity(self.file_key.len() + suffix_len);
284        material.extend_from_slice(&self.file_key);
285        let num = object_ref.object_number.to_le_bytes();
286        material.push(num[0]);
287        material.push(num[1]);
288        material.push(num[2]);
289        let generation = object_ref.generation.to_le_bytes();
290        material.push(generation[0]);
291        material.push(generation[1]);
292        if matches!(method, CryptMethod::AesV2) {
293            material.extend_from_slice(b"sAlT");
294        }
295        let digest = md5_bytes(&material);
296        let truncated_len = (self.file_key.len() + 5).min(16);
297        digest[..truncated_len].to_vec()
298    }
299}
300
301fn open_v5(
302    encrypt_dict: &PdfDictionary,
303    revision: SecurityRevision,
304    password: &[u8],
305) -> PdfResult<Option<StandardSecurityHandler>> {
306    if !matches!(revision, SecurityRevision::R5 | SecurityRevision::R6) {
307        return Err(PdfError::Unsupported(format!(
308            "V=5 Encrypt dictionary requires R=5 or R=6, got {revision:?}"
309        )));
310    }
311    let (strf, stmf) = resolve_v5_crypt_filters(encrypt_dict)?;
312
313    let encrypt_metadata = encrypt_dict
314        .get("EncryptMetadata")
315        .and_then(PdfValue::as_bool)
316        .unwrap_or(true);
317
318    let o = pdf_string_bytes(encrypt_dict, "O")?;
319    let u = pdf_string_bytes(encrypt_dict, "U")?;
320    let oe = pdf_string_bytes(encrypt_dict, "OE")?;
321    let ue = pdf_string_bytes(encrypt_dict, "UE")?;
322    if o.len() != 48 || u.len() != 48 {
323        return Err(PdfError::Corrupt(
324            "V=5 Encrypt /O and /U must each be 48 bytes".to_string(),
325        ));
326    }
327    if oe.len() != 32 || ue.len() != 32 {
328        return Err(PdfError::Corrupt(
329            "V=5 Encrypt /OE and /UE must each be 32 bytes".to_string(),
330        ));
331    }
332
333    // Passwords are UTF-8, truncated to 127 bytes per spec.
334    let truncated_password = &password[..password.len().min(127)];
335
336    // User-password attempt: hash(password || u_validation_salt) must
337    // match u[0..32]; intermediate key = hash(password || u_key_salt).
338    let u_validation_salt = &u[32..40];
339    let u_key_salt = &u[40..48];
340    let user_hash = pdf_2_b_hash(truncated_password, u_validation_salt, None, revision);
341    if user_hash[..32] == u[..32] {
342        let intermediate_key = pdf_2_b_hash(truncated_password, u_key_salt, None, revision);
343        let file_key = aes_256_cbc_decrypt_no_pad(&intermediate_key[..32], &[0u8; 16], &ue)?;
344        return Ok(Some(StandardSecurityHandler {
345            file_key,
346            string_method: strf,
347            stream_method: stmf,
348            encrypt_metadata,
349        }));
350    }
351
352    // Owner-password attempt: the hash inputs additionally include the
353    // first 48 bytes of /U, binding the owner verifier to the user
354    // record.
355    let o_validation_salt = &o[32..40];
356    let o_key_salt = &o[40..48];
357    let owner_hash = pdf_2_b_hash(
358        truncated_password,
359        o_validation_salt,
360        Some(&u[..48]),
361        revision,
362    );
363    if owner_hash[..32] == o[..32] {
364        let intermediate_key =
365            pdf_2_b_hash(truncated_password, o_key_salt, Some(&u[..48]), revision);
366        let file_key = aes_256_cbc_decrypt_no_pad(&intermediate_key[..32], &[0u8; 16], &oe)?;
367        return Ok(Some(StandardSecurityHandler {
368            file_key,
369            string_method: strf,
370            stream_method: stmf,
371            encrypt_metadata,
372        }));
373    }
374
375    Ok(None)
376}
377
378/// Algorithm 2.B (R=6) — with an R=5 short-circuit at the initial
379/// SHA-256. Returns the full 32-byte hash used by [`open_v5`] for
380/// both the verifier check and the intermediate key step.
381fn pdf_2_b_hash(
382    password: &[u8],
383    salt: &[u8],
384    user_vector: Option<&[u8]>,
385    revision: SecurityRevision,
386) -> Vec<u8> {
387    let mut hasher = Sha256::new();
388    hasher.update(password);
389    hasher.update(salt);
390    if let Some(vector) = user_vector {
391        hasher.update(vector);
392    }
393    let mut k: Vec<u8> = hasher.finalize().to_vec();
394
395    if matches!(revision, SecurityRevision::R5) {
396        return k;
397    }
398
399    // R=6 inner loop (ISO 32000-2 §7.6.4.3.3 Algorithm 2.B).
400    let user_vector = user_vector.unwrap_or(&[]);
401    let mut round: u32 = 0;
402    loop {
403        // K1 = (password || K || user_vector) repeated 64 times.
404        let mut k1 = Vec::with_capacity((password.len() + k.len() + user_vector.len()) * 64);
405        for _ in 0..64 {
406            k1.extend_from_slice(password);
407            k1.extend_from_slice(&k);
408            k1.extend_from_slice(user_vector);
409        }
410
411        // AES-128-CBC encrypt K1 without any padding (K1 is always a
412        // multiple of 16 because len(K) ∈ {32, 48, 64} and password +
413        // user_vector contribute an integer number of blocks after
414        // the 64× repetition — in practice implementations pad K1 by
415        // repetition, not PKCS#7, so no padding is added here).
416        let key: [u8; 16] = k[..16].try_into().expect("K is at least 32 bytes");
417        let iv: [u8; 16] = k[16..32].try_into().expect("K is at least 32 bytes");
418        let encrypted = aes_128_cbc_encrypt_no_pad(&key, &iv, &k1);
419
420        // Sum first-16 bytes mod 3 to choose the next hash function.
421        let selector: u32 = encrypted[..16]
422            .iter()
423            .map(|byte| u32::from(*byte % 3))
424            .sum::<u32>()
425            % 3;
426        k = match selector {
427            0 => Sha256::digest(&encrypted).to_vec(),
428            1 => Sha384::digest(&encrypted).to_vec(),
429            _ => Sha512::digest(&encrypted).to_vec(),
430        };
431
432        let last_byte = *encrypted.last().expect("AES output is non-empty");
433        round += 1;
434        if round >= 64 && u32::from(last_byte) <= round.saturating_sub(32) {
435            break;
436        }
437    }
438
439    k.truncate(32);
440    k
441}
442
443fn resolve_v5_crypt_filters(encrypt_dict: &PdfDictionary) -> PdfResult<(CryptMethod, CryptMethod)> {
444    let strf = encrypt_dict
445        .get("StrF")
446        .and_then(PdfValue::as_name)
447        .unwrap_or("Identity");
448    let stmf = encrypt_dict
449        .get("StmF")
450        .and_then(PdfValue::as_name)
451        .unwrap_or("Identity");
452    let cf = encrypt_dict.get("CF").and_then(|value| match value {
453        PdfValue::Dictionary(dict) => Some(dict),
454        _ => None,
455    });
456    Ok((
457        resolve_crypt_filter_method(cf, strf)?,
458        resolve_crypt_filter_method(cf, stmf)?,
459    ))
460}
461
462pub(crate) fn resolve_v4_crypt_filters(
463    encrypt_dict: &PdfDictionary,
464) -> PdfResult<(CryptMethod, CryptMethod)> {
465    let strf = encrypt_dict
466        .get("StrF")
467        .and_then(PdfValue::as_name)
468        .unwrap_or("Identity");
469    let stmf = encrypt_dict
470        .get("StmF")
471        .and_then(PdfValue::as_name)
472        .unwrap_or("Identity");
473    let cf = encrypt_dict.get("CF").and_then(|value| match value {
474        PdfValue::Dictionary(dict) => Some(dict),
475        _ => None,
476    });
477    Ok((
478        resolve_crypt_filter_method(cf, strf)?,
479        resolve_crypt_filter_method(cf, stmf)?,
480    ))
481}
482
483fn resolve_crypt_filter_method(cf: Option<&PdfDictionary>, name: &str) -> PdfResult<CryptMethod> {
484    // The spec reserves the `Identity` filter name for "no encryption"
485    // and specifies that it never appears in /CF; treat it as a pass-
486    // through without consulting the dictionary.
487    if name == "Identity" {
488        return Ok(CryptMethod::Identity);
489    }
490    let subfilter = cf
491        .and_then(|dict| dict.get(name))
492        .and_then(|value| match value {
493            PdfValue::Dictionary(dict) => Some(dict),
494            _ => None,
495        })
496        .ok_or_else(|| {
497            PdfError::Corrupt(format!(
498                "Encrypt /CF is missing the crypt filter entry /{name}"
499            ))
500        })?;
501    let cfm = subfilter
502        .get("CFM")
503        .and_then(PdfValue::as_name)
504        .ok_or_else(|| {
505            PdfError::Corrupt(format!("crypt filter /{name} is missing the /CFM entry"))
506        })?;
507    match cfm {
508        "V2" => Ok(CryptMethod::V2),
509        "AESV2" => Ok(CryptMethod::AesV2),
510        "AESV3" => Ok(CryptMethod::AesV3),
511        "None" => Ok(CryptMethod::Identity),
512        other => Err(PdfError::Unsupported(format!(
513            "crypt filter method /{other} is not supported (only /V2, /AESV2, and /AESV3 handled)"
514        ))),
515    }
516}
517
518/// Decrypts AES-128-CBC ciphertext whose first 16 bytes are the IV and
519/// whose payload is PKCS#7-padded. Used for V=4 /AESV2 streams and
520/// strings.
521fn aes_128_cbc_decrypt(key: &[u8], data: &[u8]) -> PdfResult<Vec<u8>> {
522    if key.len() != 16 {
523        return Err(PdfError::Corrupt(format!(
524            "AES-128 object key must be 16 bytes, got {}",
525            key.len()
526        )));
527    }
528    if data.len() < 32 || data.len() % 16 != 0 {
529        return Err(PdfError::Corrupt(format!(
530            "AES-128-CBC ciphertext must be at least 32 bytes and a multiple of 16; got {}",
531            data.len()
532        )));
533    }
534    let cipher = Aes128::new_from_slice(key)
535        .map_err(|error| PdfError::Corrupt(format!("AES-128 key rejected by cipher: {error}")))?;
536    let mut prev_block: [u8; 16] = data[..16].try_into().expect("slice is 16 bytes");
537    let mut output = Vec::with_capacity(data.len() - 16);
538    for chunk in data[16..].chunks(16) {
539        let mut block = GenericArray::clone_from_slice(chunk);
540        cipher.decrypt_block(&mut block);
541        for (plain_byte, iv_byte) in block.iter_mut().zip(prev_block.iter()) {
542            *plain_byte ^= iv_byte;
543        }
544        output.extend_from_slice(block.as_slice());
545        prev_block.copy_from_slice(chunk);
546    }
547    strip_pkcs7(output)
548}
549
550/// Decrypts AES-256-CBC ciphertext whose first 16 bytes are the IV and
551/// whose payload is PKCS#7-padded. Used by V=5 `/AESV3` strings and
552/// streams.
553fn aes_256_cbc_decrypt(key: &[u8], data: &[u8]) -> PdfResult<Vec<u8>> {
554    if key.len() != 32 {
555        return Err(PdfError::Corrupt(format!(
556            "AES-256 file key must be 32 bytes, got {}",
557            key.len()
558        )));
559    }
560    if data.len() < 32 || data.len() % 16 != 0 {
561        return Err(PdfError::Corrupt(format!(
562            "AES-256-CBC ciphertext must be at least 32 bytes and a multiple of 16; got {}",
563            data.len()
564        )));
565    }
566    let cipher = Aes256::new_from_slice(key)
567        .map_err(|error| PdfError::Corrupt(format!("AES-256 key rejected by cipher: {error}")))?;
568    let mut prev_block: [u8; 16] = data[..16].try_into().expect("slice is 16 bytes");
569    let mut output = Vec::with_capacity(data.len() - 16);
570    for chunk in data[16..].chunks(16) {
571        let mut block = GenericArray::clone_from_slice(chunk);
572        cipher.decrypt_block(&mut block);
573        for (plain_byte, iv_byte) in block.iter_mut().zip(prev_block.iter()) {
574            *plain_byte ^= iv_byte;
575        }
576        output.extend_from_slice(block.as_slice());
577        prev_block.copy_from_slice(chunk);
578    }
579    strip_pkcs7(output)
580}
581
582/// Decrypts AES-256-CBC ciphertext with a caller-supplied IV and no
583/// PKCS#7 unpadding. Used by Algorithm 2.A to recover the 32-byte file
584/// key from `/OE` / `/UE` (which are fixed 32-byte, two-block
585/// ciphertexts with an all-zero IV).
586fn aes_256_cbc_decrypt_no_pad(key: &[u8], iv: &[u8], data: &[u8]) -> PdfResult<Vec<u8>> {
587    if key.len() != 32 {
588        return Err(PdfError::Corrupt(format!(
589            "AES-256 key must be 32 bytes, got {}",
590            key.len()
591        )));
592    }
593    if iv.len() != 16 {
594        return Err(PdfError::Corrupt(format!(
595            "AES-256-CBC IV must be 16 bytes, got {}",
596            iv.len()
597        )));
598    }
599    if data.is_empty() || data.len() % 16 != 0 {
600        return Err(PdfError::Corrupt(format!(
601            "AES-256-CBC payload must be a non-empty multiple of 16 bytes; got {}",
602            data.len()
603        )));
604    }
605    let cipher = Aes256::new_from_slice(key)
606        .map_err(|error| PdfError::Corrupt(format!("AES-256 key rejected by cipher: {error}")))?;
607    let mut prev_block: [u8; 16] = iv.try_into().expect("iv length validated");
608    let mut output = Vec::with_capacity(data.len());
609    for chunk in data.chunks(16) {
610        let mut block = GenericArray::clone_from_slice(chunk);
611        cipher.decrypt_block(&mut block);
612        for (plain_byte, iv_byte) in block.iter_mut().zip(prev_block.iter()) {
613            *plain_byte ^= iv_byte;
614        }
615        output.extend_from_slice(block.as_slice());
616        prev_block.copy_from_slice(chunk);
617    }
618    Ok(output)
619}
620
621/// Encrypts the Algorithm 2.B K1 buffer with AES-128-CBC. The buffer is
622/// already a multiple of 16 bytes; the spec does not apply PKCS#7
623/// padding on this inner loop — this helper therefore expects a
624/// block-aligned input and rejects anything else.
625fn aes_128_cbc_encrypt_no_pad(key: &[u8; 16], iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
626    // The callers in this module feed only block-aligned inputs; any
627    // unaligned tail is a programming error, not a runtime one.
628    assert!(
629        data.len() % 16 == 0,
630        "Algorithm 2.B K1 must be block-aligned, got {}",
631        data.len()
632    );
633    let cipher = Aes128::new_from_slice(key).expect("key length validated at compile time");
634    let mut output = Vec::with_capacity(data.len());
635    let mut prev: [u8; 16] = *iv;
636    for chunk in data.chunks(16) {
637        let mut buf = [0u8; 16];
638        for ((b, plain), iv_byte) in buf.iter_mut().zip(chunk.iter()).zip(prev.iter()) {
639            *b = plain ^ iv_byte;
640        }
641        let mut block = GenericArray::clone_from_slice(&buf);
642        cipher.encrypt_block(&mut block);
643        output.extend_from_slice(block.as_slice());
644        prev.copy_from_slice(block.as_slice());
645    }
646    output
647}
648
649fn strip_pkcs7(mut data: Vec<u8>) -> PdfResult<Vec<u8>> {
650    let Some(&pad) = data.last() else {
651        return Err(PdfError::Corrupt(
652            "AES-128-CBC plaintext is empty — missing PKCS#7 padding".to_string(),
653        ));
654    };
655    if pad == 0 || pad > 16 || (pad as usize) > data.len() {
656        return Err(PdfError::Corrupt(format!(
657            "AES-128-CBC PKCS#7 padding byte {pad} is out of range"
658        )));
659    }
660    let new_len = data.len() - pad as usize;
661    if !data[new_len..].iter().all(|byte| *byte == pad) {
662        return Err(PdfError::Corrupt(
663            "AES-128-CBC PKCS#7 padding bytes do not match".to_string(),
664        ));
665    }
666    data.truncate(new_len);
667    Ok(data)
668}
669
670fn pdf_string_bytes(dict: &PdfDictionary, key: &str) -> PdfResult<Vec<u8>> {
671    match dict.get(key) {
672        Some(PdfValue::String(s)) => Ok(s.0.clone()),
673        Some(_) => Err(PdfError::Corrupt(format!("Encrypt /{key} is not a string"))),
674        None => Err(PdfError::Corrupt(format!(
675            "Encrypt dictionary missing /{key}"
676        ))),
677    }
678}
679
680fn compute_file_key(
681    password: &[u8],
682    o_entry: &[u8],
683    permissions: i32,
684    id_first: &[u8],
685    key_length_bytes: usize,
686    revision: SecurityRevision,
687    encrypt_metadata: bool,
688) -> Vec<u8> {
689    // Algorithm 2 (PDF 1.7 section 7.6.3.3):
690    //   1. Pad the password to 32 bytes.
691    let padded = pad_password(password);
692    let mut hasher = Md5::new();
693    hasher.update(padded);
694    //   2. Append /O.
695    hasher.update(o_entry);
696    //   3. Append /P (4 bytes little-endian).
697    hasher.update(permissions.to_le_bytes());
698    //   4. Append the first element of /ID.
699    hasher.update(id_first);
700    //   5. (R>=4 only) When /EncryptMetadata is explicitly false, append
701    //      0xFFFFFFFF. R<=3 skips this step.
702    if matches!(revision, SecurityRevision::R4) && !encrypt_metadata {
703        hasher.update([0xFFu8; 4]);
704    }
705    let mut digest = hasher.finalize_reset();
706
707    // Algorithm 2, step 6: for R>=3, re-MD5 the first n bytes 50 times.
708    if matches!(revision, SecurityRevision::R3 | SecurityRevision::R4) {
709        for _ in 0..50 {
710            hasher.update(&digest[..key_length_bytes]);
711            digest = hasher.finalize_reset();
712        }
713    }
714    digest[..key_length_bytes].to_vec()
715}
716
717fn pad_password(password: &[u8]) -> [u8; 32] {
718    let mut out = [0u8; 32];
719    let take = password.len().min(32);
720    out[..take].copy_from_slice(&password[..take]);
721    if take < 32 {
722        out[take..].copy_from_slice(&PASSWORD_PADDING[..32 - take]);
723    }
724    out
725}
726
727fn recover_user_password_from_owner(
728    owner_password: &[u8],
729    o_entry: &[u8],
730    revision: SecurityRevision,
731    key_length_bytes: usize,
732) -> Vec<u8> {
733    // Algorithm 7 (PDF 1.7 §7.6.3.4). Symmetric inverse of Algorithm 3:
734    //   1. Pad the owner password and MD5 it.
735    //   2. For R>=3 re-hash 50 times.
736    //   3. Truncate to `key_length_bytes` — this is the RC4 key used on /O.
737    //   4. For R=2, RC4-decrypt /O once with that key.
738    //      For R>=3, RC4-decrypt /O 20 times with keys (base XOR i) for i
739    //      decreasing from 19 down to 0.
740    //   5. The result is the padded user password.
741    let padded = pad_password(owner_password);
742    let mut hasher = Md5::new();
743    hasher.update(padded);
744    let mut digest = hasher.finalize_reset();
745    if matches!(revision, SecurityRevision::R3 | SecurityRevision::R4) {
746        for _ in 0..50 {
747            hasher.update(&digest[..key_length_bytes]);
748            digest = hasher.finalize_reset();
749        }
750    }
751    let base_key = digest[..key_length_bytes].to_vec();
752
753    match revision {
754        SecurityRevision::R2 => rc4(&base_key, o_entry),
755        SecurityRevision::R3 | SecurityRevision::R4 => {
756            let mut buffer = o_entry.to_vec();
757            for i in (0u8..=19).rev() {
758                let key: Vec<u8> = base_key.iter().map(|byte| byte ^ i).collect();
759                buffer = rc4(&key, &buffer);
760            }
761            buffer
762        }
763        SecurityRevision::R5 | SecurityRevision::R6 => {
764            unreachable!("V=5 takes open_v5; Algorithm 7 is not applicable to R=5 / R=6")
765        }
766    }
767}
768
769fn authenticate_user_password(
770    file_key: &[u8],
771    revision: SecurityRevision,
772    u_entry: &[u8],
773    id_first: &[u8],
774) -> bool {
775    match revision {
776        SecurityRevision::R2 => {
777            // Algorithm 4: encrypt the password padding with the file key; the
778            // full 32 bytes must equal /U.
779            let encrypted = rc4(file_key, &PASSWORD_PADDING);
780            encrypted == u_entry
781        }
782        SecurityRevision::R5 | SecurityRevision::R6 => {
783            unreachable!("V=5 takes open_v5; Algorithm 5 is not applicable to R=5 / R=6")
784        }
785        SecurityRevision::R3 | SecurityRevision::R4 => {
786            // Algorithm 5.
787            let mut hasher = Md5::new();
788            hasher.update(PASSWORD_PADDING);
789            hasher.update(id_first);
790            let seed = hasher.finalize();
791            let mut buffer = rc4(file_key, &seed);
792            for i in 1u8..=19 {
793                let key: Vec<u8> = file_key.iter().map(|byte| byte ^ i).collect();
794                buffer = rc4(&key, &buffer);
795            }
796            // The first 16 bytes of /U must match the buffer; the remaining
797            // 16 bytes are arbitrary padding.
798            buffer.as_slice() == &u_entry[..16]
799        }
800    }
801}
802
803fn md5_bytes(input: &[u8]) -> [u8; 16] {
804    let mut hasher = Md5::new();
805    hasher.update(input);
806    hasher.finalize().into()
807}
808
809fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
810    let mut s: [u8; 256] = [0; 256];
811    for (index, value) in s.iter_mut().enumerate() {
812        *value = index as u8;
813    }
814    let mut j: u8 = 0;
815    for i in 0..256 {
816        j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
817        s.swap(i, j as usize);
818    }
819    let mut output = Vec::with_capacity(data.len());
820    let mut i: u8 = 0;
821    let mut j: u8 = 0;
822    for &byte in data {
823        i = i.wrapping_add(1);
824        j = j.wrapping_add(s[i as usize]);
825        s.swap(i as usize, j as usize);
826        let k = s[(s[i as usize].wrapping_add(s[j as usize])) as usize];
827        output.push(byte ^ k);
828    }
829    output
830}
831
832#[cfg(test)]
833pub(crate) mod test_helpers {
834    //! Expose the low-level primitives so parser tests can build a tiny
835    //! encrypted PDF end-to-end — pick an arbitrary `/O`, derive a file key
836    //! from the empty password, encrypt each object's data with per-object
837    //! RC4, and then round-trip it through `parse_pdf`.
838
839    use super::*;
840
841    pub fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
842        super::rc4(key, data)
843    }
844
845    pub fn compute_file_key(
846        password: &[u8],
847        o_entry: &[u8],
848        permissions: i32,
849        id_first: &[u8],
850        key_length_bytes: usize,
851    ) -> Vec<u8> {
852        // Callers that do not care about the revision use the R=3 variant,
853        // which matches the write side of the existing RC4 fixtures.
854        super::compute_file_key(
855            password,
856            o_entry,
857            permissions,
858            id_first,
859            key_length_bytes,
860            SecurityRevision::R3,
861            true,
862        )
863    }
864
865    pub fn compute_file_key_with_revision(
866        password: &[u8],
867        o_entry: &[u8],
868        permissions: i32,
869        id_first: &[u8],
870        key_length_bytes: usize,
871        revision: SecurityRevision,
872    ) -> Vec<u8> {
873        super::compute_file_key(
874            password,
875            o_entry,
876            permissions,
877            id_first,
878            key_length_bytes,
879            revision,
880            true,
881        )
882    }
883
884    /// R=4 variant of the file-key derivation, exposed so AES-128 test
885    /// fixtures can build a matching file key and `/U` entry. Mirrors
886    /// [`compute_file_key`] but honours `encrypt_metadata` so the
887    /// Algorithm 2 step-5 branch (append 0xFFFFFFFF) can be exercised.
888    pub fn compute_file_key_r4(
889        password: &[u8],
890        o_entry: &[u8],
891        permissions: i32,
892        id_first: &[u8],
893        encrypt_metadata: bool,
894    ) -> Vec<u8> {
895        super::compute_file_key(
896            password,
897            o_entry,
898            permissions,
899            id_first,
900            16,
901            SecurityRevision::R4,
902            encrypt_metadata,
903        )
904    }
905
906    /// Produce the 32-byte `/U` value that corresponds to the empty user
907    /// password under revision 3. The first 16 bytes are the RC4 output
908    /// from algorithm 5; the remaining 16 bytes are arbitrary padding
909    /// (here zeroed, which real writers often do).
910    pub fn compute_u_r3(file_key: &[u8], id_first: &[u8]) -> Vec<u8> {
911        let mut hasher = Md5::new();
912        hasher.update(PASSWORD_PADDING);
913        hasher.update(id_first);
914        let seed = hasher.finalize();
915        let mut buffer = super::rc4(file_key, &seed);
916        for i in 1u8..=19 {
917            let key: Vec<u8> = file_key.iter().map(|byte| byte ^ i).collect();
918            buffer = super::rc4(&key, &buffer);
919        }
920        buffer.resize(32, 0);
921        buffer
922    }
923
924    /// Build the `/O` value for the Encrypt dictionary, given the owner
925    /// and user passwords and the security revision. Algorithm 3 — the
926    /// write-side inverse of Algorithm 7, used by tests to construct
927    /// synthetic encrypted PDFs with both owner and user passwords
928    /// populated.
929    pub fn compute_o(
930        owner_password: &[u8],
931        user_password: &[u8],
932        revision: SecurityRevision,
933        key_length_bytes: usize,
934    ) -> Vec<u8> {
935        let padded_owner = pad_password(owner_password);
936        let mut hasher = Md5::new();
937        hasher.update(padded_owner);
938        let mut digest = hasher.finalize_reset();
939        if matches!(revision, SecurityRevision::R3 | SecurityRevision::R4) {
940            for _ in 0..50 {
941                hasher.update(&digest[..key_length_bytes]);
942                digest = hasher.finalize_reset();
943            }
944        }
945        let base_key = digest[..key_length_bytes].to_vec();
946
947        let padded_user = pad_password(user_password);
948        match revision {
949            SecurityRevision::R2 => super::rc4(&base_key, &padded_user),
950            SecurityRevision::R3 | SecurityRevision::R4 => {
951                let mut buffer = super::rc4(&base_key, &padded_user);
952                for i in 1u8..=19 {
953                    let key: Vec<u8> = base_key.iter().map(|byte| byte ^ i).collect();
954                    buffer = super::rc4(&key, &buffer);
955                }
956                buffer
957            }
958            SecurityRevision::R5 | SecurityRevision::R6 => {
959                panic!("compute_o is not applicable to V=5 — use compute_v5_u / compute_v5_o")
960            }
961        }
962    }
963
964    /// Build the per-object RC4 key in exactly the same way the handler
965    /// does, so tests can encrypt a known plaintext and then check that
966    /// the parser's decryption path inverts the transform.
967    pub fn object_key(file_key: &[u8], object_number: u32, generation: u16) -> Vec<u8> {
968        let mut material = Vec::with_capacity(file_key.len() + 5);
969        material.extend_from_slice(file_key);
970        let num = object_number.to_le_bytes();
971        material.push(num[0]);
972        material.push(num[1]);
973        material.push(num[2]);
974        let gen_bytes = generation.to_le_bytes();
975        material.push(gen_bytes[0]);
976        material.push(gen_bytes[1]);
977        let digest = super::md5_bytes(&material);
978        let truncated_len = (file_key.len() + 5).min(16);
979        digest[..truncated_len].to_vec()
980    }
981
982    /// AES variant of [`object_key`]: appends the literal `sAlT` suffix
983    /// before the MD5 so the V=4 /AESV2 path derives a distinct key
984    /// from the RC4 path for the same indirect object.
985    pub fn object_key_aes(file_key: &[u8], object_number: u32, generation: u16) -> Vec<u8> {
986        let mut material = Vec::with_capacity(file_key.len() + 9);
987        material.extend_from_slice(file_key);
988        let num = object_number.to_le_bytes();
989        material.push(num[0]);
990        material.push(num[1]);
991        material.push(num[2]);
992        let gen_bytes = generation.to_le_bytes();
993        material.push(gen_bytes[0]);
994        material.push(gen_bytes[1]);
995        material.extend_from_slice(b"sAlT");
996        let digest = super::md5_bytes(&material);
997        let truncated_len = (file_key.len() + 5).min(16);
998        digest[..truncated_len].to_vec()
999    }
1000
1001    /// Compute the 48-byte V=5 `/U` entry plus the 32-byte `/UE` entry
1002    /// given the user password, 8-byte validation salt, 8-byte key salt,
1003    /// and 32-byte file key. Only used by tests to build synthetic V=5
1004    /// fixtures.
1005    pub fn compute_v5_u_and_ue(
1006        user_password: &[u8],
1007        validation_salt: &[u8; 8],
1008        key_salt: &[u8; 8],
1009        file_key: &[u8; 32],
1010        revision: SecurityRevision,
1011    ) -> (Vec<u8>, Vec<u8>) {
1012        let verifier = super::pdf_2_b_hash(user_password, validation_salt, None, revision);
1013        let mut u = Vec::with_capacity(48);
1014        u.extend_from_slice(&verifier[..32]);
1015        u.extend_from_slice(validation_salt);
1016        u.extend_from_slice(key_salt);
1017
1018        let intermediate = super::pdf_2_b_hash(user_password, key_salt, None, revision);
1019        let ue = aes_256_cbc_encrypt_no_pad(&intermediate[..32], &[0u8; 16], file_key);
1020        (u, ue)
1021    }
1022
1023    /// Compute the 48-byte V=5 `/O` entry plus the 32-byte `/OE` entry
1024    /// given the owner password, 8-byte validation salt, 8-byte key salt,
1025    /// the 48-byte `/U` vector (typically the user hash + salts), and the
1026    /// 32-byte file key. Only used by tests to build synthetic V=5
1027    /// fixtures.
1028    pub fn compute_v5_o_and_oe(
1029        owner_password: &[u8],
1030        validation_salt: &[u8; 8],
1031        key_salt: &[u8; 8],
1032        u_vector: &[u8; 48],
1033        file_key: &[u8; 32],
1034        revision: SecurityRevision,
1035    ) -> (Vec<u8>, Vec<u8>) {
1036        let verifier =
1037            super::pdf_2_b_hash(owner_password, validation_salt, Some(u_vector), revision);
1038        let mut o = Vec::with_capacity(48);
1039        o.extend_from_slice(&verifier[..32]);
1040        o.extend_from_slice(validation_salt);
1041        o.extend_from_slice(key_salt);
1042
1043        let intermediate = super::pdf_2_b_hash(owner_password, key_salt, Some(u_vector), revision);
1044        let oe = aes_256_cbc_encrypt_no_pad(&intermediate[..32], &[0u8; 16], file_key);
1045        (o, oe)
1046    }
1047
1048    /// AES-256-CBC encrypt used by V=5 content streams and strings. The
1049    /// ciphertext is prefixed with the 16-byte IV and PKCS#7-padded to a
1050    /// 16-byte block boundary — this matches exactly what the parser's
1051    /// decryption path expects.
1052    pub fn aes_256_cbc_encrypt(key: &[u8], iv: &[u8; 16], plaintext: &[u8]) -> Vec<u8> {
1053        assert_eq!(key.len(), 32, "AES-256 key must be 32 bytes");
1054        let cipher = Aes256::new_from_slice(key).expect("key length validated");
1055        let pad_len = 16 - (plaintext.len() % 16);
1056        let mut padded = Vec::with_capacity(plaintext.len() + pad_len);
1057        padded.extend_from_slice(plaintext);
1058        padded.extend(std::iter::repeat_n(pad_len as u8, pad_len));
1059        let mut output = Vec::with_capacity(16 + padded.len());
1060        output.extend_from_slice(iv);
1061        let mut prev: [u8; 16] = *iv;
1062        for chunk in padded.chunks(16) {
1063            let mut buf = [0u8; 16];
1064            for ((b, plain), iv_byte) in buf.iter_mut().zip(chunk.iter()).zip(prev.iter()) {
1065                *b = plain ^ iv_byte;
1066            }
1067            let mut block = GenericArray::clone_from_slice(&buf);
1068            cipher.encrypt_block(&mut block);
1069            output.extend_from_slice(block.as_slice());
1070            prev.copy_from_slice(block.as_slice());
1071        }
1072        output
1073    }
1074
1075    fn aes_256_cbc_encrypt_no_pad(key: &[u8], iv: &[u8; 16], data: &[u8]) -> Vec<u8> {
1076        assert_eq!(key.len(), 32, "AES-256 key must be 32 bytes");
1077        assert!(data.len() % 16 == 0, "plaintext must be block-aligned");
1078        let cipher = Aes256::new_from_slice(key).expect("key length validated");
1079        let mut output = Vec::with_capacity(data.len());
1080        let mut prev: [u8; 16] = *iv;
1081        for chunk in data.chunks(16) {
1082            let mut buf = [0u8; 16];
1083            for ((b, plain), iv_byte) in buf.iter_mut().zip(chunk.iter()).zip(prev.iter()) {
1084                *b = plain ^ iv_byte;
1085            }
1086            let mut block = GenericArray::clone_from_slice(&buf);
1087            cipher.encrypt_block(&mut block);
1088            output.extend_from_slice(block.as_slice());
1089            prev.copy_from_slice(block.as_slice());
1090        }
1091        output
1092    }
1093
1094    /// Encrypt `plaintext` with AES-128-CBC, PKCS#7-padded, and prefix
1095    /// the 16-byte IV — matching exactly what the parser's decryption
1096    /// path expects. Used by tests to build synthetic V=4 fixtures.
1097    pub fn aes_128_cbc_encrypt(key: &[u8], iv: &[u8; 16], plaintext: &[u8]) -> Vec<u8> {
1098        use aes::cipher::BlockEncrypt;
1099
1100        assert_eq!(key.len(), 16, "AES-128 key must be 16 bytes");
1101        let cipher = Aes128::new_from_slice(key).expect("key length validated");
1102
1103        // Pad with PKCS#7, always appending at least one byte of padding.
1104        let pad_len = 16 - (plaintext.len() % 16);
1105        let mut padded = Vec::with_capacity(plaintext.len() + pad_len);
1106        padded.extend_from_slice(plaintext);
1107        padded.extend(std::iter::repeat_n(pad_len as u8, pad_len));
1108
1109        let mut output = Vec::with_capacity(16 + padded.len());
1110        output.extend_from_slice(iv);
1111        let mut prev: [u8; 16] = *iv;
1112        for chunk in padded.chunks(16) {
1113            let mut block = [0u8; 16];
1114            for ((b, plain), iv_byte) in block.iter_mut().zip(chunk.iter()).zip(prev.iter()) {
1115                *b = plain ^ iv_byte;
1116            }
1117            let mut arr = GenericArray::clone_from_slice(&block);
1118            cipher.encrypt_block(&mut arr);
1119            output.extend_from_slice(arr.as_slice());
1120            prev.copy_from_slice(arr.as_slice());
1121        }
1122        output
1123    }
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129
1130    #[test]
1131    fn rc4_empty_input_returns_empty() {
1132        assert_eq!(rc4(b"Key", b""), Vec::<u8>::new());
1133    }
1134
1135    #[test]
1136    fn rc4_matches_known_vector() {
1137        // RFC 6229 test vector: key "Key", data "Plaintext".
1138        let key = b"Key";
1139        let plaintext = b"Plaintext";
1140        let encrypted = rc4(key, plaintext);
1141        // Decrypting with the same keystream yields the original bytes.
1142        let decrypted = rc4(key, &encrypted);
1143        assert_eq!(decrypted, plaintext);
1144        // The ciphertext should match the well-known RFC 6229 output.
1145        assert_eq!(
1146            encrypted,
1147            [0xBB, 0xF3, 0x16, 0xE8, 0xD9, 0x40, 0xAF, 0x0A, 0xD3]
1148        );
1149    }
1150
1151    #[test]
1152    fn pad_password_short_pads_with_padding_string() {
1153        let padded = pad_password(b"ab");
1154        assert_eq!(padded[0], b'a');
1155        assert_eq!(padded[1], b'b');
1156        assert_eq!(padded[2], PASSWORD_PADDING[0]);
1157        assert_eq!(padded[31], PASSWORD_PADDING[29]);
1158    }
1159
1160    #[test]
1161    fn pad_password_truncates_to_32_bytes() {
1162        let long = vec![b'x'; 64];
1163        let padded = pad_password(&long);
1164        assert_eq!(padded, [b'x'; 32]);
1165    }
1166
1167    fn build_encrypt_dict_r3(
1168        o_entry: Vec<u8>,
1169        u_entry: Vec<u8>,
1170        permissions: i32,
1171    ) -> PdfDictionary {
1172        let mut dict = PdfDictionary::default();
1173        dict.insert("Filter".to_string(), PdfValue::Name("Standard".to_string()));
1174        dict.insert("V".to_string(), PdfValue::Integer(2));
1175        dict.insert("R".to_string(), PdfValue::Integer(3));
1176        dict.insert("Length".to_string(), PdfValue::Integer(128));
1177        dict.insert(
1178            "O".to_string(),
1179            PdfValue::String(crate::types::PdfString(o_entry)),
1180        );
1181        dict.insert(
1182            "U".to_string(),
1183            PdfValue::String(crate::types::PdfString(u_entry)),
1184        );
1185        dict.insert("P".to_string(), PdfValue::Integer(permissions as i64));
1186        dict
1187    }
1188
1189    fn build_r3_handler_inputs(
1190        user_password: &[u8],
1191        owner_password: &[u8],
1192        id_first: &[u8],
1193    ) -> (PdfDictionary, Vec<u8>) {
1194        let key_length_bytes = 16;
1195        let permissions: i32 = -4;
1196        let o = test_helpers::compute_o(
1197            owner_password,
1198            user_password,
1199            SecurityRevision::R3,
1200            key_length_bytes,
1201        );
1202        let file_key = test_helpers::compute_file_key(
1203            user_password,
1204            &o,
1205            permissions,
1206            id_first,
1207            key_length_bytes,
1208        );
1209        let u = test_helpers::compute_u_r3(&file_key, id_first);
1210        (build_encrypt_dict_r3(o, u, permissions), file_key)
1211    }
1212
1213    #[test]
1214    fn open_authenticates_user_password() {
1215        let id_first = b"synthetic-id-0123456789abcdef";
1216        let (dict, expected_file_key) = build_r3_handler_inputs(b"userpw", b"ownerpw", id_first);
1217        let handler = StandardSecurityHandler::open(&dict, id_first, b"userpw")
1218            .expect("open succeeds")
1219            .expect("user password authenticates");
1220        assert_eq!(handler.file_key, expected_file_key);
1221    }
1222
1223    #[test]
1224    fn open_authenticates_owner_password() {
1225        let id_first = b"synthetic-id-0123456789abcdef";
1226        let (dict, expected_file_key) = build_r3_handler_inputs(b"userpw", b"ownerpw", id_first);
1227        let handler = StandardSecurityHandler::open(&dict, id_first, b"ownerpw")
1228            .expect("open succeeds")
1229            .expect("owner password authenticates");
1230        // File key must match the one derived from the user password — the
1231        // owner password is only a way of recovering it.
1232        assert_eq!(handler.file_key, expected_file_key);
1233    }
1234
1235    #[test]
1236    fn open_rejects_wrong_password() {
1237        let id_first = b"synthetic-id-0123456789abcdef";
1238        let (dict, _) = build_r3_handler_inputs(b"userpw", b"ownerpw", id_first);
1239        let result = StandardSecurityHandler::open(&dict, id_first, b"wrongpw")
1240            .expect("open does not fail, only reports authentication");
1241        assert!(result.is_none());
1242    }
1243
1244    #[test]
1245    fn open_accepts_utf8_password() {
1246        let id_first = b"synthetic-id-0123456789abcdef";
1247        let password = "pässwörd".as_bytes();
1248        let (dict, _) = build_r3_handler_inputs(password, b"ownerpw", id_first);
1249        let handler = StandardSecurityHandler::open(&dict, id_first, password)
1250            .expect("open succeeds")
1251            .expect("UTF-8 password authenticates");
1252        assert_eq!(handler.file_key.len(), 16);
1253    }
1254
1255    fn build_encrypt_dict_v4_aesv2(
1256        o_entry: Vec<u8>,
1257        u_entry: Vec<u8>,
1258        permissions: i32,
1259        encrypt_metadata: Option<bool>,
1260    ) -> PdfDictionary {
1261        let mut std_cf = PdfDictionary::default();
1262        std_cf.insert("CFM".to_string(), PdfValue::Name("AESV2".to_string()));
1263        std_cf.insert("Length".to_string(), PdfValue::Integer(16));
1264        std_cf.insert(
1265            "AuthEvent".to_string(),
1266            PdfValue::Name("DocOpen".to_string()),
1267        );
1268
1269        let mut cf = PdfDictionary::default();
1270        cf.insert("StdCF".to_string(), PdfValue::Dictionary(std_cf));
1271
1272        let mut dict = PdfDictionary::default();
1273        dict.insert("Filter".to_string(), PdfValue::Name("Standard".to_string()));
1274        dict.insert("V".to_string(), PdfValue::Integer(4));
1275        dict.insert("R".to_string(), PdfValue::Integer(4));
1276        dict.insert("Length".to_string(), PdfValue::Integer(128));
1277        dict.insert("CF".to_string(), PdfValue::Dictionary(cf));
1278        dict.insert("StmF".to_string(), PdfValue::Name("StdCF".to_string()));
1279        dict.insert("StrF".to_string(), PdfValue::Name("StdCF".to_string()));
1280        dict.insert(
1281            "O".to_string(),
1282            PdfValue::String(crate::types::PdfString(o_entry)),
1283        );
1284        dict.insert(
1285            "U".to_string(),
1286            PdfValue::String(crate::types::PdfString(u_entry)),
1287        );
1288        dict.insert("P".to_string(), PdfValue::Integer(permissions as i64));
1289        if let Some(value) = encrypt_metadata {
1290            dict.insert("EncryptMetadata".to_string(), PdfValue::Bool(value));
1291        }
1292        dict
1293    }
1294
1295    fn build_v4_handler_inputs(
1296        user_password: &[u8],
1297        owner_password: &[u8],
1298        id_first: &[u8],
1299        encrypt_metadata: Option<bool>,
1300    ) -> (PdfDictionary, Vec<u8>) {
1301        let permissions: i32 = -4;
1302        let o = test_helpers::compute_o(owner_password, user_password, SecurityRevision::R4, 16);
1303        let file_key = test_helpers::compute_file_key_r4(
1304            user_password,
1305            &o,
1306            permissions,
1307            id_first,
1308            encrypt_metadata.unwrap_or(true),
1309        );
1310        let u = test_helpers::compute_u_r3(&file_key, id_first);
1311        (
1312            build_encrypt_dict_v4_aesv2(o, u, permissions, encrypt_metadata),
1313            file_key,
1314        )
1315    }
1316
1317    #[test]
1318    fn open_v4_aesv2_handler_authenticates_user_password() {
1319        let id_first = b"v4-synthetic-id-0123456789";
1320        let (dict, expected_file_key) =
1321            build_v4_handler_inputs(b"userpw", b"ownerpw", id_first, None);
1322        let handler = StandardSecurityHandler::open(&dict, id_first, b"userpw")
1323            .expect("open succeeds")
1324            .expect("user password authenticates on V=4");
1325        assert_eq!(handler.file_key, expected_file_key);
1326        assert_eq!(handler.string_method, CryptMethod::AesV2);
1327        assert_eq!(handler.stream_method, CryptMethod::AesV2);
1328        assert!(handler.encrypt_metadata);
1329    }
1330
1331    #[test]
1332    fn open_v4_aesv2_handler_authenticates_owner_password() {
1333        let id_first = b"v4-synthetic-id-0123456789";
1334        let (dict, expected_file_key) =
1335            build_v4_handler_inputs(b"userpw", b"ownerpw", id_first, None);
1336        let handler = StandardSecurityHandler::open(&dict, id_first, b"ownerpw")
1337            .expect("open succeeds")
1338            .expect("owner password authenticates on V=4");
1339        assert_eq!(handler.file_key, expected_file_key);
1340    }
1341
1342    #[test]
1343    fn open_v4_honours_encrypt_metadata_false() {
1344        let id_first = b"v4-metadata-id";
1345        let (dict, _) = build_v4_handler_inputs(b"", b"ownerpw", id_first, Some(false));
1346        let handler = StandardSecurityHandler::open(&dict, id_first, b"")
1347            .expect("open succeeds")
1348            .expect("empty password authenticates");
1349        assert!(!handler.encrypts_metadata());
1350    }
1351
1352    #[test]
1353    fn open_v4_identity_crypt_filter_is_passthrough() {
1354        let id_first = b"v4-identity-id";
1355        let (dict_v4, _) = build_v4_handler_inputs(b"", b"ownerpw", id_first, None);
1356        let mut dict = dict_v4;
1357        dict.insert("StrF".to_string(), PdfValue::Name("Identity".to_string()));
1358        dict.insert("StmF".to_string(), PdfValue::Name("Identity".to_string()));
1359
1360        let handler = StandardSecurityHandler::open(&dict, id_first, b"")
1361            .expect("open succeeds")
1362            .expect("empty password authenticates");
1363        assert_eq!(handler.string_method, CryptMethod::Identity);
1364        assert_eq!(handler.stream_method, CryptMethod::Identity);
1365
1366        let ciphertext = b"hello";
1367        let plaintext = handler
1368            .decrypt_bytes(ciphertext, ObjectRef::new(4, 0), BytesKind::Stream)
1369            .expect("identity passes bytes through");
1370        assert_eq!(plaintext, ciphertext);
1371    }
1372
1373    #[test]
1374    fn open_v4_rejects_unsupported_cfm() {
1375        let id_first = b"v4-unsupported-id";
1376
1377        let (dict_v4, _) = build_v4_handler_inputs(b"", b"ownerpw", id_first, None);
1378        let mut dict = dict_v4;
1379        let mut std_cf = PdfDictionary::default();
1380        // `AESV4` is not defined in any PDF version — it should be
1381        // rejected as unsupported. We previously tested /AESV3 here, but
1382        // V=5 has since landed, so /AESV3 is now a legal method name (it
1383        // just happens to be spec-invalid under V=4).
1384        std_cf.insert("CFM".to_string(), PdfValue::Name("AESV4".to_string()));
1385        std_cf.insert("Length".to_string(), PdfValue::Integer(32));
1386        let mut cf = PdfDictionary::default();
1387        cf.insert("StdCF".to_string(), PdfValue::Dictionary(std_cf));
1388        dict.insert("CF".to_string(), PdfValue::Dictionary(cf));
1389
1390        let error = StandardSecurityHandler::open(&dict, id_first, b"")
1391            .expect_err("unknown CFM must be rejected as unsupported");
1392        assert!(matches!(error, PdfError::Unsupported(_)), "got {error:?}");
1393    }
1394
1395    #[test]
1396    fn aes_128_cbc_round_trip() {
1397        let key = [0x11u8; 16];
1398        let iv = [0x22u8; 16];
1399        let plaintext = b"redact me, please";
1400        let ciphertext = test_helpers::aes_128_cbc_encrypt(&key, &iv, plaintext);
1401        let decrypted = aes_128_cbc_decrypt(&key, &ciphertext).expect("round trip succeeds");
1402        assert_eq!(decrypted, plaintext);
1403    }
1404
1405    #[test]
1406    fn aes_128_cbc_rejects_bad_pkcs7_padding() {
1407        let key = [0x11u8; 16];
1408        let iv = [0x22u8; 16];
1409        let plaintext = b"abcdef";
1410        let mut ciphertext = test_helpers::aes_128_cbc_encrypt(&key, &iv, plaintext);
1411        // Flip the last ciphertext byte so the plaintext padding becomes
1412        // invalid (with high probability) after decryption.
1413        let last = ciphertext.len() - 1;
1414        ciphertext[last] ^= 0xFF;
1415        let error =
1416            aes_128_cbc_decrypt(&key, &ciphertext).expect_err("corrupted padding must be rejected");
1417        assert!(matches!(error, PdfError::Corrupt(_)), "got {error:?}");
1418    }
1419
1420    #[test]
1421    fn aes_128_cbc_rejects_short_ciphertext() {
1422        let key = [0x11u8; 16];
1423        let error = aes_128_cbc_decrypt(&key, &[0u8; 16])
1424            .expect_err("ciphertext shorter than IV+1 block must be rejected");
1425        assert!(matches!(error, PdfError::Corrupt(_)), "got {error:?}");
1426    }
1427
1428    fn build_encrypt_dict_v5_aesv3(
1429        o: Vec<u8>,
1430        u: Vec<u8>,
1431        oe: Vec<u8>,
1432        ue: Vec<u8>,
1433        permissions: i32,
1434        perms: Option<Vec<u8>>,
1435        revision: SecurityRevision,
1436    ) -> PdfDictionary {
1437        let mut std_cf = PdfDictionary::default();
1438        std_cf.insert("CFM".to_string(), PdfValue::Name("AESV3".to_string()));
1439        std_cf.insert("Length".to_string(), PdfValue::Integer(32));
1440        std_cf.insert(
1441            "AuthEvent".to_string(),
1442            PdfValue::Name("DocOpen".to_string()),
1443        );
1444
1445        let mut cf = PdfDictionary::default();
1446        cf.insert("StdCF".to_string(), PdfValue::Dictionary(std_cf));
1447
1448        let r_value = match revision {
1449            SecurityRevision::R5 => 5,
1450            SecurityRevision::R6 => 6,
1451            _ => panic!("test helper only supports R5 / R6"),
1452        };
1453
1454        let mut dict = PdfDictionary::default();
1455        dict.insert("Filter".to_string(), PdfValue::Name("Standard".to_string()));
1456        dict.insert("V".to_string(), PdfValue::Integer(5));
1457        dict.insert("R".to_string(), PdfValue::Integer(r_value));
1458        dict.insert("Length".to_string(), PdfValue::Integer(256));
1459        dict.insert("CF".to_string(), PdfValue::Dictionary(cf));
1460        dict.insert("StmF".to_string(), PdfValue::Name("StdCF".to_string()));
1461        dict.insert("StrF".to_string(), PdfValue::Name("StdCF".to_string()));
1462        dict.insert(
1463            "O".to_string(),
1464            PdfValue::String(crate::types::PdfString(o)),
1465        );
1466        dict.insert(
1467            "U".to_string(),
1468            PdfValue::String(crate::types::PdfString(u)),
1469        );
1470        dict.insert(
1471            "OE".to_string(),
1472            PdfValue::String(crate::types::PdfString(oe)),
1473        );
1474        dict.insert(
1475            "UE".to_string(),
1476            PdfValue::String(crate::types::PdfString(ue)),
1477        );
1478        dict.insert("P".to_string(), PdfValue::Integer(permissions as i64));
1479        if let Some(value) = perms {
1480            dict.insert(
1481                "Perms".to_string(),
1482                PdfValue::String(crate::types::PdfString(value)),
1483            );
1484        }
1485        dict
1486    }
1487
1488    fn build_v5_handler_inputs(
1489        user_password: &[u8],
1490        owner_password: &[u8],
1491        revision: SecurityRevision,
1492    ) -> (PdfDictionary, [u8; 32]) {
1493        let file_key = [0x13u8; 32];
1494        let u_validation_salt = [0xAAu8; 8];
1495        let u_key_salt = [0xBBu8; 8];
1496        let o_validation_salt = [0xCCu8; 8];
1497        let o_key_salt = [0xDDu8; 8];
1498
1499        let (u, ue) = test_helpers::compute_v5_u_and_ue(
1500            user_password,
1501            &u_validation_salt,
1502            &u_key_salt,
1503            &file_key,
1504            revision,
1505        );
1506        let u_vector: [u8; 48] = u.as_slice().try_into().expect("U is 48 bytes");
1507        let (o, oe) = test_helpers::compute_v5_o_and_oe(
1508            owner_password,
1509            &o_validation_salt,
1510            &o_key_salt,
1511            &u_vector,
1512            &file_key,
1513            revision,
1514        );
1515
1516        (
1517            build_encrypt_dict_v5_aesv3(o, u, oe, ue, -4, None, revision),
1518            file_key,
1519        )
1520    }
1521
1522    #[test]
1523    fn open_v5_r6_authenticates_user_password() {
1524        let (dict, expected_file_key) =
1525            build_v5_handler_inputs(b"userpw", b"ownerpw", SecurityRevision::R6);
1526        let handler = StandardSecurityHandler::open(&dict, b"", b"userpw")
1527            .expect("open succeeds")
1528            .expect("user password authenticates on V=5 / R=6");
1529        assert_eq!(handler.file_key, expected_file_key);
1530        assert_eq!(handler.string_method, CryptMethod::AesV3);
1531        assert_eq!(handler.stream_method, CryptMethod::AesV3);
1532    }
1533
1534    #[test]
1535    fn open_v5_r6_authenticates_owner_password() {
1536        let (dict, expected_file_key) =
1537            build_v5_handler_inputs(b"userpw", b"ownerpw", SecurityRevision::R6);
1538        let handler = StandardSecurityHandler::open(&dict, b"", b"ownerpw")
1539            .expect("open succeeds")
1540            .expect("owner password authenticates on V=5 / R=6");
1541        assert_eq!(handler.file_key, expected_file_key);
1542    }
1543
1544    #[test]
1545    fn open_v5_r6_rejects_wrong_password() {
1546        let (dict, _) = build_v5_handler_inputs(b"userpw", b"ownerpw", SecurityRevision::R6);
1547        let result = StandardSecurityHandler::open(&dict, b"", b"wrongpw")
1548            .expect("open does not fail, only reports authentication");
1549        assert!(result.is_none());
1550    }
1551
1552    #[test]
1553    fn open_v5_r5_authenticates_user_password() {
1554        let (dict, expected_file_key) =
1555            build_v5_handler_inputs(b"userpw", b"ownerpw", SecurityRevision::R5);
1556        let handler = StandardSecurityHandler::open(&dict, b"", b"userpw")
1557            .expect("open succeeds")
1558            .expect("user password authenticates on V=5 / R=5");
1559        assert_eq!(handler.file_key, expected_file_key);
1560    }
1561
1562    #[test]
1563    fn open_v5_r5_empty_password_authenticates() {
1564        let (dict, _) = build_v5_handler_inputs(b"", b"ownerpw", SecurityRevision::R5);
1565        let handler = StandardSecurityHandler::open(&dict, b"", b"")
1566            .expect("open succeeds")
1567            .expect("empty password authenticates on V=5 / R=5");
1568        assert_eq!(handler.string_method, CryptMethod::AesV3);
1569    }
1570
1571    #[test]
1572    fn aes_256_cbc_round_trip_through_handler() {
1573        let key = [0x13u8; 32];
1574        let iv = [0x77u8; 16];
1575        let plaintext = b"top secret V=5 content";
1576        let ciphertext = test_helpers::aes_256_cbc_encrypt(&key, &iv, plaintext);
1577        let decrypted = aes_256_cbc_decrypt(&key, &ciphertext).expect("round trip succeeds");
1578        assert_eq!(decrypted, plaintext);
1579    }
1580
1581    #[test]
1582    fn open_r2_authenticates_owner_password() {
1583        // Algorithm 4 / 7 divergence from R=3: single RC4 round for /O,
1584        // full 32-byte /U match.
1585        let id_first = b"r2-synthetic-id";
1586        let user_password = b"u2";
1587        let owner_password = b"o2";
1588        let key_length_bytes = 5; // 40-bit key, matching R=2 default.
1589        let permissions: i32 = -4;
1590        let o = test_helpers::compute_o(
1591            owner_password,
1592            user_password,
1593            SecurityRevision::R2,
1594            key_length_bytes,
1595        );
1596        let file_key = test_helpers::compute_file_key_with_revision(
1597            user_password,
1598            &o,
1599            permissions,
1600            id_first,
1601            key_length_bytes,
1602            SecurityRevision::R2,
1603        );
1604        // Algorithm 4: /U is RC4(file_key, PASSWORD_PADDING).
1605        let u = test_helpers::rc4(&file_key, &PASSWORD_PADDING);
1606
1607        let mut dict = PdfDictionary::default();
1608        dict.insert("Filter".to_string(), PdfValue::Name("Standard".to_string()));
1609        dict.insert("V".to_string(), PdfValue::Integer(1));
1610        dict.insert("R".to_string(), PdfValue::Integer(2));
1611        dict.insert("Length".to_string(), PdfValue::Integer(40));
1612        dict.insert(
1613            "O".to_string(),
1614            PdfValue::String(crate::types::PdfString(o)),
1615        );
1616        dict.insert(
1617            "U".to_string(),
1618            PdfValue::String(crate::types::PdfString(u)),
1619        );
1620        dict.insert("P".to_string(), PdfValue::Integer(permissions as i64));
1621
1622        let handler = StandardSecurityHandler::open(&dict, id_first, owner_password)
1623            .expect("open succeeds")
1624            .expect("owner password authenticates on R=2");
1625        assert_eq!(handler.file_key, file_key);
1626    }
1627}