Skip to main content

rpdfium_parser/
security.rs

1// Derived from PDFium's core/fpdfapi/parser/cpdf_security_handler.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! PDF Standard Security Handler (R2–R6).
7//!
8//! Implements password verification and object/stream decryption
9//! per the PDF specification's Standard Security Handler.
10
11use std::collections::HashMap;
12
13use rpdfium_core::{Name, PdfSource};
14
15use crate::crypto::{self, CryptoError};
16use crate::object::{Object, ObjectId};
17use crate::store::ObjectStore;
18
19/// Padding string used in password computation (Table 3.18 in PDF spec).
20const PASSWORD_PADDING: [u8; 32] = [
21    0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
22    0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
23];
24
25/// Errors from the security handler.
26#[derive(Debug, thiserror::Error)]
27pub enum SecurityError {
28    /// The supplied password does not match owner or user password.
29    #[error("invalid password")]
30    InvalidPassword,
31    /// The encryption version/revision is not supported.
32    #[error("unsupported encryption version: V={0}, R={1}")]
33    UnsupportedVersion(u32, u32),
34    /// A required key is missing from the encryption dictionary.
35    #[error("missing encryption dictionary key: {0}")]
36    MissingKey(String),
37    /// An underlying cryptographic error.
38    #[error("crypto error: {0}")]
39    Crypto(#[from] CryptoError),
40}
41
42/// The method used by a crypt filter.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CryptFilterMethod {
45    /// No encryption.
46    None,
47    /// RC4 encryption (V2).
48    V2,
49    /// AES-128 encryption (AESV2).
50    Aesv2,
51    /// AES-256 encryption (AESV3).
52    Aesv3,
53}
54
55/// Typed wrapper for the PDF permission bits stored in the `/P` field of the
56/// encrypt dictionary (PDF 1.7 spec, Table 3.20).
57///
58/// The raw value is a signed 32-bit integer where the upper bits are always 1
59/// per the spec; only bits 3, 5, 8, and 9 carry meaningful permission flags.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Permissions(i32);
62
63impl Permissions {
64    /// Wrap a raw `/P` integer value.
65    pub fn from_bits(bits: i32) -> Self {
66        Self(bits)
67    }
68
69    /// Return the raw integer value.
70    pub fn bits(self) -> i32 {
71        self.0
72    }
73
74    /// Bit 4: Allow modifying document content.
75    pub fn modify_content(self) -> bool {
76        self.0 & (1 << 3) != 0
77    }
78
79    /// Bit 6: Allow adding, modifying, or deleting annotations.
80    pub fn modify_annotation(self) -> bool {
81        self.0 & (1 << 5) != 0
82    }
83
84    /// Bit 9: Allow filling in form fields and signing.
85    pub fn fill_form(self) -> bool {
86        self.0 & (1 << 8) != 0
87    }
88
89    /// Bit 10: Allow extracting text and graphics for accessibility.
90    pub fn extract_for_accessibility(self) -> bool {
91        self.0 & (1 << 9) != 0
92    }
93}
94
95/// PDF Standard Security Handler — holds the derived encryption key
96/// and methods for decrypting strings and streams.
97pub struct SecurityHandler {
98    revision: u32,
99    key_length_bytes: usize,
100    encryption_key: Vec<u8>,
101    permissions: Permissions,
102    encrypt_metadata: bool,
103    stream_cf: CryptFilterMethod,
104    string_cf: CryptFilterMethod,
105    /// The /O value (owner hash) from the encrypt dictionary.
106    o_value: Vec<u8>,
107    /// The /U value (user hash) from the encrypt dictionary.
108    u_value: Vec<u8>,
109    /// The successfully-verified encoded password (Latin-1 for R2–R4, UTF-8 for R5/R6).
110    /// Corresponds to `m_EncodedPassword` / `GetEncodedPassword()` in PDFium.
111    encoded_password: Vec<u8>,
112}
113
114impl SecurityHandler {
115    /// Build a `SecurityHandler` from the `/Encrypt` dictionary, verifying the
116    /// password against the stored owner/user hashes.
117    ///
118    /// `encrypt_dict` is the resolved encryption dictionary.
119    /// `file_id` is the first element of the trailer `/ID` array.
120    pub fn from_encrypt_dict<S: PdfSource>(
121        encrypt_dict: &HashMap<Name, Object>,
122        store: &ObjectStore<S>,
123        password: &str,
124        file_id: &[u8],
125    ) -> Result<Self, SecurityError> {
126        // Parse required fields
127        let v = get_int(encrypt_dict, store, &Name::v())? as u32;
128        let r = get_int(encrypt_dict, store, &Name::r())? as u32;
129        let p = get_int(encrypt_dict, store, &Name::p())?;
130
131        let o_bytes = get_string_bytes(encrypt_dict, store, &Name::o())?;
132        let u_bytes = get_string_bytes(encrypt_dict, store, &Name::u())?;
133
134        // Key length: /Length in bits, default 40
135        let key_length_bits =
136            get_optional_int(encrypt_dict, store, &Name::length()).unwrap_or(40) as usize;
137        let key_length_bytes = key_length_bits / 8;
138
139        // Validate key length to prevent panics in key derivation.
140        // R2-R4: 40-128 bits (5-16 bytes); R5-R6 always use 256-bit (32 bytes).
141        if r <= 4 && !(5..=16).contains(&key_length_bytes) {
142            return Err(SecurityError::UnsupportedVersion(v, r));
143        }
144
145        // EncryptMetadata: default true
146        let encrypt_metadata =
147            get_optional_bool(encrypt_dict, store, &Name::encrypt_metadata()).unwrap_or(true);
148
149        // Determine crypt filter methods for streams and strings
150        let (stream_cf, string_cf) = if v >= 4 {
151            let stm = parse_single_crypt_filter(encrypt_dict, store, &Name::stm_f());
152            let str_ = parse_single_crypt_filter(encrypt_dict, store, &Name::str_f());
153            (stm, str_)
154        } else if v == 1 || v == 2 || v == 3 {
155            (CryptFilterMethod::V2, CryptFilterMethod::V2)
156        } else {
157            (CryptFilterMethod::None, CryptFilterMethod::None)
158        };
159
160        match (v, r) {
161            (1, 2) | (2, 3) | (3, 3) | (4, 4) => {
162                // R2-R4: MD5-based key derivation
163                let pwd_bytes = encode_password_latin1(password);
164                let key = derive_key_r2_r4(
165                    &pwd_bytes,
166                    &o_bytes,
167                    p as i32,
168                    file_id,
169                    r,
170                    key_length_bytes,
171                    encrypt_metadata,
172                );
173
174                // Try user password first
175                if verify_user_password_r2_r4(&key, &u_bytes, r, file_id) {
176                    return Ok(Self {
177                        revision: r,
178                        key_length_bytes,
179                        encryption_key: key,
180                        permissions: Permissions::from_bits(p as i32),
181                        encrypt_metadata,
182                        stream_cf,
183                        string_cf,
184                        o_value: o_bytes.clone(),
185                        u_value: u_bytes.clone(),
186                        encoded_password: pwd_bytes,
187                    });
188                }
189
190                // Try as owner password: recover user password from O value
191                let user_password =
192                    recover_user_password_from_owner(&pwd_bytes, &o_bytes, r, key_length_bytes);
193                let key = derive_key_r2_r4(
194                    &user_password,
195                    &o_bytes,
196                    p as i32,
197                    file_id,
198                    r,
199                    key_length_bytes,
200                    encrypt_metadata,
201                );
202
203                if verify_user_password_r2_r4(&key, &u_bytes, r, file_id) {
204                    Ok(Self {
205                        revision: r,
206                        key_length_bytes,
207                        encryption_key: key,
208                        permissions: Permissions::from_bits(p as i32),
209                        encrypt_metadata,
210                        stream_cf,
211                        string_cf,
212                        o_value: o_bytes,
213                        u_value: u_bytes,
214                        encoded_password: user_password,
215                    })
216                } else {
217                    Err(SecurityError::InvalidPassword)
218                }
219            }
220            (5, 5) | (5, 6) => {
221                // R5/R6: SHA-256 based (ISO 32000-2)
222                let oe_bytes = get_string_bytes(encrypt_dict, store, &Name::oe())?;
223                let ue_bytes = get_string_bytes(encrypt_dict, store, &Name::ue())?;
224
225                let key = if r == 5 {
226                    derive_key_r5(password, &u_bytes, &ue_bytes, &o_bytes, &oe_bytes)?
227                } else {
228                    derive_key_r6(password, &u_bytes, &ue_bytes, &o_bytes, &oe_bytes)?
229                };
230
231                match key {
232                    Some(k) => {
233                        // P2-33: Validate /Perms entry (R5/R6).
234                        //
235                        // Upstream deviation — /Perms is required:
236                        //
237                        // Upstream PDFium (`AES256_CheckPassword`) returns `false`
238                        // when `/Perms` is missing or empty. We match that behavior
239                        // by requiring the entry to be present and at least 16 bytes.
240                        // The previous implementation silently skipped validation
241                        // when `/Perms` was absent or malformed, which could allow
242                        // an attacker to strip `/Perms` and tamper with the `/P`
243                        // permission bits without detection.
244                        let perms_bytes = get_string_bytes(encrypt_dict, store, &Name::perms())?;
245                        if perms_bytes.len() < 16 {
246                            return Err(SecurityError::MissingKey(
247                                "Perms (too short, need ≥16 bytes)".into(),
248                            ));
249                        }
250                        let decrypted = crypto::aes256_ecb_decrypt_block(&k, &perms_bytes[..16])?;
251                        // Verify bytes 9-11 = "adb" (PDF spec Algorithm 2.A step 2).
252                        // Byte 8 is the EncryptMetadata flag ('T'/'F'), checked
253                        // separately. Upstream PDFium checks only bytes 9-11.
254                        if &decrypted[9..12] != b"adb" {
255                            return Err(SecurityError::InvalidPassword);
256                        }
257                        // Verify bytes 0-3 match /P as LE i32
258                        let perms_p = i32::from_le_bytes([
259                            decrypted[0],
260                            decrypted[1],
261                            decrypted[2],
262                            decrypted[3],
263                        ]);
264                        if perms_p != p as i32 {
265                            return Err(SecurityError::InvalidPassword);
266                        }
267
268                        Ok(Self {
269                            revision: r,
270                            key_length_bytes: 32,
271                            encryption_key: k,
272                            permissions: Permissions::from_bits(p as i32),
273                            encrypt_metadata,
274                            stream_cf: CryptFilterMethod::Aesv3,
275                            string_cf: CryptFilterMethod::Aesv3,
276                            o_value: o_bytes,
277                            u_value: u_bytes,
278                            encoded_password: password.as_bytes().to_vec(),
279                        })
280                    }
281                    None => Err(SecurityError::InvalidPassword),
282                }
283            }
284            _ => Err(SecurityError::UnsupportedVersion(v, r)),
285        }
286    }
287
288    /// Return the document access permissions stored in the `/P` entry.
289    pub fn permissions(&self) -> Permissions {
290        self.permissions
291    }
292
293    /// Upstream-aligned alias for [`Self::permissions()`].
294    ///
295    /// Corresponds to `CPDF_SecurityHandler::GetPermissions()` in PDFium.
296    #[inline]
297    pub fn get_permissions(&self) -> Permissions {
298        self.permissions()
299    }
300
301    /// Returns the successfully-verified encoded password bytes.
302    ///
303    /// For R2–R4, this is the Latin-1 encoded user password (or the recovered
304    /// user password when authenticated as owner). For R5/R6, this is the
305    /// UTF-8 encoded password. Useful for incremental save with the same password.
306    ///
307    /// Corresponds to `CPDF_SecurityHandler::GetEncodedPassword()` in PDFium.
308    pub fn encoded_password(&self) -> &[u8] {
309        &self.encoded_password
310    }
311
312    /// ADR-019 alias for [`encoded_password()`](Self::encoded_password).
313    ///
314    /// Corresponds to `CPDF_SecurityHandler::GetEncodedPassword()` in PDFium.
315    #[inline]
316    pub fn get_encoded_password(&self) -> &[u8] {
317        self.encoded_password()
318    }
319
320    /// Decrypt a string value belonging to the given object.
321    pub fn decrypt_string(&self, data: &[u8], obj_id: ObjectId) -> Vec<u8> {
322        if self.string_cf == CryptFilterMethod::None {
323            return data.to_vec();
324        }
325
326        if self.revision >= 5 {
327            // R5/R6: AES-256 with the file encryption key directly
328            crypto::aes256_cbc_decrypt(&self.encryption_key, data).unwrap_or_else(|_| data.to_vec())
329        } else {
330            let object_key = self.compute_object_key(obj_id, self.string_cf);
331            match self.string_cf {
332                CryptFilterMethod::V2 => {
333                    crypto::rc4_crypt(&object_key, data).unwrap_or_else(|_| data.to_vec())
334                }
335                CryptFilterMethod::Aesv2 => {
336                    crypto::aes128_cbc_decrypt(&object_key, data).unwrap_or_else(|_| data.to_vec())
337                }
338                CryptFilterMethod::Aesv3 => crypto::aes256_cbc_decrypt(&self.encryption_key, data)
339                    .unwrap_or_else(|_| data.to_vec()),
340                CryptFilterMethod::None => data.to_vec(),
341            }
342        }
343    }
344
345    /// Decrypt stream data belonging to the given object.
346    pub fn decrypt_stream(&self, data: &[u8], obj_id: ObjectId) -> Vec<u8> {
347        if self.stream_cf == CryptFilterMethod::None {
348            return data.to_vec();
349        }
350
351        if self.revision >= 5 {
352            crypto::aes256_cbc_decrypt(&self.encryption_key, data).unwrap_or_else(|_| data.to_vec())
353        } else {
354            let object_key = self.compute_object_key(obj_id, self.stream_cf);
355            match self.stream_cf {
356                CryptFilterMethod::V2 => {
357                    crypto::rc4_crypt(&object_key, data).unwrap_or_else(|_| data.to_vec())
358                }
359                CryptFilterMethod::Aesv2 => {
360                    crypto::aes128_cbc_decrypt(&object_key, data).unwrap_or_else(|_| data.to_vec())
361                }
362                CryptFilterMethod::Aesv3 => crypto::aes256_cbc_decrypt(&self.encryption_key, data)
363                    .unwrap_or_else(|_| data.to_vec()),
364                CryptFilterMethod::None => data.to_vec(),
365            }
366        }
367    }
368
369    /// Returns `true` if metadata streams are encrypted.
370    ///
371    /// Corresponds to `CPDF_SecurityHandler::IsMetadataEncrypted()` in PDFium.
372    pub fn is_metadata_encrypted(&self) -> bool {
373        self.encrypt_metadata
374    }
375
376    /// The encryption revision number.
377    pub fn revision(&self) -> u32 {
378        self.revision
379    }
380
381    /// The crypt filter method in use (returns stream filter for backwards compat).
382    pub fn crypt_filter_method(&self) -> CryptFilterMethod {
383        self.stream_cf
384    }
385
386    /// The encryption key length in bytes.
387    pub fn key_length_bytes(&self) -> usize {
388        self.key_length_bytes
389    }
390
391    /// The raw encryption key.
392    pub fn encryption_key(&self) -> &[u8] {
393        &self.encryption_key
394    }
395
396    /// The stream crypt filter method.
397    pub fn stream_crypt_filter(&self) -> CryptFilterMethod {
398        self.stream_cf
399    }
400
401    /// The string crypt filter method.
402    pub fn string_crypt_filter(&self) -> CryptFilterMethod {
403        self.string_cf
404    }
405
406    /// The /O value (owner hash) from the encrypt dictionary.
407    pub fn o_value(&self) -> &[u8] {
408        &self.o_value
409    }
410
411    /// The /U value (user hash) from the encrypt dictionary.
412    pub fn u_value(&self) -> &[u8] {
413        &self.u_value
414    }
415
416    /// The encryption version number (V value).
417    ///
418    /// Derived from the revision: R2→V1, R3/R4→V2..V4, R5/R6→V5.
419    pub fn version(&self) -> u32 {
420        match self.revision {
421            2 => 1,
422            3 | 4 => {
423                if self.stream_cf == CryptFilterMethod::V2
424                    && self.string_cf == CryptFilterMethod::V2
425                {
426                    if self.key_length_bytes == 5 { 1 } else { 2 }
427                } else {
428                    4
429                }
430            }
431            5 | 6 => 5,
432            _ => 0,
433        }
434    }
435
436    /// Encrypt a string value belonging to the given object.
437    ///
438    /// RC4 is symmetric (encrypt = decrypt). For AES, a zero IV is used
439    /// and prepended to the ciphertext (matching PDF convention where the
440    /// first 16 bytes of the encrypted data are the IV).
441    pub fn encrypt_string(&self, data: &[u8], obj_id: ObjectId) -> Vec<u8> {
442        if self.string_cf == CryptFilterMethod::None {
443            return data.to_vec();
444        }
445        self.encrypt_with_method(data, obj_id, self.string_cf)
446    }
447
448    /// Encrypt stream data belonging to the given object.
449    pub fn encrypt_stream(&self, data: &[u8], obj_id: ObjectId) -> Vec<u8> {
450        if self.stream_cf == CryptFilterMethod::None {
451            return data.to_vec();
452        }
453        self.encrypt_with_method(data, obj_id, self.stream_cf)
454    }
455
456    /// Encrypt data with the specified crypt filter method.
457    ///
458    /// # Panics
459    ///
460    /// Panics if AES encryption fails due to an invalid key length.
461    /// Key sizes are validated in [`SecurityHandler::from_encrypt_dict`], so
462    /// this should never happen in normal operation.
463    ///
464    /// # Upstream deviation — error handling
465    ///
466    /// Upstream PDFium (`CPDF_CryptoHandler::EncryptContent`) does not handle
467    /// encryption failure at this level — the C++ code assumes valid key sizes
468    /// and cannot return an error. Our previous implementation silently returned
469    /// plaintext on failure (`Err(_) => data.to_vec()`), which could expose
470    /// confidential data in the output PDF without any warning. We now panic
471    /// instead, since key sizes are validated at construction time and an error
472    /// here indicates a programming bug, not a recoverable condition.
473    fn encrypt_with_method(&self, data: &[u8], obj_id: ObjectId, cf: CryptFilterMethod) -> Vec<u8> {
474        let iv = Self::generate_iv();
475
476        if self.revision >= 5 {
477            let encrypted = crypto::aes256_cbc_encrypt(&self.encryption_key, &iv, data)
478                .expect("AES-256 encryption failed: key size validated at construction");
479            let mut result = iv.to_vec();
480            result.extend_from_slice(&encrypted);
481            result
482        } else {
483            let object_key = self.compute_object_key(obj_id, cf);
484            match cf {
485                CryptFilterMethod::V2 => crypto::rc4_crypt(&object_key, data)
486                    .expect("RC4 encryption failed: key size validated at construction"),
487                CryptFilterMethod::Aesv2 => {
488                    let encrypted = crypto::aes128_cbc_encrypt(&object_key, &iv, data)
489                        .expect("AES-128 encryption failed: key size validated at construction");
490                    let mut result = iv.to_vec();
491                    result.extend_from_slice(&encrypted);
492                    result
493                }
494                CryptFilterMethod::Aesv3 => {
495                    let encrypted = crypto::aes256_cbc_encrypt(&self.encryption_key, &iv, data)
496                        .expect("AES-256 encryption failed: key size validated at construction");
497                    let mut result = iv.to_vec();
498                    result.extend_from_slice(&encrypted);
499                    result
500                }
501                CryptFilterMethod::None => data.to_vec(),
502            }
503        }
504    }
505
506    /// Generate a cryptographically random 16-byte IV for AES encryption.
507    ///
508    /// # Upstream deviation — IV generation
509    ///
510    /// Upstream PDFium uses C `rand()` (seeded from system state) to fill IVs.
511    /// We use `getrandom` (OS CSPRNG) instead for stronger cryptographic
512    /// guarantees: upstream's `rand()` is not a CSPRNG and its output quality
513    /// varies by platform. Using a true CSPRNG ensures IVs are unpredictable
514    /// across process invocations, which is required for AES-CBC semantic
515    /// security (IND-CPA).
516    fn generate_iv() -> [u8; 16] {
517        let mut iv = [0u8; 16];
518        getrandom::fill(&mut iv).expect("OS RNG unavailable");
519        iv
520    }
521
522    /// Compute the per-object encryption key (Algorithm 1 in PDF spec, R2–R4).
523    ///
524    /// 1. Start with the file encryption key.
525    /// 2. Append the low 3 bytes of the object number (LE).
526    /// 3. Append the low 2 bytes of the generation number (LE).
527    /// 4. If AES (AESV2): append `sAlT` bytes.
528    /// 5. MD5 hash, take first min(key_length + 5, 16) bytes.
529    fn compute_object_key(&self, obj_id: ObjectId, cf: CryptFilterMethod) -> Vec<u8> {
530        let obj_num = obj_id.number;
531        let gen_num = obj_id.generation;
532
533        let mut buf = Vec::with_capacity(self.encryption_key.len() + 9);
534        buf.extend_from_slice(&self.encryption_key);
535        // Object number: 3 bytes LE
536        buf.push((obj_num & 0xFF) as u8);
537        buf.push(((obj_num >> 8) & 0xFF) as u8);
538        buf.push(((obj_num >> 16) & 0xFF) as u8);
539        // Generation number: 2 bytes LE
540        buf.push((gen_num & 0xFF) as u8);
541        buf.push(((gen_num >> 8) & 0xFF) as u8);
542
543        if cf == CryptFilterMethod::Aesv2 {
544            // "sAlT" = 0x73 0x41 0x6C 0x54
545            buf.extend_from_slice(&[0x73, 0x41, 0x6C, 0x54]);
546        }
547
548        let hash = crypto::md5(&buf);
549        let n = std::cmp::min(self.key_length_bytes + 5, 16);
550        hash[..n].to_vec()
551    }
552}
553
554// ---------------------------------------------------------------------------
555// Key derivation (R2–R4) — Algorithm 2
556// ---------------------------------------------------------------------------
557
558/// Encode a UTF-8 password to ISO 8859-1 (Latin-1) for R2-R4 compatibility.
559/// Characters outside the Latin-1 range (>255) are replaced with `?` (0x3F).
560fn encode_password_latin1(password: &str) -> Vec<u8> {
561    password
562        .chars()
563        .map(|c| {
564            let code = c as u32;
565            if code <= 255 {
566                code as u8
567            } else {
568                0x3F // '?'
569            }
570        })
571        .collect()
572}
573
574/// Pad or truncate a password to exactly 32 bytes using PASSWORD_PADDING.
575fn pad_password(password: &[u8]) -> [u8; 32] {
576    let mut padded = [0u8; 32];
577    let copy_len = std::cmp::min(password.len(), 32);
578    padded[..copy_len].copy_from_slice(&password[..copy_len]);
579    if copy_len < 32 {
580        padded[copy_len..].copy_from_slice(&PASSWORD_PADDING[..(32 - copy_len)]);
581    }
582    padded
583}
584
585/// Derive the file encryption key for R2–R4 (PDF spec Algorithm 2).
586fn derive_key_r2_r4(
587    password: &[u8],
588    o_value: &[u8],
589    permissions: i32,
590    file_id: &[u8],
591    revision: u32,
592    key_length_bytes: usize,
593    encrypt_metadata: bool,
594) -> Vec<u8> {
595    let padded = pad_password(password);
596
597    let mut buf = Vec::with_capacity(32 + o_value.len() + 4 + file_id.len() + 4);
598    buf.extend_from_slice(&padded);
599    buf.extend_from_slice(o_value);
600    buf.extend_from_slice(&(permissions as u32).to_le_bytes());
601    buf.extend_from_slice(file_id);
602
603    if revision >= 4 && !encrypt_metadata {
604        buf.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
605    }
606
607    let mut hash = crypto::md5(&buf);
608
609    if revision >= 3 {
610        for _ in 0..50 {
611            hash = crypto::md5(&hash[..key_length_bytes]);
612        }
613    }
614
615    hash[..key_length_bytes].to_vec()
616}
617
618// ---------------------------------------------------------------------------
619// Password verification (R2) — Algorithm 6
620// ---------------------------------------------------------------------------
621
622/// Verify user password for R2 (Algorithm 6).
623fn verify_user_password_r2(key: &[u8], u_value: &[u8]) -> bool {
624    let Ok(decrypted) = crypto::rc4_crypt(key, u_value) else {
625        return false;
626    };
627    let expected = crypto::md5(&PASSWORD_PADDING);
628    decrypted[..] == expected[..]
629}
630
631// ---------------------------------------------------------------------------
632// Password verification (R3–R4) — Algorithm 7
633// ---------------------------------------------------------------------------
634
635/// Verify user password for R3/R4 (Algorithm 7).
636fn verify_user_password_r3_r4(key: &[u8], u_value: &[u8], file_id: &[u8]) -> bool {
637    // Step a: MD5(PASSWORD_PADDING + file_id)
638    let mut buf = Vec::with_capacity(32 + file_id.len());
639    buf.extend_from_slice(&PASSWORD_PADDING);
640    buf.extend_from_slice(file_id);
641    let hash = crypto::md5(&buf);
642
643    // Step b: RC4 with key
644    let Ok(mut result) = crypto::rc4_crypt(key, &hash) else {
645        return false;
646    };
647
648    // Step c: 19 rounds of RC4 with modified keys
649    for i in 1..=19u8 {
650        let modified_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
651        let Ok(next) = crypto::rc4_crypt(&modified_key, &result) else {
652            return false;
653        };
654        result = next;
655    }
656
657    // Compare first 16 bytes
658    u_value.len() >= 16 && result.len() >= 16 && result[..16] == u_value[..16]
659}
660
661/// Dispatch to the correct password verification for R2–R4.
662fn verify_user_password_r2_r4(key: &[u8], u_value: &[u8], revision: u32, file_id: &[u8]) -> bool {
663    if revision == 2 {
664        verify_user_password_r2(key, u_value)
665    } else {
666        verify_user_password_r3_r4(key, u_value, file_id)
667    }
668}
669
670// ---------------------------------------------------------------------------
671// Owner password recovery (R2–R4)
672// ---------------------------------------------------------------------------
673
674/// Recover the user password from the O value using the owner password.
675fn recover_user_password_from_owner(
676    owner_password: &[u8],
677    o_value: &[u8],
678    revision: u32,
679    key_length_bytes: usize,
680) -> Vec<u8> {
681    let padded = pad_password(owner_password);
682    let mut hash = crypto::md5(&padded);
683
684    if revision >= 3 {
685        for _ in 0..50 {
686            hash = crypto::md5(&hash[..key_length_bytes]);
687        }
688    }
689
690    let key = &hash[..key_length_bytes];
691
692    if revision == 2 {
693        crypto::rc4_crypt(key, o_value)
694            .expect("RC4 key size validated from MD5 hash (always 5-16 bytes)")
695    } else {
696        let mut result = o_value.to_vec();
697        for i in (0..=19u8).rev() {
698            let modified_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
699            result = crypto::rc4_crypt(&modified_key, &result)
700                .expect("RC4 key size validated from MD5 hash (always 5-16 bytes)");
701        }
702        result
703    }
704}
705
706// ---------------------------------------------------------------------------
707// R5/R6 key derivation (ISO 32000-2)
708// ---------------------------------------------------------------------------
709
710/// Derive the file encryption key for R5 (deprecated draft, Adobe extension).
711///
712/// Returns `Some(key)` if either user or owner password matches, else `None`.
713fn derive_key_r5(
714    password: &str,
715    u_value: &[u8],
716    ue_value: &[u8],
717    o_value: &[u8],
718    oe_value: &[u8],
719) -> Result<Option<Vec<u8>>, CryptoError> {
720    let pwd = truncate_utf8_password(password);
721
722    // Try user password: hash(password + Validation Salt from U)
723    if u_value.len() >= 48 && ue_value.len() >= 32 {
724        let validation_salt = &u_value[32..40];
725        let hash = compute_r5_hash(&pwd, validation_salt);
726        if hash[..] == u_value[..32] {
727            // Decrypt UE with hash(password + Key Salt from U)
728            let key_salt = &u_value[40..48];
729            let key_hash = compute_r5_hash(&pwd, key_salt);
730            let file_key = crypto::aes256_cbc_decrypt_no_padding(&key_hash, &[0u8; 16], ue_value)?;
731            return Ok(Some(file_key));
732        }
733    }
734
735    // Try owner password: hash(password + Validation Salt from O + U[0..48])
736    if o_value.len() >= 48 && oe_value.len() >= 32 && u_value.len() >= 48 {
737        let validation_salt = &o_value[32..40];
738        let mut input = Vec::with_capacity(pwd.len() + 8 + 48);
739        input.extend_from_slice(&pwd);
740        input.extend_from_slice(validation_salt);
741        input.extend_from_slice(&u_value[..48]);
742        let hash = crypto::sha256(&input);
743        if hash[..] == o_value[..32] {
744            let key_salt = &o_value[40..48];
745            let mut key_input = Vec::with_capacity(pwd.len() + 8 + 48);
746            key_input.extend_from_slice(&pwd);
747            key_input.extend_from_slice(key_salt);
748            key_input.extend_from_slice(&u_value[..48]);
749            let key_hash = crypto::sha256(&key_input);
750            let file_key = crypto::aes256_cbc_decrypt_no_padding(&key_hash, &[0u8; 16], oe_value)?;
751            return Ok(Some(file_key));
752        }
753    }
754
755    Ok(None)
756}
757
758/// Derive the file encryption key for R6 (ISO 32000-2, Algorithm 2.A).
759///
760/// R6 uses Algorithm 2.B (iterative hash) instead of simple SHA-256.
761fn derive_key_r6(
762    password: &str,
763    u_value: &[u8],
764    ue_value: &[u8],
765    o_value: &[u8],
766    oe_value: &[u8],
767) -> Result<Option<Vec<u8>>, CryptoError> {
768    let pwd = truncate_utf8_password(password);
769
770    // Try user password: compute_r6_hash(password, validation_salt, "")
771    if u_value.len() >= 48 && ue_value.len() >= 32 {
772        let validation_salt = &u_value[32..40];
773        let hash = compute_r6_hash(&pwd, validation_salt, &[]);
774        if hash[..] == u_value[..32] {
775            // Decrypt UE with hash(password, key_salt, "")
776            let key_salt = &u_value[40..48];
777            let key_hash = compute_r6_hash(&pwd, key_salt, &[]);
778            let file_key = crypto::aes256_cbc_decrypt_no_padding(&key_hash, &[0u8; 16], ue_value)?;
779            return Ok(Some(file_key));
780        }
781    }
782
783    // Try owner password: compute_r6_hash(password, validation_salt, U[0..48])
784    if o_value.len() >= 48 && oe_value.len() >= 32 && u_value.len() >= 48 {
785        let validation_salt = &o_value[32..40];
786        let hash = compute_r6_hash(&pwd, validation_salt, &u_value[..48]);
787        if hash[..] == o_value[..32] {
788            let key_salt = &o_value[40..48];
789            let key_hash = compute_r6_hash(&pwd, key_salt, &u_value[..48]);
790            let file_key = crypto::aes256_cbc_decrypt_no_padding(&key_hash, &[0u8; 16], oe_value)?;
791            return Ok(Some(file_key));
792        }
793    }
794
795    Ok(None)
796}
797
798/// Interpret the first 16 bytes of `bytes` as a big-endian 128-bit integer and return it mod 3.
799fn big_endian_mod3(bytes: &[u8]) -> u32 {
800    bytes
801        .iter()
802        .take(16)
803        .fold(0u32, |acc, &b| (acc * 256 + b as u32) % 3)
804}
805
806/// ISO 32000-2 Algorithm 2.B — compute hash for R6 encryption.
807///
808/// Iterative hash using SHA-256/384/512 and AES-128-CBC.  Mirrors PDFium's
809/// `Revision6_Hash` exactly:
810///
811/// * K is kept at its **full** hash output width (32 / 48 / 64 bytes) so that
812///   the next round's K1 uses the correct number of bytes.  Previously K was
813///   always truncated to 32 bytes, which diverged from every other compliant
814///   implementation.
815/// * The AES-128-CBC step uses **no padding**.  K1 is always a multiple of
816///   16 bytes (`(pwd + block + u) * 64`), so no padding block should be
817///   appended.  Previously PKCS#7 padding added an extra 16-byte block,
818///   corrupting the subsequent hash and causing `IncorrectPassword` even for
819///   plain ASCII passwords.
820/// * The exit condition increments `i` before checking, matching PDFium's
821///   `do { ++i; } while (i < 64 || i−32 < E.back())`.
822fn compute_r6_hash(password: &[u8], salt: &[u8], u_value: &[u8]) -> [u8; 32] {
823    // Step a: K = SHA-256(password || salt || u_value)
824    let mut input = Vec::with_capacity(password.len() + salt.len() + u_value.len());
825    input.extend_from_slice(password);
826    input.extend_from_slice(salt);
827    input.extend_from_slice(u_value);
828    // K starts as 32 bytes and may grow to 48 or 64 bytes in subsequent rounds.
829    let mut k: Vec<u8> = crypto::sha256(&input).to_vec();
830
831    let mut i: u32 = 0;
832    loop {
833        // Step b: K1 = (password || K || u_value) repeated 64 times.
834        // K may be 32, 48, or 64 bytes depending on the previous round's hash selection.
835        let seq_len = password.len() + k.len() + u_value.len();
836        let mut k1 = Vec::with_capacity(seq_len * 64);
837        for _ in 0..64 {
838            k1.extend_from_slice(password);
839            k1.extend_from_slice(&k);
840            k1.extend_from_slice(u_value);
841        }
842
843        // Step c: E = AES-128-CBC(K[0..16], K[16..32], K1) — no padding.
844        // k1.len() = seq_len * 64 is always divisible by 16, so NoPadding is correct.
845        let aes_key = &k[..16];
846        let aes_iv = &k[16..32];
847        let e = crypto::aes128_cbc_encrypt_no_padding(aes_key, aes_iv, &k1)
848            .expect("K1 is block-aligned; key and IV derived from prior hash are always valid");
849
850        // Step d: select hash based on big-endian value of E[0..16] mod 3.
851        let hash_select = big_endian_mod3(&e);
852
853        // Step e: K = full output of selected hash (32, 48, or 64 bytes).
854        k = match hash_select {
855            0 => crypto::sha256(&e).to_vec(),
856            1 => crypto::sha384(&e).to_vec(),
857            _ => crypto::sha512(&e).to_vec(),
858        };
859
860        // Step f: increment i, then exit when i >= 64 and last byte of E <= i − 32.
861        // Mirrors PDFium: `do { …; ++i; } while (i < 64 || i−32 < E.back())`.
862        i += 1;
863        if i >= 64 && (*e.last().unwrap_or(&0) as u32) <= i - 32 {
864            break;
865        }
866    }
867
868    // Return first 32 bytes of K (K may be 32, 48, or 64 bytes at this point).
869    let mut result = [0u8; 32];
870    result.copy_from_slice(&k[..32]);
871    result
872}
873
874/// Compute R5 hash: SHA-256(password + salt).
875fn compute_r5_hash(password: &[u8], salt: &[u8]) -> [u8; 32] {
876    let mut input = Vec::with_capacity(password.len() + salt.len());
877    input.extend_from_slice(password);
878    input.extend_from_slice(salt);
879    crypto::sha256(&input)
880}
881
882/// Truncate a UTF-8 password to at most 127 bytes (R5/R6 spec).
883fn truncate_utf8_password(password: &str) -> Vec<u8> {
884    let bytes = password.as_bytes();
885    if bytes.len() <= 127 {
886        bytes.to_vec()
887    } else {
888        bytes[..127].to_vec()
889    }
890}
891
892// ---------------------------------------------------------------------------
893// Crypt filter method parsing
894// ---------------------------------------------------------------------------
895
896/// Parse a single crypt filter from the encryption dictionary by filter key name
897/// (e.g. /StmF or /StrF). Looks up the named filter in /CF to determine the method.
898fn parse_single_crypt_filter<S: PdfSource>(
899    encrypt_dict: &HashMap<Name, Object>,
900    store: &ObjectStore<S>,
901    filter_key: &Name,
902) -> CryptFilterMethod {
903    let filter_name = encrypt_dict
904        .get(filter_key)
905        .and_then(|obj| store.deep_resolve(obj).ok())
906        .and_then(|obj| obj.as_name())
907        .cloned()
908        .unwrap_or_else(|| Name::from("StdCF"));
909
910    if filter_name == Name::from("Identity") {
911        return CryptFilterMethod::None;
912    }
913
914    // Look up the filter in /CF dict
915    let cf_dict = encrypt_dict
916        .get(&Name::cf())
917        .and_then(|obj| store.deep_resolve(obj).ok())
918        .and_then(|obj| obj.as_dict());
919
920    if let Some(cf) = cf_dict {
921        if let Some(filter_obj) = cf.get(&filter_name) {
922            if let Ok(filter_resolved) = store.deep_resolve(filter_obj) {
923                if let Some(filter_dict) = filter_resolved.as_dict() {
924                    let method = filter_dict
925                        .get(&Name::cfm())
926                        .and_then(|obj| store.deep_resolve(obj).ok())
927                        .and_then(|obj| obj.as_name())
928                        .cloned();
929
930                    if let Some(m) = method {
931                        if m == Name::aesv2() {
932                            return CryptFilterMethod::Aesv2;
933                        } else if m == Name::aesv3() {
934                            return CryptFilterMethod::Aesv3;
935                        } else if m == Name::v2() {
936                            return CryptFilterMethod::V2;
937                        } else if m == Name::none() {
938                            return CryptFilterMethod::None;
939                        }
940                    }
941                }
942            }
943        }
944    }
945
946    // Default: RC4 for V4
947    CryptFilterMethod::V2
948}
949
950// ---------------------------------------------------------------------------
951// Dictionary helpers
952// ---------------------------------------------------------------------------
953
954fn get_int<S: PdfSource>(
955    dict: &HashMap<Name, Object>,
956    store: &ObjectStore<S>,
957    key: &Name,
958) -> Result<i64, SecurityError> {
959    dict.get(key)
960        .and_then(|obj| store.deep_resolve(obj).ok())
961        .and_then(|obj| obj.as_i64())
962        .ok_or_else(|| SecurityError::MissingKey(key.to_string()))
963}
964
965fn get_optional_int<S: PdfSource>(
966    dict: &HashMap<Name, Object>,
967    store: &ObjectStore<S>,
968    key: &Name,
969) -> Option<i64> {
970    dict.get(key)
971        .and_then(|obj| store.deep_resolve(obj).ok())
972        .and_then(|obj| obj.as_i64())
973}
974
975fn get_optional_bool<S: PdfSource>(
976    dict: &HashMap<Name, Object>,
977    store: &ObjectStore<S>,
978    key: &Name,
979) -> Option<bool> {
980    dict.get(key)
981        .and_then(|obj| store.deep_resolve(obj).ok())
982        .and_then(|obj| obj.as_bool())
983}
984
985fn get_string_bytes<S: PdfSource>(
986    dict: &HashMap<Name, Object>,
987    store: &ObjectStore<S>,
988    key: &Name,
989) -> Result<Vec<u8>, SecurityError> {
990    dict.get(key)
991        .and_then(|obj| store.deep_resolve(obj).ok())
992        .and_then(|obj| obj.as_string())
993        .map(|s| s.as_bytes().to_vec())
994        .ok_or_else(|| SecurityError::MissingKey(key.to_string()))
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000
1001    #[test]
1002    fn test_password_padding_empty() {
1003        let padded = pad_password(b"");
1004        assert_eq!(padded, PASSWORD_PADDING);
1005    }
1006
1007    #[test]
1008    fn test_password_padding_short() {
1009        let padded = pad_password(b"test");
1010        assert_eq!(&padded[..4], b"test");
1011        assert_eq!(&padded[4..], &PASSWORD_PADDING[..28]);
1012    }
1013
1014    #[test]
1015    fn test_password_padding_exact_32() {
1016        let pwd = b"abcdefghijklmnopqrstuvwxyz012345"; // 32 bytes
1017        let padded = pad_password(pwd);
1018        assert_eq!(&padded[..], &pwd[..]);
1019    }
1020
1021    #[test]
1022    fn test_password_padding_longer_than_32() {
1023        let pwd = b"abcdefghijklmnopqrstuvwxyz0123456789"; // 36 bytes
1024        let padded = pad_password(pwd);
1025        assert_eq!(&padded[..], &pwd[..32]);
1026    }
1027
1028    #[test]
1029    fn test_key_derivation_r2_known() {
1030        // Use empty password, known O value, P=-44, known file_id
1031        let o_value = [0u8; 32];
1032        let file_id = b"test_file_id_123";
1033        let key = derive_key_r2_r4(b"", &o_value, -44, file_id, 2, 5, true);
1034        assert_eq!(key.len(), 5);
1035        // Verify the key is deterministic
1036        let key2 = derive_key_r2_r4(b"", &o_value, -44, file_id, 2, 5, true);
1037        assert_eq!(key, key2);
1038    }
1039
1040    #[test]
1041    fn test_key_derivation_r3_known() {
1042        let o_value = [0u8; 32];
1043        let file_id = b"test_file_id_123";
1044        let key = derive_key_r2_r4(b"", &o_value, -44, file_id, 3, 16, true);
1045        assert_eq!(key.len(), 16);
1046        // R3 applies 50 additional MD5 rounds — key should differ from R2
1047        let key_r2 = derive_key_r2_r4(b"", &o_value, -44, file_id, 2, 5, true);
1048        assert_ne!(key[..5], key_r2[..]);
1049    }
1050
1051    #[test]
1052    fn test_user_password_verification_r2_round_trip() {
1053        // Derive a key, compute the expected U value, then verify
1054        let o_value = [0x42u8; 32];
1055        let file_id = b"abcdef0123456789";
1056        let key = derive_key_r2_r4(b"password", &o_value, -4, file_id, 2, 5, true);
1057
1058        // Compute U: RC4-encrypt MD5(PASSWORD_PADDING) with key
1059        let expected_u = crypto::rc4_crypt(&key, &crypto::md5(&PASSWORD_PADDING)).unwrap();
1060        assert!(verify_user_password_r2(&key, &expected_u));
1061    }
1062
1063    #[test]
1064    fn test_user_password_verification_r2_wrong_password() {
1065        let o_value = [0x42u8; 32];
1066        let file_id = b"abcdef0123456789";
1067        let key = derive_key_r2_r4(b"password", &o_value, -4, file_id, 2, 5, true);
1068        let expected_u = crypto::rc4_crypt(&key, &crypto::md5(&PASSWORD_PADDING)).unwrap();
1069
1070        // Wrong password yields different key
1071        let wrong_key = derive_key_r2_r4(b"wrong", &o_value, -4, file_id, 2, 5, true);
1072        assert!(!verify_user_password_r2(&wrong_key, &expected_u));
1073    }
1074
1075    #[test]
1076    fn test_user_password_verification_r3_round_trip() {
1077        let o_value = [0x42u8; 32];
1078        let file_id = b"abcdef0123456789";
1079        let key = derive_key_r2_r4(b"pass", &o_value, -4, file_id, 3, 16, true);
1080
1081        // Compute U for R3: MD5(PADDING + file_id), then 20 rounds of RC4
1082        let mut buf = Vec::new();
1083        buf.extend_from_slice(&PASSWORD_PADDING);
1084        buf.extend_from_slice(file_id);
1085        let hash = crypto::md5(&buf);
1086
1087        let mut result = crypto::rc4_crypt(&key, &hash).unwrap();
1088        for i in 1..=19u8 {
1089            let modified_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1090            result = crypto::rc4_crypt(&modified_key, &result).unwrap();
1091        }
1092        // Pad U to 32 bytes (remaining bytes are random, but first 16 matter)
1093        let mut u_value = result;
1094        u_value.resize(32, 0);
1095
1096        assert!(verify_user_password_r3_r4(&key, &u_value, file_id));
1097    }
1098
1099    #[test]
1100    fn test_object_key_computation() {
1101        let handler = SecurityHandler {
1102            revision: 3,
1103            key_length_bytes: 5,
1104            encryption_key: vec![0x01, 0x02, 0x03, 0x04, 0x05],
1105            permissions: Permissions::from_bits(-4),
1106            encrypt_metadata: true,
1107            stream_cf: CryptFilterMethod::V2,
1108            string_cf: CryptFilterMethod::V2,
1109            o_value: Vec::new(),
1110            u_value: Vec::new(),
1111            encoded_password: Vec::new(),
1112        };
1113
1114        let obj_id = ObjectId::new(10, 0);
1115        let key = handler.compute_object_key(obj_id, CryptFilterMethod::V2);
1116
1117        // key_length + 5 = 10, but min(10, 16) = 10
1118        assert_eq!(key.len(), 10);
1119
1120        // Verify deterministic
1121        let key2 = handler.compute_object_key(obj_id, CryptFilterMethod::V2);
1122        assert_eq!(key, key2);
1123
1124        // Different object ID should yield different key
1125        let key3 = handler.compute_object_key(ObjectId::new(11, 0), CryptFilterMethod::V2);
1126        assert_ne!(key, key3);
1127    }
1128
1129    #[test]
1130    fn test_object_key_aesv2_includes_salt() {
1131        let handler = SecurityHandler {
1132            revision: 4,
1133            key_length_bytes: 16,
1134            encryption_key: vec![0xAA; 16],
1135            permissions: Permissions::from_bits(-4),
1136            encrypt_metadata: true,
1137            stream_cf: CryptFilterMethod::Aesv2,
1138            string_cf: CryptFilterMethod::V2,
1139            o_value: Vec::new(),
1140            u_value: Vec::new(),
1141            encoded_password: Vec::new(),
1142        };
1143
1144        let obj_id = ObjectId::new(1, 0);
1145        let key_rc4 = handler.compute_object_key(obj_id, CryptFilterMethod::V2);
1146        let key_aes = handler.compute_object_key(obj_id, CryptFilterMethod::Aesv2);
1147
1148        // AES key should differ because of "sAlT" suffix
1149        assert_ne!(key_rc4, key_aes);
1150    }
1151
1152    #[test]
1153    fn test_decrypt_string_rc4_round_trip() {
1154        let handler = SecurityHandler {
1155            revision: 3,
1156            key_length_bytes: 5,
1157            encryption_key: vec![0x01, 0x02, 0x03, 0x04, 0x05],
1158            permissions: Permissions::from_bits(-4),
1159            encrypt_metadata: true,
1160            stream_cf: CryptFilterMethod::V2,
1161            string_cf: CryptFilterMethod::V2,
1162            o_value: Vec::new(),
1163            u_value: Vec::new(),
1164            encoded_password: Vec::new(),
1165        };
1166
1167        let obj_id = ObjectId::new(1, 0);
1168        let plaintext = b"Hello, encrypted world!";
1169
1170        // Encrypt (RC4 is symmetric, so encrypt = decrypt)
1171        let object_key = handler.compute_object_key(obj_id, handler.string_cf);
1172        let encrypted = crypto::rc4_crypt(&object_key, plaintext).unwrap();
1173
1174        // Decrypt via handler
1175        let decrypted = handler.decrypt_string(&encrypted, obj_id);
1176        assert_eq!(decrypted, plaintext);
1177    }
1178
1179    #[test]
1180    fn test_crypt_filter_method_none_passthrough() {
1181        let handler = SecurityHandler {
1182            revision: 4,
1183            key_length_bytes: 16,
1184            encryption_key: vec![0; 16],
1185            permissions: Permissions::from_bits(-4),
1186            encrypt_metadata: true,
1187            stream_cf: CryptFilterMethod::None,
1188            string_cf: CryptFilterMethod::None,
1189            o_value: Vec::new(),
1190            u_value: Vec::new(),
1191            encoded_password: Vec::new(),
1192        };
1193
1194        let data = b"unencrypted data";
1195        let obj_id = ObjectId::new(1, 0);
1196        assert_eq!(handler.decrypt_string(data, obj_id), data);
1197        assert_eq!(handler.decrypt_stream(data, obj_id), data);
1198    }
1199
1200    #[test]
1201    fn test_truncate_utf8_password_short() {
1202        let pwd = truncate_utf8_password("hello");
1203        assert_eq!(pwd, b"hello");
1204    }
1205
1206    #[test]
1207    fn test_truncate_utf8_password_long() {
1208        let long_pwd = "a".repeat(200);
1209        let pwd = truncate_utf8_password(&long_pwd);
1210        assert_eq!(pwd.len(), 127);
1211    }
1212
1213    #[test]
1214    fn test_compute_r6_hash_basic() {
1215        // Verify that compute_r6_hash returns a deterministic 32-byte result
1216        let hash = compute_r6_hash(b"password", &[0u8; 8], &[]);
1217        assert_eq!(hash.len(), 32);
1218        // Same input should produce same output
1219        let hash2 = compute_r6_hash(b"password", &[0u8; 8], &[]);
1220        assert_eq!(hash, hash2);
1221    }
1222
1223    #[test]
1224    fn test_compute_r6_hash_differs_from_simple_sha256() {
1225        // R6 iterative hash should differ from simple SHA-256
1226        let salt = [0x01u8; 8];
1227        let r6_hash = compute_r6_hash(b"test", &salt, &[]);
1228        let simple_hash = crypto::sha256(&{
1229            let mut v = b"test".to_vec();
1230            v.extend_from_slice(&salt);
1231            v
1232        });
1233        // The iterative hash should produce a different result than simple SHA-256
1234        // (because it goes through multiple rounds with AES + different hash functions)
1235        assert_ne!(r6_hash, simple_hash);
1236    }
1237
1238    #[test]
1239    fn test_compute_r6_hash_with_u_value() {
1240        // Hash with non-empty u_value should differ from empty u_value
1241        let salt = [0x42u8; 8];
1242        let hash_no_u = compute_r6_hash(b"pwd", &salt, &[]);
1243        let hash_with_u = compute_r6_hash(b"pwd", &salt, &[0xAAu8; 48]);
1244        assert_ne!(hash_no_u, hash_with_u);
1245    }
1246
1247    #[test]
1248    fn test_r6_user_password_round_trip() {
1249        // Simulate creating R6 encryption data and verifying it
1250        let password = b"test123";
1251        let validation_salt = [0x11u8; 8];
1252        let key_salt = [0x22u8; 8];
1253        let file_key = [0x42u8; 32]; // the actual file encryption key
1254
1255        // Compute U value: hash(password, validation_salt) || validation_salt || key_salt
1256        let u_hash = compute_r6_hash(password, &validation_salt, &[]);
1257        let mut u_value = Vec::with_capacity(48);
1258        u_value.extend_from_slice(&u_hash);
1259        u_value.extend_from_slice(&validation_salt);
1260        u_value.extend_from_slice(&key_salt);
1261
1262        // Compute UE: AES-256-CBC encrypt file_key with hash(password, key_salt), no padding.
1263        // Per ISO 32000-2 Algorithm 2.A: UE/OE store raw CBC ciphertext without a padding block.
1264        let key_hash = compute_r6_hash(password, &key_salt, &[]);
1265        let ue_value =
1266            crypto::aes256_cbc_encrypt_no_padding(&key_hash, &[0u8; 16], &file_key).unwrap();
1267
1268        // Now verify: derive_key_r6 should recover the file key
1269        let result = derive_key_r6(
1270            "test123", &u_value, &ue_value, &[0u8; 48], // dummy O
1271            &[0u8; 32], // dummy OE
1272        )
1273        .unwrap();
1274
1275        assert!(result.is_some());
1276        assert_eq!(result.unwrap(), file_key);
1277    }
1278
1279    #[test]
1280    fn test_r6_wrong_password_returns_none() {
1281        let password = b"correct";
1282        let validation_salt = [0x33u8; 8];
1283        let key_salt = [0x44u8; 8];
1284
1285        let u_hash = compute_r6_hash(password, &validation_salt, &[]);
1286        let mut u_value = Vec::with_capacity(48);
1287        u_value.extend_from_slice(&u_hash);
1288        u_value.extend_from_slice(&validation_salt);
1289        u_value.extend_from_slice(&key_salt);
1290
1291        let key_hash = compute_r6_hash(password, &key_salt, &[]);
1292        let ue_value =
1293            crypto::aes256_cbc_encrypt_no_padding(&key_hash, &[0u8; 16], &[0u8; 32]).unwrap();
1294
1295        // Wrong password should not match
1296        let result = derive_key_r6("wrong", &u_value, &ue_value, &[0u8; 48], &[0u8; 32]).unwrap();
1297
1298        assert!(result.is_none());
1299    }
1300
1301    // -----------------------------------------------------------------------
1302    // KAT: Algorithm 2.B — known-answer tests against PDFium's test fixture
1303    //
1304    // All byte values are extracted from:
1305    //   pdfium-upstream/testing/resources/encrypted_hello_world_r6.pdf
1306    // which was created by PDFium (the reference implementation) with:
1307    //   user password : "hôtel"  (UTF-8 bytes: 68 c3 b4 74 65 6c)
1308    //   owner password: "âge"   (UTF-8 bytes: c3 a2 67 65)
1309    //
1310    // These tests fail if the Algorithm 2.B implementation diverges from PDFium,
1311    // which is exactly the class of regression the self-consistent round-trip
1312    // tests (test_r6_user_password_round_trip etc.) cannot catch.
1313    // -----------------------------------------------------------------------
1314
1315    /// Upstream: PDFium `encrypted_hello_world_r6.pdf`, user-password path.
1316    /// U[0..32] must equal compute_r6_hash("hôtel", U[32..40], []).
1317    #[test]
1318    fn test_compute_r6_hash_kat_pdfium_user_validation() {
1319        // User password "hôtel" in UTF-8
1320        let password = "h\u{00f4}tel".as_bytes();
1321
1322        // U value from the PDF (48 bytes).
1323        // U[32..40] = validation salt, U[40..48] = key salt.
1324        let u: [u8; 48] = [
1325            0x07, 0x98, 0xab, 0x4b, 0x1c, 0x93, 0xd3, 0x60, // U[0..8]
1326            0xf9, 0x6b, 0x8b, 0xa4, 0x1d, 0x1a, 0xdd, 0x5b, // U[8..16]
1327            0x7e, 0xaf, 0x4b, 0x11, 0x0f, 0x01, 0x4d, 0xe8, // U[16..24]
1328            0x8a, 0x57, 0x61, 0x5f, 0xdd, 0x6f, 0x67, 0x7c, // U[24..32]  ← expected hash
1329            0x1a, 0x5e, 0x05, 0x9d, 0x15, 0xed, 0x6e, 0xed, // U[32..40] validation salt
1330            0x88, 0xd9, 0x4c, 0x03, 0x49, 0x58, 0x3f, 0x86, // U[40..48] key salt
1331        ];
1332
1333        let computed = compute_r6_hash(password, &u[32..40], &[]);
1334        assert_eq!(
1335            computed,
1336            u[..32],
1337            "Algorithm 2.B hash must match U[0..32] from PDFium-generated R6 PDF"
1338        );
1339    }
1340
1341    /// Upstream: PDFium `encrypted_hello_world_r6.pdf`, end-to-end user-password key recovery.
1342    /// derive_key_r6 must accept "hôtel" and return Some(file_key).
1343    #[test]
1344    fn test_derive_key_r6_kat_pdfium_user_password() {
1345        // Values extracted from encrypted_hello_world_r6.pdf
1346        let u: [u8; 48] = [
1347            0x07, 0x98, 0xab, 0x4b, 0x1c, 0x93, 0xd3, 0x60, 0xf9, 0x6b, 0x8b, 0xa4, 0x1d, 0x1a,
1348            0xdd, 0x5b, 0x7e, 0xaf, 0x4b, 0x11, 0x0f, 0x01, 0x4d, 0xe8, 0x8a, 0x57, 0x61, 0x5f,
1349            0xdd, 0x6f, 0x67, 0x7c, 0x1a, 0x5e, 0x05, 0x9d, 0x15, 0xed, 0x6e, 0xed, 0x88, 0xd9,
1350            0x4c, 0x03, 0x49, 0x58, 0x3f, 0x86,
1351        ];
1352        let ue: [u8; 32] = [
1353            0x9e, 0x30, 0x4c, 0x9f, 0xff, 0x64, 0x7b, 0x71, 0x53, 0x6d, 0xb3, 0x68, 0x4c, 0x72,
1354            0x91, 0x4c, 0xae, 0x38, 0x82, 0x88, 0x5e, 0xb8, 0xcf, 0x9c, 0xfb, 0xf3, 0x02, 0x6d,
1355            0xae, 0x2e, 0x1f, 0x35,
1356        ];
1357        let o: [u8; 48] = [
1358            0xd8, 0x0e, 0x61, 0x06, 0xfd, 0x39, 0x47, 0x8c, 0x88, 0x60, 0xc9, 0x14, 0x5e, 0x89,
1359            0x6f, 0x12, 0x6c, 0x3f, 0xa0, 0xc9, 0x12, 0x5a, 0xd2, 0x09, 0x6d, 0x24, 0x2c, 0x07,
1360            0x5f, 0xa6, 0x21, 0xac, 0x99, 0x22, 0x41, 0x60, 0x8c, 0xc3, 0xd3, 0x97, 0x13, 0x5a,
1361            0x2c, 0x0a, 0xec, 0x96, 0xdb, 0x3b,
1362        ];
1363        let oe: [u8; 32] = [
1364            0x90, 0x46, 0xa2, 0x2c, 0x32, 0xd3, 0x32, 0x86, 0x55, 0x95, 0x94, 0xea, 0xef, 0x09,
1365            0xb9, 0xcf, 0x49, 0x22, 0x8b, 0x58, 0xd0, 0x2b, 0xf5, 0xdd, 0xc3, 0x38, 0x3d, 0xf9,
1366            0x26, 0x32, 0x82, 0xe2,
1367        ];
1368
1369        // User password "hôtel" (UTF-8)
1370        let result = derive_key_r6("h\u{00f4}tel", &u, &ue, &o, &oe)
1371            .expect("derive_key_r6 must not error on valid R6 data");
1372        assert!(
1373            result.is_some(),
1374            "user password 'hôtel' must be accepted for PDFium-generated R6 PDF"
1375        );
1376    }
1377
1378    /// Upstream: PDFium `encrypted_hello_world_r6.pdf`, end-to-end owner-password key recovery.
1379    /// derive_key_r6 must accept "âge" (owner password) and return Some(file_key).
1380    #[test]
1381    fn test_derive_key_r6_kat_pdfium_owner_password() {
1382        let u: [u8; 48] = [
1383            0x07, 0x98, 0xab, 0x4b, 0x1c, 0x93, 0xd3, 0x60, 0xf9, 0x6b, 0x8b, 0xa4, 0x1d, 0x1a,
1384            0xdd, 0x5b, 0x7e, 0xaf, 0x4b, 0x11, 0x0f, 0x01, 0x4d, 0xe8, 0x8a, 0x57, 0x61, 0x5f,
1385            0xdd, 0x6f, 0x67, 0x7c, 0x1a, 0x5e, 0x05, 0x9d, 0x15, 0xed, 0x6e, 0xed, 0x88, 0xd9,
1386            0x4c, 0x03, 0x49, 0x58, 0x3f, 0x86,
1387        ];
1388        let ue: [u8; 32] = [
1389            0x9e, 0x30, 0x4c, 0x9f, 0xff, 0x64, 0x7b, 0x71, 0x53, 0x6d, 0xb3, 0x68, 0x4c, 0x72,
1390            0x91, 0x4c, 0xae, 0x38, 0x82, 0x88, 0x5e, 0xb8, 0xcf, 0x9c, 0xfb, 0xf3, 0x02, 0x6d,
1391            0xae, 0x2e, 0x1f, 0x35,
1392        ];
1393        let o: [u8; 48] = [
1394            0xd8, 0x0e, 0x61, 0x06, 0xfd, 0x39, 0x47, 0x8c, 0x88, 0x60, 0xc9, 0x14, 0x5e, 0x89,
1395            0x6f, 0x12, 0x6c, 0x3f, 0xa0, 0xc9, 0x12, 0x5a, 0xd2, 0x09, 0x6d, 0x24, 0x2c, 0x07,
1396            0x5f, 0xa6, 0x21, 0xac, 0x99, 0x22, 0x41, 0x60, 0x8c, 0xc3, 0xd3, 0x97, 0x13, 0x5a,
1397            0x2c, 0x0a, 0xec, 0x96, 0xdb, 0x3b,
1398        ];
1399        let oe: [u8; 32] = [
1400            0x90, 0x46, 0xa2, 0x2c, 0x32, 0xd3, 0x32, 0x86, 0x55, 0x95, 0x94, 0xea, 0xef, 0x09,
1401            0xb9, 0xcf, 0x49, 0x22, 0x8b, 0x58, 0xd0, 0x2b, 0xf5, 0xdd, 0xc3, 0x38, 0x3d, 0xf9,
1402            0x26, 0x32, 0x82, 0xe2,
1403        ];
1404
1405        // Owner password "âge" (UTF-8: c3 a2 67 65)
1406        let result = derive_key_r6("\u{00e2}ge", &u, &ue, &o, &oe)
1407            .expect("derive_key_r6 must not error on valid R6 data");
1408        assert!(
1409            result.is_some(),
1410            "owner password 'âge' must be accepted for PDFium-generated R6 PDF"
1411        );
1412    }
1413
1414    #[test]
1415    fn test_owner_password_recovery_r2() {
1416        // Set up: compute O from a known user password + owner password
1417        let user_pwd = b"user";
1418        let owner_pwd = b"owner";
1419        let key_len = 5;
1420
1421        // Compute O value: pad owner_pwd, MD5, RC4-encrypt padded user_pwd
1422        let padded_owner = pad_password(owner_pwd);
1423        let hash = crypto::md5(&padded_owner);
1424        let owner_key = &hash[..key_len];
1425        let padded_user = pad_password(user_pwd);
1426        let o_value = crypto::rc4_crypt(owner_key, &padded_user).unwrap();
1427
1428        // Now recover
1429        let recovered = recover_user_password_from_owner(owner_pwd, &o_value, 2, key_len);
1430        assert_eq!(&recovered[..], &padded_user[..]);
1431    }
1432
1433    // -----------------------------------------------------------------------
1434    // P0-1: Verify PASSWORD_PADDING matches PDF spec byte-by-byte
1435    // -----------------------------------------------------------------------
1436
1437    #[test]
1438    fn test_password_padding_matches_pdf_spec() {
1439        // Table 3.18 / ISO 32000-1:2008 section 7.6.3.3
1440        let spec_padding: [u8; 32] = [
1441            0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA,
1442            0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE,
1443            0x64, 0x53, 0x69, 0x7A,
1444        ];
1445        assert_eq!(PASSWORD_PADDING, spec_padding);
1446    }
1447
1448    // -----------------------------------------------------------------------
1449    // P0-2: Test big-endian mod 3 vs sum mod 3 divergence
1450    // -----------------------------------------------------------------------
1451
1452    #[test]
1453    fn test_big_endian_mod3_basic() {
1454        // All zeros: 0 mod 3 = 0
1455        assert_eq!(big_endian_mod3(&[0u8; 16]), 0);
1456        // All 0xFF: large number mod 3
1457        assert_eq!(big_endian_mod3(&[0xFF; 16]), big_endian_mod3(&[0xFF; 16]));
1458    }
1459
1460    #[test]
1461    fn test_big_endian_mod3_differs_from_sum_mod3() {
1462        // Find a value where sum%3 differs from big-endian%3
1463        // bytes = [0x01, 0x00, ...0x00] (15 zeros after):
1464        //   sum   = 1, 1 % 3 = 1
1465        //   big-endian = 256^15 mod 3. Since 256 mod 3 = 1, 256^15 mod 3 = 1. Same.
1466        // Try [0x02, 0x01, 0, ...]:
1467        //   sum   = 3, 3 % 3 = 0
1468        //   big-endian = 2*256 + 1 = 513 mod 3 = 0. Same.
1469        // Try [0x80, 0x00, ...0x00, 0x01]:
1470        //   sum   = 0x80 + 1 = 129, 129 % 3 = 0
1471        //   big-endian = 0x80 * 256^15 + 1. Since 256 mod 3 = 1: 128*1 + 1 = 129 mod 3 = 0. Same.
1472        // Try [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]:
1473        //   sum = 5, 5%3 = 2
1474        //   big-endian = 3*256^15 + 2. 256^15 mod 3 = 1, so 3+2=5 mod 3 = 2. Same.
1475        // Actually the two methods differ when the positional weights matter.
1476        // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0]:
1477        //   sum = 2, 2%3 = 2
1478        //   big-endian: 2*256 = 512. 512%3 = 2. Same.
1479        // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]:
1480        //   sum = 2, 2%3 = 2
1481        //   big-endian: 1*256+1 = 257. 257%3 = 2. Same.
1482        // Since 256 mod 3 = 1, positional weighting doesn't change mod3 result.
1483        // So sum%3 == big-endian%3 for mod 3 specifically.
1484        // However, the spec says to interpret as big-endian, so our implementation is still correct.
1485        // Let's just verify the function works correctly on known values.
1486        let bytes = [0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
1487        assert_eq!(big_endian_mod3(&bytes), 1);
1488        let bytes = [0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
1489        assert_eq!(big_endian_mod3(&bytes), 2);
1490        let bytes = [0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3];
1491        assert_eq!(big_endian_mod3(&bytes), 0);
1492    }
1493
1494    // -----------------------------------------------------------------------
1495    // P2-31: Test separate StrF/StmF
1496    // -----------------------------------------------------------------------
1497
1498    #[test]
1499    fn test_separate_strf_stmf_decrypt() {
1500        // Create a handler where streams use AES but strings use RC4
1501        let handler = SecurityHandler {
1502            revision: 4,
1503            key_length_bytes: 16,
1504            encryption_key: vec![0xBB; 16],
1505            permissions: Permissions::from_bits(-4),
1506            encrypt_metadata: true,
1507            stream_cf: CryptFilterMethod::Aesv2,
1508            string_cf: CryptFilterMethod::V2,
1509            o_value: Vec::new(),
1510            u_value: Vec::new(),
1511            encoded_password: Vec::new(),
1512        };
1513
1514        // Verify accessor returns stream_cf
1515        assert_eq!(handler.crypt_filter_method(), CryptFilterMethod::Aesv2);
1516
1517        let obj_id = ObjectId::new(1, 0);
1518
1519        // String uses RC4 (V2) — round-trip
1520        let plaintext = b"hello";
1521        let obj_key_str = handler.compute_object_key(obj_id, CryptFilterMethod::V2);
1522        let encrypted_str = crypto::rc4_crypt(&obj_key_str, plaintext).unwrap();
1523        let decrypted = handler.decrypt_string(&encrypted_str, obj_id);
1524        assert_eq!(decrypted, plaintext);
1525
1526        // Stream uses AESV2 — verify the object key includes sAlT
1527        let obj_key_stm = handler.compute_object_key(obj_id, CryptFilterMethod::Aesv2);
1528        assert_ne!(obj_key_str, obj_key_stm); // sAlT suffix makes them differ
1529    }
1530
1531    // -----------------------------------------------------------------------
1532    // P2-32: Test Latin-1 password encoding
1533    // -----------------------------------------------------------------------
1534
1535    #[test]
1536    fn test_encode_password_latin1_ascii() {
1537        let encoded = encode_password_latin1("hello");
1538        assert_eq!(encoded, b"hello");
1539    }
1540
1541    #[test]
1542    fn test_encode_password_latin1_latin_chars() {
1543        // e-acute (U+00E9) is within Latin-1 range
1544        let encoded = encode_password_latin1("\u{00E9}");
1545        assert_eq!(encoded, vec![0xE9]);
1546    }
1547
1548    #[test]
1549    fn test_encode_password_latin1_non_latin() {
1550        // CJK character (U+4E2D) is outside Latin-1 range, replaced with '?'
1551        let encoded = encode_password_latin1("\u{4E2D}");
1552        assert_eq!(encoded, vec![0x3F]);
1553    }
1554
1555    #[test]
1556    fn test_encode_password_latin1_mixed() {
1557        // Mix of ASCII, Latin-1, and non-Latin-1
1558        let encoded = encode_password_latin1("a\u{00FC}\u{4E2D}b");
1559        assert_eq!(encoded, vec![b'a', 0xFC, 0x3F, b'b']);
1560    }
1561
1562    // -----------------------------------------------------------------------
1563    // P2-33: Test /Perms validation
1564    // -----------------------------------------------------------------------
1565
1566    #[test]
1567    fn test_encode_password_latin1_empty() {
1568        let encoded = encode_password_latin1("");
1569        assert!(encoded.is_empty());
1570    }
1571
1572    #[test]
1573    fn test_encode_password_latin1_ascii_alphanumeric() {
1574        let encoded = encode_password_latin1("ABC123");
1575        assert_eq!(encoded, b"ABC123");
1576    }
1577
1578    #[test]
1579    fn test_encode_password_latin1_0xff() {
1580        // U+00FF (y-diaeresis) is the last character in Latin-1 range
1581        let encoded = encode_password_latin1("\u{00FF}");
1582        assert_eq!(encoded, vec![0xFF]);
1583    }
1584
1585    #[test]
1586    fn test_encode_password_latin1_over_range() {
1587        // U+0100 (Latin Extended-A) is just outside Latin-1 range
1588        let encoded = encode_password_latin1("\u{0100}");
1589        assert_eq!(encoded, vec![0x3F]); // '?' fallback
1590    }
1591
1592    #[test]
1593    fn test_truncate_utf8_password_empty() {
1594        let pwd = truncate_utf8_password("");
1595        assert!(pwd.is_empty());
1596    }
1597
1598    #[test]
1599    fn test_truncate_utf8_password_exact_127() {
1600        let input = "a".repeat(127);
1601        let pwd = truncate_utf8_password(&input);
1602        assert_eq!(pwd.len(), 127);
1603        assert_eq!(pwd, input.as_bytes());
1604    }
1605
1606    #[test]
1607    fn test_truncate_utf8_password_over_127() {
1608        let input = "a".repeat(128);
1609        let pwd = truncate_utf8_password(&input);
1610        assert_eq!(pwd.len(), 127);
1611    }
1612
1613    #[test]
1614    fn test_perms_validation_construct_and_verify() {
1615        // Build a valid /Perms block per PDF spec Algorithm 2.A:
1616        //   [0..4]  = /P value as LE i32
1617        //   [8]     = 'T' (EncryptMetadata=true) or 'F'
1618        //   [9..12] = "adb"
1619        let permissions: i32 = -4;
1620        let mut perms_plain = [0u8; 16];
1621        perms_plain[0..4].copy_from_slice(&permissions.to_le_bytes());
1622        // Bytes 4-7: can be anything
1623        perms_plain[8] = b'T'; // EncryptMetadata flag
1624        perms_plain[9] = b'a';
1625        perms_plain[10] = b'd';
1626        perms_plain[11] = b'b';
1627
1628        // Encrypt with AES-256-ECB (single block, no padding)
1629        let file_key = [0x42u8; 32];
1630        // Use aes256_ecb_encrypt_block equivalent: encrypt then decrypt round-trip
1631        // For this test, we encrypt using the raw AES block cipher
1632        use aes::cipher::{BlockEncrypt, KeyInit};
1633        let cipher = aes::Aes256::new_from_slice(&file_key).unwrap();
1634        let mut block = aes::cipher::generic_array::GenericArray::clone_from_slice(&perms_plain);
1635        cipher.encrypt_block(&mut block);
1636        let encrypted_perms: [u8; 16] = block.into();
1637
1638        // Now decrypt and validate (simulating what from_encrypt_dict does)
1639        let decrypted = crypto::aes256_ecb_decrypt_block(&file_key, &encrypted_perms).unwrap();
1640
1641        // Byte 8 = 'T' (EncryptMetadata), bytes 9-11 = "adb"
1642        assert_eq!(decrypted[8], b'T');
1643        assert_eq!(&decrypted[9..12], b"adb");
1644        let recovered_p =
1645            i32::from_le_bytes([decrypted[0], decrypted[1], decrypted[2], decrypted[3]]);
1646        assert_eq!(recovered_p, permissions);
1647    }
1648
1649    #[test]
1650    fn test_get_permissions_alias_delegates_to_permissions() {
1651        let handler = SecurityHandler {
1652            revision: 3,
1653            key_length_bytes: 5,
1654            encryption_key: vec![0x01; 5],
1655            permissions: Permissions::from_bits(-4),
1656            encrypt_metadata: true,
1657            stream_cf: CryptFilterMethod::V2,
1658            string_cf: CryptFilterMethod::V2,
1659            o_value: Vec::new(),
1660            u_value: Vec::new(),
1661            encoded_password: b"secret".to_vec(),
1662        };
1663        assert_eq!(handler.get_permissions(), handler.permissions());
1664    }
1665
1666    #[test]
1667    fn test_encoded_password_stored_and_accessible() {
1668        let handler = SecurityHandler {
1669            revision: 3,
1670            key_length_bytes: 5,
1671            encryption_key: vec![0x01; 5],
1672            permissions: Permissions::from_bits(-4),
1673            encrypt_metadata: true,
1674            stream_cf: CryptFilterMethod::V2,
1675            string_cf: CryptFilterMethod::V2,
1676            o_value: Vec::new(),
1677            u_value: Vec::new(),
1678            encoded_password: b"my_password".to_vec(),
1679        };
1680        assert_eq!(handler.encoded_password(), b"my_password");
1681    }
1682
1683    // -----------------------------------------------------------------------
1684    // Encrypted PDF round-trip tests (P3: integration)
1685    // -----------------------------------------------------------------------
1686
1687    /// Verify SecurityHandler stores encoded_password for R2/R4 user password path.
1688    ///
1689    /// Derives a key for a known password, computes the matching U value for R3,
1690    /// constructs a SecurityHandler directly, and verifies that `encoded_password()`
1691    /// returns the same Latin-1-encoded bytes used during key derivation.
1692    #[test]
1693    fn test_security_handler_stores_encoded_password_user_path() {
1694        // Derive key for a known password
1695        let password = "test";
1696        let pwd_bytes = encode_password_latin1(password);
1697        let o_value = [0x42u8; 32];
1698        let file_id = b"abcdef0123456789";
1699        let key = derive_key_r2_r4(&pwd_bytes, &o_value, -4, file_id, 3, 5, true);
1700
1701        // Compute U value for R3: MD5(PASSWORD_PADDING + file_id), then 20 rounds of RC4
1702        let mut buf = Vec::new();
1703        buf.extend_from_slice(&PASSWORD_PADDING);
1704        buf.extend_from_slice(file_id);
1705        let hash = crypto::md5(&buf);
1706        let mut result = crypto::rc4_crypt(&key, &hash).unwrap();
1707        for i in 1..=19u8 {
1708            let modified: Vec<u8> = key.iter().map(|b| b ^ i).collect();
1709            result = crypto::rc4_crypt(&modified, &result).unwrap();
1710        }
1711        let mut u_value = result;
1712        u_value.resize(32, 0);
1713
1714        // Construct a SecurityHandler directly with the same components
1715        let handler = SecurityHandler {
1716            revision: 3,
1717            key_length_bytes: 5,
1718            encryption_key: key,
1719            permissions: Permissions::from_bits(-4),
1720            encrypt_metadata: true,
1721            stream_cf: CryptFilterMethod::V2,
1722            string_cf: CryptFilterMethod::V2,
1723            o_value: o_value.to_vec(),
1724            u_value,
1725            encoded_password: pwd_bytes.clone(),
1726        };
1727
1728        assert_eq!(handler.encoded_password(), pwd_bytes.as_slice());
1729        // "test" encodes as plain ASCII bytes in Latin-1
1730        assert_eq!(handler.encoded_password(), b"test");
1731    }
1732
1733    /// Verify that `get_permissions()` alias returns the same value as `permissions()`.
1734    ///
1735    /// Uses a non-trivial permission value (-3904) to confirm both accessors agree
1736    /// and that `Permissions::bits()` round-trips correctly through the alias.
1737    #[test]
1738    fn test_security_handler_get_permissions_alias_r3() {
1739        let handler = SecurityHandler {
1740            revision: 3,
1741            key_length_bytes: 5,
1742            encryption_key: vec![0x01; 5],
1743            permissions: Permissions::from_bits(-3904),
1744            encrypt_metadata: true,
1745            stream_cf: CryptFilterMethod::V2,
1746            string_cf: CryptFilterMethod::V2,
1747            o_value: Vec::new(),
1748            u_value: Vec::new(),
1749            encoded_password: b"owner".to_vec(),
1750        };
1751        assert_eq!(handler.get_permissions().bits(), -3904);
1752        assert_eq!(handler.get_permissions(), handler.permissions());
1753    }
1754}