Skip to main content

oxidize_pdf/parser/
encryption_handler.rs

1//! PDF encryption detection and password handling
2//!
3//! This module provides functionality to detect encrypted PDFs and handle password-based
4//! decryption according to ISO 32000-1 Chapter 7.6.
5
6use super::objects::PdfDictionary;
7use super::{ParseError, ParseResult};
8use crate::encryption::{
9    EncryptionKey, OwnerPassword, Permissions, Rc4, Rc4Key, StandardSecurityHandler, UserPassword,
10};
11use crate::objects::ObjectId;
12
13/// Encryption information extracted from PDF trailer.
14///
15/// `#[non_exhaustive]`: this is parser output, not meant to be constructed by
16/// downstream code. The attribute lets future fields (like `cfm`, added for
17/// issue #364) be added without a breaking change.
18#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub struct EncryptionInfo {
21    /// Filter name (should be "Standard")
22    pub filter: String,
23    /// V entry (algorithm version)
24    pub v: i32,
25    /// R entry (revision)
26    pub r: i32,
27    /// O entry (owner password hash)
28    pub o: Vec<u8>,
29    /// U entry (user password hash)
30    pub u: Vec<u8>,
31    /// P entry (permissions)
32    pub p: i32,
33    /// Length entry (key length in bits)
34    pub length: Option<i32>,
35    /// UE entry (encrypted user key, R5/R6 only)
36    pub ue: Option<Vec<u8>>,
37    /// OE entry (encrypted owner key, R5/R6 only)
38    pub oe: Option<Vec<u8>>,
39    /// Stream crypt filter method (/CFM of the filter named by /StmF), V>=4 only.
40    /// One of "V2" (RC4), "AESV2" (AES-128), "AESV3" (AES-256), "Identity".
41    /// Determines the cipher for revision 4, where R alone is ambiguous.
42    ///
43    /// `pub(crate)`: internal cipher-selection input, not part of the public API.
44    pub(crate) cfm: Option<String>,
45    /// `/EncryptMetadata` flag (default true). When false and R >= 4, Algorithm 2
46    /// appends 0xFFFFFFFF to the key MD5 (ISO 32000-1 §7.6.3.3, step f). Real
47    /// producers often leave metadata in cleartext; honouring this is required
48    /// to unlock those files, even with the empty user password (issue #379).
49    ///
50    /// `pub(crate)`: internal key-derivation input, not part of the public API.
51    pub(crate) encrypt_metadata: bool,
52}
53
54/// PDF Encryption Handler
55pub struct EncryptionHandler {
56    /// Encryption information from trailer
57    encryption_info: EncryptionInfo,
58    /// Standard security handler
59    security_handler: StandardSecurityHandler,
60    /// Current encryption key (if unlocked)
61    encryption_key: Option<EncryptionKey>,
62    /// File ID from trailer
63    file_id: Option<Vec<u8>>,
64}
65
66impl EncryptionHandler {
67    /// Create encryption handler from encryption dictionary
68    pub fn new(encrypt_dict: &PdfDictionary, file_id: Option<Vec<u8>>) -> ParseResult<Self> {
69        let encryption_info = Self::parse_encryption_dict(encrypt_dict)?;
70
71        // Create the security handler. For V>=4 the cipher is determined by the
72        // stream crypt filter method (/CFM), not by the revision: R4 may be either
73        // RC4 (/V2) or AES-128 (/AESV2). Selecting by revision alone decrypts
74        // AES-128 streams with RC4 → garbage (issue #364).
75        let security_handler = match encryption_info.r {
76            2 => StandardSecurityHandler::rc4_40bit(),
77            3 => StandardSecurityHandler::rc4_128bit(),
78            4 => match encryption_info.cfm.as_deref() {
79                // RC4-128 crypt filter under R4 (legacy, but valid).
80                Some("V2") => StandardSecurityHandler::rc4_128bit(),
81                // AES-128 is the conventional R4 cipher and what this crate writes.
82                // Default to it for /AESV2, a missing /CFM, or any other value: R4
83                // construction must stay infallible (it always built a handler
84                // before #364), so non-RC4 cases fall back to AES rather than error.
85                _ => StandardSecurityHandler::aes_128_r4(),
86            },
87            5 => StandardSecurityHandler::aes_256_r5(),
88            6 => StandardSecurityHandler::aes_256_r6(),
89            _ => {
90                return Err(ParseError::SyntaxError {
91                    position: 0,
92                    message: format!("Encryption revision {} not supported", encryption_info.r),
93                });
94            }
95        };
96
97        Ok(Self {
98            encryption_info,
99            security_handler,
100            encryption_key: None,
101            file_id,
102        })
103    }
104
105    /// Parse encryption dictionary from PDF trailer
106    fn parse_encryption_dict(dict: &PdfDictionary) -> ParseResult<EncryptionInfo> {
107        // Get Filter (required)
108        let filter = dict
109            .get("Filter")
110            .and_then(|obj| obj.as_name())
111            .map(|name| name.0.as_str())
112            .ok_or_else(|| ParseError::MissingKey("Filter".to_string()))?;
113
114        if filter != "Standard" {
115            return Err(ParseError::SyntaxError {
116                position: 0,
117                message: format!("Encryption filter '{filter}' not supported"),
118            });
119        }
120
121        // Get V (algorithm version)
122        let v = dict
123            .get("V")
124            .and_then(|obj| obj.as_integer())
125            .map(|i| i as i32)
126            .unwrap_or(0);
127
128        // Get R (revision)
129        let r = dict
130            .get("R")
131            .and_then(|obj| obj.as_integer())
132            .map(|i| i as i32)
133            .ok_or_else(|| ParseError::MissingKey("R".to_string()))?;
134
135        // Get O (owner password hash)
136        let o = dict
137            .get("O")
138            .and_then(|obj| obj.as_string())
139            .ok_or_else(|| ParseError::MissingKey("O".to_string()))?
140            .as_bytes()
141            .to_vec();
142
143        // Get U (user password hash)
144        let u = dict
145            .get("U")
146            .and_then(|obj| obj.as_string())
147            .ok_or_else(|| ParseError::MissingKey("U".to_string()))?
148            .as_bytes()
149            .to_vec();
150
151        // Get P (permissions)
152        let p = dict
153            .get("P")
154            .and_then(|obj| obj.as_integer())
155            .map(|i| i as i32)
156            .ok_or_else(|| ParseError::MissingKey("P".to_string()))?;
157
158        // Get Length (optional, defaults based on revision)
159        let length = dict
160            .get("Length")
161            .and_then(|obj| obj.as_integer())
162            .map(|i| i as i32);
163
164        // Get UE entry (R5/R6 only — encrypted user key)
165        let ue = dict
166            .get("UE")
167            .and_then(|obj| obj.as_string())
168            .map(|s| s.as_bytes().to_vec());
169
170        // Get OE entry (R5/R6 only — encrypted owner key)
171        let oe = dict
172            .get("OE")
173            .and_then(|obj| obj.as_string())
174            .map(|s| s.as_bytes().to_vec());
175
176        // Get the stream crypt filter method (V>=4). For V<4 the cipher is fixed
177        // by V/R (RC4), so /CF is absent and cfm stays None.
178        let cfm = if v >= 4 {
179            Self::parse_stream_cfm(dict)
180        } else {
181            None
182        };
183
184        // Get EncryptMetadata (default true). Only meaningful for R >= 4; older
185        // revisions always encrypt metadata.
186        let encrypt_metadata = dict
187            .get("EncryptMetadata")
188            .and_then(|obj| obj.as_bool())
189            .unwrap_or(true);
190
191        Ok(EncryptionInfo {
192            filter: filter.to_string(),
193            v,
194            r,
195            o,
196            u,
197            p,
198            length,
199            ue,
200            oe,
201            cfm,
202            encrypt_metadata,
203        })
204    }
205
206    /// Resolve the crypt filter method (/CFM) of the filter named by /StmF.
207    ///
208    /// `/Encrypt` carries `/CF << /StdCF << /CFM /AESV2 >> >>` and `/StmF /StdCF`.
209    /// Returns the CFM name (e.g. "AESV2", "V2"), or "Identity" when streams are
210    /// not encrypted, or None when no crypt filter is declared.
211    fn parse_stream_cfm(dict: &PdfDictionary) -> Option<String> {
212        let stmf = dict
213            .get("StmF")
214            .and_then(|o| o.as_name())
215            .map(|n| n.0.as_str())
216            .unwrap_or("Identity"); // ISO 32000-1 §7.6.5: default /StmF is Identity
217
218        if stmf == "Identity" {
219            return Some("Identity".to_string());
220        }
221
222        let cf = dict.get("CF").and_then(|o| o.as_dict())?;
223        cf.get(stmf)
224            .and_then(|o| o.as_dict())
225            .and_then(|f| f.get("CFM"))
226            .and_then(|o| o.as_name())
227            .map(|n| n.0.clone())
228    }
229
230    /// Check if PDF is encrypted by looking for Encrypt entry in trailer
231    pub fn detect_encryption(trailer: &PdfDictionary) -> bool {
232        trailer.contains_key("Encrypt")
233    }
234
235    /// Try to unlock PDF with user password
236    pub fn unlock_with_user_password(&mut self, password: &str) -> ParseResult<bool> {
237        let user_password = UserPassword(password.to_string());
238
239        match self.encryption_info.r {
240            5 | 6 => self.unlock_user_r5_r6(&user_password),
241            _ => self.unlock_user_r2_r4(&user_password),
242        }
243    }
244
245    /// R2-R4 user password unlock (MD5/RC4 based)
246    fn unlock_user_r2_r4(&mut self, user_password: &UserPassword) -> ParseResult<bool> {
247        let permissions = Permissions::from_bits(self.encryption_info.p as u32);
248
249        // The /EncryptMetadata 0xFFFFFFFF append (Algorithm 2, step f) applies
250        // only for R >= 4; older revisions always encrypt metadata. Gate on the
251        // document revision here — the security handler's revision is a cipher
252        // proxy (R4-with-RC4 reports R3).
253        let encrypt_metadata = self.encryption_info.encrypt_metadata || self.encryption_info.r < 4;
254
255        let computed_u = self
256            .security_handler
257            .compute_user_hash_with_metadata(
258                user_password,
259                &self.encryption_info.o,
260                permissions,
261                self.file_id.as_deref(),
262                encrypt_metadata,
263            )
264            .map_err(|e| ParseError::SyntaxError {
265                position: 0,
266                message: format!("Failed to compute user hash: {e}"),
267            })?;
268
269        // Compare with stored U entry (first 16 bytes for R3+)
270        let comparison_length = if self.encryption_info.r >= 3 { 16 } else { 32 };
271
272        if computed_u.len() < comparison_length || self.encryption_info.u.len() < comparison_length
273        {
274            return Ok(false);
275        }
276
277        let matches =
278            computed_u[..comparison_length] == self.encryption_info.u[..comparison_length];
279
280        if matches {
281            let key = self
282                .security_handler
283                .compute_encryption_key_with_metadata(
284                    user_password,
285                    &self.encryption_info.o,
286                    permissions,
287                    self.file_id.as_deref(),
288                    encrypt_metadata,
289                )
290                .map_err(|e| ParseError::SyntaxError {
291                    position: 0,
292                    message: format!("Failed to compute encryption key: {e}"),
293                })?;
294            self.encryption_key = Some(key);
295        }
296
297        Ok(matches)
298    }
299
300    /// R5/R6 user password unlock (SHA-256/AES-256 based per ISO 32000-2)
301    fn unlock_user_r5_r6(&mut self, user_password: &UserPassword) -> ParseResult<bool> {
302        let u_entry = &self.encryption_info.u;
303
304        // Validate using the proper R5/R6 algorithm
305        let is_valid = if self.encryption_info.r == 5 {
306            self.security_handler
307                .validate_r5_user_password(user_password, u_entry)
308        } else {
309            self.security_handler
310                .validate_r6_user_password(user_password, u_entry)
311        }
312        .map_err(|e| ParseError::SyntaxError {
313            position: 0,
314            message: format!(
315                "Failed to validate R{} user password: {e}",
316                self.encryption_info.r
317            ),
318        })?;
319
320        if is_valid {
321            // Recover encryption key from UE entry
322            let ue_entry =
323                self.encryption_info
324                    .ue
325                    .as_deref()
326                    .ok_or_else(|| ParseError::SyntaxError {
327                        position: 0,
328                        message: format!(
329                            "R{} encryption requires UE entry but it is missing",
330                            self.encryption_info.r
331                        ),
332                    })?;
333
334            let key = if self.encryption_info.r == 5 {
335                self.security_handler
336                    .recover_r5_encryption_key(user_password, u_entry, ue_entry)
337            } else {
338                self.security_handler
339                    .recover_r6_encryption_key(user_password, u_entry, ue_entry)
340            }
341            .map_err(|e| ParseError::SyntaxError {
342                position: 0,
343                message: format!(
344                    "Failed to recover R{} encryption key: {e}",
345                    self.encryption_info.r
346                ),
347            })?;
348
349            self.encryption_key = Some(key);
350        }
351
352        Ok(is_valid)
353    }
354
355    /// R5/R6 owner password unlock (SHA-256/AES-256 based per ISO 32000-2).
356    ///
357    /// Mirrors [`unlock_user_r5_r6`](Self::unlock_user_r5_r6) but validates the
358    /// owner password against the `/O` entry and recovers the file key from `/OE`.
359    fn unlock_owner_r5_r6(&mut self, owner_password: &OwnerPassword) -> ParseResult<bool> {
360        let o_entry = self.encryption_info.o.clone();
361        let u_entry = self.encryption_info.u.clone();
362
363        let is_valid = if self.encryption_info.r == 5 {
364            self.security_handler
365                .validate_r5_owner_password(owner_password, &o_entry, &u_entry)
366        } else {
367            self.security_handler
368                .validate_r6_owner_password(owner_password, &o_entry, &u_entry)
369        }
370        .map_err(|e| ParseError::SyntaxError {
371            position: 0,
372            message: format!(
373                "Failed to validate R{} owner password: {e}",
374                self.encryption_info.r
375            ),
376        })?;
377
378        if is_valid {
379            let oe_entry =
380                self.encryption_info
381                    .oe
382                    .as_deref()
383                    .ok_or_else(|| ParseError::SyntaxError {
384                        position: 0,
385                        message: format!(
386                            "R{} encryption requires OE entry but it is missing",
387                            self.encryption_info.r
388                        ),
389                    })?;
390
391            let key = if self.encryption_info.r == 5 {
392                self.security_handler.recover_r5_owner_encryption_key(
393                    owner_password,
394                    &o_entry,
395                    &u_entry,
396                    oe_entry,
397                )
398            } else {
399                self.security_handler.recover_r6_owner_encryption_key(
400                    owner_password,
401                    &o_entry,
402                    &u_entry,
403                    oe_entry,
404                )
405            }
406            .map_err(|e| ParseError::SyntaxError {
407                position: 0,
408                message: format!(
409                    "Failed to recover R{} owner encryption key: {e}",
410                    self.encryption_info.r
411                ),
412            })?;
413
414            self.encryption_key = Some(EncryptionKey::new(key));
415        }
416
417        Ok(is_valid)
418    }
419
420    /// Try to unlock PDF with owner password
421    ///
422    /// Owner password authentication works by:
423    /// 1. Deriving an RC4 key from the owner password
424    /// 2. Decrypting the O entry to recover the user password
425    /// 3. Using the recovered user password to compute the encryption key
426    pub fn unlock_with_owner_password(&mut self, password: &str) -> ParseResult<bool> {
427        // R5/R6 (AES-256) use a completely different owner-password algorithm.
428        // The MD5/RC4 path below assumes key_length <= 16 (MD5 output); for R5/R6
429        // key_length is 32, so `hash[..key_length]` would panic. Dispatch first.
430        if self.encryption_info.r >= 5 {
431            return self.unlock_owner_r5_r6(&OwnerPassword(password.to_string()));
432        }
433
434        // Standard 32-byte padding from PDF spec
435        const PADDING: [u8; 32] = [
436            0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA,
437            0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE,
438            0x64, 0x53, 0x69, 0x7A,
439        ];
440
441        // Step 1: Pad owner password to 32 bytes
442        let mut padded = [0u8; 32];
443        let password_bytes = password.as_bytes();
444        let len = password_bytes.len().min(32);
445        padded[..len].copy_from_slice(&password_bytes[..len]);
446        if len < 32 {
447            padded[len..].copy_from_slice(&PADDING[..32 - len]);
448        }
449
450        // Step 2: MD5 hash the padded password
451        let mut hash = md5::compute(&padded).to_vec();
452
453        // Step 3: For R3+, do 50 additional MD5 iterations
454        let key_length = self.security_handler.key_length;
455        if self.encryption_info.r >= 3 {
456            for _ in 0..50 {
457                hash = md5::compute(&hash).to_vec();
458            }
459        }
460
461        // Step 4: Decrypt O entry to get user password. `/O` is a 32-byte string
462        // by spec; a short one from a malformed file would panic the slice below.
463        if self.encryption_info.o.len() < 32 {
464            return Ok(false);
465        }
466        let mut decrypted = self.encryption_info.o[..32].to_vec();
467
468        if self.encryption_info.r >= 3 {
469            // For R3+, decrypt with keys XOR'd with 19, 18, ..., 0
470            for i in (0..20).rev() {
471                let mut key_bytes = hash[..key_length].to_vec();
472                for byte in &mut key_bytes {
473                    *byte ^= i as u8;
474                }
475                let rc4_key = Rc4Key::from_slice(&key_bytes);
476                let mut cipher = Rc4::new(&rc4_key);
477                decrypted = cipher.process(&decrypted);
478            }
479        } else {
480            // For R2, single RC4 decryption
481            let rc4_key = Rc4Key::from_slice(&hash[..key_length]);
482            let mut cipher = Rc4::new(&rc4_key);
483            decrypted = cipher.process(&decrypted);
484        }
485
486        // Step 5: `decrypted` IS the padded user password (Algorithm 3, step e).
487        // Authenticate it exactly as the user path does, straight from these 32
488        // bytes — never reconstruct a `&str` and re-pad it. The old code searched
489        // for where the standard padding began (byte 0x28, '(') and truncated
490        // there; with a wrong owner password the bytes are garbage, and whenever
491        // the first byte was 0x28 the "password" collapsed to "", which re-pads
492        // to the exact standard padding and then authenticates ANY document with
493        // an empty user password. That granted owner access on ~1/256 of wrong
494        // attempts (owner-unlock fail-open). Running the raw bytes through the
495        // standard verifier removes the truncation and the bypass.
496        let permissions = Permissions::from_bits(self.encryption_info.p as u32);
497        let encrypt_metadata = self.encryption_info.encrypt_metadata || self.encryption_info.r < 4;
498
499        let computed_u = self
500            .security_handler
501            .compute_user_hash_from_padded(
502                &decrypted,
503                &self.encryption_info.o,
504                permissions,
505                self.file_id.as_deref(),
506                encrypt_metadata,
507            )
508            .map_err(|e| ParseError::SyntaxError {
509                position: 0,
510                message: format!("Failed to compute user hash from /O: {e}"),
511            })?;
512
513        // R3+ compares the first 16 bytes of /U; R2 compares all 32.
514        let comparison_length = if self.encryption_info.r >= 3 { 16 } else { 32 };
515        if computed_u.len() < comparison_length || self.encryption_info.u.len() < comparison_length
516        {
517            return Ok(false);
518        }
519        if computed_u[..comparison_length] != self.encryption_info.u[..comparison_length] {
520            return Ok(false);
521        }
522
523        // Owner authenticated: derive and retain the file key from the same bytes.
524        let key = self
525            .security_handler
526            .compute_key_from_padded(
527                &decrypted,
528                &self.encryption_info.o,
529                permissions,
530                self.file_id.as_deref(),
531                encrypt_metadata,
532            )
533            .map_err(|e| ParseError::SyntaxError {
534                position: 0,
535                message: format!("Failed to compute key from /O: {e}"),
536            })?;
537        self.encryption_key = Some(key);
538        Ok(true)
539    }
540
541    /// Try to unlock with empty password (common case)
542    pub fn try_empty_password(&mut self) -> ParseResult<bool> {
543        self.unlock_with_user_password("")
544    }
545
546    /// Check if the PDF is currently unlocked
547    pub fn is_unlocked(&self) -> bool {
548        self.encryption_key.is_some()
549    }
550
551    /// Get the current encryption key (if unlocked)
552    pub fn encryption_key(&self) -> Option<&EncryptionKey> {
553        self.encryption_key.as_ref()
554    }
555
556    /// Decrypt a string object.
557    ///
558    /// Uses the error-propagating `try_*` variant so an AES decryption failure
559    /// surfaces as an error rather than silently yielding empty content (#364).
560    pub fn decrypt_string(&self, data: &[u8], obj_id: &ObjectId) -> ParseResult<Vec<u8>> {
561        match &self.encryption_key {
562            Some(key) => self
563                .security_handler
564                .try_decrypt_string(data, key, obj_id)
565                .map_err(|e| ParseError::SyntaxError {
566                    position: 0,
567                    message: format!("Failed to decrypt string for object {obj_id:?}: {e}"),
568                }),
569            None => Err(ParseError::EncryptionNotSupported),
570        }
571    }
572
573    /// Decrypt a stream object.
574    ///
575    /// Uses the error-propagating `try_*` variant so an AES decryption failure
576    /// surfaces as an error rather than silently yielding empty content (#364).
577    pub fn decrypt_stream(&self, data: &[u8], obj_id: &ObjectId) -> ParseResult<Vec<u8>> {
578        match &self.encryption_key {
579            Some(key) => self
580                .security_handler
581                .try_decrypt_stream(data, key, obj_id)
582                .map_err(|e| ParseError::SyntaxError {
583                    position: 0,
584                    message: format!("Failed to decrypt stream for object {obj_id:?}: {e}"),
585                }),
586            None => Err(ParseError::EncryptionNotSupported),
587        }
588    }
589
590    /// Get encryption algorithm information
591    pub fn algorithm_info(&self) -> String {
592        match (
593            self.encryption_info.r,
594            self.encryption_info.length.unwrap_or(40),
595        ) {
596            (2, _) => "RC4 40-bit".to_string(),
597            (3, len) => format!("RC4 {len}-bit"),
598            // R4 may be RC4 or AES-128 depending on the /CFM crypt filter (#364).
599            (4, len) => match self.encryption_info.cfm.as_deref() {
600                Some("V2") => format!("RC4 {len}-bit with metadata control"),
601                _ => "AES-128 (Revision 4)".to_string(),
602            },
603            (5, _) => "AES-256 (Revision 5)".to_string(),
604            (6, _) => "AES-256 (Revision 6, Unicode passwords)".to_string(),
605            (r, len) => format!("Unknown revision {r} with {len}-bit key"),
606        }
607    }
608
609    /// Get permissions information
610    pub fn permissions(&self) -> Permissions {
611        Permissions::from_bits(self.encryption_info.p as u32)
612    }
613
614    /// Check if file ID is available
615    pub fn has_file_id(&self) -> bool {
616        self.file_id.is_some()
617    }
618
619    /// Get the encryption revision
620    pub fn revision(&self) -> i32 {
621        self.encryption_info.r
622    }
623
624    /// Check if strings should be encrypted
625    pub fn encrypt_strings(&self) -> bool {
626        // For standard security handler, strings are always encrypted
627        true
628    }
629
630    /// Check if streams should be encrypted  
631    pub fn encrypt_streams(&self) -> bool {
632        // For standard security handler, streams are always encrypted
633        true
634    }
635
636    /// Whether document metadata is encrypted (`/EncryptMetadata`, default true).
637    pub fn encrypt_metadata(&self) -> bool {
638        self.encryption_info.encrypt_metadata
639    }
640}
641
642/// Password prompt result
643#[derive(Debug)]
644pub enum PasswordResult {
645    /// Password was accepted
646    Success,
647    /// Password was rejected
648    Rejected,
649    /// User cancelled password entry
650    Cancelled,
651}
652
653/// Trait for password prompting
654pub trait PasswordProvider {
655    /// Prompt for user password
656    fn prompt_user_password(&self) -> ParseResult<Option<String>>;
657
658    /// Prompt for owner password
659    fn prompt_owner_password(&self) -> ParseResult<Option<String>>;
660}
661
662/// Console-based password provider
663pub struct ConsolePasswordProvider;
664
665impl PasswordProvider for ConsolePasswordProvider {
666    fn prompt_user_password(&self) -> ParseResult<Option<String>> {
667        tracing::debug!(
668            "PDF is encrypted. Enter user password (or press Enter for empty password):"
669        );
670
671        let mut input = String::new();
672        std::io::stdin()
673            .read_line(&mut input)
674            .map_err(|e| ParseError::SyntaxError {
675                position: 0,
676                message: format!("Failed to read password: {e}"),
677            })?;
678
679        // Remove trailing newline
680        input.truncate(input.trim_end().len());
681        Ok(Some(input))
682    }
683
684    fn prompt_owner_password(&self) -> ParseResult<Option<String>> {
685        tracing::debug!("User password failed. Enter owner password:");
686
687        let mut input = String::new();
688        std::io::stdin()
689            .read_line(&mut input)
690            .map_err(|e| ParseError::SyntaxError {
691                position: 0,
692                message: format!("Failed to read password: {e}"),
693            })?;
694
695        // Remove trailing newline
696        input.truncate(input.trim_end().len());
697        Ok(Some(input))
698    }
699}
700
701/// Interactive decryption helper
702pub struct InteractiveDecryption<P: PasswordProvider> {
703    password_provider: P,
704}
705
706impl<P: PasswordProvider> InteractiveDecryption<P> {
707    /// Create new interactive decryption helper
708    pub fn new(password_provider: P) -> Self {
709        Self { password_provider }
710    }
711
712    /// Attempt to unlock PDF interactively
713    pub fn unlock_pdf(&self, handler: &mut EncryptionHandler) -> ParseResult<PasswordResult> {
714        // First try empty password
715        if handler.try_empty_password()? {
716            return Ok(PasswordResult::Success);
717        }
718
719        // Try user password
720        if let Some(password) = self.password_provider.prompt_user_password()? {
721            if handler.unlock_with_user_password(&password)? {
722                return Ok(PasswordResult::Success);
723            }
724        } else {
725            return Ok(PasswordResult::Cancelled);
726        }
727
728        // Try owner password
729        if let Some(password) = self.password_provider.prompt_owner_password()? {
730            if handler.unlock_with_owner_password(&password)? {
731                return Ok(PasswordResult::Success);
732            }
733        } else {
734            return Ok(PasswordResult::Cancelled);
735        }
736
737        Ok(PasswordResult::Rejected)
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use crate::parser::objects::{PdfDictionary, PdfName, PdfObject, PdfString};
745
746    fn create_test_encryption_dict() -> PdfDictionary {
747        let mut dict = PdfDictionary::new();
748        dict.insert(
749            "Filter".to_string(),
750            PdfObject::Name(PdfName("Standard".to_string())),
751        );
752        dict.insert("V".to_string(), PdfObject::Integer(1));
753        dict.insert("R".to_string(), PdfObject::Integer(2));
754        dict.insert(
755            "O".to_string(),
756            PdfObject::String(PdfString::new(vec![0u8; 32])),
757        );
758        dict.insert(
759            "U".to_string(),
760            PdfObject::String(PdfString::new(vec![0u8; 32])),
761        );
762        dict.insert("P".to_string(), PdfObject::Integer(-4));
763        dict
764    }
765
766    #[test]
767    fn test_encryption_detection() {
768        let mut trailer = PdfDictionary::new();
769        assert!(!EncryptionHandler::detect_encryption(&trailer));
770
771        trailer.insert("Encrypt".to_string(), PdfObject::Reference(1, 0));
772        assert!(EncryptionHandler::detect_encryption(&trailer));
773    }
774
775    #[test]
776    fn test_encryption_info_parsing() {
777        let dict = create_test_encryption_dict();
778        let info = EncryptionHandler::parse_encryption_dict(&dict).unwrap();
779
780        assert_eq!(info.filter, "Standard");
781        assert_eq!(info.v, 1);
782        assert_eq!(info.r, 2);
783        assert_eq!(info.o.len(), 32);
784        assert_eq!(info.u.len(), 32);
785        assert_eq!(info.p, -4);
786        // V<4 has no crypt filters → cfm is None.
787        assert_eq!(info.cfm, None);
788    }
789
790    /// Build a V=4/R=4 encryption dict whose StdCF crypt filter uses `cfm`.
791    fn create_v4_encryption_dict(cfm: &str) -> PdfDictionary {
792        let mut dict = PdfDictionary::new();
793        dict.insert(
794            "Filter".to_string(),
795            PdfObject::Name(PdfName("Standard".to_string())),
796        );
797        dict.insert("V".to_string(), PdfObject::Integer(4));
798        dict.insert("R".to_string(), PdfObject::Integer(4));
799        dict.insert("Length".to_string(), PdfObject::Integer(128));
800        dict.insert(
801            "O".to_string(),
802            PdfObject::String(PdfString::new(vec![0u8; 32])),
803        );
804        dict.insert(
805            "U".to_string(),
806            PdfObject::String(PdfString::new(vec![0u8; 32])),
807        );
808        dict.insert("P".to_string(), PdfObject::Integer(-4));
809
810        let mut std_cf = PdfDictionary::new();
811        std_cf.insert("CFM".to_string(), PdfObject::Name(PdfName(cfm.to_string())));
812        let mut cf = PdfDictionary::new();
813        cf.insert("StdCF".to_string(), PdfObject::Dictionary(std_cf));
814        dict.insert("CF".to_string(), PdfObject::Dictionary(cf));
815        dict.insert(
816            "StmF".to_string(),
817            PdfObject::Name(PdfName("StdCF".to_string())),
818        );
819        dict
820    }
821
822    #[test]
823    fn test_cfm_parsed_from_crypt_filter() {
824        // Issue #364: R4 cipher is decided by /CFM, not the revision.
825        let aes =
826            EncryptionHandler::parse_encryption_dict(&create_v4_encryption_dict("AESV2")).unwrap();
827        assert_eq!(aes.cfm.as_deref(), Some("AESV2"));
828
829        let rc4 =
830            EncryptionHandler::parse_encryption_dict(&create_v4_encryption_dict("V2")).unwrap();
831        assert_eq!(rc4.cfm.as_deref(), Some("V2"));
832    }
833
834    #[test]
835    fn test_r4_algorithm_info_reflects_cipher() {
836        // AESV2 under R4 must report AES-128, not RC4 (#364).
837        let aes = EncryptionHandler::new(&create_v4_encryption_dict("AESV2"), None).unwrap();
838        assert_eq!(aes.algorithm_info(), "AES-128 (Revision 4)");
839
840        // A legacy R4 PDF using the V2 (RC4) crypt filter still reports RC4.
841        let rc4 = EncryptionHandler::new(&create_v4_encryption_dict("V2"), None).unwrap();
842        assert!(
843            rc4.algorithm_info().starts_with("RC4"),
844            "got: {}",
845            rc4.algorithm_info()
846        );
847    }
848
849    #[test]
850    fn test_encryption_handler_creation() {
851        let dict = create_test_encryption_dict();
852        let handler = EncryptionHandler::new(&dict, None).unwrap();
853
854        assert_eq!(handler.encryption_info.r, 2);
855        assert!(!handler.is_unlocked());
856        assert_eq!(handler.algorithm_info(), "RC4 40-bit");
857    }
858
859    #[test]
860    fn test_empty_password_attempt() {
861        let dict = create_test_encryption_dict();
862        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
863
864        // Empty password should not work with test data
865        let result = handler.try_empty_password().unwrap();
866        assert!(!result);
867        assert!(!handler.is_unlocked());
868    }
869
870    #[test]
871    fn test_permissions() {
872        let dict = create_test_encryption_dict();
873        let handler = EncryptionHandler::new(&dict, None).unwrap();
874
875        let permissions = handler.permissions();
876        // P value of -4 should result in specific permissions
877        assert!(permissions.bits() != 0);
878    }
879
880    #[test]
881    fn test_encryption_flags() {
882        let dict = create_test_encryption_dict();
883        let handler = EncryptionHandler::new(&dict, None).unwrap();
884
885        assert!(handler.encrypt_strings());
886        assert!(handler.encrypt_streams());
887        assert!(handler.encrypt_metadata());
888    }
889
890    #[test]
891    fn test_decrypt_without_key() {
892        let dict = create_test_encryption_dict();
893        let handler = EncryptionHandler::new(&dict, None).unwrap();
894
895        let obj_id = ObjectId::new(1, 0);
896        let result = handler.decrypt_string(b"test", &obj_id);
897        assert!(result.is_err());
898    }
899
900    #[test]
901    fn test_unsupported_filter() {
902        let mut dict = PdfDictionary::new();
903        dict.insert(
904            "Filter".to_string(),
905            PdfObject::Name(PdfName("UnsupportedFilter".to_string())),
906        );
907        dict.insert("R".to_string(), PdfObject::Integer(2));
908        dict.insert(
909            "O".to_string(),
910            PdfObject::String(PdfString::new(vec![0u8; 32])),
911        );
912        dict.insert(
913            "U".to_string(),
914            PdfObject::String(PdfString::new(vec![0u8; 32])),
915        );
916        dict.insert("P".to_string(), PdfObject::Integer(-4));
917
918        let result = EncryptionHandler::new(&dict, None);
919        assert!(result.is_err());
920    }
921
922    #[test]
923    fn test_unsupported_revision() {
924        let mut dict = create_test_encryption_dict();
925        dict.insert("R".to_string(), PdfObject::Integer(99)); // Unsupported revision
926
927        let result = EncryptionHandler::new(&dict, None);
928        assert!(result.is_err());
929    }
930
931    #[test]
932    fn test_missing_required_keys() {
933        let test_cases = vec![
934            ("Filter", PdfObject::Name(PdfName("Standard".to_string()))),
935            ("R", PdfObject::Integer(2)),
936            ("O", PdfObject::String(PdfString::new(vec![0u8; 32]))),
937            ("U", PdfObject::String(PdfString::new(vec![0u8; 32]))),
938            ("P", PdfObject::Integer(-4)),
939        ];
940
941        for (skip_key, _) in test_cases {
942            let mut dict = create_test_encryption_dict();
943            dict.0.remove(&PdfName(skip_key.to_string()));
944
945            let result = EncryptionHandler::parse_encryption_dict(&dict);
946            assert!(result.is_err(), "Should fail when {skip_key} is missing");
947        }
948    }
949
950    /// Mock password provider for testing
951    struct MockPasswordProvider {
952        user_password: Option<String>,
953        owner_password: Option<String>,
954    }
955
956    impl PasswordProvider for MockPasswordProvider {
957        fn prompt_user_password(&self) -> ParseResult<Option<String>> {
958            Ok(self.user_password.clone())
959        }
960
961        fn prompt_owner_password(&self) -> ParseResult<Option<String>> {
962            Ok(self.owner_password.clone())
963        }
964    }
965
966    #[test]
967    fn test_interactive_decryption_cancelled() {
968        let provider = MockPasswordProvider {
969            user_password: None,
970            owner_password: None,
971        };
972
973        let decryption = InteractiveDecryption::new(provider);
974        let dict = create_test_encryption_dict();
975        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
976
977        let result = decryption.unlock_pdf(&mut handler).unwrap();
978        matches!(result, PasswordResult::Cancelled);
979    }
980
981    #[test]
982    fn test_interactive_decryption_rejected() {
983        let provider = MockPasswordProvider {
984            user_password: Some("wrong_password".to_string()),
985            owner_password: Some("also_wrong".to_string()),
986        };
987
988        let decryption = InteractiveDecryption::new(provider);
989        let dict = create_test_encryption_dict();
990        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
991
992        let result = decryption.unlock_pdf(&mut handler).unwrap();
993        matches!(result, PasswordResult::Rejected);
994    }
995
996    // ===== ADVANCED EDGE CASE TESTS =====
997
998    #[test]
999    fn test_malformed_encryption_dictionary_invalid_types() {
1000        // Test with wrong type for Filter
1001        let mut dict = PdfDictionary::new();
1002        dict.insert("Filter".to_string(), PdfObject::Integer(123)); // Should be Name
1003        dict.insert("R".to_string(), PdfObject::Integer(2));
1004        dict.insert(
1005            "O".to_string(),
1006            PdfObject::String(PdfString::new(vec![0u8; 32])),
1007        );
1008        dict.insert(
1009            "U".to_string(),
1010            PdfObject::String(PdfString::new(vec![0u8; 32])),
1011        );
1012        dict.insert("P".to_string(), PdfObject::Integer(-4));
1013
1014        let result = EncryptionHandler::parse_encryption_dict(&dict);
1015        assert!(result.is_err());
1016
1017        // Test with wrong type for R
1018        let mut dict = create_test_encryption_dict();
1019        dict.insert(
1020            "R".to_string(),
1021            PdfObject::Name(PdfName("not_a_number".to_string())),
1022        );
1023        let result = EncryptionHandler::parse_encryption_dict(&dict);
1024        assert!(result.is_err());
1025    }
1026
1027    #[test]
1028    fn test_encryption_dictionary_edge_values() {
1029        // Test with extreme revision values
1030        let mut dict = create_test_encryption_dict();
1031        dict.insert("R".to_string(), PdfObject::Integer(0)); // Very low revision
1032        let result = EncryptionHandler::new(&dict, None);
1033        assert!(result.is_err());
1034
1035        // Test with negative revision
1036        let mut dict = create_test_encryption_dict();
1037        dict.insert("R".to_string(), PdfObject::Integer(-1));
1038        let result = EncryptionHandler::new(&dict, None);
1039        assert!(result.is_err());
1040
1041        // Test with very high revision
1042        let mut dict = create_test_encryption_dict();
1043        dict.insert("R".to_string(), PdfObject::Integer(1000));
1044        let result = EncryptionHandler::new(&dict, None);
1045        assert!(result.is_err());
1046    }
1047
1048    #[test]
1049    fn test_encryption_dictionary_invalid_hash_lengths() {
1050        // Test with O hash too short
1051        let mut dict = create_test_encryption_dict();
1052        dict.insert(
1053            "O".to_string(),
1054            PdfObject::String(PdfString::new(vec![0u8; 16])),
1055        ); // Should be 32
1056        let result = EncryptionHandler::parse_encryption_dict(&dict);
1057        // Should still work but be invalid data
1058        assert!(result.is_ok());
1059
1060        // Test with U hash too long
1061        let mut dict = create_test_encryption_dict();
1062        dict.insert(
1063            "U".to_string(),
1064            PdfObject::String(PdfString::new(vec![0u8; 64])),
1065        ); // Should be 32
1066        let result = EncryptionHandler::parse_encryption_dict(&dict);
1067        assert!(result.is_ok());
1068
1069        // Test with empty hashes
1070        let mut dict = create_test_encryption_dict();
1071        dict.insert("O".to_string(), PdfObject::String(PdfString::new(vec![])));
1072        dict.insert("U".to_string(), PdfObject::String(PdfString::new(vec![])));
1073        let result = EncryptionHandler::parse_encryption_dict(&dict);
1074        assert!(result.is_ok());
1075    }
1076
1077    #[test]
1078    fn test_encryption_with_different_key_lengths() {
1079        // Test Rev 2 (40-bit)
1080        let mut dict = create_test_encryption_dict();
1081        dict.insert("R".to_string(), PdfObject::Integer(2));
1082        let handler = EncryptionHandler::new(&dict, None).unwrap();
1083        assert_eq!(handler.algorithm_info(), "RC4 40-bit");
1084
1085        // Test Rev 3 (128-bit)
1086        let mut dict = create_test_encryption_dict();
1087        dict.insert("R".to_string(), PdfObject::Integer(3));
1088        dict.insert("Length".to_string(), PdfObject::Integer(128));
1089        let handler = EncryptionHandler::new(&dict, None).unwrap();
1090        assert_eq!(handler.algorithm_info(), "RC4 128-bit");
1091
1092        // Test Rev 4 without a V2 crypt filter → AES-128 (the conventional R4
1093        // cipher and this crate's default). A legacy RC4 R4 PDF would carry an
1094        // explicit /CFM /V2 crypt filter; see test_r4_algorithm_info_reflects_cipher.
1095        let mut dict = create_test_encryption_dict();
1096        dict.insert("R".to_string(), PdfObject::Integer(4));
1097        dict.insert("Length".to_string(), PdfObject::Integer(128));
1098        let handler = EncryptionHandler::new(&dict, None).unwrap();
1099        assert_eq!(handler.algorithm_info(), "AES-128 (Revision 4)");
1100
1101        // Test Rev 5 (AES-256)
1102        let mut dict = create_test_encryption_dict();
1103        dict.insert("R".to_string(), PdfObject::Integer(5));
1104        dict.insert("V".to_string(), PdfObject::Integer(5));
1105        let handler = EncryptionHandler::new(&dict, None).unwrap();
1106        assert_eq!(handler.algorithm_info(), "AES-256 (Revision 5)");
1107
1108        // Test Rev 6 (AES-256 with Unicode)
1109        let mut dict = create_test_encryption_dict();
1110        dict.insert("R".to_string(), PdfObject::Integer(6));
1111        dict.insert("V".to_string(), PdfObject::Integer(5));
1112        let handler = EncryptionHandler::new(&dict, None).unwrap();
1113        assert_eq!(
1114            handler.algorithm_info(),
1115            "AES-256 (Revision 6, Unicode passwords)"
1116        );
1117    }
1118
1119    #[test]
1120    fn test_file_id_handling() {
1121        let dict = create_test_encryption_dict();
1122
1123        // Test with file ID
1124        let file_id = Some(b"test_file_id_12345678".to_vec());
1125        let handler = EncryptionHandler::new(&dict, file_id.clone()).unwrap();
1126        // File ID should be stored
1127        assert_eq!(handler.file_id, file_id);
1128
1129        // Test without file ID
1130        let handler = EncryptionHandler::new(&dict, None).unwrap();
1131        assert_eq!(handler.file_id, None);
1132
1133        // Test with empty file ID
1134        let empty_file_id = Some(vec![]);
1135        let handler = EncryptionHandler::new(&dict, empty_file_id.clone()).unwrap();
1136        assert_eq!(handler.file_id, empty_file_id);
1137    }
1138
1139    #[test]
1140    fn test_permissions_edge_cases() {
1141        // Test with different permission values
1142        let permission_values = vec![0, -1, -4, -44, -100, i32::MAX, i32::MIN];
1143
1144        for p_value in permission_values {
1145            let mut dict = create_test_encryption_dict();
1146            dict.insert("P".to_string(), PdfObject::Integer(p_value as i64));
1147            let handler = EncryptionHandler::new(&dict, None).unwrap();
1148
1149            let permissions = handler.permissions();
1150            assert_eq!(permissions.bits(), p_value as u32);
1151        }
1152    }
1153
1154    #[test]
1155    fn test_decrypt_with_different_object_ids() {
1156        let dict = create_test_encryption_dict();
1157        let handler = EncryptionHandler::new(&dict, None).unwrap();
1158        let test_data = b"test data";
1159
1160        // Test with different object IDs (should all fail since not unlocked)
1161        let object_ids = vec![
1162            ObjectId::new(1, 0),
1163            ObjectId::new(999, 0),
1164            ObjectId::new(1, 999),
1165            ObjectId::new(u32::MAX, u16::MAX),
1166        ];
1167
1168        for obj_id in object_ids {
1169            let result = handler.decrypt_string(test_data, &obj_id);
1170            assert!(result.is_err());
1171
1172            let result = handler.decrypt_stream(test_data, &obj_id);
1173            assert!(result.is_err());
1174        }
1175    }
1176
1177    #[test]
1178    fn test_password_scenarios_comprehensive() {
1179        let dict = create_test_encryption_dict();
1180        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
1181
1182        // Test various password scenarios (using String for uniformity)
1183        let test_passwords = vec![
1184            "".to_string(),                     // Empty
1185            " ".to_string(),                    // Single space
1186            "   ".to_string(),                  // Multiple spaces
1187            "password".to_string(),             // Simple
1188            "Password123!@#".to_string(),       // Complex
1189            "a".repeat(32),                     // Exactly 32 chars
1190            "a".repeat(50),                     // Over 32 chars
1191            "unicode_ñáéíóú".to_string(),       // Unicode
1192            "pass\nwith\nnewlines".to_string(), // Newlines
1193            "pass\twith\ttabs".to_string(),     // Tabs
1194            "pass with spaces".to_string(),     // Spaces
1195            "🔐🗝️📄".to_string(),               // Emojis
1196        ];
1197
1198        for password in test_passwords {
1199            // All should fail with test data but not crash
1200            let result = handler.unlock_with_user_password(&password);
1201            assert!(result.is_ok());
1202            assert!(!result.unwrap());
1203
1204            let result = handler.unlock_with_owner_password(&password);
1205            assert!(result.is_ok());
1206            assert!(!result.unwrap());
1207        }
1208    }
1209
1210    #[test]
1211    fn test_encryption_handler_thread_safety_simulation() {
1212        // Simulate what would happen in multi-threaded access
1213        let dict = create_test_encryption_dict();
1214        let handler = EncryptionHandler::new(&dict, None).unwrap();
1215
1216        // Test multiple read operations (safe)
1217        for _ in 0..100 {
1218            assert!(!handler.is_unlocked());
1219            assert_eq!(handler.algorithm_info(), "RC4 40-bit");
1220            assert!(handler.encrypt_strings());
1221            assert!(handler.encrypt_streams());
1222            assert!(handler.encrypt_metadata());
1223        }
1224    }
1225
1226    #[test]
1227    fn test_encryption_state_transitions() {
1228        let dict = create_test_encryption_dict();
1229        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
1230
1231        // Initial state
1232        assert!(!handler.is_unlocked());
1233
1234        // Try unlock (should fail with test data)
1235        let result = handler.try_empty_password().unwrap();
1236        assert!(!result);
1237        assert!(!handler.is_unlocked());
1238
1239        // Try user password (should fail with test data)
1240        let result = handler.unlock_with_user_password("test").unwrap();
1241        assert!(!result);
1242        assert!(!handler.is_unlocked());
1243
1244        // Try owner password (should fail with test data)
1245        let result = handler.unlock_with_owner_password("test").unwrap();
1246        assert!(!result);
1247        assert!(!handler.is_unlocked());
1248
1249        // State should remain consistent
1250        assert!(!handler.is_unlocked());
1251    }
1252
1253    #[test]
1254    fn test_interactive_decryption_edge_cases() {
1255        // Test provider that returns None for both passwords
1256        let provider = MockPasswordProvider {
1257            user_password: None,
1258            owner_password: None,
1259        };
1260
1261        let decryption = InteractiveDecryption::new(provider);
1262        let dict = create_test_encryption_dict();
1263        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
1264
1265        let result = decryption.unlock_pdf(&mut handler).unwrap();
1266        matches!(result, PasswordResult::Cancelled);
1267
1268        // Test provider that returns empty strings
1269        let provider = MockPasswordProvider {
1270            user_password: Some("".to_string()),
1271            owner_password: Some("".to_string()),
1272        };
1273
1274        let decryption = InteractiveDecryption::new(provider);
1275        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
1276
1277        let result = decryption.unlock_pdf(&mut handler).unwrap();
1278        matches!(result, PasswordResult::Rejected);
1279    }
1280
1281    /// Test custom MockPasswordProvider for edge cases
1282    struct EdgeCasePasswordProvider {
1283        call_count: std::cell::RefCell<usize>,
1284        passwords: Vec<Option<String>>,
1285    }
1286
1287    impl EdgeCasePasswordProvider {
1288        fn new(passwords: Vec<Option<String>>) -> Self {
1289            Self {
1290                call_count: std::cell::RefCell::new(0),
1291                passwords,
1292            }
1293        }
1294    }
1295
1296    impl PasswordProvider for EdgeCasePasswordProvider {
1297        fn prompt_user_password(&self) -> ParseResult<Option<String>> {
1298            let mut count = self.call_count.borrow_mut();
1299            if *count < self.passwords.len() {
1300                let result = self.passwords[*count].clone();
1301                *count += 1;
1302                Ok(result)
1303            } else {
1304                Ok(None)
1305            }
1306        }
1307
1308        fn prompt_owner_password(&self) -> ParseResult<Option<String>> {
1309            self.prompt_user_password()
1310        }
1311    }
1312
1313    #[test]
1314    fn test_interactive_decryption_with_sequence() {
1315        let passwords = vec![
1316            Some("first_attempt".to_string()),
1317            Some("second_attempt".to_string()),
1318            None, // Cancelled
1319        ];
1320
1321        let provider = EdgeCasePasswordProvider::new(passwords);
1322        let decryption = InteractiveDecryption::new(provider);
1323        let dict = create_test_encryption_dict();
1324        let mut handler = EncryptionHandler::new(&dict, None).unwrap();
1325
1326        let result = decryption.unlock_pdf(&mut handler).unwrap();
1327        matches!(result, PasswordResult::Cancelled);
1328    }
1329}