1use crate::decrypt::r5_r6::{algorithm_10, algorithm_8, algorithm_9};
33use crate::decrypt::{md5, rc4, CryptMethod, StandardHandler};
34use crate::error::PdfError;
35use crate::objects::{Dict, Object};
36
37const PAD: [u8; 32] = [
39 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
40 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
41];
42
43fn pad_password(password: &[u8]) -> [u8; 32] {
45 let mut out = [0u8; 32];
46 let take = password.len().min(32);
47 out[..take].copy_from_slice(&password[..take]);
48 if take < 32 {
49 out[take..].copy_from_slice(&PAD[..32 - take]);
50 }
51 out
52}
53
54#[derive(Clone, Debug)]
59pub struct EncryptionConfig {
60 pub revision: u8,
63 pub length_bits: usize,
66 pub user_password: Vec<u8>,
68 pub owner_password: Vec<u8>,
71 pub p: i32,
73 pub encrypt_metadata: bool,
75 pub method: CryptMethod,
77 pub file_id: Vec<u8>,
81 pub u_salt_validate: [u8; 8],
83 pub u_salt_key: [u8; 8],
85 pub o_salt_validate: [u8; 8],
87 pub o_salt_key: [u8; 8],
89 pub file_key_v5: Option<[u8; 32]>,
93 pub perms_padding: [u8; 4],
95 pub aes_iv: [u8; 16],
98}
99
100impl EncryptionConfig {
101 pub fn aes_128(user_password: &[u8], file_id: &[u8]) -> Self {
104 Self {
105 revision: 4,
106 length_bits: 128,
107 user_password: user_password.to_vec(),
108 owner_password: Vec::new(),
109 p: -4,
110 encrypt_metadata: true,
111 method: CryptMethod::Aes128,
112 file_id: file_id.to_vec(),
113 u_salt_validate: [0; 8],
114 u_salt_key: [0; 8],
115 o_salt_validate: [0; 8],
116 o_salt_key: [0; 8],
117 file_key_v5: None,
118 perms_padding: [0; 4],
119 aes_iv: [0; 16],
120 }
121 }
122
123 pub fn rc4_128(user_password: &[u8], file_id: &[u8]) -> Self {
125 Self {
126 revision: 3,
127 length_bits: 128,
128 user_password: user_password.to_vec(),
129 owner_password: Vec::new(),
130 p: -4,
131 encrypt_metadata: true,
132 method: CryptMethod::Rc4,
133 file_id: file_id.to_vec(),
134 u_salt_validate: [0; 8],
135 u_salt_key: [0; 8],
136 o_salt_validate: [0; 8],
137 o_salt_key: [0; 8],
138 file_key_v5: None,
139 perms_padding: [0; 4],
140 aes_iv: [0; 16],
141 }
142 }
143
144 pub fn rc4_40(user_password: &[u8], file_id: &[u8]) -> Self {
146 Self {
147 revision: 2,
148 length_bits: 40,
149 user_password: user_password.to_vec(),
150 owner_password: Vec::new(),
151 p: -4,
152 encrypt_metadata: true,
153 method: CryptMethod::Rc4,
154 file_id: file_id.to_vec(),
155 u_salt_validate: [0; 8],
156 u_salt_key: [0; 8],
157 o_salt_validate: [0; 8],
158 o_salt_key: [0; 8],
159 file_key_v5: None,
160 perms_padding: [0; 4],
161 aes_iv: [0; 16],
162 }
163 }
164
165 pub fn aes_256_r5(user_password: &[u8], file_id: &[u8]) -> Self {
167 Self {
168 revision: 5,
169 length_bits: 256,
170 user_password: user_password.to_vec(),
171 owner_password: Vec::new(),
172 p: -4,
173 encrypt_metadata: true,
174 method: CryptMethod::Aes256,
175 file_id: file_id.to_vec(),
176 u_salt_validate: [0x55; 8],
177 u_salt_key: [0x55; 8],
178 o_salt_validate: [0xAA; 8],
179 o_salt_key: [0xAA; 8],
180 file_key_v5: None,
181 perms_padding: [0xCA, 0xFE, 0xBA, 0xBE],
182 aes_iv: [0; 16],
183 }
184 }
185
186 pub fn aes_256_r6(user_password: &[u8], file_id: &[u8]) -> Self {
188 let mut c = Self::aes_256_r5(user_password, file_id);
189 c.revision = 6;
190 c
191 }
192
193 pub fn with_owner_password(mut self, owner: &[u8]) -> Self {
195 self.owner_password = owner.to_vec();
196 self
197 }
198
199 pub fn with_permissions(mut self, p: i32) -> Self {
201 self.p = p;
202 self
203 }
204}
205
206#[derive(Clone, Debug)]
211pub struct EncryptionState {
212 pub handler: StandardHandler,
214 pub encrypt_dict: Dict,
217 pub file_id: Vec<u8>,
219 pub aes_iv: [u8; 16],
224}
225
226impl EncryptionState {
227 pub fn build(config: &EncryptionConfig) -> Result<Self, PdfError> {
229 if !(2..=6).contains(&config.revision) {
231 return Err(PdfError::other(format!(
232 "PDF encrypt: revision R={} not supported (R∈[2,6])",
233 config.revision
234 )));
235 }
236 if config.revision >= 5 && config.length_bits != 256 {
237 return Err(PdfError::other(format!(
238 "PDF encrypt: V=5 requires Length=256 bits (got {})",
239 config.length_bits
240 )));
241 }
242 if config.revision <= 4
243 && (config.length_bits % 8 != 0 || !(40..=128).contains(&config.length_bits))
244 {
245 return Err(PdfError::other(format!(
246 "PDF encrypt: V≤4 requires Length∈[40..=128] (mult of 8); got {}",
247 config.length_bits
248 )));
249 }
250
251 if config.revision >= 5 {
252 Self::build_v5(config)
253 } else {
254 Self::build_v_le_4(config)
255 }
256 }
257
258 fn build_v_le_4(c: &EncryptionConfig) -> Result<Self, PdfError> {
259 let n = c.length_bits / 8;
260
261 let o = algorithm_3(&c.user_password, &c.owner_password, c.revision, n);
263
264 let key = algorithm_2_filekey(
266 &c.user_password,
267 &o,
268 c.p,
269 &c.file_id,
270 c.revision,
271 n,
272 c.encrypt_metadata,
273 );
274
275 let u = if c.revision == 2 {
277 let mut out = [0u8; 32];
279 out.copy_from_slice(&rc4(&key, &PAD));
280 out
281 } else {
282 let mut hash_input = Vec::with_capacity(32 + c.file_id.len());
284 hash_input.extend_from_slice(&PAD);
285 hash_input.extend_from_slice(&c.file_id);
286 let h = md5(&hash_input);
287 let mut data = rc4(&key, &h);
288 for i in 1u8..=19 {
289 let xkey: Vec<u8> = key.iter().map(|b| b ^ i).collect();
290 data = rc4(&xkey, &data);
291 }
292 let mut out = [0u8; 32];
293 out[..16].copy_from_slice(&data[..16]);
294 out
297 };
298
299 let handler = StandardHandler {
300 key: key.clone(),
301 method: c.method,
302 revision: c.revision,
303 };
304
305 let v: i64 = match (c.revision, c.method) {
307 (2, _) => 1,
308 (3, _) => 2,
309 (4, _) => 4,
310 _ => unreachable!("V≤4 path checked at entry"),
311 };
312 let mut dict = Dict::new()
313 .with("Filter", Object::Name("Standard".into()))
314 .with("V", Object::Integer(v))
315 .with("R", Object::Integer(c.revision as i64))
316 .with("Length", Object::Integer(c.length_bits as i64))
317 .with("O", Object::LiteralString(o.to_vec()))
318 .with("U", Object::LiteralString(u.to_vec()))
319 .with("P", Object::Integer(c.p as i64));
320 if !c.encrypt_metadata {
321 dict.set("EncryptMetadata", Object::Bool(false));
322 }
323
324 if c.revision == 4 {
327 let cfm = match c.method {
328 CryptMethod::Aes128 => "AESV2",
329 CryptMethod::Rc4 => "V2",
330 CryptMethod::Aes256 => {
331 return Err(PdfError::other(
332 "PDF encrypt: AES-256 requires V=5/R=5+ (got V=4)",
333 ));
334 }
335 };
336 let crypt_filter_len = match c.method {
337 CryptMethod::Aes128 => 16,
338 CryptMethod::Rc4 => 16,
339 CryptMethod::Aes256 => 32,
340 };
341 let std_cf = Dict::new()
342 .with("Type", Object::Name("CryptFilter".into()))
343 .with("CFM", Object::Name(cfm.into()))
344 .with("Length", Object::Integer(crypt_filter_len));
345 let cf = Dict::new().with("StdCF", Object::Dict(std_cf));
346 dict.set("CF", Object::Dict(cf));
347 dict.set("StmF", Object::Name("StdCF".into()));
348 dict.set("StrF", Object::Name("StdCF".into()));
349 }
350
351 Ok(EncryptionState {
352 handler,
353 encrypt_dict: dict,
354 file_id: c.file_id.clone(),
355 aes_iv: c.aes_iv,
356 })
357 }
358
359 fn build_v5(c: &EncryptionConfig) -> Result<Self, PdfError> {
360 let file_key = c
362 .file_key_v5
363 .unwrap_or_else(default_file_key_v5_for_password);
364 let user_pw = if c.user_password.len() > 127 {
365 &c.user_password[..127]
366 } else {
367 &c.user_password[..]
368 };
369 let owner_pw = if c.owner_password.is_empty() {
370 user_pw
371 } else if c.owner_password.len() > 127 {
372 &c.owner_password[..127]
373 } else {
374 &c.owner_password[..]
375 };
376
377 let (u, ue) = algorithm_9(
379 c.revision,
380 user_pw,
381 &file_key,
382 &c.u_salt_validate,
383 &c.u_salt_key,
384 );
385 let (o, oe) = algorithm_8(
387 c.revision,
388 owner_pw,
389 &u,
390 &file_key,
391 &c.o_salt_validate,
392 &c.o_salt_key,
393 );
394 let perms = algorithm_10(&file_key, c.p, c.encrypt_metadata, &c.perms_padding);
396
397 let handler = StandardHandler {
398 key: file_key.to_vec(),
399 method: CryptMethod::Aes256,
400 revision: c.revision,
401 };
402
403 let std_cf = Dict::new()
404 .with("Type", Object::Name("CryptFilter".into()))
405 .with("CFM", Object::Name("AESV3".into()))
406 .with("Length", Object::Integer(32));
407 let cf = Dict::new().with("StdCF", Object::Dict(std_cf));
408 let mut dict = Dict::new()
409 .with("Filter", Object::Name("Standard".into()))
410 .with("V", Object::Integer(5))
411 .with("R", Object::Integer(c.revision as i64))
412 .with("Length", Object::Integer(256))
413 .with("CF", Object::Dict(cf))
414 .with("StmF", Object::Name("StdCF".into()))
415 .with("StrF", Object::Name("StdCF".into()))
416 .with("O", Object::LiteralString(o.to_vec()))
417 .with("U", Object::LiteralString(u.to_vec()))
418 .with("OE", Object::LiteralString(oe.to_vec()))
419 .with("UE", Object::LiteralString(ue.to_vec()))
420 .with("Perms", Object::LiteralString(perms.to_vec()))
421 .with("P", Object::Integer(c.p as i64));
422 if !c.encrypt_metadata {
423 dict.set("EncryptMetadata", Object::Bool(false));
424 }
425
426 Ok(EncryptionState {
427 handler,
428 encrypt_dict: dict,
429 file_id: c.file_id.clone(),
430 aes_iv: c.aes_iv,
431 })
432 }
433}
434
435fn algorithm_3(user_password: &[u8], owner_password: &[u8], revision: u8, n: usize) -> [u8; 32] {
438 let owner_src = if owner_password.is_empty() {
440 user_password
441 } else {
442 owner_password
443 };
444 let opad = pad_password(owner_src);
445 let mut h = md5(&opad);
447 if revision >= 3 {
449 for _ in 0..50 {
450 h = md5(&h[..n]);
451 }
452 }
453 let okey = h[..n].to_vec();
454 let upad = pad_password(user_password);
456 let mut buf = rc4(&okey, &upad);
458 if revision >= 3 {
460 for i in 1u8..=19 {
461 let xkey: Vec<u8> = okey.iter().map(|b| b ^ i).collect();
462 buf = rc4(&xkey, &buf);
463 }
464 }
465 let mut out = [0u8; 32];
466 out.copy_from_slice(&buf);
467 out
468}
469
470fn algorithm_2_filekey(
473 user_password: &[u8],
474 o: &[u8; 32],
475 p: i32,
476 file_id: &[u8],
477 revision: u8,
478 n: usize,
479 encrypt_metadata: bool,
480) -> Vec<u8> {
481 let pwd = pad_password(user_password);
482 let mut buf = Vec::with_capacity(32 + 32 + 4 + file_id.len() + 4);
483 buf.extend_from_slice(&pwd);
484 buf.extend_from_slice(o);
485 buf.extend_from_slice(&(p as u32).to_le_bytes());
486 buf.extend_from_slice(file_id);
487 if revision >= 4 && !encrypt_metadata {
488 buf.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
489 }
490 let mut h = md5(&buf);
491 if revision >= 3 {
492 for _ in 0..50 {
493 h = md5(&h[..n]);
494 }
495 }
496 h[..n].to_vec()
497}
498
499fn default_file_key_v5_for_password() -> [u8; 32] {
504 [
505 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
506 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
507 0xF0, 0x01,
508 ]
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::decrypt::{open_with_password, CryptMethod};
515
516 #[test]
517 fn algorithm_3_with_empty_owner_password_falls_back_to_user() {
518 let o_a = algorithm_3(b"hello", b"", 3, 16);
519 let o_b = algorithm_3(b"hello", b"hello", 3, 16);
520 assert_eq!(o_a, o_b);
521 }
522
523 #[test]
524 fn build_v_le_4_round_trips_via_decrypt_authenticator() {
525 let cfg = EncryptionConfig::rc4_128(b"hello", b"OXIDEAV-FIXTURE-ID-FIXED-VALUE!");
529 let state = EncryptionState::build(&cfg).unwrap();
530 let opened = open_with_password(&state.encrypt_dict, &state.file_id, b"hello").expect("ok");
532 assert!(opened.is_some(), "user password should authenticate");
533 }
534
535 #[test]
536 fn build_v_le_4_rejects_wrong_password() {
537 let cfg = EncryptionConfig::rc4_128(b"correctpw", b"OXIDEAV-FIXTURE-ID-FIXED-VALUE!");
538 let state = EncryptionState::build(&cfg).unwrap();
539 let opened = open_with_password(&state.encrypt_dict, &state.file_id, b"wrong").unwrap();
540 assert!(opened.is_none());
541 }
542
543 #[test]
544 fn build_v_le_4_owner_password_authenticates() {
545 let cfg = EncryptionConfig::rc4_128(b"userpw", b"OXIDEAV-FIXTURE-ID-FIXED-VALUE!")
546 .with_owner_password(b"ownerpw");
547 let state = EncryptionState::build(&cfg).unwrap();
548 let user_ok = open_with_password(&state.encrypt_dict, &state.file_id, b"userpw").unwrap();
550 assert!(user_ok.is_some());
551 let owner_ok = open_with_password(&state.encrypt_dict, &state.file_id, b"ownerpw").unwrap();
552 assert!(owner_ok.is_some());
553 }
554
555 #[test]
556 fn build_v5_r5_round_trips() {
557 let cfg = EncryptionConfig::aes_256_r5(b"hunter2", b"FIXED-FILE-ID-32-BYTES-FOR-V5-XX");
558 let state = EncryptionState::build(&cfg).unwrap();
559 let opened = open_with_password(&state.encrypt_dict, &state.file_id, b"hunter2").unwrap();
560 assert!(opened.is_some(), "R=5 user pw should authenticate");
561 assert_eq!(opened.unwrap().method, CryptMethod::Aes256);
562 }
563
564 #[test]
565 fn build_v5_r6_round_trips() {
566 let cfg =
567 EncryptionConfig::aes_256_r6(b"correct horse", b"FIXED-FILE-ID-32-BYTES-FOR-R6-X");
568 let state = EncryptionState::build(&cfg).unwrap();
569 let opened =
570 open_with_password(&state.encrypt_dict, &state.file_id, b"correct horse").unwrap();
571 assert!(opened.is_some(), "R=6 user pw should authenticate");
572 }
573
574 #[test]
575 fn build_v5_r5_owner_password() {
576 let cfg = EncryptionConfig::aes_256_r5(b"userpw", b"FIXED-FILE-ID-32-BYTES-FOR-V5-OW")
577 .with_owner_password(b"ownerpw");
578 let state = EncryptionState::build(&cfg).unwrap();
579 let owner_ok = open_with_password(&state.encrypt_dict, &state.file_id, b"ownerpw").unwrap();
580 assert!(owner_ok.is_some());
581 }
582
583 #[test]
584 fn build_aes_128_r4_round_trips() {
585 let cfg = EncryptionConfig::aes_128(b"aespw", b"AES-FIXTURE-FILE-ID-LONG-ENOUGH!");
586 let state = EncryptionState::build(&cfg).unwrap();
587 let opened = open_with_password(&state.encrypt_dict, &state.file_id, b"aespw").unwrap();
588 assert!(opened.is_some(), "R=4 AES-128 should authenticate");
589 assert_eq!(opened.unwrap().method, CryptMethod::Aes128);
590 }
591
592 #[test]
593 fn build_rc4_40_r2_round_trips() {
594 let cfg = EncryptionConfig::rc4_40(b"shorty", b"R2-FIXTURE-FILE-ID-LONG-ENOUGH!");
595 let state = EncryptionState::build(&cfg).unwrap();
596 let opened = open_with_password(&state.encrypt_dict, &state.file_id, b"shorty").unwrap();
597 assert!(opened.is_some(), "R=2 RC4-40 should authenticate");
598 assert_eq!(opened.unwrap().key.len(), 5);
599 }
600}