html2pdf_secure/
encryption.rs

1//! PDF encryption and password protection functionality.
2
3use crate::error::Result;
4use lopdf::Document;
5use serde::{Deserialize, Serialize};
6use rand::Rng;
7
8/// Password options for PDF encryption.
9#[derive(Debug, Clone)]
10pub struct PasswordOptions {
11    /// User password (for opening the document)
12    pub user_password: String,
13    
14    /// Owner password (for full access/editing)
15    pub owner_password: String,
16    
17    /// Encryption level
18    pub encryption_level: EncryptionLevel,
19    
20    /// Permissions for the document
21    pub permissions: PdfPermissions,
22}
23
24impl PasswordOptions {
25    /// Create new password options with user and owner passwords.
26    /// Uses AES-256 encryption by default for maximum security.
27    pub fn new<S1: Into<String>, S2: Into<String>>(user_password: S1, owner_password: S2) -> Self {
28        Self {
29            user_password: user_password.into(),
30            owner_password: owner_password.into(),
31            encryption_level: EncryptionLevel::Aes256, // Use strong encryption by default
32            permissions: PdfPermissions::default(),
33        }
34    }
35    
36    /// Set the encryption level.
37    pub fn with_encryption_level(mut self, level: EncryptionLevel) -> Self {
38        self.encryption_level = level;
39        self
40    }
41    
42    /// Set the permissions.
43    pub fn with_permissions(mut self, permissions: PdfPermissions) -> Self {
44        self.permissions = permissions;
45        self
46    }
47}
48
49/// Encryption levels supported.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub enum EncryptionLevel {
52    /// Standard 40-bit RC4 encryption
53    Standard40,
54    /// Standard 128-bit RC4 encryption
55    Standard128,
56    /// AES 128-bit encryption
57    Aes128,
58    /// AES 256-bit encryption
59    Aes256,
60}
61
62/// PDF permissions configuration.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PdfPermissions {
65    /// Allow printing
66    pub allow_printing: bool,
67    
68    /// Allow modifying the document
69    pub allow_modify_contents: bool,
70    
71    /// Allow copying text and graphics
72    pub allow_copy: bool,
73    
74    /// Allow adding or modifying annotations
75    pub allow_modify_annotations: bool,
76    
77    /// Allow filling in form fields
78    pub allow_fill_forms: bool,
79    
80    /// Allow extracting text and graphics for accessibility
81    pub allow_extract_for_accessibility: bool,
82    
83    /// Allow assembling the document
84    pub allow_assemble: bool,
85    
86    /// Allow high-quality printing
87    pub allow_print_high_quality: bool,
88}
89
90impl Default for PdfPermissions {
91    fn default() -> Self {
92        Self {
93            allow_printing: true,
94            allow_modify_contents: false,
95            allow_copy: true,
96            allow_modify_annotations: false,
97            allow_fill_forms: true,
98            allow_extract_for_accessibility: true,
99            allow_assemble: false,
100            allow_print_high_quality: true,
101        }
102    }
103}
104
105impl PdfPermissions {
106    /// Create permissions that allow everything.
107    pub fn allow_all() -> Self {
108        Self {
109            allow_printing: true,
110            allow_modify_contents: true,
111            allow_copy: true,
112            allow_modify_annotations: true,
113            allow_fill_forms: true,
114            allow_extract_for_accessibility: true,
115            allow_assemble: true,
116            allow_print_high_quality: true,
117        }
118    }
119    
120    /// Create permissions that deny everything except viewing.
121    pub fn deny_all() -> Self {
122        Self {
123            allow_printing: false,
124            allow_modify_contents: false,
125            allow_copy: false,
126            allow_modify_annotations: false,
127            allow_fill_forms: false,
128            allow_extract_for_accessibility: false,
129            allow_assemble: false,
130            allow_print_high_quality: false,
131        }
132    }
133    
134    /// Convert permissions to PDF permission flags.
135    pub fn to_permission_flags(&self) -> i32 {
136        let mut flags = 0i32;
137        
138        if self.allow_printing { flags |= 1 << 2; }
139        if self.allow_modify_contents { flags |= 1 << 3; }
140        if self.allow_copy { flags |= 1 << 4; }
141        if self.allow_modify_annotations { flags |= 1 << 5; }
142        if self.allow_fill_forms { flags |= 1 << 8; }
143        if self.allow_extract_for_accessibility { flags |= 1 << 9; }
144        if self.allow_assemble { flags |= 1 << 10; }
145        if self.allow_print_high_quality { flags |= 1 << 11; }
146        
147        flags
148    }
149}
150
151/// Apply password protection to a PDF document.
152///
153/// **Important**: This function attempts to use qpdf first for proper encryption.
154/// If qpdf is not available, it falls back to an unencrypted PDF with a warning.
155///
156/// For guaranteed password protection, use `encrypt_pdf_with_qpdf` directly
157/// and handle the error if qpdf is not available.
158pub fn encrypt_pdf(pdf_data: Vec<u8>, password_options: &PasswordOptions) -> Result<Vec<u8>> {
159    // Try qpdf first
160    match encrypt_pdf_with_qpdf(pdf_data.clone(), password_options, None) {
161        Ok(encrypted_data) => {
162            log::info!("Successfully encrypted PDF using qpdf");
163            Ok(encrypted_data)
164        }
165        Err(e) => {
166            log::warn!("qpdf encryption failed: {}", e);
167            log::warn!("Falling back to unencrypted PDF");
168            log::warn!("Install qpdf for proper password protection: https://qpdf.sourceforge.io/");
169
170            // Return unencrypted PDF with warning
171            encrypt_pdf_fallback(pdf_data, password_options)
172        }
173    }
174}
175
176/// Fallback function that returns an unencrypted PDF with clear warnings.
177/// This is used when qpdf is not available.
178fn encrypt_pdf_fallback(pdf_data: Vec<u8>, password_options: &PasswordOptions) -> Result<Vec<u8>> {
179    // Load the PDF document
180    let mut doc = Document::load_mem(&pdf_data)?;
181
182    // Log the encryption attempt
183    log::error!("⚠️  WARNING: PDF is NOT password protected!");
184    log::error!("⚠️  qpdf is required for password protection but was not found");
185    log::error!("⚠️  Install qpdf to enable password protection:");
186    log::error!("   - Ubuntu/Debian: sudo apt-get install qpdf");
187    log::error!("   - macOS: brew install qpdf");
188    log::error!("   - Windows: Download from https://qpdf.sourceforge.io/");
189
190    log::info!("Requested encryption settings (NOT APPLIED):");
191    log::info!("  User password length: {}", password_options.user_password.len());
192    log::info!("  Owner password length: {}", password_options.owner_password.len());
193    log::info!("  Encryption level: {:?}", password_options.encryption_level);
194
195    // Save the document without encryption
196    let mut output = Vec::new();
197    doc.save_to(&mut output)?;
198
199    Ok(output)
200}
201
202/// Apply password protection using external qpdf tool (if available).
203/// This is a more robust solution for production use.
204///
205/// # Prerequisites
206/// - qpdf must be installed on the system
207/// - Sufficient disk space for temporary files
208///
209/// # Arguments
210/// - `pdf_data`: The PDF data to encrypt
211/// - `password_options`: Password and encryption settings
212/// - `temp_dir`: Optional temporary directory (defaults to /tmp)
213///
214/// # Returns
215/// - `Ok(Vec<u8>)`: Encrypted PDF data
216/// - `Err(Html2PdfError)`: If qpdf is not available or encryption fails
217pub fn encrypt_pdf_with_qpdf(
218    pdf_data: Vec<u8>,
219    password_options: &PasswordOptions,
220    temp_dir: Option<&str>
221) -> Result<Vec<u8>> {
222    use std::process::Command;
223    use std::fs;
224
225    // First check if qpdf is available
226    let qpdf_check = Command::new("qpdf")
227        .arg("--version")
228        .output();
229
230    match qpdf_check {
231        Ok(output) if output.status.success() => {
232            log::info!("qpdf found: {}", String::from_utf8_lossy(&output.stdout).trim());
233        }
234        Ok(_) => {
235            return Err(crate::error::Html2PdfError::encryption(
236                "qpdf command failed - may not be properly installed".to_string()
237            ));
238        }
239        Err(_) => {
240            return Err(crate::error::Html2PdfError::encryption(
241                "qpdf not found - install qpdf for password protection".to_string()
242            ));
243        }
244    }
245
246    // Create temporary directory
247    let temp_dir = temp_dir.unwrap_or("/tmp");
248    let mut rng = rand::rng();
249    let input_path = format!("{}/input_{}.pdf", temp_dir, rng.random::<u32>());
250    let output_path = format!("{}/output_{}.pdf", temp_dir, rng.random::<u32>());
251
252    // Write input PDF to temporary file
253    fs::write(&input_path, &pdf_data)?;
254
255    // Build qpdf command based on encryption level
256    let mut cmd = Command::new("qpdf");
257
258    match password_options.encryption_level {
259        EncryptionLevel::Standard40 => {
260            // 40-bit RC4 (very weak, requires --allow-weak-crypto)
261            cmd.arg("--allow-weak-crypto")
262               .arg("--encrypt")
263               .arg(&password_options.user_password)
264               .arg(&password_options.owner_password)
265               .arg("40")
266               .arg("--")
267               .arg(&input_path)
268               .arg(&output_path);
269        }
270        EncryptionLevel::Standard128 => {
271            // 128-bit RC4 (weak, requires --allow-weak-crypto)
272            cmd.arg("--allow-weak-crypto")
273               .arg("--encrypt")
274               .arg(&password_options.user_password)
275               .arg(&password_options.owner_password)
276               .arg("128")
277               .arg("--")
278               .arg(&input_path)
279               .arg(&output_path);
280        }
281        EncryptionLevel::Aes128 => {
282            // 128-bit AES (modern, secure)
283            cmd.arg("--encrypt")
284               .arg(&password_options.user_password)
285               .arg(&password_options.owner_password)
286               .arg("128")
287               .arg("--use-aes=y")
288               .arg("--")
289               .arg(&input_path)
290               .arg(&output_path);
291        }
292        EncryptionLevel::Aes256 => {
293            // 256-bit AES (strongest, recommended)
294            cmd.arg("--encrypt")
295               .arg(&password_options.user_password)
296               .arg(&password_options.owner_password)
297               .arg("256")
298               .arg("--")
299               .arg(&input_path)
300               .arg(&output_path);
301        }
302    }
303
304    // Execute qpdf
305    let output = cmd.output();
306
307    // Clean up input file
308    let _ = fs::remove_file(&input_path);
309
310    match output {
311        Ok(result) => {
312            if result.status.success() {
313                // Read encrypted PDF
314                let encrypted_data = fs::read(&output_path)?;
315                // Clean up output file
316                let _ = fs::remove_file(&output_path);
317                Ok(encrypted_data)
318            } else {
319                // Clean up output file if it exists
320                let _ = fs::remove_file(&output_path);
321                Err(crate::error::Html2PdfError::encryption(format!(
322                    "qpdf failed: {}",
323                    String::from_utf8_lossy(&result.stderr)
324                )))
325            }
326        }
327        Err(e) => {
328            // Clean up output file if it exists
329            let _ = fs::remove_file(&output_path);
330            Err(crate::error::Html2PdfError::encryption(format!(
331                "Failed to execute qpdf: {}. Make sure qpdf is installed.", e
332            )))
333        }
334    }
335}