Skip to main content

oxideav_pdf/decrypt/
mod.rs

1//! PDF *decryption* support — ISO 32000-1 §7.6 / ISO 32000-2 §7.6
2//! Standard Security Handler.
3//!
4//! The reader can open password-protected PDFs across the full
5//! revision spectrum the Standard handler defines. Public-key
6//! (`adbe.pkcs7.s3` / `s4` / `s5`) handlers are out of scope.
7//!
8//! Coverage:
9//!
10//! * **R=2**: RC4-40 (V=1, Length 40).
11//! * **R=3**: RC4-128 (V=2, Length up to 128).
12//! * **R=4**: AES-128 CBC or RC4-128 selected by the crypt-filter
13//!   `CFM` entry (`AESV2` vs `V2`).
14//! * **R=5**: AES-256 CBC, V=5, `CFM=AESV3`. Adobe extension level 3
15//!   (PDF 1.7) — simpler password derivation than R=6 (no Algorithm
16//!   2.B hash chain — just plain SHA-256 over the password and the
17//!   appropriate salt).
18//! * **R=6**: AES-256 CBC, V=5, `CFM=AESV3`. ISO 32000-2:2020
19//!   (PDF 2.0) — the iterated SHA-256/384/512 chain of Algorithm 2.B
20//!   plus the Perms-block validation of Algorithm 13.
21//!
22//! # Algorithms used (numbered per ISO 32000)
23//!
24//! * **Algorithm 1** — per-object encryption key for V≤4: extend file
25//!   key with `objnum` LE3 + `gennum` LE2 (+ `"sAlT"` for AES), MD5,
26//!   take first `n+5` ≤ 16 bytes.
27//! * **Algorithm 2** (V≤4) — encryption key: pad password to 32 bytes
28//!   with the canonical pad string, MD5(pad ‖ O ‖ P ‖ ID[0] ‖ optional
29//!   `0xFFFFFFFF`); for R≥3, loop 50× MD5; final key is first `n`
30//!   bytes.
31//! * **Algorithm 2.A** — open-with-password orchestration for V=5
32//!   (R=5 / R=6). Combines Algorithms 11 + 12 + 13 with the right
33//!   per-revision derivation of the *file* encryption key.
34//! * **Algorithm 2.B** — the iterated SHA-{256/384/512} hash function
35//!   used by R=6 to derive intermediate keys; R=5 falls back to plain
36//!   SHA-256.
37//! * **Algorithms 4 / 5 / 6 / 7** — RC4 / AES-128 password
38//!   authentication and recovery (see ISO 32000-1 §7.6.3.4).
39//! * **Algorithms 8 / 9 / 10** — V=5 *writer* paths: compute O, U,
40//!   and the encrypted Perms blob from a password + file key.
41//! * **Algorithm 11** — V=5 user-password authentication. SHA-256 over
42//!   `password ‖ U[32..40]` (validation salt) → compare to `U[..32]`.
43//! * **Algorithm 12** — V=5 owner-password authentication. SHA-256
44//!   over `password ‖ O[32..40] ‖ U[..48]` → compare to `O[..32]`.
45//! * **Algorithm 13** — V=5 permissions-block decryption: AES-256 ECB
46//!   on `Perms` with the file key; bytes 0..3 reproduce P (LE), bytes
47//!   8..12 are `T` or `F` for `EncryptMetadata`, bytes 9..11 are
48//!   `"adb"`.
49//!
50//! Strings + streams in the encryption dictionary are NOT decrypted
51//! (per §7.6.1). Strings inside the trailer's `/ID` array are also
52//! plaintext.
53//!
54//! ## Provenance
55//!
56//! Implemented from the spec PDFs only:
57//! `docs/document/pdf/PDF32000_2008.pdf` §7.6 (Tables 20–22, Algorithms
58//! 1–7) plus `docs/document/pdf/PDF32000_2020.pdf` §7.6.4.4 (Algorithms
59//! 2.A / 2.B / 8 / 9 / 10 / 11 / 12 / 13). RC4 and MD5 are hand-rolled
60//! per RSA's RC4 / RFC 1321 references; SHA-256/384/512 come from the
61//! pure-Rust `sha2` crate (RustCrypto). AES-128/256 CBC come from the
62//! `aes` + `cbc` RustCrypto crates — pure-Rust, constant-time, no
63//! `*-sys` wrappers.
64
65pub mod r5_r6;
66
67use crate::error::PdfError;
68use crate::objects::{Object, ObjectId};
69
70/// The 32-byte password-padding string from §7.6.3.3 Algorithm 2 step (a).
71/// Used both to pad short passwords up to 32 bytes and as the *plaintext*
72/// the user-validation algorithms encrypt to populate `/U`.
73const PAD: [u8; 32] = [
74    0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
75    0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
76];
77
78/// Algorithm 1 step (b) salt for AES — bytes "sAlT" (0x73 41 6C 54).
79const AES_SALT: [u8; 4] = [0x73, 0x41, 0x6C, 0x54];
80
81/// Per-object encryption modes. Picked from the file's encryption
82/// dictionary at open time — every crypt operation uses the same mode
83/// (round-4 doesn't yet support per-stream crypt-filter overrides; if
84/// the file requires that, decryption surfaces a clear error).
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum CryptMethod {
87    /// Algorithm 1 + RC4. Used when V ∈ {1, 2}, or V=4 with `CFM=V2`.
88    Rc4,
89    /// Algorithm 1 + AES-128 CBC. Used when V=4 with `CFM=AESV2`.
90    Aes128,
91    /// AES-256 CBC. Used when V=5 (R=5 / R=6) with `CFM=AESV3`. Per
92    /// ISO 32000-2:2020 §7.6.3.1 the per-object key derivation
93    /// (Algorithm 1) is **not** applied — the file encryption key is
94    /// fed to AES-256 directly. The IV is still the leading 16 bytes
95    /// of the ciphertext as for AESV2.
96    Aes256,
97}
98
99/// File-level encryption parameters resolved from the trailer's
100/// `/Encrypt` dictionary. Carries the master key (Algorithm 2 output)
101/// plus the per-object cipher selection.
102#[derive(Debug, Clone)]
103pub struct StandardHandler {
104    /// File encryption key — `n` bytes, where `n` ∈ {5, 16}.
105    pub key: Vec<u8>,
106    /// Default crypt method for streams + strings.
107    pub method: CryptMethod,
108    /// Revision (2..=4 supported).
109    pub revision: u8,
110}
111
112impl StandardHandler {
113    /// Decrypt the data of an indirect object whose id is `id` and
114    /// whose payload is `data` (a string body or stream body, after
115    /// any `/Filter` decoding). Returns the cleartext bytes.
116    ///
117    /// AESV3 (R=5 / R=6) intentionally ignores `id`: ISO 32000-2:2020
118    /// §7.6.3.1 specifies that for V=5 the file encryption key is fed
119    /// to AES-256 directly, with no per-object derivation.
120    pub fn decrypt_object(&self, id: ObjectId, data: &[u8]) -> Result<Vec<u8>, PdfError> {
121        match self.method {
122            CryptMethod::Rc4 => {
123                let obj_key = self.object_key(id);
124                Ok(rc4(&obj_key, data))
125            }
126            CryptMethod::Aes128 => {
127                let obj_key = self.object_key(id);
128                aes128_cbc_decrypt(&obj_key, data)
129            }
130            CryptMethod::Aes256 => aes256_cbc_decrypt(&self.key, data),
131        }
132    }
133
134    /// Encrypt the cleartext payload of an indirect object — the inverse
135    /// of [`Self::decrypt_object`] used by the writer to populate `/Encrypt`-
136    /// protected strings + streams. RC4 is symmetric (so this re-uses
137    /// [`rc4`]); AES uses a fresh IV which must be embedded as the
138    /// leading 16 bytes of the ciphertext per §7.6.2.
139    ///
140    /// `iv` (only consulted for `Aes128` / `Aes256`) lets fixture builders
141    /// pin the IV for byte-for-byte deterministic outputs. Production
142    /// callers pass a per-call-randomised IV.
143    pub fn encrypt_object(
144        &self,
145        id: ObjectId,
146        data: &[u8],
147        iv: &[u8; 16],
148    ) -> Result<Vec<u8>, PdfError> {
149        match self.method {
150            CryptMethod::Rc4 => {
151                let obj_key = self.object_key(id);
152                Ok(rc4(&obj_key, data))
153            }
154            CryptMethod::Aes128 => {
155                let obj_key = self.object_key(id);
156                aes128_cbc_encrypt(&obj_key, data, iv)
157            }
158            CryptMethod::Aes256 => aes256_cbc_encrypt(&self.key, data, iv),
159        }
160    }
161
162    /// Algorithm 1: derive the per-object encryption key for V≤4.
163    ///
164    /// `extended = key ‖ obj_num_le3 ‖ gen_le2 [‖ "sAlT" if AES]` →
165    /// MD5 → take first `n+5` (capped at 16) bytes.
166    ///
167    /// Not used for AESV3 — the V=5 spec disables per-object derivation
168    /// entirely (the AES-256 key is the file key itself).
169    fn object_key(&self, id: ObjectId) -> Vec<u8> {
170        let n = self.key.len();
171        let extra = if self.method == CryptMethod::Aes128 {
172            5 + 4
173        } else {
174            5
175        };
176        let mut buf = Vec::with_capacity(n + extra);
177        buf.extend_from_slice(&self.key);
178        buf.push((id.number & 0xFF) as u8);
179        buf.push(((id.number >> 8) & 0xFF) as u8);
180        buf.push(((id.number >> 16) & 0xFF) as u8);
181        buf.push((id.generation & 0xFF) as u8);
182        buf.push(((id.generation >> 8) & 0xFF) as u8);
183        if self.method == CryptMethod::Aes128 {
184            buf.extend_from_slice(&AES_SALT);
185        }
186        let h = md5(&buf);
187        let take = (n + 5).min(16);
188        h[..take].to_vec()
189    }
190}
191
192/// Resolve the file-level encryption key from the encryption dict + the
193/// file's trailer `/ID[0]`, given a candidate password. Returns `Ok(Some)`
194/// if the password authenticates as the user OR owner password; `Ok(None)`
195/// if neither matches; `Err` for malformed `/Encrypt`.
196///
197/// `password` is the raw bytes the caller supplied (often `b""` for the
198/// "default user password" path described in §7.6.3.1).
199pub fn open_with_password(
200    encrypt: &crate::objects::Dict,
201    file_id: &[u8],
202    password: &[u8],
203) -> Result<Option<StandardHandler>, PdfError> {
204    let params = parse_encrypt_dict(encrypt)?;
205
206    // V=5 (R=5 / R=6) is its own module — the password derivation is
207    // SHA-256-based with validation/key salts, with no overlap to the
208    // MD5+RC4 ladder of R≤4.
209    if params.revision == 5 || params.revision == 6 {
210        return r5_r6::open_with_password(&params, password);
211    }
212
213    // Try as user password first (Algorithm 6).
214    if let Some(handler) = try_user_password(&params, file_id, password) {
215        return Ok(Some(handler));
216    }
217    // Try as owner password (Algorithm 7).
218    if let Some(handler) = try_owner_password(&params, file_id, password) {
219        return Ok(Some(handler));
220    }
221    Ok(None)
222}
223
224/// Parsed-but-not-yet-validated `/Encrypt` parameters. Non-V5
225/// entries (`o`, `u`) are 32 bytes; V5 entries are 48 bytes (the
226/// extra 16 bytes split into validation salt + key salt). The new
227/// `oe` / `ue` / `perms` slots are V5-only (zero-length on V≤4).
228#[derive(Debug, Clone)]
229pub(crate) struct EncryptParams {
230    pub(crate) revision: u8,
231    /// Length in bits.
232    pub(crate) length_bits: usize,
233    /// `/O` entry — 32 bytes (R≤4) or 48 bytes (R=5 / R=6).
234    pub(crate) o: Vec<u8>,
235    /// `/U` entry — 32 bytes (R≤4) or 48 bytes (R=5 / R=6).
236    pub(crate) u: Vec<u8>,
237    /// `/OE` entry — 32 bytes, V5-only (Algorithm 8 output).
238    pub(crate) oe: Vec<u8>,
239    /// `/UE` entry — 32 bytes, V5-only (Algorithm 9 output).
240    pub(crate) ue: Vec<u8>,
241    /// `/Perms` entry — 16 bytes, V5-only (Algorithm 10 output).
242    pub(crate) perms: Vec<u8>,
243    /// P (signed 32-bit). Stored as i32 → reinterpreted as little-endian
244    /// bytes when fed into Algorithm 2 step (d) and Algorithm 10.
245    pub(crate) p: i32,
246    /// EncryptMetadata flag. R≥4, default true; round-4 honours it.
247    pub(crate) encrypt_metadata: bool,
248    /// Per-stream / per-string crypt method for V=4 / V=5.
249    pub(crate) cfm: CryptMethod,
250}
251
252fn parse_encrypt_dict(d: &crate::objects::Dict) -> Result<EncryptParams, PdfError> {
253    fn lookup<'a>(d: &'a crate::objects::Dict, key: &str) -> Option<&'a crate::objects::Object> {
254        d.entries().iter().find(|(k, _)| k == key).map(|(_, v)| v)
255    }
256
257    // Filter must be /Standard.
258    match lookup(d, "Filter") {
259        Some(Object::Name(s)) if s == "Standard" => {}
260        Some(other) => {
261            return Err(PdfError::other(format!(
262            "PDF decrypt: only the Standard security handler is supported (got Filter={other:?})"
263        )))
264        }
265        None => return Err(PdfError::other("PDF decrypt: /Encrypt missing /Filter")),
266    }
267
268    let v = match lookup(d, "V") {
269        Some(Object::Integer(n)) => *n,
270        _ => return Err(PdfError::other("PDF decrypt: /Encrypt missing /V")),
271    };
272    let r = match lookup(d, "R") {
273        Some(Object::Integer(n)) => *n,
274        _ => return Err(PdfError::other("PDF decrypt: /Encrypt missing /R")),
275    };
276    if !(2..=6).contains(&r) {
277        return Err(PdfError::other(format!(
278            "PDF decrypt: revision R={r} not supported (R∈[2,6])"
279        )));
280    }
281
282    // Length defaults: 40 bits for V≤2, 256 bits for V=5 (Table 21).
283    let length_bits_default = if v >= 5 { 256 } else { 40 };
284    let length_bits = match lookup(d, "Length") {
285        Some(Object::Integer(n)) => *n as usize,
286        _ => length_bits_default,
287    };
288    let length_ok = if v >= 5 {
289        length_bits == 256
290    } else {
291        (40..=128).contains(&length_bits) && length_bits % 8 == 0
292    };
293    if !length_ok {
294        return Err(PdfError::other(format!(
295            "PDF decrypt: /Length {length_bits} bits invalid for V={v} (V≤2: 40..=128 multiple of 8; V=5: must be 256)"
296        )));
297    }
298
299    let o = match lookup(d, "O") {
300        Some(Object::LiteralString(s)) | Some(Object::HexString(s)) => s.clone(),
301        _ => return Err(PdfError::other("PDF decrypt: /Encrypt missing /O")),
302    };
303    let u = match lookup(d, "U") {
304        Some(Object::LiteralString(s)) | Some(Object::HexString(s)) => s.clone(),
305        _ => return Err(PdfError::other("PDF decrypt: /Encrypt missing /U")),
306    };
307    let o_expected = if r >= 5 { 48 } else { 32 };
308    let u_expected = if r >= 5 { 48 } else { 32 };
309    if o.len() != o_expected {
310        return Err(PdfError::other(format!(
311            "PDF decrypt: /O must be {o_expected} bytes for R={r} (got {})",
312            o.len()
313        )));
314    }
315    if u.len() != u_expected {
316        return Err(PdfError::other(format!(
317            "PDF decrypt: /U must be {u_expected} bytes for R={r} (got {})",
318            u.len()
319        )));
320    }
321
322    let p = match lookup(d, "P") {
323        Some(Object::Integer(n)) => *n as i32,
324        _ => return Err(PdfError::other("PDF decrypt: /Encrypt missing /P")),
325    };
326    let encrypt_metadata = match lookup(d, "EncryptMetadata") {
327        Some(Object::Bool(b)) => *b,
328        _ => true,
329    };
330
331    // V=5 — pull /OE, /UE, /Perms (all required per ISO 32000-2 Table 21).
332    let (oe, ue, perms) = if r >= 5 {
333        let oe = match lookup(d, "OE") {
334            Some(Object::LiteralString(s)) | Some(Object::HexString(s)) => s.clone(),
335            _ => return Err(PdfError::other("PDF decrypt: V=5 /Encrypt missing /OE")),
336        };
337        let ue = match lookup(d, "UE") {
338            Some(Object::LiteralString(s)) | Some(Object::HexString(s)) => s.clone(),
339            _ => return Err(PdfError::other("PDF decrypt: V=5 /Encrypt missing /UE")),
340        };
341        let perms = match lookup(d, "Perms") {
342            Some(Object::LiteralString(s)) | Some(Object::HexString(s)) => s.clone(),
343            _ => return Err(PdfError::other("PDF decrypt: V=5 /Encrypt missing /Perms")),
344        };
345        if oe.len() != 32 {
346            return Err(PdfError::other(format!(
347                "PDF decrypt: /OE must be 32 bytes (got {})",
348                oe.len()
349            )));
350        }
351        if ue.len() != 32 {
352            return Err(PdfError::other(format!(
353                "PDF decrypt: /UE must be 32 bytes (got {})",
354                ue.len()
355            )));
356        }
357        if perms.len() != 16 {
358            return Err(PdfError::other(format!(
359                "PDF decrypt: /Perms must be 16 bytes (got {})",
360                perms.len()
361            )));
362        }
363        (oe, ue, perms)
364    } else {
365        (Vec::new(), Vec::new(), Vec::new())
366    };
367
368    // Pick the crypt method.
369    let cfm = match (v, r) {
370        (1, _) | (2, _) | (_, 2) | (_, 3) => CryptMethod::Rc4,
371        (4, _) | (5, _) => {
372            let stmf = match lookup(d, "StmF") {
373                Some(Object::Name(s)) => s.as_str(),
374                _ => "Identity",
375            };
376            if stmf == "Identity" {
377                // No stream encryption — degrade to a default suited
378                // for the version. V=4 historical default is RC4
379                // (CFM=V2); V=5 only ever pairs with AESV3.
380                if v >= 5 {
381                    CryptMethod::Aes256
382                } else {
383                    CryptMethod::Rc4
384                }
385            } else {
386                let cf = lookup(d, "CF").ok_or_else(|| {
387                    PdfError::other("PDF decrypt: V=4/V=5 /Encrypt missing /CF dictionary")
388                })?;
389                let Object::Dict(cf_dict) = cf else {
390                    return Err(PdfError::other("PDF decrypt: /CF must be a dictionary"));
391                };
392                let filter = lookup(cf_dict, stmf).ok_or_else(|| {
393                    PdfError::other(format!("PDF decrypt: /CF missing crypt filter `{stmf}`"))
394                })?;
395                let Object::Dict(filter_dict) = filter else {
396                    return Err(PdfError::other(format!(
397                        "PDF decrypt: /CF/{stmf} must be a dict"
398                    )));
399                };
400                match lookup(filter_dict, "CFM") {
401                    Some(Object::Name(s)) if s == "V2" => CryptMethod::Rc4,
402                    Some(Object::Name(s)) if s == "AESV2" => CryptMethod::Aes128,
403                    Some(Object::Name(s)) if s == "AESV3" => CryptMethod::Aes256,
404                    Some(Object::Name(s)) if s == "None" => {
405                        return Err(PdfError::other(
406                            "PDF decrypt: CFM=None requires a custom security handler",
407                        ))
408                    }
409                    Some(other) => {
410                        return Err(PdfError::other(format!(
411                            "PDF decrypt: unsupported CFM={other:?}"
412                        )))
413                    }
414                    None => {
415                        if v >= 5 {
416                            CryptMethod::Aes256
417                        } else {
418                            CryptMethod::Rc4
419                        }
420                    }
421                }
422            }
423        }
424        _ => {
425            return Err(PdfError::other(format!(
426                "PDF decrypt: V={v} not supported (handler accepts V∈[1,2,4,5])"
427            )))
428        }
429    };
430
431    Ok(EncryptParams {
432        revision: r as u8,
433        length_bits,
434        o,
435        u,
436        oe,
437        ue,
438        perms,
439        p,
440        encrypt_metadata,
441        cfm,
442    })
443}
444
445/// Algorithm 2 — compute the file encryption key from the user
446/// password.
447fn compute_key(p: &EncryptParams, file_id: &[u8], password: &[u8]) -> Vec<u8> {
448    let n = p.length_bits / 8;
449    // (a) pad / truncate password to 32 bytes.
450    let pwd = pad_password(password);
451    // (b) initialise MD5 hash.
452    let mut buf = Vec::with_capacity(32 + 32 + 4 + file_id.len() + 4);
453    buf.extend_from_slice(&pwd);
454    // (c) feed O.
455    buf.extend_from_slice(&p.o);
456    // (d) feed P, low byte first.
457    let pbytes = (p.p as u32).to_le_bytes();
458    buf.extend_from_slice(&pbytes);
459    // (e) feed file ID[0].
460    buf.extend_from_slice(file_id);
461    // (f) for R≥4 with EncryptMetadata=false, feed 0xFFFFFFFF.
462    if p.revision >= 4 && !p.encrypt_metadata {
463        buf.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
464    }
465    // (g) finish.
466    let mut h = md5(&buf);
467    // (h) for R≥3, loop 50 times MD5 of first n bytes.
468    if p.revision >= 3 {
469        for _ in 0..50 {
470            h = md5(&h[..n]);
471        }
472    }
473    // (i) take first n bytes.
474    h[..n].to_vec()
475}
476
477/// Pad / truncate a password to exactly 32 bytes per Algorithm 2 step (a).
478fn pad_password(password: &[u8]) -> [u8; 32] {
479    let mut out = [0u8; 32];
480    let take = password.len().min(32);
481    out[..take].copy_from_slice(&password[..take]);
482    if take < 32 {
483        out[take..].copy_from_slice(&PAD[..32 - take]);
484    }
485    out
486}
487
488/// Algorithm 6 — authenticate a candidate user password.
489fn try_user_password(
490    p: &EncryptParams,
491    file_id: &[u8],
492    password: &[u8],
493) -> Option<StandardHandler> {
494    let key = compute_key(p, file_id, password);
495    let derived_u = derive_u(p, file_id, &key);
496    // R=2: full 32-byte match. R≥3: only first 16 bytes match (the rest
497    // is arbitrary padding — see Algorithm 5 step (f)).
498    let cmp_len = if p.revision >= 3 { 16 } else { 32 };
499    if constant_time_eq(&derived_u[..cmp_len], &p.u[..cmp_len]) {
500        Some(StandardHandler {
501            key,
502            method: p.cfm,
503            revision: p.revision,
504        })
505    } else {
506        None
507    }
508}
509
510/// Re-derive the `/U` value the writer would have stored, given a
511/// candidate file key. This is the meat of Algorithms 4 and 5 — once
512/// it produces something matching the `/U` in the dict, the password
513/// is correct.
514fn derive_u(p: &EncryptParams, file_id: &[u8], key: &[u8]) -> [u8; 32] {
515    if p.revision == 2 {
516        // Algorithm 4: encrypt the 32-byte pad with the file key.
517        let cipher = rc4(key, &PAD);
518        let mut out = [0u8; 32];
519        out.copy_from_slice(&cipher);
520        out
521    } else {
522        // Algorithm 5: hash(pad ‖ file_id), encrypt with file key, then
523        // 19 more rounds of RC4 with byte-XOR'd keys.
524        let mut hash_input = Vec::with_capacity(32 + file_id.len());
525        hash_input.extend_from_slice(&PAD);
526        hash_input.extend_from_slice(file_id);
527        let h = md5(&hash_input);
528        let mut data = rc4(key, &h);
529        for i in 1u8..=19 {
530            let xor_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
531            data = rc4(&xor_key, &data);
532        }
533        // Pad with 16 arbitrary bytes — the algorithm uses anything;
534        // we use zeros. Authentication only compares the first 16 bytes
535        // for R≥3 anyway.
536        let mut out = [0u8; 32];
537        out[..16].copy_from_slice(&data[..16]);
538        out
539    }
540}
541
542/// Algorithm 7 — authenticate a candidate owner password. The /O
543/// entry is a double encryption of the user password by an MD5 chain
544/// of the owner password. We undo the chain to recover the user
545/// password and then try Algorithm 6 on it.
546fn try_owner_password(
547    p: &EncryptParams,
548    file_id: &[u8],
549    password: &[u8],
550) -> Option<StandardHandler> {
551    // Steps (a)..(d) of Algorithm 3: derive an RC4 key from the owner
552    // password (or the user password if no owner password is set —
553    // which is exactly what we're attempting here).
554    let n = p.length_bits / 8;
555    let pwd = pad_password(password);
556    let mut h = md5(&pwd);
557    if p.revision >= 3 {
558        for _ in 0..50 {
559            h = md5(&h[..n]);
560        }
561    }
562    let owner_key = h[..n].to_vec();
563
564    // (b) of Algorithm 7: undo the RC4 ladder on /O.
565    let recovered_user_pwd = if p.revision == 2 {
566        rc4(&owner_key, &p.o)
567    } else {
568        let mut buf = p.o.clone();
569        // Iterations 19..=0, each with the owner_key XOR i.
570        for i in (0u8..=19).rev() {
571            let xor_key: Vec<u8> = owner_key.iter().map(|b| b ^ i).collect();
572            buf = rc4(&xor_key, &buf);
573        }
574        buf
575    };
576
577    // (c) — `recovered_user_pwd` is the padded user password (32 bytes).
578    // Drop the pad string suffix to get the original user password.
579    let user_pwd = strip_pad(&recovered_user_pwd);
580    try_user_password(p, file_id, &user_pwd)
581}
582
583/// Strip the canonical pad suffix from a 32-byte padded password,
584/// returning the original. The padded buffer has shape
585/// `password ‖ PAD[..32 - password.len()]`; we find the smallest
586/// `L` such that `padded[L..]` equals `PAD[..32 - L]` and return
587/// `padded[..L]`.
588///
589/// If the buffer has no recognisable pad suffix the original was 32
590/// bytes long with no padding — return all 32 bytes.
591fn strip_pad(padded: &[u8]) -> Vec<u8> {
592    let n = padded.len();
593    for l in 0..=n {
594        if padded[l..] == PAD[..n - l] {
595            return padded[..l].to_vec();
596        }
597    }
598    padded.to_vec()
599}
600
601pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
602    if a.len() != b.len() {
603        return false;
604    }
605    let mut diff = 0u8;
606    for (x, y) in a.iter().zip(b.iter()) {
607        diff |= x ^ y;
608    }
609    diff == 0
610}
611
612// ───────────────────────── RC4 ─────────────────────────
613
614/// RC4 stream cipher — output XOR'd with input.
615///
616/// Symmetric: `rc4(key, rc4(key, plain)) == plain`. Pure 40-line
617/// implementation; the algorithm's tiny S-table key schedule + PRGA
618/// is well-described in any cryptography textbook.
619pub fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
620    debug_assert!(!key.is_empty(), "RC4 key must not be empty");
621    let mut s: [u8; 256] = [0; 256];
622    for (i, b) in s.iter_mut().enumerate() {
623        *b = i as u8;
624    }
625    // KSA — key-scheduling algorithm.
626    let mut j: u8 = 0;
627    for i in 0..256 {
628        j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
629        s.swap(i, j as usize);
630    }
631    // PRGA — pseudo-random generation.
632    let mut i: u8 = 0;
633    j = 0;
634    let mut out = Vec::with_capacity(data.len());
635    for &byte in data {
636        i = i.wrapping_add(1);
637        j = j.wrapping_add(s[i as usize]);
638        s.swap(i as usize, j as usize);
639        let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
640        out.push(byte ^ k);
641    }
642    out
643}
644
645// ───────────────────────── AES-128 CBC ─────────────────────────
646
647/// AES-256 CBC ECB-mode primitive (single 16-byte block, no IV)
648/// used by Algorithms 8 / 9 / 10 / 13 of ISO 32000-2 §7.6.4.4. The
649/// spec calls this "AES-256, no padding, with an IV of zero" — which
650/// is the ECB mode of AES-256 over a single block. Used to undo the
651/// Algorithm 8/9 wrap of the file key, and the Algorithm 10 Perms
652/// block.
653pub(crate) fn aes256_ecb_decrypt_block(key: &[u8], block: &[u8; 16]) -> Result<[u8; 16], PdfError> {
654    use aes::cipher::{BlockDecrypt, KeyInit};
655    if key.len() != 32 {
656        return Err(PdfError::other(format!(
657            "PDF decrypt: AES-256 requires a 32-byte key (got {} bytes)",
658            key.len()
659        )));
660    }
661    let cipher = aes::Aes256::new(key.into());
662    let mut buf = aes::cipher::generic_array::GenericArray::clone_from_slice(block);
663    cipher.decrypt_block(&mut buf);
664    let mut out = [0u8; 16];
665    out.copy_from_slice(&buf);
666    Ok(out)
667}
668
669/// Decrypt an AES-256-CBC blob with the first 16 bytes being the IV
670/// (per ISO 32000-2 §7.6.3.1 — V=5 uses the same IV-prepended layout
671/// as AESV2). Removes PKCS#7 padding.
672fn aes256_cbc_decrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>, PdfError> {
673    use aes::cipher::{BlockDecryptMut, KeyIvInit};
674    type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
675    if data.len() < 16 {
676        return Err(PdfError::other(
677            "PDF decrypt: AES-256 ciphertext shorter than IV",
678        ));
679    }
680    if (data.len() - 16) % 16 != 0 {
681        return Err(PdfError::other(format!(
682            "PDF decrypt: AES-256 ciphertext length {} not aligned to 16-byte blocks (after IV)",
683            data.len() - 16
684        )));
685    }
686    if key.len() != 32 {
687        return Err(PdfError::other(format!(
688            "PDF decrypt: AES-256 expects a 32-byte key (got {} bytes)",
689            key.len()
690        )));
691    }
692    let iv = &data[..16];
693    let ct = &data[16..];
694    let dec = Aes256CbcDec::new(key.into(), iv.into());
695    let mut buf = ct.to_vec();
696    let pt = dec
697        .decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf)
698        .map_err(|e| PdfError::other(format!("PDF decrypt: AES-256 padding error: {e:?}")))?;
699    Ok(pt.to_vec())
700}
701
702/// Encrypt with AES-256 CBC (PKCS#7 padding), prepending the IV. The
703/// inverse of [`aes256_cbc_decrypt`].
704fn aes256_cbc_encrypt(key: &[u8], data: &[u8], iv: &[u8; 16]) -> Result<Vec<u8>, PdfError> {
705    use aes::cipher::{BlockEncryptMut, KeyIvInit};
706    type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
707    if key.len() != 32 {
708        return Err(PdfError::other(format!(
709            "PDF encrypt: AES-256 expects a 32-byte key (got {} bytes)",
710            key.len()
711        )));
712    }
713    let enc = Aes256CbcEnc::new(key.into(), iv.into());
714    let pad_block = (data.len() / 16) + 1;
715    let mut buf = vec![0u8; pad_block * 16];
716    let n = enc
717        .encrypt_padded_b2b_mut::<aes::cipher::block_padding::Pkcs7>(data, &mut buf)
718        .map_err(|e| PdfError::other(format!("PDF encrypt: AES-256 padding error: {e:?}")))?
719        .len();
720    buf.truncate(n);
721    let mut out = Vec::with_capacity(16 + n);
722    out.extend_from_slice(iv);
723    out.extend_from_slice(&buf);
724    Ok(out)
725}
726
727/// Encrypt with AES-128 CBC (PKCS#7 padding), prepending the IV. The
728/// inverse of [`aes128_cbc_decrypt`].
729fn aes128_cbc_encrypt(key: &[u8], data: &[u8], iv: &[u8; 16]) -> Result<Vec<u8>, PdfError> {
730    use aes::cipher::{BlockEncryptMut, KeyIvInit};
731    type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
732    if key.len() != 16 {
733        return Err(PdfError::other(format!(
734            "PDF encrypt: AES-128 expects a 16-byte key (got {} bytes)",
735            key.len()
736        )));
737    }
738    let enc = Aes128CbcEnc::new(key.into(), iv.into());
739    let pad_block = (data.len() / 16) + 1;
740    let mut buf = vec![0u8; pad_block * 16];
741    let n = enc
742        .encrypt_padded_b2b_mut::<aes::cipher::block_padding::Pkcs7>(data, &mut buf)
743        .map_err(|e| PdfError::other(format!("PDF encrypt: AES-128 padding error: {e:?}")))?
744        .len();
745    buf.truncate(n);
746    let mut out = Vec::with_capacity(16 + n);
747    out.extend_from_slice(iv);
748    out.extend_from_slice(&buf);
749    Ok(out)
750}
751
752/// Decrypt an AES-128-CBC blob whose first 16 bytes are the IV
753/// (per §7.6.2 Algorithm 1, AES-only paragraph). Removes PKCS#7
754/// padding.
755fn aes128_cbc_decrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>, PdfError> {
756    use aes::cipher::{BlockDecryptMut, KeyIvInit};
757    type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
758    if data.len() < 16 {
759        return Err(PdfError::other(
760            "PDF decrypt: AES-128 ciphertext shorter than IV",
761        ));
762    }
763    if (data.len() - 16) % 16 != 0 {
764        return Err(PdfError::other(format!(
765            "PDF decrypt: AES-128 ciphertext length {} not aligned to 16-byte blocks (after IV)",
766            data.len() - 16
767        )));
768    }
769    if key.len() != 16 {
770        return Err(PdfError::other(format!(
771            "PDF decrypt: AES-128 expects a 16-byte key (got {} bytes)",
772            key.len()
773        )));
774    }
775    let iv = &data[..16];
776    let ct = &data[16..];
777    let dec = Aes128CbcDec::new(key.into(), iv.into());
778    let mut buf = ct.to_vec();
779    let pt = dec
780        .decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf)
781        .map_err(|e| PdfError::other(format!("PDF decrypt: AES-128 padding error: {e:?}")))?;
782    Ok(pt.to_vec())
783}
784
785// ───────────────────────── MD5 (RFC 1321) ─────────────────────────
786//
787// 80-line reference implementation. Used only for password / key
788// derivation per §7.6 — never for content authentication. A constant-time
789// implementation isn't required because all inputs are derived from
790// the password, which the caller is expected to know.
791
792const MD5_S: [u32; 64] = [
793    7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, // round 1
794    5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, // round 2
795    4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, // round 3
796    6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, // round 4
797];
798
799const MD5_K: [u32; 64] = [
800    0xD76AA478, 0xE8C7B756, 0x242070DB, 0xC1BDCEEE, 0xF57C0FAF, 0x4787C62A, 0xA8304613, 0xFD469501,
801    0x698098D8, 0x8B44F7AF, 0xFFFF5BB1, 0x895CD7BE, 0x6B901122, 0xFD987193, 0xA679438E, 0x49B40821,
802    0xF61E2562, 0xC040B340, 0x265E5A51, 0xE9B6C7AA, 0xD62F105D, 0x02441453, 0xD8A1E681, 0xE7D3FBC8,
803    0x21E1CDE6, 0xC33707D6, 0xF4D50D87, 0x455A14ED, 0xA9E3E905, 0xFCEFA3F8, 0x676F02D9, 0x8D2A4C8A,
804    0xFFFA3942, 0x8771F681, 0x6D9D6122, 0xFDE5380C, 0xA4BEEA44, 0x4BDECFA9, 0xF6BB4B60, 0xBEBFBC70,
805    0x289B7EC6, 0xEAA127FA, 0xD4EF3085, 0x04881D05, 0xD9D4D039, 0xE6DB99E5, 0x1FA27CF8, 0xC4AC5665,
806    0xF4292244, 0x432AFF97, 0xAB9423A7, 0xFC93A039, 0x655B59C3, 0x8F0CCC92, 0xFFEFF47D, 0x85845DD1,
807    0x6FA87E4F, 0xFE2CE6E0, 0xA3014314, 0x4E0811A1, 0xF7537E82, 0xBD3AF235, 0x2AD7D2BB, 0xEB86D391,
808];
809
810/// MD5 of the input bytes. Returns the 16-byte digest.
811pub fn md5(input: &[u8]) -> [u8; 16] {
812    let mut state: [u32; 4] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476];
813
814    // Pad to a multiple of 64 bytes: append 0x80, then zeros, then the
815    // 64-bit input length (in bits) little-endian.
816    let mut buf = input.to_vec();
817    let bit_len = (input.len() as u64).wrapping_mul(8);
818    buf.push(0x80);
819    while buf.len() % 64 != 56 {
820        buf.push(0);
821    }
822    buf.extend_from_slice(&bit_len.to_le_bytes());
823
824    for chunk in buf.chunks_exact(64) {
825        let mut m = [0u32; 16];
826        for (i, w) in chunk.chunks_exact(4).enumerate() {
827            m[i] = u32::from_le_bytes([w[0], w[1], w[2], w[3]]);
828        }
829        let mut a = state[0];
830        let mut b = state[1];
831        let mut c = state[2];
832        let mut d = state[3];
833        for i in 0..64 {
834            let (f, g) = match i {
835                0..=15 => ((b & c) | (!b & d), i),
836                16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
837                32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
838                _ => (c ^ (b | !d), (7 * i) % 16),
839            };
840            let temp = d;
841            d = c;
842            c = b;
843            b = b.wrapping_add(
844                a.wrapping_add(f)
845                    .wrapping_add(MD5_K[i])
846                    .wrapping_add(m[g])
847                    .rotate_left(MD5_S[i]),
848            );
849            a = temp;
850        }
851        state[0] = state[0].wrapping_add(a);
852        state[1] = state[1].wrapping_add(b);
853        state[2] = state[2].wrapping_add(c);
854        state[3] = state[3].wrapping_add(d);
855    }
856
857    let mut out = [0u8; 16];
858    for (i, s) in state.iter().enumerate() {
859        out[4 * i..4 * (i + 1)].copy_from_slice(&s.to_le_bytes());
860    }
861    out
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867
868    // ─── MD5 known-answer tests (RFC 1321 §A.5) ────────────────
869    #[test]
870    fn md5_empty_string() {
871        assert_eq!(
872            md5(b""),
873            [
874                0xD4, 0x1D, 0x8C, 0xD9, 0x8F, 0x00, 0xB2, 0x04, 0xE9, 0x80, 0x09, 0x98, 0xEC, 0xF8,
875                0x42, 0x7E
876            ]
877        );
878    }
879
880    #[test]
881    fn md5_short_inputs() {
882        // "a" → 0CC175B9C0F1B6A831C399E269772661
883        assert_eq!(
884            md5(b"a"),
885            [
886                0x0C, 0xC1, 0x75, 0xB9, 0xC0, 0xF1, 0xB6, 0xA8, 0x31, 0xC3, 0x99, 0xE2, 0x69, 0x77,
887                0x26, 0x61
888            ]
889        );
890        // "abc" → 900150983CD24FB0D6963F7D28E17F72
891        assert_eq!(
892            md5(b"abc"),
893            [
894                0x90, 0x01, 0x50, 0x98, 0x3C, 0xD2, 0x4F, 0xB0, 0xD6, 0x96, 0x3F, 0x7D, 0x28, 0xE1,
895                0x7F, 0x72
896            ]
897        );
898        // "message digest" → F96B697D7CB7938D525A2F31AAF161D0
899        assert_eq!(
900            md5(b"message digest"),
901            [
902                0xF9, 0x6B, 0x69, 0x7D, 0x7C, 0xB7, 0x93, 0x8D, 0x52, 0x5A, 0x2F, 0x31, 0xAA, 0xF1,
903                0x61, 0xD0
904            ]
905        );
906    }
907
908    #[test]
909    fn md5_long_block() {
910        // "abcdefghijklmnopqrstuvwxyz" → C3FCD3D76192E4007DFB496CCA67E13B
911        assert_eq!(
912            md5(b"abcdefghijklmnopqrstuvwxyz"),
913            [
914                0xC3, 0xFC, 0xD3, 0xD7, 0x61, 0x92, 0xE4, 0x00, 0x7D, 0xFB, 0x49, 0x6C, 0xCA, 0x67,
915                0xE1, 0x3B
916            ]
917        );
918    }
919
920    #[test]
921    fn md5_multi_block() {
922        // 80-byte input crosses the 64-byte block boundary.
923        let s = b"12345678901234567890123456789012345678901234567890123456789012345678901234567890";
924        // Expected per RFC 1321 §A.5
925        assert_eq!(
926            md5(s),
927            [
928                0x57, 0xED, 0xF4, 0xA2, 0x2B, 0xE3, 0xC9, 0x55, 0xAC, 0x49, 0xDA, 0x2E, 0x21, 0x07,
929                0xB6, 0x7A
930            ]
931        );
932    }
933
934    // ─── RC4 known-answer tests (RFC 6229 §2) ──────────────────
935    #[test]
936    fn rc4_rfc6229_key0102030405() {
937        // Key = 0x0102030405, plaintext = 16 zero bytes
938        // Expected first 16 keystream bytes per RFC 6229:
939        //   b2 39 63 05 f0 3d c0 27 cc c3 52 4a 0a 11 18 a8
940        let key = [0x01, 0x02, 0x03, 0x04, 0x05];
941        let pt = [0u8; 16];
942        let ct = rc4(&key, &pt);
943        assert_eq!(
944            ct,
945            vec![
946                0xB2, 0x39, 0x63, 0x05, 0xF0, 0x3D, 0xC0, 0x27, 0xCC, 0xC3, 0x52, 0x4A, 0x0A, 0x11,
947                0x18, 0xA8
948            ]
949        );
950    }
951
952    #[test]
953    fn rc4_rfc6229_key0102030405060708() {
954        // Key = 0x0102030405060708 (8 bytes), plaintext = 16 zero bytes.
955        // Expected keystream: 97 ab 8a 1b f0 af b9 61 32 f2 f6 72 58 da 15 a8
956        let key = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
957        let pt = [0u8; 16];
958        let ct = rc4(&key, &pt);
959        assert_eq!(
960            ct,
961            vec![
962                0x97, 0xAB, 0x8A, 0x1B, 0xF0, 0xAF, 0xB9, 0x61, 0x32, 0xF2, 0xF6, 0x72, 0x58, 0xDA,
963                0x15, 0xA8
964            ]
965        );
966    }
967
968    #[test]
969    fn rc4_self_inverse() {
970        // RC4 is symmetric.
971        let key = b"Key";
972        let plain = b"Plaintext";
973        let cipher = rc4(key, plain);
974        let recovered = rc4(key, &cipher);
975        assert_eq!(&recovered, plain);
976    }
977
978    // ─── AES-128 CBC known-answer (FIPS 197 reformulated for CBC) ──
979    #[test]
980    fn aes128_cbc_decrypt_round_trips_pkcs7() {
981        use aes::cipher::{BlockEncryptMut, KeyIvInit};
982        type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
983        let key = [0x42u8; 16];
984        let iv = [0x17u8; 16];
985        let pt = b"hello world".to_vec();
986        let mut buf = vec![0u8; 16 + ((pt.len() / 16) + 1) * 16];
987        let enc = Aes128CbcEnc::new((&key).into(), (&iv).into());
988        let n = enc
989            .encrypt_padded_b2b_mut::<aes::cipher::block_padding::Pkcs7>(&pt, &mut buf[16..])
990            .unwrap()
991            .len();
992        buf[..16].copy_from_slice(&iv);
993        buf.truncate(16 + n);
994        let recovered = aes128_cbc_decrypt(&key, &buf).unwrap();
995        assert_eq!(recovered, pt);
996    }
997
998    // ─── Algorithm 2 (encryption-key derivation) ────────────────
999    #[test]
1000    fn algorithm_2_known_answer_r3_empty_password() {
1001        // Hand-computed test vector. With:
1002        //   password = b""
1003        //   O        = 32 bytes of 0xAA
1004        //   P        = -4 (printing + copy + modify allowed; bits cleared)
1005        //   ID[0]    = 16 bytes of 0xBB
1006        //   R = 3, Length = 128, EncryptMetadata = true (default)
1007        // The key is the first 16 bytes of MD5^51(pad ‖ O ‖ P_le ‖ ID).
1008        let p = EncryptParams {
1009            revision: 3,
1010            length_bits: 128,
1011            o: vec![0xAA; 32],
1012            u: vec![0; 32],
1013            oe: Vec::new(),
1014            ue: Vec::new(),
1015            perms: Vec::new(),
1016            p: -4,
1017            encrypt_metadata: true,
1018            cfm: CryptMethod::Rc4,
1019        };
1020        let id = vec![0xBB; 16];
1021        let k = compute_key(&p, &id, b"");
1022        // Recompute by hand to lock the test vector.
1023        let mut buf = Vec::new();
1024        buf.extend_from_slice(&PAD);
1025        buf.extend_from_slice(&p.o);
1026        buf.extend_from_slice(&((-4i32) as u32).to_le_bytes());
1027        buf.extend_from_slice(&id);
1028        let mut h = md5(&buf);
1029        for _ in 0..50 {
1030            h = md5(&h[..16]);
1031        }
1032        assert_eq!(k, h[..16].to_vec());
1033    }
1034
1035    // ─── Algorithm 1 — per-object key derivation ────────────────
1036    #[test]
1037    fn algorithm_1_object_key_rc4() {
1038        let h = StandardHandler {
1039            key: vec![0x01, 0x02, 0x03, 0x04, 0x05], // 40-bit
1040            method: CryptMethod::Rc4,
1041            revision: 2,
1042        };
1043        let id = ObjectId {
1044            number: 0x010203,
1045            generation: 0x0405,
1046        };
1047        let k = h.object_key(id);
1048        // n + 5 = 10 bytes; capped at 16. n=5 so we expect 10.
1049        assert_eq!(k.len(), 10);
1050        // Verify the input to the MD5 hash is right.
1051        let mut buf = Vec::new();
1052        buf.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05]);
1053        buf.extend_from_slice(&[0x03, 0x02, 0x01]); // obj_num le3
1054        buf.extend_from_slice(&[0x05, 0x04]); // gen_num le2
1055        let h2 = md5(&buf);
1056        assert_eq!(k, h2[..10].to_vec());
1057    }
1058
1059    #[test]
1060    fn algorithm_1_object_key_aes_appends_salt() {
1061        let h = StandardHandler {
1062            key: vec![0u8; 16],
1063            method: CryptMethod::Aes128,
1064            revision: 4,
1065        };
1066        let id = ObjectId {
1067            number: 1,
1068            generation: 0,
1069        };
1070        let k = h.object_key(id);
1071        // n + 5 = 21, capped at 16.
1072        assert_eq!(k.len(), 16);
1073        let mut buf = Vec::new();
1074        buf.extend_from_slice(&h.key);
1075        buf.extend_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00]);
1076        buf.extend_from_slice(&AES_SALT);
1077        let h2 = md5(&buf);
1078        assert_eq!(k, h2[..16].to_vec());
1079    }
1080
1081    // ─── Self-roundtrip — encrypt + decrypt a known string ──────
1082    #[test]
1083    fn rc4_object_roundtrip_via_handler() {
1084        let h = StandardHandler {
1085            key: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42],
1086            method: CryptMethod::Rc4,
1087            revision: 2,
1088        };
1089        let id = ObjectId {
1090            number: 7,
1091            generation: 0,
1092        };
1093        let plain = b"Hello, encrypted world!".to_vec();
1094        // Using the public API: decrypt(decrypt(plain)) == plain (RC4
1095        // is symmetric).
1096        let cipher = h.decrypt_object(id, &plain).unwrap();
1097        let recovered = h.decrypt_object(id, &cipher).unwrap();
1098        assert_eq!(recovered, plain);
1099    }
1100
1101    // ─── Strip-pad helper ───────────────────────────────────────
1102    #[test]
1103    fn strip_pad_recovers_short_passwords() {
1104        let mut padded = Vec::from(b"hello".as_slice());
1105        padded.extend_from_slice(&PAD[..27]);
1106        assert_eq!(strip_pad(&padded), b"hello".to_vec());
1107    }
1108
1109    #[test]
1110    fn strip_pad_empty_password() {
1111        assert_eq!(strip_pad(&PAD), Vec::<u8>::new());
1112    }
1113}