html2pdf_secure/
encryption.rs1use crate::error::Result;
4use lopdf::Document;
5use serde::{Deserialize, Serialize};
6use rand::Rng;
7
8#[derive(Debug, Clone)]
10pub struct PasswordOptions {
11 pub user_password: String,
13
14 pub owner_password: String,
16
17 pub encryption_level: EncryptionLevel,
19
20 pub permissions: PdfPermissions,
22}
23
24impl PasswordOptions {
25 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, permissions: PdfPermissions::default(),
33 }
34 }
35
36 pub fn with_encryption_level(mut self, level: EncryptionLevel) -> Self {
38 self.encryption_level = level;
39 self
40 }
41
42 pub fn with_permissions(mut self, permissions: PdfPermissions) -> Self {
44 self.permissions = permissions;
45 self
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub enum EncryptionLevel {
52 Standard40,
54 Standard128,
56 Aes128,
58 Aes256,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PdfPermissions {
65 pub allow_printing: bool,
67
68 pub allow_modify_contents: bool,
70
71 pub allow_copy: bool,
73
74 pub allow_modify_annotations: bool,
76
77 pub allow_fill_forms: bool,
79
80 pub allow_extract_for_accessibility: bool,
82
83 pub allow_assemble: bool,
85
86 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 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 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 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
151pub fn encrypt_pdf(pdf_data: Vec<u8>, password_options: &PasswordOptions) -> Result<Vec<u8>> {
159 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 encrypt_pdf_fallback(pdf_data, password_options)
172 }
173 }
174}
175
176fn encrypt_pdf_fallback(pdf_data: Vec<u8>, password_options: &PasswordOptions) -> Result<Vec<u8>> {
179 let mut doc = Document::load_mem(&pdf_data)?;
181
182 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 let mut output = Vec::new();
197 doc.save_to(&mut output)?;
198
199 Ok(output)
200}
201
202pub 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 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 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 fs::write(&input_path, &pdf_data)?;
254
255 let mut cmd = Command::new("qpdf");
257
258 match password_options.encryption_level {
259 EncryptionLevel::Standard40 => {
260 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 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 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 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 let output = cmd.output();
306
307 let _ = fs::remove_file(&input_path);
309
310 match output {
311 Ok(result) => {
312 if result.status.success() {
313 let encrypted_data = fs::read(&output_path)?;
315 let _ = fs::remove_file(&output_path);
317 Ok(encrypted_data)
318 } else {
319 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 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}