Skip to main content

pdfrum_crypt/
handler.rs

1//! The standard security handler: which cipher, which key, and what the
2//! password unlocked.
3
4use pdfrum_object::{Dict, Name, ObjRef, Resolve, names};
5use zeroize::Zeroize;
6
7use crate::key::SmallKey;
8use crate::object::{self, CryptClass, Iv};
9use crate::permissions::Permissions;
10use crate::standard::{self, Cipher, EncryptParams, PasswordEncoding, parse_encrypt_dict};
11
12/// What went wrong building a security handler.
13///
14/// The C++ collapses every one of these into a single "password error" at the
15/// parser boundary; splitting them changes no document's fate but lets a
16/// caller tell "this needs a password" from "we cannot do this document's
17/// cryptography".
18#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
19#[non_exhaustive]
20pub enum Error {
21    /// The password is neither the user nor the owner password.
22    #[error("the supplied password is not the user or owner password")]
23    WrongPassword,
24    /// The operating system's cryptographic generator is unavailable, so no
25    /// file key can be minted. Raised only when creating an encrypted file;
26    /// opening one needs no randomness.
27    #[error("the operating system's random generator is unavailable")]
28    NoEntropy,
29    /// `/Filter` names a handler other than `/Standard`. Public-key handlers
30    /// (`/Adobe.PubSec`) land here.
31    #[error("/Filter {0:?} is not the standard security handler")]
32    UnsupportedHandler(Box<[u8]>),
33    /// `[oracle-bug]` **Never constructed.** §7.6.5 table 20 makes `/StmF`
34    /// and `/StrF` independent, so a pair naming different crypt filters is
35    /// conformant and this crate resolves each class separately. The variant
36    /// is kept because it is a public enum member and removing it is a
37    /// breaking change no caller gains from; nothing produces it.
38    // [oracle-bug] cpdf_security_handler.cpp:305 and :325 return false on a
39    // differing pair.
40    #[error("/StmF and /StrF name different crypt filters")]
41    MismatchedCryptFilters,
42    /// The named crypt filter is not a key in `/CF`. An **empty** name is
43    /// §7.6.5's `/Identity` default rather than an error.
44    // [oracle-bug] cpdf_security_handler.cpp:305 rejects the absent pair.
45    #[error("crypt filter {0:?} is not present in /CF")]
46    MissingCryptFilter(Box<[u8]>),
47    /// The dictionary is structurally unusable.
48    #[error("/Encrypt is malformed: {0}")]
49    MalformedEncryptDict(&'static str),
50    /// The resolved key length is one this cipher does not accept.
51    #[error("key length {len} bytes is invalid for {cipher}")]
52    CipherKeyLength {
53        /// The cipher that rejected the length.
54        cipher: &'static str,
55        /// The length in bytes.
56        len: usize,
57    },
58}
59
60/// A document's decryption state: which cipher, which key, and what the
61/// password unlocked.
62///
63/// A closed enum rather than a trait, because the set of standard security
64/// handlers is closed: adding a variant must break every `match` on it.
65/// `Rc4V2` covers `/V 1` to `/V 4` without an AES crypt filter; `AesV4` is
66/// AESV2, whose per-object key is an MD5 over the object number and the four
67/// bytes `sAlT`; `AesV5` is AESV3, whose 32-byte key is used verbatim with no
68/// per-object derivation; `Identity` is both an unencrypted document and one
69/// naming `/Identity` as its crypt filter.
70#[derive(Debug, Clone)]
71pub enum SecurityHandler {
72    /// RC4, with a 5- to 16-byte file key.
73    Rc4V2 {
74        /// The file encryption key.
75        key: SmallKey,
76        /// `/R`, the handler revision.
77        revision: u8,
78        /// `/P`, as the unsigned word permissions are compared as.
79        permissions: u32,
80        /// Whether the owner password was the one that opened the document.
81        owner_unlocked: bool,
82        /// `/EncryptMetadata`.
83        encrypt_metadata: bool,
84        /// Which password spelling worked.
85        encoding: PasswordEncoding,
86        /// The cipher `/EFF` names for embedded file streams, when it differs
87        /// from this variant's own (ISO 32000-1 §7.6.5 table 20). `None` is
88        /// table 20's default: the embedded class uses the stream cipher.
89        embedded_cipher: Option<Cipher>,
90        /// `[oracle-bug]` Whether `/StrF` resolved to `/Identity` while
91        /// `/StmF` did not, so strings pass through undeciphered while
92        /// streams are enciphered. §7.6.5 makes the two entries independent,
93        /// so such a document is conformant and is opened.
94        // [oracle-bug] cpdf_security_handler.cpp:305 refuses it outright.
95        strings_identity: bool,
96    },
97    /// AESV2: a 16- or 24-byte file key with per-object `sAlT` derivation.
98    AesV4 {
99        /// The file encryption key.
100        key: SmallKey,
101        /// `/R`, the handler revision.
102        revision: u8,
103        /// `/P`, as the unsigned word permissions are compared as.
104        permissions: u32,
105        /// Whether the owner password was the one that opened the document.
106        owner_unlocked: bool,
107        /// `/EncryptMetadata`.
108        encrypt_metadata: bool,
109        /// Which password spelling worked.
110        encoding: PasswordEncoding,
111        /// The cipher `/EFF` names for embedded file streams, when it differs
112        /// from this variant's own (ISO 32000-1 §7.6.5 table 20). `None` is
113        /// table 20's default: the embedded class uses the stream cipher.
114        embedded_cipher: Option<Cipher>,
115        /// `[oracle-bug]` Whether `/StrF` resolved to `/Identity` while
116        /// `/StmF` did not, so strings pass through undeciphered while
117        /// streams are enciphered. §7.6.5 makes the two entries independent,
118        /// so such a document is conformant and is opened.
119        // [oracle-bug] cpdf_security_handler.cpp:305 refuses it outright.
120        strings_identity: bool,
121    },
122    /// AESV3 (`/V 5`, revision 5 or 6): the 32-byte key is used as-is.
123    AesV5 {
124        /// The file encryption key.
125        key: Box<[u8; 32]>,
126        /// `/R`, the handler revision.
127        revision: u8,
128        /// `/P`, as the unsigned word permissions are compared as.
129        permissions: u32,
130        /// Whether the owner password was the one that opened the document.
131        owner_unlocked: bool,
132        /// `/EncryptMetadata`.
133        encrypt_metadata: bool,
134        /// Which password spelling worked.
135        encoding: PasswordEncoding,
136        /// The cipher `/EFF` names for embedded file streams, when it differs
137        /// from this variant's own (ISO 32000-1 §7.6.5 table 20). `None` is
138        /// table 20's default: the embedded class uses the stream cipher.
139        embedded_cipher: Option<Cipher>,
140        /// `[oracle-bug]` Whether `/StrF` resolved to `/Identity` while
141        /// `/StmF` did not, so strings pass through undeciphered while
142        /// streams are enciphered. §7.6.5 makes the two entries independent,
143        /// so such a document is conformant and is opened.
144        // [oracle-bug] cpdf_security_handler.cpp:305 refuses it outright.
145        strings_identity: bool,
146    },
147    /// No encryption, or `/StrF /Identity`.
148    Identity,
149}
150
151impl Drop for SecurityHandler {
152    fn drop(&mut self) {
153        if let Self::AesV5 { key, .. } = self {
154            key.zeroize();
155        }
156    }
157}
158
159impl SecurityHandler {
160    /// Build a handler from the trailer's `/Encrypt` dictionary.
161    ///
162    /// `file_id` is the first element of the trailer's `/ID` array as raw
163    /// bytes; pass `&[]` when `/ID` is absent, which contributes nothing to
164    /// the key rather than an empty marker. `password` is raw bytes, uncapped
165    /// — the specification's 127-byte limit is not enforced. A non-empty
166    /// password is tried as the owner password first and only then as the
167    /// user password; an empty one is only ever a user password.
168    ///
169    /// ```
170    /// use pdfrum_crypt::{Error, SecurityHandler};
171    /// use pdfrum_object::{Dict, NoResolve, Object, PdfString, names};
172    ///
173    /// // A public-key handler is not the standard one.
174    /// let dict = Dict::from_pairs([(
175    ///     names::FILTER.clone(),
176    ///     Object::Name(pdfrum_object::Name::from("Adobe.PubSec")),
177    /// )]);
178    /// assert!(matches!(
179    ///     SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve),
180    ///     Err(Error::UnsupportedHandler(_))
181    /// ));
182    /// # let _ = PdfString::literal(b"");
183    /// ```
184    ///
185    /// # Errors
186    ///
187    /// [`Error::WrongPassword`] when neither role accepts the password, and
188    /// the parse errors of [`parse_encrypt_dict`] when the dictionary itself
189    /// cannot be used.
190    pub fn from_encrypt_dict(
191        dict: &Dict,
192        file_id: &[u8],
193        password: &[u8],
194        r: &impl Resolve,
195    ) -> Result<Self, Error> {
196        let params = parse_encrypt_dict(dict, r)?;
197        // Only a document whose *stream* class is Identity has nothing to
198        // decipher through this handler; a `/StrF /Identity` beside an
199        // enciphering `/StmF` is carried as `strings_identity` instead.
200        if params.cipher == Cipher::None {
201            return Ok(Self::Identity);
202        }
203
204        if !password.is_empty()
205            && let Some(unlocked) = standard::try_password(&params, password, true, file_id)
206        {
207            return Ok(Self::assemble(&params, unlocked, true));
208        }
209        standard::try_password(&params, password, false, file_id)
210            .map(|unlocked| Self::assemble(&params, unlocked, false))
211            .ok_or(Error::WrongPassword)
212    }
213
214    /// Pick the variant the resolved cipher and key length call for.
215    fn assemble(
216        params: &EncryptParams,
217        unlocked: standard::Unlocked,
218        owner_unlocked: bool,
219    ) -> Self {
220        let standard::Unlocked { key, encoding } = unlocked;
221        let revision = u8::try_from(params.revision).unwrap_or(u8::MAX);
222        let permissions = params.permissions;
223        let encrypt_metadata = params.encrypt_metadata;
224        // `[oracle-bug]` `/StrF /Identity` beside an enciphering `/StmF` is a
225        // conformant document (§7.6.5), not the refusal
226        // `cpdf_security_handler.cpp:305` gives it.
227        let strings_identity = params.string_cipher == Cipher::None;
228        match params.cipher {
229            Cipher::None => Self::Identity,
230            Cipher::Rc4 => Self::Rc4V2 {
231                key,
232                revision,
233                permissions,
234                owner_unlocked,
235                encrypt_metadata,
236                encoding,
237                embedded_cipher: params.embedded_cipher,
238                strings_identity,
239            },
240            // AESV3 is exactly "AES with a 32-byte key"; PDFium never reads
241            // the /CFM name to tell the two apart.
242            Cipher::Aes if key.len() == SmallKey::MAX_LEN => {
243                let mut full = [0u8; 32];
244                if let Some(head) = full.get_mut(..key.len()) {
245                    head.copy_from_slice(key.bytes());
246                }
247                Self::AesV5 {
248                    key: Box::new(full),
249                    revision,
250                    permissions,
251                    owner_unlocked,
252                    encrypt_metadata,
253                    encoding,
254                    embedded_cipher: params.embedded_cipher,
255                    strings_identity,
256                }
257            }
258            Cipher::Aes => Self::AesV4 {
259                key,
260                revision,
261                permissions,
262                owner_unlocked,
263                encrypt_metadata,
264                encoding,
265                embedded_cipher: params.embedded_cipher,
266                strings_identity,
267            },
268        }
269    }
270
271    /// `[oracle-bug]` Whether the string class resolved to `/Identity` while
272    /// the stream class did not, so strings in this document are plaintext.
273    ///
274    /// Always `false` for [`Self::Identity`], which has nothing to contrast
275    /// against — an unencrypted document's strings are plaintext anyway.
276    #[must_use]
277    pub const fn strings_identity(&self) -> bool {
278        match self {
279            Self::Identity => false,
280            Self::Rc4V2 {
281                strings_identity, ..
282            }
283            | Self::AesV4 {
284                strings_identity, ..
285            }
286            | Self::AesV5 {
287                strings_identity, ..
288            } => *strings_identity,
289        }
290    }
291
292    /// Decrypt one string or stream payload belonging to indirect object
293    /// `obj`.
294    ///
295    /// Infallible by design: PDFium never fails a decrypt, it produces a
296    /// best-effort result. An AES payload shorter than seventeen bytes, a
297    /// trailing partial block, and a final block whose padding byte is out of
298    /// range all yield less output than input rather than an error.
299    ///
300    /// `obj` must be the *enclosing indirect object*, not a nested one: a
301    /// direct string inside an indirect dictionary is keyed by the
302    /// dictionary's number and generation.
303    ///
304    /// ```
305    /// use pdfrum_crypt::{CryptClass, SecurityHandler};
306    /// use pdfrum_object::ObjRef;
307    ///
308    /// // Fewer than seventeen bytes of AES ciphertext is all initialisation
309    /// // vector and no payload.
310    /// let handler = SecurityHandler::AesV5 {
311    ///     key: Box::new([0; 32]),
312    ///     revision: 6,
313    ///     permissions: 0xFFFF_FFFC,
314    ///     owner_unlocked: false,
315    ///     encrypt_metadata: true,
316    ///     encoding: pdfrum_crypt::PasswordEncoding::AsGiven,
317    ///     embedded_cipher: None,   // no /EFF: the stream cipher serves
318    ///     strings_identity: false,
319    /// };
320    /// assert!(handler.decrypt(ObjRef::new(4, 0), CryptClass::String, &[0; 16]).is_empty());
321    /// ```
322    #[must_use]
323    pub fn decrypt(&self, obj: ObjRef, class: CryptClass, data: &[u8]) -> Vec<u8> {
324        // `[oracle-bug]`: the *string* class branches. §7.6.5 lets
325        // `/StrF` resolve to `/Identity` beside an enciphering `/StmF`, and
326        // such a document's strings are plaintext.
327        if class == CryptClass::String && self.strings_identity() {
328            return data.to_vec();
329        }
330        // `/EFF` is the one class that can genuinely name another cipher, and
331        // does so by cipher only — §7.6.5 gives every `/CF` entry the same
332        // file key.
333        if let (CryptClass::Embedded, Some(cipher)) = (class, self.embedded_cipher()) {
334            return self.decrypt_with(obj, cipher, data);
335        }
336        match self {
337            Self::Identity => data.to_vec(),
338            Self::Rc4V2 { key, .. } => object::decrypt_rc4(key, obj, data),
339            Self::AesV4 { key, .. } => object::decrypt_aes_v4(key, obj, data),
340            Self::AesV5 { key, .. } => object::decrypt_aes_v5(key, data),
341        }
342    }
343
344    /// The `/EFF` cipher override, or `None` when the embedded class uses the
345    /// stream cipher — which ISO 32000-1 §7.6.5 table 20 makes the default.
346    #[must_use]
347    pub fn embedded_cipher(&self) -> Option<Cipher> {
348        match self {
349            Self::Identity => None,
350            Self::Rc4V2 {
351                embedded_cipher, ..
352            }
353            | Self::AesV4 {
354                embedded_cipher, ..
355            }
356            | Self::AesV5 {
357                embedded_cipher, ..
358            } => *embedded_cipher,
359        }
360    }
361
362    /// Decrypt with a named cipher over this handler's own file key — the
363    /// `/EFF` path, where the algorithm differs from the stream class's but
364    /// the key does not.
365    ///
366    /// The AES arm branches on key length exactly as [`Self::assemble`] does,
367    /// because AESV2 and AESV3 differ only there: a 32-byte key is used
368    /// verbatim, anything shorter takes the `sAlT` per-object derivation.
369    fn decrypt_with(&self, obj: ObjRef, cipher: Cipher, data: &[u8]) -> Vec<u8> {
370        let Some(key) = self.file_key() else {
371            return data.to_vec();
372        };
373        match cipher {
374            Cipher::None => data.to_vec(),
375            Cipher::Rc4 => object::decrypt_rc4(&key, obj, data),
376            Cipher::Aes => match <[u8; 32]>::try_from(key.bytes()) {
377                Ok(full) => object::decrypt_aes_v5(&full, data),
378                Err(_) => object::decrypt_aes_v4(&key, obj, data),
379            },
380        }
381    }
382
383    /// Encipher one string or stream payload belonging to indirect object
384    /// `obj`, the inverse of [`SecurityHandler::decrypt`].
385    ///
386    /// `iv` must be fresh per payload for the cipher to be sound; the RC4
387    /// handler and [`Self::Identity`] ignore it. Infallible, like its inverse.
388    ///
389    /// RC4 preserves length exactly. AES grows a payload of `n` bytes to
390    /// `32 + 16 * (n / 16)`: sixteen for the vector, and a PKCS#7 pad that is
391    /// always present, so an already block-aligned payload gains a whole
392    /// block. An empty payload is the exception and stays empty, so a save
393    /// does not grow every empty string in a document by 32 bytes.
394    ///
395    /// ```
396    /// use pdfrum_crypt::{CryptClass, Iv, SecurityHandler};
397    /// use pdfrum_object::ObjRef;
398    ///
399    /// let handler = SecurityHandler::AesV5 {
400    ///     key: Box::new([0; 32]),
401    ///     revision: 6,
402    ///     permissions: 0xFFFF_FFFC,
403    ///     owner_unlocked: false,
404    ///     encrypt_metadata: true,
405    ///     encoding: pdfrum_crypt::PasswordEncoding::AsGiven,
406    ///     embedded_cipher: None,   // no /EFF: the stream cipher serves
407    ///     strings_identity: false,
408    /// };
409    /// let obj = ObjRef::new(4, 0);
410    /// let sealed = handler.encrypt(obj, CryptClass::String, Iv([7; 16]), b"secret");
411    /// // Sixteen of vector, one block of ciphertext.
412    /// assert_eq!(sealed.len(), 32);
413    /// assert_eq!(handler.decrypt(obj, CryptClass::String, &sealed), b"secret");
414    /// ```
415    #[must_use]
416    pub fn encrypt(&self, obj: ObjRef, class: CryptClass, iv: Iv, data: &[u8]) -> Vec<u8> {
417        // `[oracle-bug]`: a pass-through string class, exactly as on the
418        // decrypt side.
419        if class == CryptClass::String && self.strings_identity() {
420            return data.to_vec();
421        }
422        // `CPDF_Encryptor::Encrypt` returns before reaching the cipher on an
423        // empty payload; see the `# Lengths` note.
424        if data.is_empty() {
425            return Vec::new();
426        }
427        // `/EFF`'s override, the mirror of the decrypt side. A document we
428        // write does not itself set `/EFF` — `pdfrum-edit` writes one filter
429        // — but a handler opened from a file that does must re-seal what it
430        // opened with the same cipher, or the round trip is not one.
431        if let (CryptClass::Embedded, Some(cipher)) = (class, self.embedded_cipher()) {
432            return self.encrypt_with(obj, cipher, iv, data);
433        }
434        match self {
435            Self::Identity => data.to_vec(),
436            Self::Rc4V2 { key, .. } => object::encrypt_rc4(key, obj, data),
437            Self::AesV4 { key, .. } => object::encrypt_aes_v4(key, obj, iv.bytes(), data),
438            Self::AesV5 { key, .. } => object::encrypt_aes_v5(key, iv.bytes(), data),
439        }
440    }
441
442    /// Encipher with a named cipher over this handler's own file key, the
443    /// inverse of [`Self::decrypt_with`].
444    fn encrypt_with(&self, obj: ObjRef, cipher: Cipher, iv: Iv, data: &[u8]) -> Vec<u8> {
445        let Some(key) = self.file_key() else {
446            return data.to_vec();
447        };
448        match cipher {
449            Cipher::None => data.to_vec(),
450            Cipher::Rc4 => object::encrypt_rc4(&key, obj, data),
451            Cipher::Aes => match <[u8; 32]>::try_from(key.bytes()) {
452                Ok(full) => object::encrypt_aes_v5(&full, iv.bytes(), data),
453                Err(_) => object::encrypt_aes_v4(&key, obj, iv.bytes(), data),
454            },
455        }
456    }
457
458    /// This handler's file encryption key, or `None` for [`Self::Identity`],
459    /// which has none. Every `/CF` entry shares it (ISO 32000-1 §7.6.5), so
460    /// it is what the `/EFF` override runs its own cipher over.
461    fn file_key(&self) -> Option<SmallKey> {
462        match self {
463            Self::Identity => None,
464            Self::Rc4V2 { key, .. } | Self::AesV4 { key, .. } => Some(key.clone()),
465            Self::AesV5 { key, .. } => Some(SmallKey::from_full(**key)),
466        }
467    }
468
469    /// What the document permits, for the password that opened it.
470    ///
471    /// A document opened with the **owner** password reports what `/P`
472    /// allows, the same as a user reading of it; the owner's own unrestricted
473    /// view is [`SecurityHandler::owner_permissions`]. An unencrypted document
474    /// has no restrictions at all.
475    ///
476    /// The ISO table-22 decode lives in [`Permissions`], next to the `/P`
477    /// word, so no caller has to spell `bits & 0x100`.
478    ///
479    /// ```
480    /// # use pdfrum_crypt::{Permissions, SecurityHandler};
481    /// assert_eq!(SecurityHandler::Identity.permissions(), Permissions::ALL);
482    /// ```
483    #[must_use]
484    pub fn permissions(&self) -> Permissions {
485        Permissions::from_bits(self.permission_word(false))
486    }
487
488    /// What the document permits under the owner's view.
489    ///
490    /// A document the **owner password** opened reports every permission
491    /// granted here, whatever `/P` says, because the owner may lift every
492    /// restriction. For a document the user password opened — and for an
493    /// unencrypted one — this is the same answer as
494    /// [`SecurityHandler::permissions`].
495    ///
496    /// ```
497    /// # use pdfrum_crypt::{Permissions, SecurityHandler};
498    /// assert_eq!(SecurityHandler::Identity.owner_permissions(), Permissions::ALL);
499    /// ```
500    #[must_use]
501    pub fn owner_permissions(&self) -> Permissions {
502        Permissions::from_bits(self.permission_word(true))
503    }
504
505    /// The permission word as the C++ reports it.
506    ///
507    /// `owner` selects the owner-unlocked override: a document opened with
508    /// the owner password reports all permissions granted, while the user
509    /// reading of the same document still reports what `/P` allows. The
510    /// standard handler then clears the two reserved low bits and forces bits
511    /// 7 through 32 set, so `/P 4092` reports `0xFFFFFFFC` either way.
512    ///
513    /// An unencrypted document has no restrictions at all.
514    ///
515    /// Private: the word itself is `pdfrum-crypt`'s business, and the two
516    /// public methods above are the whole of what leaves the crate. The
517    /// forcing is kept exactly as the C++ has it because it is behaviour —
518    /// clearing bits 1 and 2 is what makes `/P 4092` and `/P 4095` report
519    /// alike — and only the channel changed.
520    pub(crate) fn permission_word(&self, owner: bool) -> u32 {
521        let (permissions, owner_unlocked) = match self {
522            Self::Identity => return 0xFFFF_FFFF,
523            Self::Rc4V2 {
524                permissions,
525                owner_unlocked,
526                ..
527            }
528            | Self::AesV4 {
529                permissions,
530                owner_unlocked,
531                ..
532            }
533            | Self::AesV5 {
534                permissions,
535                owner_unlocked,
536                ..
537            } => (*permissions, *owner_unlocked),
538        };
539        let base = if owner_unlocked && owner {
540            0xFFFF_FFFF
541        } else {
542            permissions
543        };
544        (base & 0xFFFF_FFFC) | 0xFFFF_F0C0
545    }
546
547    /// Whether the document's metadata stream is encrypted (`/EncryptMetadata`,
548    /// default true).
549    ///
550    /// The parser consults this to decide whether to skip decrypting the
551    /// object `/Root/Metadata` points at.
552    #[must_use]
553    pub fn encrypt_metadata(&self) -> bool {
554        match self {
555            Self::Identity => true,
556            Self::Rc4V2 {
557                encrypt_metadata, ..
558            }
559            | Self::AesV4 {
560                encrypt_metadata, ..
561            }
562            | Self::AesV5 {
563                encrypt_metadata, ..
564            } => *encrypt_metadata,
565        }
566    }
567
568    /// `/R`, the handler revision. Zero for an unencrypted document.
569    #[must_use]
570    pub fn revision(&self) -> u8 {
571        match self {
572            Self::Identity => 0,
573            Self::Rc4V2 { revision, .. }
574            | Self::AesV4 { revision, .. }
575            | Self::AesV5 { revision, .. } => *revision,
576        }
577    }
578
579    /// Whether the owner password opened this document.
580    #[must_use]
581    pub fn owner_unlocked(&self) -> bool {
582        match self {
583            Self::Identity => false,
584            Self::Rc4V2 { owner_unlocked, .. }
585            | Self::AesV4 { owner_unlocked, .. }
586            | Self::AesV5 { owner_unlocked, .. } => *owner_unlocked,
587        }
588    }
589
590    /// Which spelling of the password unlocked the document.
591    #[must_use]
592    pub fn password_encoding(&self) -> PasswordEncoding {
593        match self {
594            Self::Identity => PasswordEncoding::AsGiven,
595            Self::Rc4V2 { encoding, .. }
596            | Self::AesV4 { encoding, .. }
597            | Self::AesV5 { encoding, .. } => *encoding,
598        }
599    }
600}
601
602/// Whether `dict` is a signature dictionary, whose `/Contents` must stay
603/// undecrypted.
604///
605/// The test is on the *direct* `/Type`, falling back to `/FT` only when
606/// `/Type` is absent entirely — a `/Type` of some other value does not let
607/// `/FT` speak. The decrypt walk calls this after the enclosing dictionary
608/// has been decrypted, because until then both names are ciphertext.
609///
610/// ```
611/// use pdfrum_crypt::is_signature_dict;
612/// use pdfrum_object::{Dict, Name, Object, names};
613///
614/// let sig = Dict::from_pairs([(names::TYPE.clone(), Object::Name(names::SIG.clone()))]);
615/// assert!(is_signature_dict(&sig));
616///
617/// // A field dictionary of signature type counts too, via /FT.
618/// let field = Dict::from_pairs([(names::FT.clone(), Object::Name(names::SIG.clone()))]);
619/// assert!(is_signature_dict(&field));
620///
621/// // But a /Type that is present and something else wins over /FT.
622/// let annot = Dict::from_pairs([
623///     (names::TYPE.clone(), Object::Name(Name::from("Annot"))),
624///     (names::FT.clone(), Object::Name(names::SIG.clone())),
625/// ]);
626/// assert!(!is_signature_dict(&annot));
627/// ```
628#[must_use]
629pub fn is_signature_dict(dict: &Dict) -> bool {
630    let key = if dict.contains_key(names::TYPE) {
631        names::TYPE
632    } else {
633        names::FT
634    };
635    signature_valued(dict, key)
636}
637
638/// Whether `key`'s value spells `Sig`, as a name or as a string.
639///
640/// The C++ reads the value through an accessor that gives a name and a string
641/// the same spelling, so a `/Type (Sig)` counts.
642fn signature_valued(dict: &Dict, key: &Name) -> bool {
643    dict.raw(key).is_some_and(|value| {
644        value.as_name().is_some_and(|n| n == names::SIG)
645            || value
646                .as_string()
647                .is_some_and(|s| &*s.bytes == names::SIG.as_bytes())
648    })
649}