Skip to main content

justpdf_core/crypto/
encrypt.rs

1//! Object-level encryption for PDF writing.
2//!
3//! Encrypts strings and stream data before serialization.
4
5use crate::error::Result;
6use crate::object::{PdfDict, PdfObject};
7
8use super::aes_cipher;
9use super::key;
10use super::rc4;
11use super::types::{CryptMethod, EncryptionDict, Permissions, SecurityState};
12
13/// Configuration for encrypting a new PDF document.
14#[derive(Debug, Clone)]
15pub struct EncryptionConfig {
16    /// User password (may be empty for open access).
17    pub user_password: Vec<u8>,
18    /// Owner password (for full control).
19    pub owner_password: Vec<u8>,
20    /// Permission flags.
21    pub permissions: Permissions,
22    /// Encryption method.
23    pub method: EncryptionMethod,
24    /// Whether to encrypt metadata streams.
25    pub encrypt_metadata: bool,
26}
27
28/// Supported encryption methods for writing.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum EncryptionMethod {
31    /// RC4 128-bit (V=2, R=3)
32    RC4_128,
33    /// AES-128 CBC (V=4, R=4)
34    AES128,
35    /// AES-256 CBC (V=5, R=6)
36    AES256,
37}
38
39impl EncryptionConfig {
40    /// Build the encryption dict, generate keys, and return a SecurityState
41    /// suitable for encrypting objects during serialization.
42    ///
43    /// Also returns the encryption PdfDict to be added as an indirect object,
44    /// and the /ID array to be added to the trailer.
45    pub fn build(
46        &self,
47        file_id: &[u8],
48    ) -> Result<(SecurityState, PdfDict, Vec<PdfObject>)> {
49        match self.method {
50            EncryptionMethod::RC4_128 => self.build_r3(file_id),
51            EncryptionMethod::AES128 => self.build_r4(file_id),
52            EncryptionMethod::AES256 => self.build_r6(file_id),
53        }
54    }
55
56    fn build_r3(&self, file_id: &[u8]) -> Result<(SecurityState, PdfDict, Vec<PdfObject>)> {
57        let mut ed = EncryptionDict {
58            filter: b"Standard".to_vec(),
59            v: 2,
60            length: 128,
61            r: 3,
62            o: vec![0u8; 32],
63            u: vec![0u8; 32],
64            p: self.permissions.bits,
65            encrypt_metadata: self.encrypt_metadata,
66            oe: None,
67            ue: None,
68            perms: None,
69            cf: None,
70            stm_f: None,
71            str_f: None,
72        };
73
74        let (o, u, file_key) = key::generate_o_u_values_r234(
75            &self.user_password,
76            &self.owner_password,
77            &ed,
78            file_id,
79        );
80        ed.o = o;
81        ed.u = u;
82
83        let pdf_dict = ed.to_pdf_dict();
84        let id_array = make_id_array(file_id);
85
86        let mut state = SecurityState::new(ed, file_id.to_vec(), None);
87        state.file_key = Some(file_key);
88        state.string_method = CryptMethod::V2;
89        state.stream_method = CryptMethod::V2;
90
91        Ok((state, pdf_dict, id_array))
92    }
93
94    fn build_r4(&self, file_id: &[u8]) -> Result<(SecurityState, PdfDict, Vec<PdfObject>)> {
95        let mut ed = EncryptionDict {
96            filter: b"Standard".to_vec(),
97            v: 4,
98            length: 128,
99            r: 4,
100            o: vec![0u8; 32],
101            u: vec![0u8; 32],
102            p: self.permissions.bits,
103            encrypt_metadata: self.encrypt_metadata,
104            oe: None,
105            ue: None,
106            perms: None,
107            cf: Some(super::types::CryptFilterMap {
108                filters: vec![(
109                    b"StdCF".to_vec(),
110                    super::types::CryptFilter {
111                        cfm: CryptMethod::AESV2,
112                        key_length: 16,
113                    },
114                )],
115            }),
116            stm_f: Some(b"StdCF".to_vec()),
117            str_f: Some(b"StdCF".to_vec()),
118        };
119
120        let (o, u, file_key) = key::generate_o_u_values_r234(
121            &self.user_password,
122            &self.owner_password,
123            &ed,
124            file_id,
125        );
126        ed.o = o;
127        ed.u = u;
128
129        let pdf_dict = ed.to_pdf_dict();
130        let id_array = make_id_array(file_id);
131
132        let mut state = SecurityState::new(ed, file_id.to_vec(), None);
133        state.file_key = Some(file_key);
134        state.string_method = CryptMethod::AESV2;
135        state.stream_method = CryptMethod::AESV2;
136
137        Ok((state, pdf_dict, id_array))
138    }
139
140    fn build_r6(&self, file_id: &[u8]) -> Result<(SecurityState, PdfDict, Vec<PdfObject>)> {
141        // Generate a random 32-byte file encryption key
142        let file_key = generate_random_key();
143
144        // Generate random salts
145        let uvs = generate_random_salt();
146        let uks = generate_random_salt();
147        let ovs = generate_random_salt();
148        let oks = generate_random_salt();
149
150        let (o, u, oe, ue, perms) = key::generate_values_r6(
151            &self.user_password,
152            &self.owner_password,
153            self.permissions.bits,
154            self.encrypt_metadata,
155            &file_key,
156            &uvs,
157            &uks,
158            &ovs,
159            &oks,
160        );
161
162        let ed = EncryptionDict {
163            filter: b"Standard".to_vec(),
164            v: 5,
165            length: 256,
166            r: 6,
167            o,
168            u,
169            p: self.permissions.bits,
170            encrypt_metadata: self.encrypt_metadata,
171            oe: Some(oe),
172            ue: Some(ue),
173            perms: Some(perms),
174            cf: Some(super::types::CryptFilterMap {
175                filters: vec![(
176                    b"StdCF".to_vec(),
177                    super::types::CryptFilter {
178                        cfm: CryptMethod::AESV3,
179                        key_length: 32,
180                    },
181                )],
182            }),
183            stm_f: Some(b"StdCF".to_vec()),
184            str_f: Some(b"StdCF".to_vec()),
185        };
186
187        let pdf_dict = ed.to_pdf_dict();
188        let id_array = make_id_array(file_id);
189
190        let mut state = SecurityState::new(ed, file_id.to_vec(), None);
191        state.file_key = Some(file_key.to_vec());
192        state.string_method = CryptMethod::AESV3;
193        state.stream_method = CryptMethod::AESV3;
194
195        Ok((state, pdf_dict, id_array))
196    }
197}
198
199/// Encrypt a PdfObject for writing.
200pub fn encrypt_object(
201    obj: &PdfObject,
202    state: &SecurityState,
203    obj_num: u32,
204    gen_num: u16,
205) -> Result<PdfObject> {
206    let file_key = match &state.file_key {
207        Some(k) => k,
208        None => return Ok(obj.clone()),
209    };
210
211    // Don't encrypt the encryption dictionary itself
212    if let Some(enc_num) = state.encrypt_obj_num {
213        if obj_num == enc_num {
214            return Ok(obj.clone());
215        }
216    }
217
218    match obj {
219        PdfObject::String(data) => {
220            let encrypted = encrypt_bytes(
221                file_key,
222                data,
223                obj_num,
224                gen_num,
225                state.string_method,
226            )?;
227            Ok(PdfObject::String(encrypted))
228        }
229        PdfObject::Stream { dict, data } => {
230            let encrypted = encrypt_bytes(
231                file_key,
232                data,
233                obj_num,
234                gen_num,
235                state.stream_method,
236            )?;
237            Ok(PdfObject::Stream {
238                dict: dict.clone(),
239                data: encrypted,
240            })
241        }
242        PdfObject::Dict(d) => {
243            let mut new_dict = PdfDict::new();
244            for (k, v) in d.iter() {
245                let encrypted_val = encrypt_object(v, state, obj_num, gen_num)?;
246                new_dict.insert(k.clone(), encrypted_val);
247            }
248            Ok(PdfObject::Dict(new_dict))
249        }
250        PdfObject::Array(arr) => {
251            let mut new_arr = Vec::with_capacity(arr.len());
252            for item in arr {
253                new_arr.push(encrypt_object(item, state, obj_num, gen_num)?);
254            }
255            Ok(PdfObject::Array(new_arr))
256        }
257        other => Ok(other.clone()),
258    }
259}
260
261/// Encrypt raw bytes using the appropriate method.
262fn encrypt_bytes(
263    file_key: &[u8],
264    data: &[u8],
265    obj_num: u32,
266    gen_num: u16,
267    method: CryptMethod,
268) -> Result<Vec<u8>> {
269    if data.is_empty() {
270        return Ok(Vec::new());
271    }
272
273    match method {
274        CryptMethod::None => Ok(data.to_vec()),
275        CryptMethod::V2 => {
276            let obj_key = key::compute_object_key(file_key, obj_num, gen_num, false);
277            Ok(rc4::rc4(&obj_key, data))
278        }
279        CryptMethod::AESV2 => {
280            let obj_key = key::compute_object_key(file_key, obj_num, gen_num, true);
281            let iv = generate_iv();
282            aes_cipher::encrypt_aes_cbc(&obj_key, data, &iv)
283        }
284        CryptMethod::AESV3 => {
285            let iv = generate_iv();
286            aes_cipher::encrypt_aes_cbc(file_key, data, &iv)
287        }
288    }
289}
290
291/// Generate file ID array for the trailer.
292fn make_id_array(file_id: &[u8]) -> Vec<PdfObject> {
293    vec![
294        PdfObject::String(file_id.to_vec()),
295        PdfObject::String(file_id.to_vec()),
296    ]
297}
298
299/// Generate a random 16-byte IV.
300fn generate_iv() -> [u8; 16] {
301    let mut iv = [0u8; 16];
302    // Use a simple deterministic approach for now — in production, use OsRng
303    // For each encryption we use the current time hash as entropy source
304    let seed = std::time::SystemTime::now()
305        .duration_since(std::time::UNIX_EPOCH)
306        .unwrap_or_default()
307        .as_nanos();
308    let hash = {
309        use md5::Digest;
310        let mut h = md5::Md5::new();
311        h.update(seed.to_le_bytes());
312        h.update(b"justpdf-iv");
313        h.finalize()
314    };
315    iv.copy_from_slice(&hash);
316    iv
317}
318
319/// Generate a random 32-byte file key for AES-256.
320fn generate_random_key() -> [u8; 32] {
321    let mut key = [0u8; 32];
322    let seed = std::time::SystemTime::now()
323        .duration_since(std::time::UNIX_EPOCH)
324        .unwrap_or_default()
325        .as_nanos();
326    let hash1 = {
327        use sha2::Digest;
328        let mut h = sha2::Sha256::new();
329        h.update(seed.to_le_bytes());
330        h.update(b"justpdf-key-1");
331        h.finalize()
332    };
333    key.copy_from_slice(&hash1);
334    key
335}
336
337/// Generate a random 8-byte salt.
338fn generate_random_salt() -> [u8; 8] {
339    let mut salt = [0u8; 8];
340    static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
341    let count = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
342    let seed = std::time::SystemTime::now()
343        .duration_since(std::time::UNIX_EPOCH)
344        .unwrap_or_default()
345        .as_nanos()
346        .wrapping_add(count as u128);
347    let hash = {
348        use md5::Digest;
349        let mut h = md5::Md5::new();
350        h.update(seed.to_le_bytes());
351        h.update(b"justpdf-salt");
352        h.finalize()
353    };
354    salt.copy_from_slice(&hash[..8]);
355    salt
356}
357
358/// Generate a file ID based on document content.
359pub fn generate_file_id(title: &[u8], timestamp: u64) -> Vec<u8> {
360    use md5::Digest;
361    let mut h = md5::Md5::new();
362    h.update(title);
363    h.update(timestamp.to_le_bytes());
364    h.update(b"justpdf");
365    h.finalize().to_vec()
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    fn make_state_for_encrypt(method: CryptMethod) -> SecurityState {
373        let ed = EncryptionDict {
374            filter: b"Standard".to_vec(),
375            v: if method == CryptMethod::AESV3 { 5 } else { 2 },
376            length: if method == CryptMethod::AESV3 { 256 } else { 128 },
377            r: match method {
378                CryptMethod::V2 => 3,
379                CryptMethod::AESV2 => 4,
380                CryptMethod::AESV3 => 6,
381                CryptMethod::None => 3,
382            },
383            o: vec![0u8; 32],
384            u: vec![0u8; 32],
385            p: -4,
386            encrypt_metadata: true,
387            oe: None,
388            ue: None,
389            perms: None,
390            cf: None,
391            stm_f: None,
392            str_f: None,
393        };
394
395        let key_len = if method == CryptMethod::AESV3 { 32 } else { 16 };
396        let mut state = SecurityState::new(ed, b"id".to_vec(), None);
397        state.file_key = Some(vec![0x42u8; key_len]);
398        state.string_method = method;
399        state.stream_method = method;
400        state
401    }
402
403    #[test]
404    fn test_encrypt_decrypt_rc4_roundtrip() {
405        let state = make_state_for_encrypt(CryptMethod::V2);
406
407        let original = PdfObject::String(b"Hello RC4!".to_vec());
408        let encrypted = encrypt_object(&original, &state, 1, 0).unwrap();
409        assert_ne!(encrypted, original);
410
411        let decrypted =
412            super::super::decrypt::decrypt_object(encrypted, &state, 1, 0).unwrap();
413        assert_eq!(decrypted, original);
414    }
415
416    #[test]
417    fn test_encrypt_decrypt_aes128_roundtrip() {
418        let state = make_state_for_encrypt(CryptMethod::AESV2);
419
420        let original = PdfObject::String(b"Hello AES-128!".to_vec());
421        let encrypted = encrypt_object(&original, &state, 1, 0).unwrap();
422        assert_ne!(encrypted, original);
423
424        let decrypted =
425            super::super::decrypt::decrypt_object(encrypted, &state, 1, 0).unwrap();
426        assert_eq!(decrypted, original);
427    }
428
429    #[test]
430    fn test_encrypt_decrypt_aes256_roundtrip() {
431        let state = make_state_for_encrypt(CryptMethod::AESV3);
432
433        let original = PdfObject::String(b"Hello AES-256!".to_vec());
434        let encrypted = encrypt_object(&original, &state, 1, 0).unwrap();
435        assert_ne!(encrypted, original);
436
437        let decrypted =
438            super::super::decrypt::decrypt_object(encrypted, &state, 1, 0).unwrap();
439        assert_eq!(decrypted, original);
440    }
441
442    #[test]
443    fn test_encrypt_config_r3() {
444        let config = EncryptionConfig {
445            user_password: b"user".to_vec(),
446            owner_password: b"owner".to_vec(),
447            permissions: Permissions::allow_all(),
448            method: EncryptionMethod::RC4_128,
449            encrypt_metadata: true,
450        };
451
452        let file_id = b"test-file-id-cfg";
453        let (state, pdf_dict, id_arr) = config.build(file_id).unwrap();
454
455        assert!(state.file_key.is_some());
456        assert_eq!(pdf_dict.get_i64(b"V"), Some(2));
457        assert_eq!(pdf_dict.get_i64(b"R"), Some(3));
458        assert_eq!(id_arr.len(), 2);
459    }
460
461    #[test]
462    fn test_encrypt_config_r4() {
463        let config = EncryptionConfig {
464            user_password: vec![],
465            owner_password: b"secret".to_vec(),
466            permissions: Permissions::allow_all(),
467            method: EncryptionMethod::AES128,
468            encrypt_metadata: true,
469        };
470
471        let (state, pdf_dict, _) = config.build(b"id").unwrap();
472        assert_eq!(pdf_dict.get_i64(b"V"), Some(4));
473        assert_eq!(pdf_dict.get_i64(b"R"), Some(4));
474        assert_eq!(state.string_method, CryptMethod::AESV2);
475    }
476
477    #[test]
478    fn test_encrypt_config_r6() {
479        let config = EncryptionConfig {
480            user_password: b"user256".to_vec(),
481            owner_password: b"owner256".to_vec(),
482            permissions: Permissions::allow_all(),
483            method: EncryptionMethod::AES256,
484            encrypt_metadata: true,
485        };
486
487        let (state, pdf_dict, _) = config.build(b"id256").unwrap();
488        assert_eq!(pdf_dict.get_i64(b"V"), Some(5));
489        assert_eq!(pdf_dict.get_i64(b"R"), Some(6));
490        assert_eq!(state.string_method, CryptMethod::AESV3);
491        assert_eq!(state.file_key.as_ref().unwrap().len(), 32);
492    }
493
494    #[test]
495    fn test_encrypt_skips_encrypt_dict_obj() {
496        let mut state = make_state_for_encrypt(CryptMethod::V2);
497        state.encrypt_obj_num = Some(10);
498
499        let original = PdfObject::String(b"no encrypt".to_vec());
500        let result = encrypt_object(&original, &state, 10, 0).unwrap();
501        assert_eq!(result, original); // not encrypted
502    }
503
504    #[test]
505    fn test_generate_file_id() {
506        let id1 = generate_file_id(b"Doc 1", 12345);
507        let id2 = generate_file_id(b"Doc 2", 12345);
508        assert_eq!(id1.len(), 16);
509        assert_ne!(id1, id2);
510    }
511}