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(¶ms, password, true, file_id)
206 {
207 return Ok(Self::assemble(¶ms, unlocked, true));
208 }
209 standard::try_password(¶ms, password, false, file_id)
210 .map(|unlocked| Self::assemble(¶ms, 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}