pdfrum_crypt/standard.rs
1//! The `/Filter /Standard` security handler: reading `/Encrypt` and running
2//! the revision 2 to 6 password algorithms (ISO 32000 §7.6.3, ISO 32000-2
3//! §7.6.4).
4//!
5//! Two stages, kept apart. [`EncryptParams`] is a record of what the file
6//! said — the only place that touches a [`Dict`] — and the algorithms below
7//! are free functions over it. That split is why the algorithm tests can be
8//! known-answer tests over literal parameters instead of end-to-end document
9//! opens.
10//!
11//! Which accessor reads which key is load-bearing rather than incidental. The
12//! C++ reads `/Filter` name-typed (a reference or a string there is not the
13//! standard handler), `/V`, `/R`, `/P` and `/Length` through an accessor that
14//! coerces any type and follows one reference, `/EncryptMetadata`
15//! boolean-typed before resolving (so an `Int(1)` is not `true`), and `/CF`
16//! after resolving. Files in the wild depend on each of those.
17
18use pdfrum_object::{Dict, Name, Resolve, names};
19
20use crate::Error;
21use crate::key::SmallKey;
22use crate::primitives::{
23 BLOCK, aes_cbc_decrypt, aes_cbc_encrypt, ct_eq, md5, md5_parts, sha256, sha256_parts, sha384,
24 sha512,
25};
26use crate::rc4::{rc4, rc4_in_place};
27
28/// The 32-byte padding string every revision 2 to 4 password is padded with
29/// (ISO 32000 §7.6.3.3, "Algorithm 2" step a).
30pub const PAD: [u8; 32] = [
31 0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
32 0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a,
33];
34
35/// The cipher a document's crypt filter resolves to.
36///
37/// `Aes` covers both AESV2 and AESV3: PDFium never distinguishes them by
38/// name, only by whether the key is 32 bytes long.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Cipher {
41 /// `/StrF /Identity` — payloads pass through untouched.
42 None,
43 /// RC4, for `/V 1..4` without an AES crypt-filter method.
44 Rc4,
45 /// AES-CBC, for `/CFM /AESV2` or `/CFM /AESV3`.
46 Aes,
47}
48
49impl Cipher {
50 /// The spelling used in [`Error::CipherKeyLength`].
51 const fn label(self) -> &'static str {
52 match self {
53 Self::None => "identity",
54 Self::Rc4 => "RC4",
55 Self::Aes => "AES",
56 }
57 }
58
59 /// Whether `len` bytes is a key length this cipher accepts.
60 ///
61 /// RC4's floor of 5 bytes is what rejects a `/V 2 /Length 8` document:
62 /// the revision-under-4 path divides by 8 without the promotion the
63 /// version-4 path applies, leaving a 1-byte key.
64 const fn accepts_key_len(self, len: usize) -> bool {
65 match self {
66 Self::None => true,
67 Self::Rc4 => 5 <= len && len <= 16,
68 Self::Aes => matches!(len, 16 | 24 | 32),
69 }
70 }
71}
72
73/// The `/Encrypt` dictionary as a record of what the file said.
74///
75/// Nothing here is validated against a password yet; `cipher` and `key_len`
76/// are the one derived pair, because resolving them is where a malformed
77/// dictionary is rejected.
78#[derive(Debug, Clone)]
79pub struct EncryptParams {
80 /// `/V`, the algorithm version. Default 0.
81 pub version: i64,
82 /// `/R`, the handler revision. Default 0.
83 pub revision: i64,
84 /// `/P`, the permission flags, as the unsigned word they are compared as.
85 /// Default `0xFFFF_FFFF`.
86 pub permissions: u32,
87 /// The cipher `/V`, `/CF` and `/CFM` resolve to.
88 ///
89 /// This is the **stream** class's cipher (`/StmF`). See [`string_cipher`].
90 ///
91 /// [`string_cipher`]: EncryptParams::string_cipher
92 pub cipher: Cipher,
93 /// The cipher `/EFF` resolves to when it differs from [`Self::cipher`]
94 /// (ISO 32000-1 §7.6.5 table 20), and `None` when the embedded class uses
95 /// the stream cipher — which table 20 makes the default.
96 pub embedded_cipher: Option<Cipher>,
97 /// `[oracle-bug]` The cipher the **string** class (`/StrF`) resolves to,
98 /// independently of `/StmF`.
99 ///
100 /// §7.6.5 defines `/StmF` and `/StrF` as two independent entries, each
101 /// defaulting to `Identity`, and says nothing forbidding them from
102 /// differing. So `/StmF /StdCF /StrF /StdCF2` opens even when the two
103 /// `/CF` entries are identical, and so does a V≥4 document with
104 /// **neither** entry present, which is two `Identity` defaults.
105 // [oracle-bug] cpdf_security_handler.cpp:305 and :325 return false on a
106 // raw name inequality, and the comparison runs *before* the default is
107 // applied, so an absent pair reads as two empty names and is refused
108 // too. pdf.js applies the defaults and consults the two independently,
109 // with no equality check (crypto.js:1116-1120).
110 ///
111 /// Only the two values §7.6.5 makes observable at this seam are carried:
112 /// a class is either the file's cipher or `Identity`. A document naming
113 /// two *different non-Identity* filters would need two keys, which no
114 /// corpus file does and which this record deliberately does not model —
115 /// such a file resolves both classes to the stream filter's cipher.
116 pub string_cipher: Cipher,
117 /// The file encryption key length in bytes, 0 to 32.
118 pub key_len: usize,
119 /// `/EncryptMetadata`. Default `true`.
120 pub encrypt_metadata: bool,
121 /// `/O`, the owner password entry.
122 pub o: Box<[u8]>,
123 /// `/U`, the user password entry.
124 pub u: Box<[u8]>,
125 /// `/OE`, the owner encrypted file key (revision 5 and up).
126 pub oe: Box<[u8]>,
127 /// `/UE`, the user encrypted file key (revision 5 and up).
128 pub ue: Box<[u8]>,
129 /// `/Perms`, the encrypted permission block (revision 5 and up).
130 pub perms: Box<[u8]>,
131}
132
133/// Read an `/Encrypt` dictionary into an [`EncryptParams`].
134///
135/// # Errors
136///
137/// [`Error::UnsupportedHandler`] for a `/Filter` other than `/Standard`,
138/// [`Error::MissingCryptFilter`] when a named filter is not in `/CF`, and
139/// [`Error::MalformedEncryptDict`] or [`Error::CipherKeyLength`] when the key
140/// length does not resolve. `[oracle-bug]` naming *different* filters in
141/// `/StmF` and `/StrF` is **not** an error — see
142/// [`EncryptParams::string_cipher`].
143pub fn parse_encrypt_dict(dict: &Dict, r: &impl Resolve) -> Result<EncryptParams, Error> {
144 // The parser rejects a non-standard handler on the name-typed reading; a
145 // string-typed /Filter reads as absent here and so is unsupported too.
146 let filter = dict.name(names::FILTER);
147 if filter != Some(names::STANDARD) {
148 let spelling = filter.map(|n| n.as_bytes().into()).unwrap_or_default();
149 return Err(Error::UnsupportedHandler(spelling));
150 }
151
152 let version = dict.int(names::V, r).unwrap_or(0);
153 let revision = dict.int(names::R, r).unwrap_or(0);
154 let permissions = as_u32(dict.int(names::P, r).unwrap_or(-1));
155 let encrypt_metadata = dict.bool(names::ENCRYPT_METADATA).unwrap_or(true);
156
157 let (cipher, string_cipher, key_len) = resolve_cipher(dict, version, r)?;
158 let embedded_cipher = embedded_cipher(dict, version, cipher, r)?;
159
160 Ok(EncryptParams {
161 version,
162 embedded_cipher,
163 revision,
164 permissions,
165 cipher,
166 string_cipher,
167 key_len,
168 encrypt_metadata,
169 o: byte_string(dict, names::O, r),
170 u: byte_string(dict, names::U, r),
171 oe: byte_string(dict, names::OE, r),
172 ue: byte_string(dict, names::UE, r),
173 perms: byte_string(dict, names::PERMS, r),
174 })
175}
176
177/// The `/P` word as the unsigned value every comparison uses.
178///
179/// `/P` is written as a signed integer and compared as a `uint32`; a file may
180/// also spell it as the already-unsigned `4294967232`, which the integer
181/// reading has narrowed to `-64` by the time it arrives here.
182fn as_u32(value: i64) -> u32 {
183 #[expect(
184 clippy::cast_possible_truncation,
185 reason = "the C-int wrap is the semantic"
186 )]
187 let narrowed = value as i32;
188 narrowed.cast_unsigned()
189}
190
191/// A string-valued entry as raw bytes, empty when absent.
192fn byte_string(dict: &Dict, key: &Name, r: &impl Resolve) -> Box<[u8]> {
193 dict.byte_string(key, r).unwrap_or_default().into()
194}
195
196/// Resolve `/V`, `/Length`, `/CF` and `/CFM` into the two class ciphers and a
197/// key length.
198///
199/// The version 4 branch carries two quirks that keep real files opening: a
200/// `/Length` under 40 is read as *bytes* and multiplied by 8 (so a file
201/// writing `/Length 16` for a 128-bit key works), and a `/CFM` PDFium does
202/// not recognise leaves the cipher as RC4 rather than failing.
203///
204/// `[oracle-bug]` The stream and string classes are resolved **independently**
205/// and each defaults to `/Identity` — see [`EncryptParams::string_cipher`].
206fn resolve_cipher(
207 dict: &Dict,
208 version: i64,
209 r: &impl Resolve,
210) -> Result<(Cipher, Cipher, usize), Error> {
211 let (cipher, string_cipher, key_bits) = if version >= 4 {
212 let (stream_name, string_name) = crypt_filter_names(dict, r);
213 let stream_identity = is_identity(&stream_name);
214 let string_identity = is_identity(&string_name);
215 if stream_identity && string_identity {
216 return Ok((Cipher::None, Cipher::None, 0));
217 }
218 let filters = dict.dict(names::CF, r).ok_or(Error::MalformedEncryptDict(
219 "/CF is missing or not a dictionary",
220 ))?;
221 // The non-Identity class names the filter that supplies the cipher and
222 // key length; when both do and they differ, the stream's wins, which
223 // is the case this record deliberately does not model.
224 let name = if stream_identity {
225 string_name
226 } else {
227 stream_name
228 };
229 let filter = filters
230 .dict(&name, r)
231 .ok_or_else(|| Error::MissingCryptFilter(name.as_bytes().into()))?;
232
233 // At version 4 the per-filter /Length wins, falling back to the
234 // document's; from version 5 the per-filter one is ignored outright.
235 let bits = if version == 4 {
236 match filter.int(names::LENGTH, r).unwrap_or(0) {
237 0 => dict.int(names::LENGTH, r).unwrap_or(128),
238 bits => bits,
239 }
240 } else {
241 dict.int(names::LENGTH, r).unwrap_or(256)
242 };
243 if bits < 0 {
244 return Err(Error::MalformedEncryptDict("/Length is negative"));
245 }
246 let bits = if bits < 40 { bits * 8 } else { bits };
247
248 let method = filter.byte_string(names::CFM, r).unwrap_or_default();
249 let resolved = if method == b"AESV2" || method == b"AESV3" {
250 Cipher::Aes
251 } else {
252 Cipher::Rc4
253 };
254 let stream = if stream_identity {
255 Cipher::None
256 } else {
257 resolved
258 };
259 let string = if string_identity {
260 Cipher::None
261 } else {
262 resolved
263 };
264 (stream, string, bits)
265 } else if version > 1 {
266 let bits = dict.int(names::LENGTH, r).unwrap_or(40);
267 (Cipher::Rc4, Cipher::Rc4, bits)
268 } else {
269 // Version 1 is 40-bit RC4 by definition; its /Length is ignored.
270 (Cipher::Rc4, Cipher::Rc4, 40)
271 };
272
273 let key_len = usize::try_from(key_bits / 8)
274 .map_err(|_| Error::MalformedEncryptDict("/Length is negative"))?;
275 // The key length is a property of the filter, so it is checked against
276 // whichever class is not Identity.
277 let effective = if cipher == Cipher::None {
278 string_cipher
279 } else {
280 cipher
281 };
282 if key_len > 32 || !effective.accepts_key_len(key_len) {
283 return Err(Error::CipherKeyLength {
284 cipher: effective.label(),
285 len: key_len,
286 });
287 }
288 Ok((cipher, string_cipher, key_len))
289}
290
291/// The cipher an embedded-file stream is decrypted with — `/EFF`'s filter
292/// (ISO 32000-1 §7.6.5 table 20), or `None` when `/EFF` is absent or names
293/// the same filter the streams use.
294///
295/// `None` is not "no encryption": it means the embedded class needs no
296/// override, and [`crate::CryptClass::Embedded`] falls back to the stream
297/// cipher, which is table 20's own default for a missing `/EFF`.
298///
299/// Only the *cipher* can differ. §7.6.5 gives every `/CF` entry the one file
300/// encryption key and a `/CFM` of its own, so a differing `/EFF` changes
301/// which algorithm decrypts an embedded file, never which key.
302//
303// [oracle-bug] `grep '"EFF"' core/ fpdfsdk/` over the oracle returns **zero
304// hits**: `/EFF` is read nowhere in PDFium. `CPDF_SecurityHandler::LoadDict`
305// (cpdf_security_handler.cpp:303-311) takes one filter name and builds one
306// `CPDF_CryptoHandler`, so an embedded file stream is decrypted with the
307// stream filter whatever `/EFF` says — and a document whose `/EFF` names an
308// AES filter while `/StmF` names an RC4 one silently produces garbage for
309// every attachment. §7.6.5 table 20 defines `/EFF` as a distinct default for
310// embedded file streams, independent of `/StmF`. pdf.js carries it
311// separately: `crypto.js:1120` reads it with the `/StmF` default
312// (`eff = dict.get("EFF") || stmf`), consults it at `:1206` and hands it to
313// the cipher transform as `embeddedFilterName` at `:1336`.
314fn embedded_cipher(
315 dict: &Dict,
316 version: i64,
317 stream_cipher: Cipher,
318 r: &impl Resolve,
319) -> Result<Option<Cipher>, Error> {
320 // Below version 4 there are no crypt filters at all, so there is nothing
321 // for `/EFF` to name.
322 if version < 4 {
323 return Ok(None);
324 }
325 let Some(name) = dict.byte_string(names::EFF, r) else {
326 return Ok(None);
327 };
328 // Table 20's default for an absent `/EFF` is `/StmF`, so naming `/StmF`'s
329 // own filter is the default written out and needs no override.
330 if name == dict.byte_string(names::STM_F, r).unwrap_or_default() {
331 return Ok(None);
332 }
333 if name == names::IDENTITY.as_bytes() {
334 return Ok(Some(Cipher::None));
335 }
336 let filters = dict.dict(names::CF, r).ok_or(Error::MalformedEncryptDict(
337 "/CF is missing or not a dictionary",
338 ))?;
339 let Some(filter) = filters.dict(&Name::from(name.as_slice()), r) else {
340 // An `/EFF` naming a filter `/CF` does not have is damage, not a
341 // reason to refuse the document: the streams still decrypt. Fall back
342 // to the stream cipher, which is what the absent-key default gives.
343 return Ok(None);
344 };
345 let method = filter.byte_string(names::CFM, r).unwrap_or_default();
346 let cipher = if method == b"AESV2" || method == b"AESV3" {
347 Cipher::Aes
348 } else {
349 Cipher::Rc4
350 };
351 Ok((cipher != stream_cipher).then_some(cipher))
352}
353
354/// The crypt filter both `/StmF` and `/StrF` must name.
355/// `/StmF` and `/StrF`, each defaulting to `/Identity`.
356///
357/// `[oracle-bug]` §7.6.5 table 20 defines both as independent entries whose
358/// default is `Identity`, so the two are resolved separately and a document
359/// naming neither is two `Identity` defaults rather than an error.
360// [oracle-bug] cpdf_security_handler.cpp:305 and :325 compare the two raw
361// names and return false on inequality — and the comparison runs *before*
362// any default is applied, so an absent entry reads as the empty name, which
363// is neither Identity nor a key in /CF, and a V>=4 document with neither
364// entry present is refused as well. pdf.js applies the defaults and consults
365// the two independently (crypto.js:1116-1120).
366fn crypt_filter_names(dict: &Dict, r: &impl Resolve) -> (Name, Name) {
367 let named = |key| match dict.byte_string(key, r) {
368 Some(bytes) if !bytes.is_empty() => Name::from(bytes.as_slice()),
369 _ => names::IDENTITY.clone(),
370 };
371 (named(names::STM_F), named(names::STR_F))
372}
373
374/// Whether a resolved class filter is the `Identity` filter.
375fn is_identity(name: &Name) -> bool {
376 name.as_bytes() == names::IDENTITY.as_bytes()
377}
378
379/// Pad a password to the fixed 32 bytes every revision 2 to 4 algorithm
380/// hashes (ISO 32000 §7.6.3.3, "Algorithm 2" step a).
381///
382/// The first bytes are the password, up to 32; the rest come from the *front*
383/// of the pad, not from the pad position they sit at. A password of 32 bytes
384/// or more is truncated with no padding at all, so no length is ever encoded.
385fn pad_password(password: &[u8]) -> [u8; 32] {
386 let mut out = [0u8; 32];
387 let taken = password.len().min(out.len());
388 if let (Some(head), Some(source)) = (out.get_mut(..taken), password.get(..taken)) {
389 head.copy_from_slice(source);
390 }
391 if let (Some(tail), Some(fill)) = (out.get_mut(taken..), PAD.get(..32 - taken)) {
392 tail.copy_from_slice(fill);
393 }
394 out
395}
396
397/// ISO 32000 Algorithm 2 — the revision 2 to 4 file encryption key.
398///
399/// The MD5 order is exact and every optional piece contributes nothing at all
400/// when absent, not a length marker: the padded password, `/O` verbatim at
401/// whatever length the file wrote it, `/P` as a little-endian word, the first
402/// `/ID` element when non-empty, and — unless `ignore_metadata` overrides it —
403/// four `0xFF` bytes when revision 3 or later turned `/EncryptMetadata` off.
404///
405/// The revision 3 strengthening loop hashes only the first `key_len` bytes of
406/// each digest while writing a full 16-byte one, fifty times.
407fn file_key_r234(
408 p: &EncryptParams,
409 password: &[u8],
410 file_id: &[u8],
411 ignore_metadata: bool,
412) -> SmallKey {
413 let passcode = pad_password(password);
414 let perm = p.permissions.to_le_bytes();
415 let metadata_tag = [0xFFu8; 4];
416 let revision_3_or_later = p.revision >= 3;
417 let mut parts: Vec<&[u8]> = vec![&passcode, &p.o, &perm];
418 if !file_id.is_empty() {
419 parts.push(file_id);
420 }
421 if !ignore_metadata && revision_3_or_later && !p.encrypt_metadata {
422 parts.push(&metadata_tag);
423 }
424 let mut digest = md5_parts(&parts);
425
426 let copy_len = p.key_len.min(digest.len());
427 if revision_3_or_later {
428 for _ in 0..50 {
429 digest = digest.get(..copy_len).map_or(digest, md5);
430 }
431 }
432 SmallKey::from_prefix(&digest, p.key_len)
433}
434
435/// ISO 32000 Algorithms 4 and 5 — does `password` unlock the document as the
436/// user password? Answers with the derived file key.
437///
438/// Only the first 16 bytes of `/U` are ever compared, at every revision. A
439/// `/U` shorter than that is a damage guard, but one of 16 to 31 bytes is
440/// accepted and zero-padded into the 32-byte working buffer.
441fn check_user_password_r234(
442 p: &EncryptParams,
443 password: &[u8],
444 file_id: &[u8],
445 ignore_metadata: bool,
446) -> Option<SmallKey> {
447 let key = file_key_r234(p, password, file_id, ignore_metadata);
448 let stored = p.u.get(..16)?;
449
450 if p.revision == 2 {
451 let encrypted = rc4(key.bytes(), &PAD);
452 return encrypted
453 .get(..16)
454 .is_some_and(|got| ct_eq(got, stored))
455 .then_some(key);
456 }
457
458 // Revision 3 and up: undo twenty rounds of RC4 under keys derived by
459 // xor-ing the file key with the round number, then compare against the
460 // hash of the pad and the file id.
461 let mut test = [0u8; 32];
462 let copied = p.u.len().min(test.len());
463 if let (Some(head), Some(source)) = (test.get_mut(..copied), p.u.get(..copied)) {
464 head.copy_from_slice(source);
465 }
466 let mut round_key = [0u8; 32];
467 for round in (0..20u8).rev() {
468 for (slot, byte) in round_key.iter_mut().zip(key.bytes()) {
469 *slot = byte ^ round;
470 }
471 rc4_in_place(round_key.get(..key.len()).unwrap_or_default(), &mut test);
472 }
473
474 let expected = if file_id.is_empty() {
475 md5(&PAD)
476 } else {
477 md5_parts(&[&PAD, file_id])
478 };
479 test.get(..16)
480 .zip(expected.get(..16))
481 .is_some_and(|(got, want)| ct_eq(got, want))
482 .then_some(key)
483}
484
485/// ISO 32000 Algorithm 7 — recover the user password from the owner password.
486///
487/// An `/O` shorter than 32 bytes yields an empty recovered password, which
488/// then fails the user check; that guard is what keeps a truncated `/O` from
489/// reading out of bounds.
490///
491/// The trailing strip is deliberately not a general "remove the padding": it
492/// compares each tail byte against the *pad byte at the same index*, so a
493/// recovered password whose own last byte happens to equal the pad byte there
494/// loses one byte too many. Files were produced against this behavior.
495fn recover_user_password(p: &EncryptParams, owner_password: &[u8]) -> Vec<u8> {
496 let Some(stored) = p.o.get(..32) else {
497 return Vec::new();
498 };
499
500 let mut digest = md5(&pad_password(owner_password));
501 if p.revision >= 3 {
502 // Unlike Algorithm 2's loop, this one re-hashes the whole digest.
503 for _ in 0..50 {
504 digest = md5(&digest);
505 }
506 }
507 let key = SmallKey::from_prefix(&digest, p.key_len);
508
509 let mut buf = [0u8; 32];
510 buf.copy_from_slice(stored);
511 if p.revision == 2 {
512 rc4_in_place(key.bytes(), &mut buf);
513 } else {
514 let mut round_key = [0u8; 32];
515 for round in (0..20u8).rev() {
516 for (slot, byte) in round_key.iter_mut().zip(key.bytes()) {
517 *slot = byte ^ round;
518 }
519 rc4_in_place(round_key.get(..key.len()).unwrap_or_default(), &mut buf);
520 }
521 }
522
523 let mut len = buf.len();
524 while len > 0 && PAD.get(len - 1) == buf.get(len - 1) {
525 len -= 1;
526 }
527 buf.get(..len).unwrap_or_default().to_vec()
528}
529
530/// ISO 32000-2 Algorithm 2.A — the revision 5 and 6 password check.
531///
532/// Returns the 32-byte file key on success. Both `/O` and `/U` must be at
533/// least 48 bytes whichever role is being checked, because the owner check
534/// hashes the whole of `/U` alongside the password.
535fn check_password_aes256(p: &EncryptParams, password: &[u8], owner: bool) -> Option<[u8; 32]> {
536 let owner_entry: &[u8; 48] = p.o.get(..48)?.try_into().ok()?;
537 let user_entry: &[u8; 48] = p.u.get(..48)?.try_into().ok()?;
538 let entry = if owner { owner_entry } else { user_entry };
539 let vector = owner.then_some(user_entry);
540
541 let validation_salt: [u8; 8] = entry.get(32..40)?.try_into().ok()?;
542 let key_salt: [u8; 8] = entry.get(40..48)?.try_into().ok()?;
543
544 let hash = |salt: [u8; 8]| -> [u8; 32] {
545 if p.revision >= 6 {
546 revision6_hash(password, salt, vector)
547 } else {
548 match vector {
549 Some(v) => sha256_parts(&[password, &salt, v]),
550 None => sha256_parts(&[password, &salt]),
551 }
552 }
553 };
554
555 if !entry
556 .get(..32)
557 .is_some_and(|got| ct_eq(got, hash(validation_salt).as_slice()))
558 {
559 return None;
560 }
561
562 let intermediate = hash(key_salt);
563 let encrypted = if owner { &p.oe } else { &p.ue };
564 let mut file_key: [u8; 32] = encrypted.get(..32)?.try_into().ok()?;
565 aes_cbc_decrypt(&intermediate, &[0u8; BLOCK], &mut file_key).ok()?;
566
567 check_perms(p, &file_key).then_some(file_key)
568}
569
570/// Validate `/Perms`, the encrypted copy of the permission word.
571///
572/// A short `/Perms` is zero-padded into the single block rather than
573/// rejected, and the metadata comparison is deliberately one-sided: producers
574/// disagree with themselves often enough that the decrypted block is treated
575/// as the truth, and the only rejected combination is a block claiming
576/// metadata *is* encrypted while `/EncryptMetadata` says it is not.
577fn check_perms(p: &EncryptParams, file_key: &[u8; 32]) -> bool {
578 if p.perms.is_empty() {
579 return false;
580 }
581 let mut block = [0u8; BLOCK];
582 let copied = p.perms.len().min(block.len());
583 match (block.get_mut(..copied), p.perms.get(..copied)) {
584 (Some(head), Some(source)) => head.copy_from_slice(source),
585 _ => return false,
586 }
587 if aes_cbc_decrypt(file_key, &[0u8; BLOCK], &mut block).is_err() {
588 return false;
589 }
590
591 if block.get(9..12) != Some(b"adb") {
592 return false;
593 }
594 let Some(word) = block.get(..4).and_then(|w| <[u8; 4]>::try_from(w).ok()) else {
595 return false;
596 };
597 if u32::from_le_bytes(word) != p.permissions {
598 return false;
599 }
600 block.get(8) == Some(&b'F') || p.encrypt_metadata
601}
602
603/// ISO 32000-2 Algorithm 2.B — the revision 6 hardened iterated hash.
604///
605/// Each round encrypts sixty-four repetitions of
606/// `password || K[..block_size] || vector?` under AES-128-CBC keyed by the
607/// *current* `K`, then re-hashes the whole ciphertext with SHA-256, -384 or
608/// -512 as selected by the ciphertext's first sixteen bytes modulo three. The
609/// sixty-four-fold repetition is what guarantees the buffer is block-aligned
610/// for any password length.
611///
612/// The loop runs at least 64 rounds and stops once the last byte of the
613/// *whole* ciphertext — not of the digest — falls to `i - 32`, which caps it
614/// at 287.
615pub(crate) fn revision6_hash(
616 password: &[u8],
617 salt: [u8; 8],
618 vector: Option<&[u8; 48]>,
619) -> [u8; 32] {
620 revision6_hash_counted(password, salt, vector).0
621}
622
623/// [`revision6_hash`] with the round count, so tests can assert the loop's
624/// bounds without inferring them from timings.
625fn revision6_hash_counted(
626 password: &[u8],
627 salt: [u8; 8],
628 vector: Option<&[u8; 48]>,
629) -> ([u8; 32], u32) {
630 let mut state: Vec<u8> = match vector {
631 Some(v) => sha256_parts(&[password, &salt, v]),
632 None => sha256_parts(&[password, &salt]),
633 }
634 .to_vec();
635
636 let mut block_size = 32usize;
637 let mut round = 0u32;
638 loop {
639 let piece = state.get(..block_size).unwrap_or(&state);
640 let mut content = Vec::with_capacity(64 * (password.len() + piece.len() + 48));
641 for _ in 0..64 {
642 content.extend_from_slice(password);
643 content.extend_from_slice(piece);
644 if let Some(v) = vector {
645 content.extend_from_slice(v);
646 }
647 }
648
649 // The key and IV come from the head of the current state regardless
650 // of how long `block_size` has grown.
651 let (Some(key), Some(iv)) = (
652 state.get(..16),
653 state.get(16..32).and_then(|s| <[u8; 16]>::try_from(s).ok()),
654 ) else {
655 return ([0u8; 32], round);
656 };
657 if aes_cbc_encrypt(key, &iv, &mut content).is_err() {
658 return ([0u8; 32], round);
659 }
660
661 let Some(head) = content.get(..16) else {
662 return ([0u8; 32], round);
663 };
664 state = match big_order_64_bits_mod3(head) {
665 0 => {
666 block_size = 32;
667 sha256(&content).to_vec()
668 }
669 1 => {
670 block_size = 48;
671 sha384(&content).to_vec()
672 }
673 _ => {
674 block_size = 64;
675 sha512(&content).to_vec()
676 }
677 };
678
679 round += 1;
680 // The comparison is `round - 32 < last_byte`, evaluated after the
681 // increment and against the last byte of the *whole* ciphertext
682 // rather than of the digest. At `round >= 64` the left side is at
683 // least 32, so a last byte of 255 caps the loop at 287 rounds.
684 let last = u32::from(content.last().copied().unwrap_or(0));
685 if round >= 64 && round.saturating_sub(32) >= last {
686 break;
687 }
688 }
689
690 let hash = state
691 .get(..32)
692 .and_then(|s| <[u8; 32]>::try_from(s).ok())
693 .unwrap_or([0u8; 32]);
694 (hash, round)
695}
696
697/// Fold four big-endian words of `data` modulo three.
698///
699/// Arithmetically this equals the byte sum modulo three, because `2^32 ≡ 1
700/// (mod 3)`, but the fold is written as the C++ writes it so no equivalence
701/// argument sits between the specification and the code.
702fn big_order_64_bits_mod3(data: &[u8]) -> u64 {
703 let mut acc = 0u64;
704 for chunk in data.as_chunks::<4>().0.iter().take(4) {
705 let word = u32::from_be_bytes(*chunk);
706 acc = ((acc << 32) | u64::from(word)) % 3;
707 }
708 acc
709}
710
711/// ISO 32000-2 §7.6.4.3.3 (Algorithm 2.A) step (a): the byte length a
712/// revision-6 password is truncated to, **after** UTF-8 encoding.
713const R6_PASSWORD_BYTES: usize = 127;
714
715/// Which spelling of a password unlocked a document.
716///
717/// The authentication path tries a document's password in several spellings
718/// (see `try_password`) and this records the one that worked. This crate
719/// never *sets* a password — building an `/Encrypt` dictionary is out of
720/// scope, and the save path re-uses the file key the original password already
721/// produced — so the value is reportable state rather than an input to
722/// anything here.
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
724pub enum PasswordEncoding {
725 /// The password bytes as supplied unlocked the document.
726 #[default]
727 AsGiven,
728 /// The bytes were valid UTF-8, `SASLprep` (RFC 4013) changed them, and the
729 /// prepared form — re-encoded as UTF-8 and cut to 127 bytes — unlocked the
730 /// document. ISO 32000-2 §7.6.4.3.3's own preparation, revision 6 only.
731 SaslPrepped,
732 /// Each byte was read as a Latin-1 scalar and re-encoded as UTF-8
733 /// (revision 5 and up, where passwords are nominally UTF-8).
734 Latin1ToUtf8,
735 /// The bytes were decoded as UTF-8 and narrowed to Latin-1 (revision 2 to
736 /// 4, which hash raw bytes).
737 Utf8ToLatin1,
738}
739
740/// A password that unlocked a document: the key it produced, the role it
741/// played, and the encoding that worked.
742#[derive(Debug, Clone)]
743pub(crate) struct Unlocked {
744 pub key: SmallKey,
745 pub encoding: PasswordEncoding,
746}
747
748/// Try `password` in the given role, in each spelling the format admits, and
749/// return the first that authenticates.
750///
751/// The candidates, in order:
752///
753/// 1. **The specification's preparation**, revision 6 only: `SASLprep`
754/// (RFC 4013), UTF-8, truncated to 127 *bytes* — ISO 32000-2 §7.6.4.3.3
755/// Algorithm 2.A step (a). Skipped when the password is not valid UTF-8
756/// (nothing to prepare), when `SASLprep` refuses it (a prohibited character
757/// or a bidirectional violation), and when preparation is the identity, in
758/// which case candidate 2 already covers it.
759///
760/// Revision 5 is deliberately **not** prepared. Algorithm 2.A is what
761/// revision 6 is; the Adobe extension level 3 algorithm that revision 5
762/// implements has no preparation step, and pdf.js draws the line at the
763/// same place — `crypto.js:1142` guards the `saslPrep` call with
764/// `revision === 6`, and its `algorithm === 5` branch two lines later
765/// encodes UTF-8 with no preparation at all.
766///
767/// 2. **The bytes as given.** A file whose producer skipped the preparation
768/// hashed the raw bytes, so the raw bytes must still be tried. This is
769/// pdf.js's tolerance, at `crypto.js:1178-1180`, where a prepped password
770/// that differs from the raw one yields *two* candidates rather than one.
771///
772/// 3. **A transcode retry**, `[oracle-bug]`. A non-ASCII password is retried
773/// with a Latin-1 to UTF-8 transcode (revision 5 and up) or a UTF-8 to
774/// Latin-1 one (revisions 2 to 4). That is not the specification and it is
775/// not `PDFDocEncoding` either — the three disagree across `0x80..0x9F` —
776/// but it rescues a real class of embedder mis-encoding (a host that
777/// handed the library bytes in the wrong one of two encodings), no
778/// independent implementation contradicts it, and by running last it can
779/// only turn a failure into a success. Kept as a tolerance, tried after
780/// the two conforming spellings.
781// [oracle-bug] The transcode is cpdf_security_handler.cpp:425-455, which
782// performs none of the specification's three preparation steps. pdf.js has
783// no equivalent: crypto.js:1136-1152 transcodes nothing.
784///
785/// A pure-ASCII password is a fixed point of every one of these conversions,
786/// so all three candidates collapse to one attempt — the early returns make
787/// that observable as the absence of extra work, which is what the
788/// ASCII-password fixtures pin.
789pub(crate) fn try_password(
790 p: &EncryptParams,
791 password: &[u8],
792 owner: bool,
793 file_id: &[u8],
794) -> Option<Unlocked> {
795 // (1) The specification's preparation.
796 if let Some(prepped) = r6_prepared(p.revision, password)
797 && prepped.as_slice() != password.get(..R6_PASSWORD_BYTES).unwrap_or(password)
798 && let Some(key) = check_password(p, &prepped, owner, file_id)
799 {
800 return Some(Unlocked {
801 key,
802 encoding: PasswordEncoding::SaslPrepped,
803 });
804 }
805
806 // (2) The bytes as given.
807 if let Some(key) = check_password(p, password, owner, file_id) {
808 return Some(Unlocked {
809 key,
810 encoding: PasswordEncoding::AsGiven,
811 });
812 }
813
814 // (3) [oracle-bug] PDFium's transcode retry, kept last as a tolerance:
815 // `cpdf_security_handler.cpp:425-455` performs none of ISO 32000-2
816 // §7.6.4.3.3's three preparation steps and substitutes this instead;
817 // pdf.js transcodes nothing (`crypto.js:1136-1152`). Running after the
818 // two correct candidates, it can only turn a failure into a success.
819 if password.is_ascii() {
820 return None;
821 }
822 let (converted, encoding) = if p.revision >= 5 {
823 (latin1_to_utf8(password), PasswordEncoding::Latin1ToUtf8)
824 } else {
825 (utf8_to_latin1(password), PasswordEncoding::Utf8ToLatin1)
826 };
827 check_password(p, &converted, owner, file_id).map(|key| Unlocked { key, encoding })
828}
829
830/// ISO 32000-2 §7.6.4.3.3 Algorithm 2.A step (a) applied to `password`, or
831/// `None` when the revision is not 6, the bytes are not UTF-8, or `SASLprep`
832/// refuses them.
833///
834/// The truncation cuts the **UTF-8 byte string**, not the character sequence,
835/// which is what the specification says and what pdf.js does
836/// (`crypto.js:896-897`, `Math.min(127, password.length)` over the already
837/// encoded byte array). A multi-byte character straddling byte 127 is
838/// therefore cut mid-sequence, leaving bytes that are not valid UTF-8 — and
839/// that is correct, because the hash is over bytes and both implementations
840/// hash the same ones.
841///
842/// Cutting here as well as in [`check_password`] is not redundant: it is what
843/// makes the `prepped != password` test below compare the bytes that will
844/// actually be hashed, so a preparation whose only effect lies past byte 127
845/// does not buy a second identical attempt.
846pub(crate) fn r6_prepared(revision: i64, password: &[u8]) -> Option<Vec<u8>> {
847 if revision != 6 {
848 return None;
849 }
850 let text = core::str::from_utf8(password).ok()?;
851 let prepared = crate::saslprep::saslprep(text)?;
852 let mut bytes = prepared.into_bytes();
853 bytes.truncate(R6_PASSWORD_BYTES);
854 Some(bytes)
855}
856
857/// One password attempt with no encoding fallback.
858///
859/// Below revision 5 the user check runs twice, once honoring
860/// `/EncryptMetadata` and once ignoring it, for files that wrote
861/// `/EncryptMetadata false` but computed `/U` without the tag. The key kept is
862/// the one the *successful* attempt derived.
863///
864/// At revision 5 and up the password is first cut to 127 bytes — ISO 32000-2
865/// §7.6.4.3.3 Algorithm 2.A step (a). The cut belongs *here* rather than to
866/// one candidate because it is a property of the AES-256 hash, not of the
867/// preparation, so every candidate tried is cut. `[oracle-bug]`
868// [oracle-bug] pdf.js applies the cut inside the key derivation
869// (crypto.js:896-897), so every candidate it tries is cut too. PDFium
870// applies it nowhere and hashes a 200-byte password whole
871// (cpdf_security_handler.cpp:425-455).
872fn check_password(
873 p: &EncryptParams,
874 password: &[u8],
875 owner: bool,
876 file_id: &[u8],
877) -> Option<SmallKey> {
878 if p.revision >= 5 {
879 let capped = password.get(..R6_PASSWORD_BYTES).unwrap_or(password);
880 return check_password_aes256(p, capped, owner).map(SmallKey::from_full);
881 }
882 let effective = if owner {
883 recover_user_password(p, password)
884 } else {
885 password.to_vec()
886 };
887 check_user_password_r234(p, &effective, file_id, false)
888 .or_else(|| check_user_password_r234(p, &effective, file_id, true))
889}
890
891/// Read each byte as a Latin-1 scalar and re-encode the run as UTF-8.
892fn latin1_to_utf8(bytes: &[u8]) -> Vec<u8> {
893 let mut out = Vec::with_capacity(bytes.len());
894 for &byte in bytes {
895 let mut buf = [0u8; 4];
896 out.extend_from_slice(char::from(byte).encode_utf8(&mut buf).as_bytes());
897 }
898 out
899}
900
901/// Decode UTF-8 the way PDFium does, then narrow each scalar to one byte.
902///
903/// The decoder is deliberately lenient rather than strict or replacing: a
904/// stray continuation byte outside a sequence is *dropped*, a truncated
905/// sequence contributes nothing, and overlong or surrogate encodings are
906/// accepted as whatever they decode to. Nothing becomes `U+FFFD`. The
907/// narrowing then keeps the low byte of each scalar, so `U+00E2` and `U+2AE2`
908/// both narrow to `0xE2`.
909fn utf8_to_latin1(bytes: &[u8]) -> Vec<u8> {
910 const MAX_CODE_POINT: u32 = 0x0010_FFFF;
911 let mut out = Vec::with_capacity(bytes.len());
912 let mut remaining = 0u32;
913 let mut code_point = 0u32;
914 let emit = |cp: u32, out: &mut Vec<u8>| {
915 if cp <= MAX_CODE_POINT {
916 #[expect(clippy::cast_possible_truncation, reason = "narrowing is the semantic")]
917 out.push(cp as u8);
918 }
919 };
920 for &unit in bytes {
921 match unit {
922 0x00..=0x7F => {
923 remaining = 0;
924 emit(u32::from(unit), &mut out);
925 }
926 0x80..=0xBF => {
927 if remaining > 0 {
928 remaining -= 1;
929 code_point = (code_point << 6) | u32::from(unit & 0x3F);
930 if remaining == 0 {
931 emit(code_point, &mut out);
932 }
933 }
934 }
935 0xC0..=0xDF => {
936 remaining = 1;
937 code_point = u32::from(unit & 0x1F);
938 }
939 0xE0..=0xEF => {
940 remaining = 2;
941 code_point = u32::from(unit & 0x0F);
942 }
943 0xF0..=0xF7 => {
944 remaining = 3;
945 code_point = u32::from(unit & 0x07);
946 }
947 0xF8..=0xFF => remaining = 0,
948 }
949 }
950 out
951}
952
953#[cfg(test)]
954mod tests {
955 use super::{
956 Cipher, PasswordEncoding, big_order_64_bits_mod3, latin1_to_utf8, pad_password,
957 recover_user_password, revision6_hash, revision6_hash_counted, try_password,
958 utf8_to_latin1,
959 };
960 use crate::test_fixtures::{self, unhex};
961
962 // ISO 32000 Algorithm 2 step a: the tail comes from the front of the pad,
963 // not from the pad position it sits at.
964 #[test]
965 fn padding_fills_from_the_front_of_the_pad() {
966 assert_eq!(pad_password(b""), super::PAD);
967 let padded = pad_password(b"abc");
968 assert_eq!(padded.get(..3), Some(&b"abc"[..]));
969 assert_eq!(padded.get(3..), super::PAD.get(..29));
970 }
971
972 // A password of 32 bytes or more is truncated with no padding, so its
973 // length is never encoded anywhere.
974 #[test]
975 fn a_long_password_is_truncated_to_thirty_two_bytes() {
976 let long = [b'z'; 40];
977 assert_eq!(pad_password(&long), [b'z'; 32]);
978 assert_eq!(pad_password(&long), pad_password(&long[..32]));
979 }
980
981 // The loop runs at least 64 rounds and, since the stop test compares
982 // `round - 32` against a single byte, at most 32 + 255 = 287.
983 #[test]
984 fn the_hardened_hash_runs_between_sixty_four_and_two_hundred_eighty_seven_rounds() {
985 for seed in 0..6u8 {
986 let salt = [seed; 8];
987 let vector = [seed.wrapping_mul(3); 48];
988 for vec in [None, Some(&vector)] {
989 let (_, rounds) = revision6_hash_counted(b"password", salt, vec);
990 assert!(
991 (64..=287).contains(&rounds),
992 "{rounds} rounds for seed {seed}"
993 );
994 }
995 }
996 }
997
998 // The vector is part of the hash, so an owner check and a user check with
999 // the same password and salt land on different digests.
1000 #[test]
1001 fn the_hardened_hash_depends_on_every_input() {
1002 let salt = [1u8; 8];
1003 let vector = [2u8; 48];
1004 let plain = revision6_hash(b"pw", salt, None);
1005 assert_ne!(plain, revision6_hash(b"pw", salt, Some(&vector)));
1006 assert_ne!(plain, revision6_hash(b"pX", salt, None));
1007 assert_ne!(plain, revision6_hash(b"pw", [2u8; 8], None));
1008 // Deterministic: the same inputs always give the same digest.
1009 assert_eq!(plain, revision6_hash(b"pw", salt, None));
1010 }
1011
1012 // An empty password is legal and must not divide by zero building the
1013 // sixty-four repetitions.
1014 #[test]
1015 fn the_hardened_hash_accepts_an_empty_password() {
1016 let (hash, rounds) = revision6_hash_counted(b"", [0u8; 8], None);
1017 assert_ne!(hash, [0u8; 32]);
1018 assert!((64..=287).contains(&rounds));
1019 }
1020
1021 #[test]
1022 fn mod3_fold_agrees_with_the_byte_sum() {
1023 for seed in 0..64u8 {
1024 let data: Vec<u8> = (0..16u8)
1025 .map(|i| i.wrapping_mul(seed).wrapping_add(i))
1026 .collect();
1027 let sum: u32 = data.iter().map(|&b| u32::from(b)).sum();
1028 assert_eq!(
1029 big_order_64_bits_mod3(&data),
1030 u64::from(sum % 3),
1031 "data {data:?}"
1032 );
1033 }
1034 }
1035
1036 #[test]
1037 fn mod3_fold_reads_only_the_first_sixteen_bytes() {
1038 let mut data = vec![0u8; 32];
1039 assert_eq!(big_order_64_bits_mod3(&data), 0);
1040 // A change past byte 16 cannot move the result.
1041 if let Some(byte) = data.get_mut(20) {
1042 *byte = 1;
1043 }
1044 assert_eq!(big_order_64_bits_mod3(&data), 0);
1045 if let Some(byte) = data.get_mut(3) {
1046 *byte = 1;
1047 }
1048 assert_eq!(big_order_64_bits_mod3(&data), 1);
1049 }
1050
1051 #[test]
1052 fn latin1_widening_is_a_scalar_per_byte() {
1053 assert_eq!(latin1_to_utf8(b"\xe2ge"), b"\xc3\xa2ge");
1054 assert_eq!(latin1_to_utf8(b"h\xf4tel"), b"h\xc3\xb4tel");
1055 assert_eq!(latin1_to_utf8(b"ascii"), b"ascii");
1056 assert!(latin1_to_utf8(b"").is_empty());
1057 }
1058
1059 #[test]
1060 fn utf8_narrowing_keeps_the_low_byte() {
1061 assert_eq!(utf8_to_latin1(b"\xc3\xa2ge"), b"\xe2ge");
1062 assert_eq!(utf8_to_latin1(b"h\xc3\xb4tel"), b"h\xf4tel");
1063 assert_eq!(utf8_to_latin1(b"ascii"), b"ascii");
1064 // U+2AE2 narrows to its low byte, not to a substitution character.
1065 assert_eq!(utf8_to_latin1("\u{2ae2}".as_bytes()), b"\xe2");
1066 }
1067
1068 // PDFium's decoder drops what it cannot use instead of substituting
1069 // U+FFFD; a lone continuation byte and a truncated sequence both vanish.
1070 #[test]
1071 fn utf8_narrowing_drops_invalid_bytes_silently() {
1072 assert_eq!(utf8_to_latin1(b"a\x80b"), b"ab");
1073 assert_eq!(utf8_to_latin1(b"a\xc3"), b"a");
1074 assert_eq!(utf8_to_latin1(b"\xf8\xff"), b"");
1075 assert_eq!(utf8_to_latin1(b"a\xc3\xa2"), b"a\xe2");
1076 }
1077
1078 #[test]
1079 fn conversions_round_trip_on_latin1_text() {
1080 for text in [&b"\xe2ge"[..], b"h\xf4tel", b"", b"plain"] {
1081 assert_eq!(utf8_to_latin1(&latin1_to_utf8(text)), text);
1082 }
1083 }
1084
1085 // T5 — encrypted_hello_world_r2.pdf: /V 1 forces a five-byte key
1086 // regardless of /Length, and both password spellings unlock it.
1087 #[test]
1088 fn revision_2_fixture() {
1089 let p = test_fixtures::r2();
1090 let id = unhex("2b778de1bcef1733b35e680882812409");
1091 assert_eq!((p.cipher, p.key_len), (Cipher::Rc4, 5));
1092
1093 for owner_password in [&b"\xe2ge"[..], b"\xc3\xa2ge"] {
1094 let unlocked = try_password(&p, owner_password, true, &id)
1095 .unwrap_or_else(|| panic!("owner {owner_password:?}"));
1096 assert_eq!(unlocked.key.len(), 5);
1097 }
1098 for user_password in [&b"h\xf4tel"[..], b"h\xc3\xb4tel"] {
1099 assert!(
1100 try_password(&p, user_password, false, &id).is_some(),
1101 "user {user_password:?}"
1102 );
1103 }
1104 assert!(try_password(&p, b"tiger", true, &id).is_none());
1105 assert!(try_password(&p, b"tiger", false, &id).is_none());
1106 }
1107
1108 // The encoding fallback direction flips at revision 5: below it, a UTF-8
1109 // password is narrowed to Latin-1.
1110 #[test]
1111 fn revision_2_records_the_encoding_that_worked() {
1112 let p = test_fixtures::r2();
1113 let id = unhex("2b778de1bcef1733b35e680882812409");
1114 let latin1 = try_password(&p, b"\xe2ge", true, &id).expect("latin-1 owner");
1115 assert_eq!(latin1.encoding, PasswordEncoding::AsGiven);
1116 let utf8 = try_password(&p, b"\xc3\xa2ge", true, &id).expect("utf-8 owner");
1117 assert_eq!(utf8.encoding, PasswordEncoding::Utf8ToLatin1);
1118 // Both spellings arrive at the same file key.
1119 assert_eq!(latin1.key.bytes(), utf8.key.bytes());
1120 }
1121
1122 // T6 — encrypted_hello_world_r3.pdf: a 16-byte key through the fifty-round
1123 // strengthening loop, and a /U whose trailing sixteen bytes are zero,
1124 // which pins that only /U[0..16] is compared.
1125 #[test]
1126 fn revision_3_fixture() {
1127 let p = test_fixtures::r3();
1128 let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
1129 assert_eq!((p.cipher, p.key_len), (Cipher::Rc4, 16));
1130 assert_eq!(p.u.get(16..), Some(&[0u8; 16][..]));
1131
1132 for owner_password in [&b"\xe2ge"[..], b"\xc3\xa2ge"] {
1133 assert!(
1134 try_password(&p, owner_password, true, &id).is_some(),
1135 "owner {owner_password:?}"
1136 );
1137 }
1138 for user_password in [&b"h\xf4tel"[..], b"h\xc3\xb4tel"] {
1139 let unlocked = try_password(&p, user_password, false, &id)
1140 .unwrap_or_else(|| panic!("user {user_password:?}"));
1141 assert_eq!(unlocked.key.len(), 16);
1142 }
1143 assert!(try_password(&p, b"tiger", false, &id).is_none());
1144 }
1145
1146 // T11 — a truncated /O yields an empty recovered password rather than an
1147 // out-of-bounds read (crbug.com/42270437).
1148 #[test]
1149 fn short_owner_entry_recovers_nothing() {
1150 for len in [0usize, 1, 16, 31] {
1151 let mut p = test_fixtures::r3();
1152 p.o = vec![0xAB; len].into();
1153 assert!(
1154 recover_user_password(&p, b"a").is_empty(),
1155 "/O of {len} bytes"
1156 );
1157 let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
1158 assert!(try_password(&p, b"a", true, &id).is_none());
1159 }
1160 }
1161
1162 // T16 — with no /ID the contribution is skipped entirely in both the key
1163 // derivation and the Algorithm 5 comparison hash; the fixture's passwords
1164 // then no longer unlock it, which is exactly what proves the id took part.
1165 #[test]
1166 fn a_missing_file_id_changes_the_derived_key() {
1167 let p = test_fixtures::r3();
1168 let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
1169 assert!(try_password(&p, b"h\xf4tel", false, &id).is_some());
1170 assert!(try_password(&p, b"h\xf4tel", false, &[]).is_none());
1171 }
1172
1173 // ---- A31: ISO 32000-2 §7.6.4.3.3 password preparation ----
1174
1175 // The end-to-end proof that the preparation is *required*, not merely
1176 // permitted: pdf.js's `saslprep-r6.pdf`, whose /U was computed from the
1177 // prepared spelling of `S\u{00AA}SL\u{00AD}prep`. Neither the raw bytes
1178 // nor either Latin-1↔UTF-8 transcode opens it, so this file fails on the
1179 // old behaviour and is what candidate (1) exists for.
1180 #[test]
1181 fn the_pdfjs_saslprep_fixture_needs_the_preparation() {
1182 let dict = test_fixtures::saslprep_r6_dict();
1183 let p = super::parse_encrypt_dict(&dict, &pdfrum_object::NoResolve)
1184 .unwrap_or_else(|e| panic!("{e:?}"));
1185
1186 let raw = "S\u{00AA}SL\u{00AD}prep".as_bytes();
1187 let unlocked =
1188 try_password(&p, raw, false, &[]).unwrap_or_else(|| panic!("the prepared candidate"));
1189 assert_eq!(unlocked.encoding, PasswordEncoding::SaslPrepped);
1190
1191 // The prepared spelling given directly opens it too, and reports
1192 // itself as the bytes as given — nothing was left to prepare.
1193 let prepped = try_password(&p, b"SaSLprep", false, &[])
1194 .unwrap_or_else(|| panic!("the prepared spelling"));
1195 assert_eq!(prepped.encoding, PasswordEncoding::AsGiven);
1196 assert_eq!(unlocked.key.bytes(), prepped.key.bytes());
1197
1198 // And the wrong password still fails, so the ladder is not a
1199 // universal acceptor.
1200 assert!(try_password(&p, b"SASLprep", false, &[]).is_none());
1201 }
1202
1203 // Normalisation is two-directional: either spelling of an accented
1204 // password opens a file keyed on the other, because NFKC sends both to
1205 // the same string.
1206 #[test]
1207 fn a_decomposed_and_a_composed_password_prepare_alike() {
1208 assert_eq!(
1209 super::r6_prepared(6, "cafe\u{0301}".as_bytes()),
1210 super::r6_prepared(6, "caf\u{00E9}".as_bytes()),
1211 );
1212 assert_eq!(
1213 super::r6_prepared(6, "cafe\u{0301}".as_bytes()).as_deref(),
1214 Some("caf\u{00E9}".as_bytes()),
1215 );
1216 }
1217
1218 // Revision 5 is not prepared: Algorithm 2.A is revision 6's, and pdf.js
1219 // draws the same line at `crypto.js:1142`.
1220 #[test]
1221 fn only_revision_six_is_prepared() {
1222 let decomposed = "cafe\u{0301}".as_bytes();
1223 assert!(super::r6_prepared(6, decomposed).is_some());
1224 for revision in [2i64, 3, 4, 5] {
1225 assert_eq!(
1226 super::r6_prepared(revision, decomposed),
1227 None,
1228 "R{revision}"
1229 );
1230 }
1231 }
1232
1233 // The truncation cuts the UTF-8 *bytes*, so a multi-byte character
1234 // straddling byte 127 is cut mid-sequence — which is what pdf.js does at
1235 // `crypto.js:896-897`, where the cut is applied to the encoded array.
1236 #[test]
1237 fn the_cut_is_at_byte_one_hundred_twenty_seven_not_at_a_character() {
1238 // 126 ASCII bytes then a two-byte character: byte 127 is that
1239 // character's lead byte, and the trail byte is dropped.
1240 let mut password = "a".repeat(126);
1241 password.push('\u{00E9}');
1242 let prepared =
1243 super::r6_prepared(6, password.as_bytes()).unwrap_or_else(|| panic!("preparable"));
1244 assert_eq!(prepared.len(), 127);
1245 assert_eq!(prepared.get(126), Some(&0xC3));
1246 assert!(core::str::from_utf8(&prepared).is_err());
1247
1248 // And an all-ASCII password of 130 bytes keeps its first 127.
1249 let long = "z".repeat(130);
1250 let cut = super::r6_prepared(6, long.as_bytes()).unwrap_or_else(|| panic!("preparable"));
1251 assert_eq!(cut, "z".repeat(127).into_bytes());
1252 }
1253
1254 // A password 130 bytes long opens a file keyed on its first 127 — the
1255 // truncation applies to every candidate, because it lives in the
1256 // revision-5-and-up check rather than in the preparation. ISO 32000-2
1257 // caps the password at 127 bytes.
1258 #[test]
1259 fn a_password_past_one_hundred_twenty_seven_bytes_is_cut_for_every_candidate() {
1260 let dict = test_fixtures::saslprep_r6_dict();
1261 let p = super::parse_encrypt_dict(&dict, &pdfrum_object::NoResolve)
1262 .unwrap_or_else(|e| panic!("{e:?}"));
1263 let mut overlong = b"SaSLprep".to_vec();
1264 overlong.resize(200, b'!');
1265 // The first 127 bytes are not the password, so this must still fail —
1266 // the point of the assertion is that it is *the cut bytes* that are
1267 // hashed, which the next assertion pins from the other side.
1268 assert!(try_password(&p, &overlong, false, &[]).is_none());
1269
1270 let mut padded = b"SaSLprep".to_vec();
1271 padded.resize(127, b'!');
1272 let short = try_password(&p, &padded, false, &[]);
1273 let mut long = padded.clone();
1274 long.resize(130, b'?');
1275 // Two byte strings agreeing on their first 127 bytes authenticate
1276 // identically.
1277 assert_eq!(
1278 short.is_some(),
1279 try_password(&p, &long, false, &[]).is_some()
1280 );
1281 }
1282
1283 // A password SASLprep refuses skips candidate (1) and falls through to the
1284 // raw bytes, which is what keeps a file whose producer skipped the
1285 // preparation opening.
1286 #[test]
1287 fn a_prohibited_password_falls_through_to_the_raw_bytes() {
1288 // U+202A is table C.8; the preparation therefore yields nothing.
1289 assert_eq!(super::r6_prepared(6, "a\u{202A}b".as_bytes()), None);
1290 // Invalid UTF-8 has nothing to prepare either.
1291 assert_eq!(super::r6_prepared(6, b"\xe2ge"), None);
1292
1293 // And the revision-6 fixture, whose passwords are the raw Latin-1
1294 // bytes, still opens through candidates (2) and (3).
1295 let dict = test_fixtures::r6_dict();
1296 let p = super::parse_encrypt_dict(&dict, &pdfrum_object::NoResolve)
1297 .unwrap_or_else(|e| panic!("{e:?}"));
1298 let raw = try_password(&p, b"h\xf4tel", false, &[])
1299 .unwrap_or_else(|| panic!("the transcode candidate"));
1300 assert_eq!(raw.encoding, PasswordEncoding::Latin1ToUtf8);
1301 let utf8 = try_password(&p, "h\u{00F4}tel".as_bytes(), false, &[])
1302 .unwrap_or_else(|| panic!("the bytes as given"));
1303 assert_eq!(utf8.encoding, PasswordEncoding::AsGiven);
1304 }
1305
1306 // An ASCII password is a fixed point of every conversion, so the ladder
1307 // collapses to one attempt and reports the identity.
1308 #[test]
1309 fn an_ascii_password_reports_the_bytes_as_given() {
1310 let dict = test_fixtures::r6_dict();
1311 let p = super::parse_encrypt_dict(&dict, &pdfrum_object::NoResolve)
1312 .unwrap_or_else(|e| panic!("{e:?}"));
1313 assert!(try_password(&p, b"tiger", false, &[]).is_none());
1314 assert_eq!(
1315 super::r6_prepared(6, b"tiger").as_deref(),
1316 Some(&b"tiger"[..])
1317 );
1318 }
1319}