Files
vectorcraft/crates/engine/src/cmd/fileio/tests_pdfsecurity.rs
T
bflatasticandClaude Opus 5.5 d8d1d472cf M4.85: PDF security: encryption at every compatibility level, permissions, either password opens
- pdf/src/encrypt.rs encrypts a written PDF with the standard security handler: 128-bit RC4
  (PDF 1.4, revision 3; PDF 1.5, revision 4), 128-bit AES (1.6) or 256-bit AES (1.7/2.0,
  revision 6), every string and stream, plaintext XMP metadata when asked, the permission bits
  (printing none/low/high, five change levels, copy, screen readers). Crypto from RustCrypto
  (aes, md-5, sha2: MIT OR Apache-2.0, pure Rust, wasm).
- PDF/A and PDF/X refuse passwords; equal passwords and passwords the algorithm can't take are
  refused.
- Opening: the permissions password opens RC4/AES-128 files too; editing data comes back from
  an encrypted PDF opened with its password.
- Save PDF › Security: the passwords, permissions and plaintext metadata, the encryption level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:24:16 +07:00

59 lines
3.5 KiB
Rust

//! Opening password-protected PDFs: the open password or the permissions password unlocks them.
use serde_json::{Value, json};
use vectorcraft_testkit::pdf::{PdfPage, pdf};
use super::*;
#[test]
fn the_permissions_password_opens_a_protected_pdf_too() {
let mut s = Session::new();
// The test file's permissions (owner) password is its open password followed by `-owner`.
let locked = vectorcraft_format::base64_encode(&pdf(&[PdfPage::new(80.0, 40.0, "0 g 0 0 10 10 re f")], Some("pw")));
let info = |s: &mut Session, pw: &str| s.execute("document.pdfInfo", &json!({"dataBase64": locked, "password": pw})).unwrap();
assert_eq!(info(&mut s, "pw-owner")["pages"], 1);
assert_eq!(info(&mut s, "pw-owners")["wrongPassword"], true);
let open = |s: &mut Session, pw: &str| s.execute("document.open", &json!({"name": "locked.pdf", "dataBase64": locked, "password": pw}));
let e = open(&mut s, "nope").unwrap_err().to_string();
assert!(e.contains("password is wrong"), "{e}");
open(&mut s, "pw-owner").unwrap();
assert_eq!(s.doc().unwrap().doc.artboards[0].rect.width(), 80.0);
}
#[test]
fn export_with_passwords_writes_a_pdf_that_only_its_passwords_open() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap();
for compatibility in ["1.4", "1.5", "1.6", "1.7", "2.0"] {
let p = json!({"compatibility": compatibility, "security": {"openPassword": "open", "permissionsPassword": "owner", "printing": "none"}});
let out = s.execute("document.exportPdf", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
let data = out["dataBase64"].as_str().unwrap().to_string();
let bytes = vectorcraft_format::base64_decode(&data).unwrap();
assert!(String::from_utf8_lossy(&bytes).contains("/Encrypt"), "{compatibility}: not encrypted");
let info = |s: &mut Session, p: Value| s.execute("document.pdfInfo", &p).unwrap();
assert_eq!(info(&mut s, json!({"dataBase64": data}))["needsPassword"], true, "{compatibility}");
assert_eq!(info(&mut s, json!({"dataBase64": data, "password": "nope"}))["wrongPassword"], true, "{compatibility}");
for pw in ["open", "owner"] {
assert_eq!(info(&mut s, json!({"dataBase64": data, "password": pw}))["pages"], 1, "{compatibility} {pw}");
}
// The open password restores the editable document (Preserve Editing is on by default).
let mut r = Session::new();
r.execute("document.open", &json!({"name": "locked.pdf", "dataBase64": data, "password": "open"})).unwrap();
assert!(r.doc().unwrap().doc.node_count() >= 2, "{compatibility}: the art came back");
}
}
#[test]
fn permissions_without_a_password_warn_and_a_password_applies_them() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap();
let warns = |v: &Value| v["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("permissions need a password"));
let open = s.execute("document.exportPdf", &json!({"security": {"printing": "none"}})).unwrap();
assert!(warns(&open), "{open}");
let locked = s.execute("document.exportPdf", &json!({"security": {"permissionsPassword": "owner", "printing": "none"}})).unwrap();
assert!(!warns(&locked), "{locked}");
let bytes = vectorcraft_format::base64_decode(locked["dataBase64"].as_str().unwrap()).unwrap();
assert!(String::from_utf8_lossy(&bytes).contains("/Encrypt"));
}