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