Skip to main content

pdfrum_crypt/
lib.rs

1#![doc = include_str!("../README.md")]
2// Revisions 2 to 4 derive an RC4 or AES-128 key by an MD5 ladder over the
3// padded password; revisions 5 and 6 verify a SHA-2 hash and unwrap a 32-byte
4// AES-256 key that the file stores directly.
5//
6// The AES-CBC initialisation vector is an argument rather than something this
7// crate mints, because a decrypting caller reads it off the ciphertext and
8// only an encrypting one has to produce it. `pdfrum-edit` draws vectors from
9// the operating system for every save.
10//
11// Building an `/Encrypt` dictionary is out of scope: `/O`, `/U`, `/OE`, `/UE`
12// and `/Perms` are written by whoever chose the passwords, and a
13// password-preserving save copies the dictionary the file already had.
14//
15// Two crypto-driven behaviors also live outside this crate, because both need
16// to walk the object graph, which this crate deliberately cannot:
17//
18// - The signature exemption. A `/Contents` value whose parent dictionary has a
19//   `/Type` or `/FT` key is deferred during the decrypt walk; once the parent
20//   has been decrypted its type can finally be read, and a parent that turns
21//   out to be a signature dictionary (`/Type /Sig`, or `/FT /Sig` when `/Type`
22//   is absent) keeps its contents undecrypted. The test cannot be made earlier
23//   because those names are themselves encrypted strings until the parent is
24//   done. `is_signature_dict` is what the walker calls.
25// - The metadata exemption. When `SecurityHandler::encrypt_metadata` is false
26//   the object `/Root/Metadata` points at is not decrypted.
27#![forbid(unsafe_code)]
28#![cfg_attr(docsrs, feature(doc_cfg))]
29// Every byte reaching this crate came from an untrusted file or a password:
30// index with `get()`.
31#![warn(clippy::indexing_slicing)]
32
33mod create;
34mod handler;
35mod key;
36mod object;
37mod permissions;
38mod primitives;
39mod rc4;
40mod saslprep;
41mod standard;
42
43#[cfg(test)]
44mod test_fixtures;
45
46pub use create::{ENTROPY_LEN, KeyMaterial, standard_r6};
47pub use handler::{Error, SecurityHandler, is_signature_dict};
48pub use key::SmallKey;
49pub use object::{CryptClass, Iv};
50pub use permissions::Permissions;
51pub use primitives::{md5, sha1};
52pub use rc4::rc4;
53pub use standard::{Cipher, EncryptParams, PAD, PasswordEncoding, parse_encrypt_dict};
54
55#[cfg(test)]
56mod tests {
57    use super::{CryptClass, Error, Iv, Permissions, SecurityHandler, is_signature_dict};
58    use crate::standard::Cipher;
59    use crate::test_fixtures::{self, unhex};
60    use pdfrum_object::{Dict, Name, NoResolve, ObjRef, Object, PdfString, names};
61
62    /// The parse-only view a key-length test wants.
63    fn cipher_of(dict: &Dict) -> Result<(Cipher, usize), Error> {
64        super::parse_encrypt_dict(dict, &NoResolve).map(|p| (p.cipher, p.key_len))
65    }
66
67    // ---- T7: the AESV2 fixture, and the /Length promotion it depends on ----
68
69    #[test]
70    fn aes_v2_fixture_promotes_a_byte_length_to_bits() {
71        let dict = test_fixtures::encrypted_pdf_dict();
72        assert_eq!(cipher_of(&dict), Ok((Cipher::Aes, 16)));
73    }
74
75    #[test]
76    fn aes_v2_fixture_opens_with_either_password() {
77        let dict = test_fixtures::encrypted_pdf_dict();
78        let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
79
80        let user = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
81            .expect("the user password");
82        assert!(matches!(user, SecurityHandler::AesV4 { .. }));
83        assert!(!user.owner_unlocked());
84        assert_eq!(user.permission_word(false), 0xFFFF_F2C0);
85        assert_eq!(user.permission_word(true), 0xFFFF_F2C0);
86        assert_eq!(user.revision(), 4);
87
88        let owner = SecurityHandler::from_encrypt_dict(&dict, &id, b"5678", &NoResolve)
89            .expect("the owner password");
90        assert!(owner.owner_unlocked());
91        assert_eq!(owner.permission_word(true), 0xFFFF_FFFC);
92        assert_eq!(owner.permission_word(false), 0xFFFF_F2C0);
93    }
94
95    #[test]
96    fn aes_v2_fixture_rejects_the_wrong_password() {
97        let dict = test_fixtures::encrypted_pdf_dict();
98        let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
99        for password in [&b""[..], b"tiger"] {
100            assert_eq!(
101                SecurityHandler::from_encrypt_dict(&dict, &id, password, &NoResolve).unwrap_err(),
102                Error::WrongPassword,
103                "password {password:?}"
104            );
105        }
106    }
107
108    // ---- T8 / T9 / T10: the AES-256 revisions ----
109
110    #[test]
111    fn revision_5_fixture_opens_with_all_four_spellings() {
112        let dict = test_fixtures::r5_dict();
113        let id = unhex("7ca64129d20fc9745f1bfc0e4166590a");
114
115        let owner_keys: Vec<_> = [&b"\xe2ge"[..], b"\xc3\xa2ge"]
116            .iter()
117            .map(|password| {
118                let handler = SecurityHandler::from_encrypt_dict(&dict, &id, password, &NoResolve)
119                    .unwrap_or_else(|_| panic!("owner {password:?}"));
120                assert!(handler.owner_unlocked());
121                assert_eq!(handler.revision(), 5);
122                match &handler {
123                    SecurityHandler::AesV5 { key, .. } => **key,
124                    _ => panic!("expected AesV5"),
125                }
126            })
127            .collect();
128        // Both spellings of the same role arrive at the same file key.
129        assert_eq!(owner_keys.first(), owner_keys.last());
130
131        for password in [&b"h\xf4tel"[..], b"h\xc3\xb4tel"] {
132            let handler = SecurityHandler::from_encrypt_dict(&dict, &id, password, &NoResolve)
133                .unwrap_or_else(|_| panic!("user {password:?}"));
134            assert!(!handler.owner_unlocked());
135        }
136    }
137
138    // At revision 5 the /ID plays no part: the same passwords work without it.
139    #[test]
140    fn revision_5_ignores_the_file_id() {
141        let dict = test_fixtures::r5_dict();
142        assert!(SecurityHandler::from_encrypt_dict(&dict, &[], b"h\xf4tel", &NoResolve).is_ok());
143        assert!(SecurityHandler::from_encrypt_dict(&dict, &[], b"\xe2ge", &NoResolve).is_ok());
144    }
145
146    // T8 — the /Perms block is genuinely checked, not just decrypted.
147    #[test]
148    fn a_tampered_perms_block_rejects_the_password() {
149        let mut dict = test_fixtures::r5_dict();
150        let mut perms = unhex("c954c264d796dfd131ddb784f5a8b1bf");
151        if let Some(byte) = perms.get_mut(9) {
152            *byte ^= 0xFF;
153        }
154        dict.push(
155            names::PERMS.clone(),
156            Object::Str(PdfString::literal(&perms)),
157        );
158        assert_eq!(
159            SecurityHandler::from_encrypt_dict(&dict, &[], b"h\xf4tel", &NoResolve).unwrap_err(),
160            Error::WrongPassword
161        );
162    }
163
164    // T9 — the only test that drives the hardened hash's 64-round loop, the
165    // SHA-384/512 branches and the mod-3 selector.
166    #[test]
167    fn revision_6_fixture_opens_with_all_four_spellings() {
168        let dict = test_fixtures::r6_dict();
169        for password in [&b"\xe2ge"[..], b"\xc3\xa2ge"] {
170            let handler = SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve)
171                .unwrap_or_else(|_| panic!("owner {password:?}"));
172            assert!(handler.owner_unlocked());
173            assert_eq!(handler.revision(), 6);
174        }
175        for password in [&b"h\xf4tel"[..], b"h\xc3\xb4tel"] {
176            let handler = SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve)
177                .unwrap_or_else(|_| panic!("user {password:?}"));
178            assert!(!handler.owner_unlocked());
179        }
180        assert_eq!(
181            SecurityHandler::from_encrypt_dict(&dict, &[], b"tiger", &NoResolve).unwrap_err(),
182            Error::WrongPassword
183        );
184    }
185
186    // T10 — bug_644.pdf: ASCII passwords, so the encoding fallback must not
187    // fire, and a /P of 4092 that masks to the same word for both roles.
188    //
189    // The roles are the reverse of what the C++ test *names* suggest: `b` is
190    // the owner password and `a` the user one. The embedder test cannot tell,
191    // because both roles report the same permissions here — which is exactly
192    // why `/P 4092` was picked for that fixture.
193    #[test]
194    fn revision_5_alternate_fixture() {
195        let dict = test_fixtures::bug_644_dict();
196        let owner = SecurityHandler::from_encrypt_dict(&dict, &[], b"b", &NoResolve)
197            .expect("the owner password");
198        assert!(owner.owner_unlocked());
199        assert_eq!(owner.permission_word(true), 0xFFFF_FFFC);
200        assert_eq!(owner.permission_word(false), 0xFFFF_FFFC);
201
202        let user = SecurityHandler::from_encrypt_dict(&dict, &[], b"a", &NoResolve)
203            .expect("the user password");
204        assert!(!user.owner_unlocked());
205        assert_eq!(user.permission_word(false), 0xFFFF_FFFC);
206        // Both roles reach the same file key, since /OE and /UE wrap it.
207        assert_eq!(
208            format!("{:?}", (owner.revision(), user.revision())),
209            "(5, 5)"
210        );
211
212        for password in [&b""[..], b"tiger"] {
213            assert_eq!(
214                SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve).unwrap_err(),
215                Error::WrongPassword,
216                "password {password:?}"
217            );
218        }
219        assert_eq!(
220            owner.password_encoding(),
221            crate::PasswordEncoding::AsGiven,
222            "an ASCII password never converts"
223        );
224    }
225
226    // ---- T14: the key-length resolution table ----
227
228    /// One row: `/V`, `/Length`, the crypt filter's own `/Length`, `/CFM`,
229    /// and what the pair should resolve to.
230    type KeyLengthRow = (
231        i64,
232        Option<i64>,
233        Option<i64>,
234        Option<&'static str>,
235        Result<(Cipher, usize), Error>,
236    );
237
238    #[test]
239    fn key_length_resolution_table() {
240        use test_fixtures::encrypt_dict;
241        let cases: [KeyLengthRow; 14] = [
242            (1, None, None, None, Ok((Cipher::Rc4, 5))),
243            // /V 1 is 40-bit by definition; its /Length is ignored outright.
244            (1, Some(128), None, None, Ok((Cipher::Rc4, 5))),
245            (2, None, None, None, Ok((Cipher::Rc4, 5))),
246            (2, Some(40), None, None, Ok((Cipher::Rc4, 5))),
247            (2, Some(128), None, None, Ok((Cipher::Rc4, 16))),
248            (
249                2,
250                Some(256),
251                None,
252                None,
253                Err(Error::CipherKeyLength {
254                    cipher: "RC4",
255                    len: 32,
256                }),
257            ),
258            // The `< 40 ⇒ × 8` promotion lives only in the /V >= 4 branch, so
259            // /Length 8 here is a bare divide to a one-byte key.
260            (
261                2,
262                Some(8),
263                None,
264                None,
265                Err(Error::CipherKeyLength {
266                    cipher: "RC4",
267                    len: 1,
268                }),
269            ),
270            (4, Some(128), None, Some("V2"), Ok((Cipher::Rc4, 16))),
271            (4, Some(128), Some(16), Some("AESV2"), Ok((Cipher::Aes, 16))),
272            (
273                4,
274                Some(128),
275                Some(128),
276                Some("AESV2"),
277                Ok((Cipher::Aes, 16)),
278            ),
279            (4, None, None, Some("AESV2"), Ok((Cipher::Aes, 16))),
280            (
281                4,
282                Some(128),
283                Some(40),
284                Some("AESV2"),
285                Err(Error::CipherKeyLength {
286                    cipher: "AES",
287                    len: 5,
288                }),
289            ),
290            (5, Some(256), Some(32), Some("AESV3"), Ok((Cipher::Aes, 32))),
291            (5, None, None, Some("AESV3"), Ok((Cipher::Aes, 32))),
292        ];
293        for (version, length, filter_length, method, expected) in cases {
294            let dict = encrypt_dict(version, length, filter_length, method);
295            assert_eq!(
296                cipher_of(&dict),
297                expected,
298                "/V {version} /Length {length:?} /CF Length {filter_length:?} /CFM {method:?}"
299            );
300        }
301    }
302
303    #[test]
304    fn a_negative_filter_length_is_malformed() {
305        let dict = test_fixtures::encrypt_dict(4, Some(128), Some(-8), Some("AESV2"));
306        assert!(matches!(
307            cipher_of(&dict),
308            Err(Error::MalformedEncryptDict(_))
309        ));
310    }
311
312    // The /Identity crypt filter is a handler, not a failure.
313    #[test]
314    fn an_identity_crypt_filter_yields_the_identity_handler() {
315        let dict = test_fixtures::identity_dict();
316        assert_eq!(cipher_of(&dict), Ok((Cipher::None, 0)));
317        let handler = SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve)
318            .expect("identity needs no password");
319        assert!(matches!(handler, SecurityHandler::Identity));
320        assert_eq!(
321            handler.decrypt(ObjRef::new(3, 0), CryptClass::Stream, b"plain"),
322            b"plain"
323        );
324    }
325
326    // ---- T15: the crypt-filter class rules ----
327
328    /// §7.6.5 table 20 makes `/StmF` and `/StrF` two independent entries, so
329    /// differing names are conformant — the stream filter supplies the
330    /// cipher this record models.
331    // [oracle-bug] cpdf_security_handler.cpp:305 and :325 return false on a
332    // raw name inequality.
333    #[test]
334    fn differing_stream_and_string_filters_open_rather_than_refusing() {
335        let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
336        dict.push(names::STR_F.clone(), Object::Name(Name::from("Other")));
337        assert_eq!(cipher_of(&dict), Ok((Cipher::Aes, 16)));
338    }
339
340    /// An absent `/StmF` against an explicit `/StrF`. PDFium compares the
341    /// looked-up bytes *before* applying any default and absent reads as
342    /// empty. §7.6.5's default is `/Identity`, so the streams pass through
343    /// while the strings are enciphered by `/StrF`'s filter.
344    #[test]
345    fn an_absent_stream_filter_defaults_to_identity_beside_a_named_string_filter() {
346        let mut dict = test_fixtures::bare_v4_dict();
347        dict.push(names::STR_F.clone(), Object::Name(Name::from("StdCF")));
348        let params =
349            super::parse_encrypt_dict(&dict, &NoResolve).expect("a defaulted /StmF is conformant");
350        assert_eq!(params.cipher, Cipher::None, "/StmF defaults to /Identity");
351        assert_eq!(params.string_cipher, Cipher::Aes, "/StrF names StdCF");
352    }
353
354    /// Both class filters absent. §7.6.5 defaults **both** to `/Identity`, so
355    /// `/CF` is never consulted and nothing is enciphered.
356    #[test]
357    fn both_class_filters_absent_default_to_identity() {
358        let dict = test_fixtures::bare_v4_dict();
359        assert_eq!(cipher_of(&dict), Ok((Cipher::None, 0)));
360        let handler = SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve)
361            .expect("a document with neither class filter opens");
362        assert!(matches!(handler, SecurityHandler::Identity));
363    }
364
365    /// A V4 dictionary with no `/CF`. With both classes defaulting to
366    /// `/Identity`, `/CF` is never consulted, so the dictionary resolves to no
367    /// cipher rather than to a malformation.
368    #[test]
369    fn a_missing_crypt_filter_dictionary_defaults_to_identity() {
370        let dict = Dict::from_pairs([
371            (names::FILTER.clone(), Object::Name(names::STANDARD.clone())),
372            (names::V.clone(), Object::Int(4)),
373            (names::R.clone(), Object::Int(4)),
374        ]);
375        assert_eq!(cipher_of(&dict), Ok((Cipher::None, 0)));
376    }
377
378    /// `/StrF /Identity` beside an enciphering `/StmF`. PDFium refuses the
379    /// document; §7.6.5 says its strings are plaintext while its streams are
380    /// enciphered.
381    #[test]
382    fn an_identity_string_filter_leaves_strings_plaintext_beside_an_enciphering_stream() {
383        let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
384        dict.push(names::STR_F.clone(), Object::Name(names::IDENTITY.clone()));
385        let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("conformant per §7.6.5");
386        assert_eq!(params.cipher, Cipher::Aes);
387        assert_eq!(params.string_cipher, Cipher::None);
388    }
389
390    // ---- Handler-level facts ----
391
392    #[test]
393    fn a_non_standard_filter_is_unsupported() {
394        for spelling in ["Adobe.PubSec", "Nonesuch"] {
395            let dict =
396                Dict::from_pairs([(names::FILTER.clone(), Object::Name(Name::from(spelling)))]);
397            assert_eq!(
398                SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve).unwrap_err(),
399                Error::UnsupportedHandler(spelling.as_bytes().into())
400            );
401        }
402    }
403
404    // The /Filter check is name-typed, so a string-valued one is not the
405    // standard handler even though it spells "Standard".
406    #[test]
407    fn a_string_valued_filter_is_not_the_standard_handler() {
408        let dict = Dict::from_pairs([(
409            names::FILTER.clone(),
410            Object::Str(PdfString::literal(b"Standard")),
411        )]);
412        assert_eq!(
413            SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve).unwrap_err(),
414            Error::UnsupportedHandler(Box::default())
415        );
416    }
417
418    // /EncryptMetadata is read boolean-typed before resolving, so an Int(0)
419    // there does not turn metadata encryption off.
420    #[test]
421    fn encrypt_metadata_reads_only_a_boolean() {
422        let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
423        dict.push(names::ENCRYPT_METADATA.clone(), Object::Int(0));
424        let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("parses");
425        assert!(params.encrypt_metadata, "an integer is not a boolean");
426
427        let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
428        dict.push(names::ENCRYPT_METADATA.clone(), Object::Bool(false));
429        let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("parses");
430        assert!(!params.encrypt_metadata);
431    }
432
433    #[test]
434    fn identity_reports_no_restrictions_and_no_revision() {
435        let handler = SecurityHandler::Identity;
436        assert_eq!(handler.permission_word(false), 0xFFFF_FFFF);
437        assert_eq!(handler.permission_word(true), 0xFFFF_FFFF);
438        assert_eq!(handler.revision(), 0);
439        assert!(handler.encrypt_metadata());
440        assert!(!handler.owner_unlocked());
441    }
442
443    #[test]
444    fn the_permission_mask_clears_reserved_bits_and_forces_the_high_ones() {
445        let dict = test_fixtures::encrypted_pdf_dict();
446        let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
447        let handler = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
448            .expect("the user password");
449        let reported = handler.permission_word(false);
450        assert_eq!(reported & 0b11, 0, "the two reserved bits are cleared");
451        assert_eq!(
452            reported & 0xFFFF_F0C0,
453            0xFFFF_F0C0,
454            "the forced bits are set"
455        );
456    }
457
458    // The two public methods are the private word, decoded. The AESV2
459    // fixture's `/P` reports `0xFFFF_F2C0`, which grants neither form filling
460    // (bit 9) nor annotation modification (bit 6) — and the owner's view of
461    // the same document grants everything.
462    #[test]
463    fn the_public_methods_decode_the_word_the_private_one_reports() {
464        let dict = test_fixtures::encrypted_pdf_dict();
465        let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
466
467        let user = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
468            .expect("the user password");
469        let granted = user.permissions();
470        assert_eq!(granted, Permissions::from_bits(user.permission_word(false)));
471        // `0xFFFF_F2C0` sets exactly one of table 22's eight named bits — 10,
472        // accessibility extraction. Neither of the two the form session asks
473        // about is granted, and neither is printing.
474        assert_eq!(
475            granted,
476            Permissions {
477                extract: true,
478                ..Permissions::NONE
479            }
480        );
481        // The user's own owner view is still the user's, since the user
482        // password opened it, not the owner's.
483        assert_eq!(user.owner_permissions(), granted);
484
485        let owner = SecurityHandler::from_encrypt_dict(&dict, &id, b"5678", &NoResolve)
486            .expect("the owner password");
487        assert_eq!(owner.owner_permissions(), Permissions::ALL);
488        assert_eq!(owner.permissions(), granted);
489    }
490
491    // ---- T12 / T13: damage tolerance ----
492
493    #[test]
494    fn a_short_user_entry_rejects_rather_than_reading_out_of_bounds() {
495        for len in 0..16usize {
496            let dict = test_fixtures::r3_dict_with_user_entry(&vec![0xCD; len]);
497            let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
498            assert_eq!(
499                SecurityHandler::from_encrypt_dict(&dict, &id, b"h\xf4tel", &NoResolve)
500                    .unwrap_err(),
501                Error::WrongPassword,
502                "/U of {len} bytes"
503            );
504        }
505    }
506
507    // A /U of 16 to 31 bytes is zero-padded into the working buffer rather
508    // than rejected, so the comparison still runs over its first 16 bytes.
509    #[test]
510    fn a_partial_user_entry_is_zero_padded_and_still_compared() {
511        for len in 16..32usize {
512            let dict = test_fixtures::r3_dict_with_user_entry(&vec![0xCD; len]);
513            let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
514            // No panic; the wrong bytes simply do not match.
515            assert!(
516                SecurityHandler::from_encrypt_dict(&dict, &id, b"h\xf4tel", &NoResolve).is_err(),
517                "/U of {len} bytes"
518            );
519        }
520    }
521
522    // T13 — /O and /U must each be at least 48 bytes whichever role is
523    // checked, because the owner check hashes the whole of /U alongside the
524    // password; /UE must be 32 for a user open and /OE for an owner one.
525    #[test]
526    fn short_version_five_entries_reject_rather_than_panicking() {
527        for len in [0usize, 1, 31, 47] {
528            let short = vec![0xEFu8; len];
529            // Both password entries gate both roles.
530            for key in [names::O, names::U, names::PERMS] {
531                let mut dict = test_fixtures::r5_dict();
532                dict.push(key.clone(), Object::Str(PdfString::literal(&short)));
533                for password in [&b"h\xf4tel"[..], b"\xe2ge"] {
534                    assert!(
535                        SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve)
536                            .is_err(),
537                        "{key:?} of {len} bytes with {password:?}"
538                    );
539                }
540            }
541            // The wrapped-key entries gate only the role that unwraps them.
542            for (key, password) in [(names::UE, &b"h\xf4tel"[..]), (names::OE, b"\xe2ge")] {
543                let mut dict = test_fixtures::r5_dict();
544                dict.push(key.clone(), Object::Str(PdfString::literal(&short)));
545                assert!(
546                    SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve).is_err(),
547                    "{key:?} of {len} bytes"
548                );
549            }
550        }
551    }
552
553    #[test]
554    fn an_empty_perms_entry_rejects() {
555        let mut dict = test_fixtures::r5_dict();
556        dict.push(names::PERMS.clone(), Object::Str(PdfString::literal(b"")));
557        assert_eq!(
558            SecurityHandler::from_encrypt_dict(&dict, &[], b"h\xf4tel", &NoResolve).unwrap_err(),
559            Error::WrongPassword
560        );
561    }
562
563    // T11 — the bad-okey fixtures: a truncated /O must fail the open, not
564    // read past its end (crbug.com/42270437).
565    #[test]
566    fn a_truncated_owner_entry_fails_the_open() {
567        for revision in [2i64, 3] {
568            for len in [0usize, 1, 31] {
569                let dict = test_fixtures::rc4_dict_with_owner_entry(revision, &vec![0x11; len]);
570                assert_eq!(
571                    SecurityHandler::from_encrypt_dict(&dict, &[], b"a", &NoResolve).unwrap_err(),
572                    Error::WrongPassword,
573                    "/R {revision} with /O of {len} bytes"
574                );
575            }
576        }
577    }
578
579    // ---- The signature-dictionary predicate ----
580
581    #[test]
582    fn signature_dictionaries_are_recognised_by_type_then_field_type() {
583        let sig_type = Dict::from_pairs([(names::TYPE.clone(), Object::Name(names::SIG.clone()))]);
584        assert!(is_signature_dict(&sig_type));
585
586        let sig_field = Dict::from_pairs([(names::FT.clone(), Object::Name(names::SIG.clone()))]);
587        assert!(is_signature_dict(&sig_field));
588
589        // /Type present and not Sig shuts /FT out.
590        let annot = Dict::from_pairs([
591            (names::TYPE.clone(), Object::Name(Name::from("Annot"))),
592            (names::FT.clone(), Object::Name(names::SIG.clone())),
593        ]);
594        assert!(!is_signature_dict(&annot));
595
596        // Neither key at all.
597        assert!(!is_signature_dict(&Dict::new()));
598    }
599
600    // The value is read through an accessor that spells a name and a string
601    // alike, so a string-valued /Type counts.
602    #[test]
603    fn a_string_valued_type_still_names_a_signature() {
604        let dict =
605            Dict::from_pairs([(names::TYPE.clone(), Object::Str(PdfString::literal(b"Sig")))]);
606        assert!(is_signature_dict(&dict));
607    }
608
609    // ---- The public decrypt entry point ----
610
611    /// The three real fixtures, as opened handlers, for the payload tests.
612    fn opened_handlers() -> Vec<(&'static str, SecurityHandler)> {
613        let aes_v2_id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
614        let rc4_id = unhex("9b744068bb5efbe920baaba6da63c2bf");
615        vec![
616            (
617                "RC4 (/R 3)",
618                SecurityHandler::from_encrypt_dict(
619                    &test_fixtures::r3_dict(),
620                    &rc4_id,
621                    b"h\xf4tel",
622                    &NoResolve,
623                )
624                .expect("the r3 user password"),
625            ),
626            (
627                "AESV2 (/R 4)",
628                SecurityHandler::from_encrypt_dict(
629                    &test_fixtures::encrypted_pdf_dict(),
630                    &aes_v2_id,
631                    b"1234",
632                    &NoResolve,
633                )
634                .expect("the encrypted.pdf user password"),
635            ),
636            (
637                "AESV3 (/R 6)",
638                SecurityHandler::from_encrypt_dict(
639                    &test_fixtures::r6_dict(),
640                    &[],
641                    b"h\xf4tel",
642                    &NoResolve,
643                )
644                .expect("the r6 user password"),
645            ),
646        ]
647    }
648
649    // RC4 is symmetric, so decrypting twice restores the payload; AES is not,
650    // so only its length behavior is asserted here.
651    #[test]
652    fn rc4_decrypt_round_trips_through_the_public_api() {
653        let rc4_id = unhex("9b744068bb5efbe920baaba6da63c2bf");
654        let handler = SecurityHandler::from_encrypt_dict(
655            &test_fixtures::r3_dict(),
656            &rc4_id,
657            b"h\xf4tel",
658            &NoResolve,
659        )
660        .expect("the r3 user password");
661        let obj = ObjRef::new(12, 3);
662        let payload = b"Hello, encrypted world.".to_vec();
663        for class in [CryptClass::Stream, CryptClass::String, CryptClass::Embedded] {
664            let once = handler.decrypt(obj, class, &payload);
665            assert_ne!(once, payload, "{class:?} actually enciphered");
666            assert_eq!(handler.decrypt(obj, class, &once), payload, "{class:?}");
667        }
668    }
669
670    // D1 — all three classes resolve to the same cipher and key, so the same
671    // bytes decrypt identically whichever class they are labelled with.
672    #[test]
673    fn every_crypt_class_decrypts_the_same_way() {
674        for (name, handler) in opened_handlers() {
675            let obj = ObjRef::new(7, 0);
676            let payload: Vec<u8> = (0..64u8).collect();
677            let stream = handler.decrypt(obj, CryptClass::Stream, &payload);
678            assert_eq!(
679                handler.decrypt(obj, CryptClass::String, &payload),
680                stream,
681                "{name}"
682            );
683            assert_eq!(
684                handler.decrypt(obj, CryptClass::Embedded, &payload),
685                stream,
686                "{name}"
687            );
688        }
689    }
690
691    // -----------------------------------------------------------------
692    // `/EFF`. The oracle reads the key nowhere
693    // (`grep '"EFF"' core/ fpdfsdk/` is empty), so an embedded file stream
694    // decrypts with the stream filter whatever `/EFF` says. The corpus has
695    // no file with an `/EFF` at all, let alone one differing from `/StmF`,
696    // so these fixtures are constructed rather than taken from it.
697    // -----------------------------------------------------------------
698
699    /// The `encrypted.pdf` dictionary — whose `/StmF` is AESV2 and whose
700    /// `/O`/`/U` are real, so it opens with `1234` — extended with a second
701    /// `/CF` entry that `/EFF` names.
702    fn eff_dict(embedded_method: &str) -> Dict {
703        use pdfrum_object::Name;
704        let mut dict = test_fixtures::encrypted_pdf_dict();
705        let std_cf = Dict::from_pairs([
706            (names::CFM.clone(), Object::Name(Name::from("AESV2"))),
707            (names::LENGTH.clone(), Object::Int(16)),
708        ]);
709        let emb_cf = Dict::from_pairs([
710            (
711                names::CFM.clone(),
712                Object::Name(Name::from(embedded_method)),
713            ),
714            (names::LENGTH.clone(), Object::Int(16)),
715        ]);
716        dict.push(
717            names::CF.clone(),
718            Object::Dict(Dict::from_pairs([
719                (Name::from("StdCF"), Object::Dict(std_cf)),
720                (Name::from("EmbCF"), Object::Dict(emb_cf)),
721            ])),
722        );
723        dict.push(names::EFF.clone(), Object::Name(Name::from("EmbCF")));
724        dict
725    }
726
727    /// The handler `eff_dict` opens to, under `encrypted.pdf`'s password.
728    fn eff_handler(dict: &Dict) -> SecurityHandler {
729        SecurityHandler::from_encrypt_dict(
730            dict,
731            &unhex("1B0FD0F5E29AD84DBF67775E9E3B009F"),
732            b"1234",
733            &NoResolve,
734        )
735        .expect("the encrypted.pdf user password")
736    }
737
738    /// An `/EFF` naming a filter with a different `/CFM` gives the embedded
739    /// class its own cipher — pdf.js `crypto.js:1120`, `:1336`; ISO 32000-1
740    /// §7.6.5 table 20. The embedded class is not folded onto `Stream`.
741    #[test]
742    fn an_eff_naming_another_filter_decrypts_embedded_files_with_it() {
743        let dict = eff_dict("V2");
744        let params = super::parse_encrypt_dict(&dict, &NoResolve).unwrap();
745        assert_eq!(params.cipher, Cipher::Aes);
746        assert_eq!(params.embedded_cipher, Some(Cipher::Rc4));
747
748        let handler = eff_handler(&dict);
749        assert_eq!(handler.embedded_cipher(), Some(Cipher::Rc4));
750
751        let obj = ObjRef::new(9, 0);
752        let payload: Vec<u8> = (0..48u8).collect();
753        // The two classes now genuinely differ: AES reads the first sixteen
754        // bytes as an initialisation vector, RC4 preserves length.
755        let as_stream = handler.decrypt(obj, CryptClass::Stream, &payload);
756        let as_embedded = handler.decrypt(obj, CryptClass::Embedded, &payload);
757        assert_eq!(as_embedded.len(), payload.len());
758        assert_ne!(as_embedded, as_stream);
759        // And the string class follows the stream, as `/StrF` names `StdCF`.
760        assert_eq!(
761            handler.decrypt(obj, CryptClass::String, &payload),
762            as_stream
763        );
764    }
765
766    /// The embedded class round-trips through its own cipher.
767    #[test]
768    fn the_embedded_class_round_trips_under_its_own_cipher() {
769        let dict = eff_dict("V2");
770        let handler = eff_handler(&dict);
771        let obj = ObjRef::new(9, 0);
772        let payload = b"an attachment".to_vec();
773        let sealed = handler.encrypt(obj, CryptClass::Embedded, Iv([3; 16]), &payload);
774        assert_eq!(handler.decrypt(obj, CryptClass::Embedded, &sealed), payload);
775        // Sealed under RC4, so it is not what the stream cipher would make.
776        assert_ne!(
777            sealed,
778            handler.encrypt(obj, CryptClass::Stream, Iv([3; 16]), &payload)
779        );
780    }
781
782    /// Table 20's default for an absent `/EFF` is `/StmF`, so a document
783    /// without the key — every file in the corpus — is unchanged.
784    #[test]
785    fn an_absent_eff_leaves_every_class_on_the_stream_cipher() {
786        for (name, handler) in opened_handlers() {
787            assert_eq!(handler.embedded_cipher(), None, "{name}");
788        }
789        let dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
790        let params = super::parse_encrypt_dict(&dict, &NoResolve).unwrap();
791        assert_eq!(params.embedded_cipher, None);
792    }
793
794    /// Naming `/StmF`'s own filter is the default written out, and an `/EFF`
795    /// whose `/CFM` resolves to the same cipher needs no override either.
796    #[test]
797    fn an_eff_that_agrees_with_the_stream_filter_is_no_override() {
798        use pdfrum_object::Name;
799        let mut same = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
800        same.push(names::EFF.clone(), Object::Name(Name::from("StdCF")));
801        assert_eq!(
802            super::parse_encrypt_dict(&same, &NoResolve)
803                .unwrap()
804                .embedded_cipher,
805            None
806        );
807        // Different filter name, same cipher — AESV3 and AESV2 are both
808        // `Cipher::Aes`, which is the level `/EFF` can actually change.
809        let agreeing = eff_dict("AESV3");
810        assert_eq!(
811            super::parse_encrypt_dict(&agreeing, &NoResolve)
812                .unwrap()
813                .embedded_cipher,
814            None
815        );
816    }
817
818    /// `/EFF /Identity` leaves embedded files in the clear while the streams
819    /// stay enciphered — the case that makes `/EFF` worth reading at all.
820    #[test]
821    fn an_identity_eff_leaves_embedded_files_unenciphered() {
822        let mut dict = test_fixtures::encrypted_pdf_dict();
823        dict.push(names::EFF.clone(), Object::Name(names::IDENTITY.clone()));
824        let handler = eff_handler(&dict);
825        assert_eq!(handler.embedded_cipher(), Some(Cipher::None));
826        let obj = ObjRef::new(9, 0);
827        let payload: Vec<u8> = (0..48u8).collect();
828        assert_eq!(
829            handler.decrypt(obj, CryptClass::Embedded, &payload),
830            payload
831        );
832        assert_ne!(handler.decrypt(obj, CryptClass::Stream, &payload), payload);
833    }
834
835    /// An `/EFF` naming a filter `/CF` does not have is damage, not a reason
836    /// to refuse the document: the streams still decrypt, and the embedded
837    /// class falls back to the stream cipher — the absent-key default.
838    #[test]
839    fn an_eff_naming_a_missing_filter_falls_back_rather_than_failing() {
840        use pdfrum_object::Name;
841        let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
842        dict.push(names::EFF.clone(), Object::Name(Name::from("NoSuchCF")));
843        let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("still opens");
844        assert_eq!(params.embedded_cipher, None);
845    }
846
847    // The object number keys the payload for RC4 and AESV2 but not for
848    // AESV3, whose key is the file key itself.
849    #[test]
850    fn only_the_pre_version_five_handlers_key_by_object() {
851        let payload: Vec<u8> = (0..48u8).map(|i| i.wrapping_mul(5)).collect();
852        for (name, handler) in opened_handlers() {
853            let first = handler.decrypt(ObjRef::new(1, 0), CryptClass::Stream, &payload);
854            let second = handler.decrypt(ObjRef::new(2, 0), CryptClass::Stream, &payload);
855            if handler.revision() >= 5 {
856                assert_eq!(second, first, "{name} uses the file key verbatim");
857            } else {
858                assert_ne!(second, first, "{name} salts by object number");
859            }
860        }
861    }
862
863    // No input length may panic, and nothing may produce more bytes than it
864    // was given.
865    #[test]
866    fn decrypt_never_panics_and_never_grows_a_payload() {
867        for (name, handler) in opened_handlers() {
868            for len in 0..80usize {
869                let payload = vec![0xA5u8; len];
870                let out = handler.decrypt(ObjRef::new(9, 1), CryptClass::Stream, &payload);
871                assert!(out.len() <= len, "{name} at {len} bytes");
872            }
873        }
874        // The identity handler is the one that returns exactly its input.
875        for len in 0..40usize {
876            let payload = vec![0x5Au8; len];
877            assert_eq!(
878                SecurityHandler::Identity.decrypt(ObjRef::new(0, 0), CryptClass::String, &payload),
879                payload
880            );
881        }
882    }
883
884    // ---- encrypt-then-decrypt, per revision ----
885
886    /// Every revision the corpus exercises, as an opened handler.
887    ///
888    /// `opened_handlers` covers three; this adds R2 and R5 so the round-trip
889    /// matrix spans /R 2 through /R 6, which is what byte-identity per
890    /// revision requires.
891    fn every_revision() -> Vec<(&'static str, SecurityHandler)> {
892        let r2_id = unhex("2b778de1bcef1733b35e680882812409");
893        let mut all = vec![(
894            "RC4 (/R 2)",
895            SecurityHandler::from_encrypt_dict(
896                &test_fixtures::r2_dict(),
897                &r2_id,
898                b"h\xf4tel",
899                &NoResolve,
900            )
901            .expect("the r2 user password"),
902        )];
903        all.extend(opened_handlers());
904        all.push((
905            "AESV3 (/R 5)",
906            SecurityHandler::from_encrypt_dict(
907                &test_fixtures::r5_dict(),
908                &[],
909                b"h\xf4tel",
910                &NoResolve,
911            )
912            .expect("the r5 user password"),
913        ));
914        all
915    }
916
917    // The KAT: whatever we encipher, our own decipher returns byte for byte,
918    // at every revision, every class and every length that straddles a block
919    // boundary.
920    #[test]
921    fn every_revision_round_trips_encrypt_then_decrypt() {
922        for (name, handler) in every_revision() {
923            let obj = ObjRef::new(11, 0);
924            for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 64, 127] {
925                let payload: Vec<u8> = (0..len)
926                    .map(|i| u8::try_from(i % 253).unwrap_or(0))
927                    .collect();
928                for class in [CryptClass::Stream, CryptClass::String, CryptClass::Embedded] {
929                    let iv = Iv([u8::try_from(len % 256).unwrap_or(0); 16]);
930                    let sealed = handler.encrypt(obj, class, iv, &payload);
931                    assert_eq!(
932                        handler.decrypt(obj, class, &sealed),
933                        payload,
934                        "{name} {class:?} at {len} bytes"
935                    );
936                }
937            }
938        }
939    }
940
941    // A payload really is enciphered — a handler that returned its input
942    // would pass the round-trip test above and write a plaintext file.
943    #[test]
944    fn an_encrypted_payload_is_not_its_own_plaintext() {
945        let payload = b"Hello, encrypted world.".to_vec();
946        for (name, handler) in every_revision() {
947            let sealed = handler.encrypt(
948                ObjRef::new(4, 0),
949                CryptClass::Stream,
950                Iv([0x5A; 16]),
951                &payload,
952            );
953            assert_ne!(sealed, payload, "{name}");
954        }
955        // Identity is the one handler that passes bytes through untouched.
956        assert_eq!(
957            SecurityHandler::Identity.encrypt(
958                ObjRef::new(4, 0),
959                CryptClass::Stream,
960                Iv([0; 16]),
961                &payload
962            ),
963            payload
964        );
965    }
966
967    // The object reference keys the payload for RC4 and AESV2 and does not
968    // for AESV3 — the same split the decrypt side has, since it is the same
969    // derivation.
970    #[test]
971    fn only_the_pre_version_five_handlers_key_an_encryption_by_object() {
972        let payload = b"payload".to_vec();
973        for (name, handler) in every_revision() {
974            let iv = Iv([1; 16]);
975            let first = handler.encrypt(ObjRef::new(1, 0), CryptClass::Stream, iv, &payload);
976            let second = handler.encrypt(ObjRef::new(2, 0), CryptClass::Stream, iv, &payload);
977            if handler.revision() >= 5 {
978                assert_eq!(second, first, "{name} uses the file key verbatim");
979            } else {
980                assert_ne!(second, first, "{name} salts by object number");
981            }
982        }
983    }
984
985    // Determinism is a parameter here too: the same vector gives the same
986    // bytes, which is what lets `pdfrum-edit` snapshot a whole encrypted file.
987    #[test]
988    fn the_same_vector_produces_the_same_ciphertext() {
989        for (name, handler) in every_revision() {
990            let obj = ObjRef::new(6, 0);
991            let once = handler.encrypt(obj, CryptClass::Stream, Iv([2; 16]), b"stable");
992            let again = handler.encrypt(obj, CryptClass::Stream, Iv([2; 16]), b"stable");
993            assert_eq!(once, again, "{name}");
994            // And a different vector does not, for the ciphers that read it.
995            let other = handler.encrypt(obj, CryptClass::Stream, Iv([3; 16]), b"stable");
996            if handler.revision() >= 4 {
997                assert_ne!(other, once, "{name} mixes the vector in");
998            }
999        }
1000    }
1001
1002    // No length may panic, and the growth is exactly the documented law.
1003    #[test]
1004    fn encrypt_never_panics_and_grows_by_the_documented_amount() {
1005        for (name, handler) in every_revision() {
1006            for len in 0..80usize {
1007                let out = handler.encrypt(
1008                    ObjRef::new(9, 1),
1009                    CryptClass::Stream,
1010                    Iv([0xC3; 16]),
1011                    &vec![0xA5u8; len],
1012                );
1013                let expected = if len == 0 || matches!(handler, SecurityHandler::Rc4V2 { .. }) {
1014                    len
1015                } else {
1016                    32 + (len / 16) * 16
1017                };
1018                assert_eq!(out.len(), expected, "{name} at {len} bytes");
1019            }
1020        }
1021    }
1022
1023    // The one length that skips the cipher entirely. Without it a save would
1024    // rewrite every `()` in a document as 32 bytes of vector and padding —
1025    // which round-trips, but is not what the oracle writes.
1026    #[test]
1027    fn an_empty_payload_stays_empty_at_every_revision() {
1028        for (name, handler) in every_revision() {
1029            for class in [CryptClass::Stream, CryptClass::String, CryptClass::Embedded] {
1030                assert!(
1031                    handler
1032                        .encrypt(ObjRef::new(2, 0), class, Iv([0xFF; 16]), b"")
1033                        .is_empty(),
1034                    "{name} {class:?}"
1035                );
1036            }
1037        }
1038    }
1039
1040    // ---- The properties a fuzzer would look for ----
1041    //
1042    // Every byte of an /Encrypt dictionary comes from the file, so no
1043    // combination of them may panic. These sweep the shape space
1044    // deterministically rather than randomly: a failure names the exact input
1045    // instead of a corpus file.
1046
1047    /// A cheap deterministic byte sequence — no dependency, and reproducible.
1048    fn pseudo_random(seed: u64, len: usize) -> Vec<u8> {
1049        let mut state = seed.wrapping_mul(0x2545_F491_4F6C_DD1D) | 1;
1050        (0..len)
1051            .map(|_| {
1052                state ^= state << 13;
1053                state ^= state >> 7;
1054                state ^= state << 17;
1055                u8::try_from(state >> 56).unwrap_or(0)
1056            })
1057            .collect()
1058    }
1059
1060    #[test]
1061    fn arbitrary_encrypt_dictionaries_never_panic() {
1062        for seed in 0..8u64 {
1063            for version in [-1i64, 0, 1, 2, 3, 4, 5, 6, 99] {
1064                for revision in [0i64, 2, 3, 4, 5, 6, 7] {
1065                    // The extremes are `INT_RANGE`'s: a lexer folds anything
1066                    // wider to zero, so nothing outside it can reach here.
1067                    for length in [
1068                        *pdfrum_object::INT_RANGE.start(),
1069                        -8,
1070                        0,
1071                        8,
1072                        40,
1073                        128,
1074                        256,
1075                        4096,
1076                        *pdfrum_object::INT_RANGE.end(),
1077                    ] {
1078                        let mut dict = test_fixtures::encrypt_dict(
1079                            version,
1080                            Some(length),
1081                            Some(length),
1082                            Some("AESV2"),
1083                        );
1084                        dict.push(names::R.clone(), Object::Int(revision));
1085                        for key in [names::O, names::U, names::OE, names::UE, names::PERMS] {
1086                            let entry = pseudo_random(
1087                                seed.wrapping_add(key.as_bytes().len() as u64),
1088                                usize::try_from(seed % 60).unwrap_or(0),
1089                            );
1090                            dict.push(key.clone(), Object::Str(PdfString::literal(entry)));
1091                        }
1092                        let password = pseudo_random(seed, usize::try_from(seed % 9).unwrap_or(0));
1093                        let file_id =
1094                            pseudo_random(seed + 1, usize::try_from(seed % 20).unwrap_or(0));
1095                        // The only requirement is that it returns.
1096                        let _ = SecurityHandler::from_encrypt_dict(
1097                            &dict, &file_id, &password, &NoResolve,
1098                        );
1099                    }
1100                }
1101            }
1102        }
1103    }
1104
1105    // The revision 6 loop is the one unbounded-looking construction; a
1106    // hostile /U salt cannot make it run away, and a long password only
1107    // enlarges each round rather than adding rounds.
1108    #[test]
1109    fn arbitrary_version_five_entries_terminate() {
1110        for seed in 0..4u64 {
1111            let mut dict = test_fixtures::r6_dict();
1112            for key in [names::O, names::U, names::OE, names::UE, names::PERMS] {
1113                let entry = pseudo_random(seed, 48);
1114                dict.push(key.clone(), Object::Str(PdfString::literal(entry)));
1115            }
1116            // Long enough that each round moves real data, short enough that
1117            // the worst case — 287 rounds of 64 repetitions — stays quick.
1118            let password = pseudo_random(seed, 64);
1119            let _ = SecurityHandler::from_encrypt_dict(&dict, &[], &password, &NoResolve);
1120        }
1121    }
1122
1123    // T11/T12/T13 as a sweep: any length of any password entry, at any
1124    // revision, must return rather than panic.
1125    #[test]
1126    fn every_password_entry_length_is_survivable() {
1127        for len in 0..64usize {
1128            let entry = pseudo_random(len as u64, len);
1129            // Revision 6 is covered by its own sweep above; running it for
1130            // every length here would spend minutes on the hardened hash.
1131            for base in [
1132                test_fixtures::r2_dict(),
1133                test_fixtures::r3_dict(),
1134                test_fixtures::encrypted_pdf_dict(),
1135                test_fixtures::r5_dict(),
1136            ] {
1137                for key in [names::O, names::U, names::OE, names::UE, names::PERMS] {
1138                    let mut dict = base.clone();
1139                    dict.push(key.clone(), Object::Str(PdfString::literal(&entry)));
1140                    let _ = SecurityHandler::from_encrypt_dict(&dict, &[], b"pw", &NoResolve);
1141                }
1142            }
1143        }
1144    }
1145
1146    #[test]
1147    fn handlers_are_send_and_sync() {
1148        const fn assert_send_sync<T: Send + Sync>() {}
1149        assert_send_sync::<SecurityHandler>();
1150        assert_send_sync::<Error>();
1151        assert_send_sync::<crate::EncryptParams>();
1152    }
1153
1154    #[test]
1155    fn a_handler_debug_dump_never_shows_key_material() {
1156        let dict = test_fixtures::encrypted_pdf_dict();
1157        let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
1158        let handler = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
1159            .expect("the user password");
1160        let dump = format!("{handler:?}");
1161        assert!(dump.contains("redacted"), "{dump}");
1162    }
1163}