M11: Convert to PDF/A needs the owner's permission on an encrypted document (#3921) (#4203)

Save as PDF/A removes the encryption (PDF/A forbids it), but it was gated
only by the modify permission, so a document opened with the user password
whose owner withheld printing or copying could be saved unencrypted with
every restriction gone. It now needs the same rights as Protect and Remove
security, also inside a batch.

Fixes #3921
This commit is contained in:
Jayesh Prajapati
2026-10-23 10:42:14 -04:00
committed by GitHub
parent 9b5624afc1
commit 6765ae0b5c
3 changed files with 40 additions and 4 deletions
+9 -1
View File
@@ -2274,6 +2274,15 @@ fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), Ed
Err(EditError::NotPermitted("changing security"))
}
}
// Converting to PDF/A takes the encryption off (PDF/A forbids it), so on an encrypted
// document it is a change of security (#3921).
Edit::ConvertPdfA { .. } => {
if unrestricted(p) {
Ok(())
} else {
Err(EditError::NotPermitted("removing the encryption, which PDF/A forbids"))
}
}
Edit::SetInfo { .. }
| Edit::AddHeaderFooter { .. }
| Edit::AddWatermark { .. }
@@ -2314,7 +2323,6 @@ fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), Ed
| Edit::AddOcrText { .. }
| Edit::SetDocumentScript { .. }
| Edit::SetFieldScript { .. }
| Edit::ConvertPdfA { .. }
| Edit::RemoveStandardId { .. }
| Edit::EditTextLine { .. }
| Edit::EditTextBlock { .. }
+28
View File
@@ -1888,6 +1888,34 @@ fn permissions_password_restricts_others_but_not_this_session() {
assert!(s2.get(id3).unwrap().allows_security_change());
}
/// Converting to PDF/A takes the encryption off (PDF/A forbids it), so on an encrypted document
/// it needs the same rights as removing security, not just the right to make changes (#3921).
#[test]
fn convert_to_pdfa_needs_the_owners_rights_on_an_encrypted_document() {
let pdfa = || Edit::ConvertPdfA { level: pdfcraft_preflight::Level::A2b };
let refused = Err(EditError::NotPermitted("removing the encryption, which PDF/A forbids"));
// Changes allowed; printing (bit 3) and copying (bit 5) withheld.
let bytes = protected("pw", "owner", !((1 << 2) | (1 << 4)));
let mut s = Session::new();
let id = s.open("x.pdf", None, bytes.clone(), Some("pw")).unwrap();
assert!(s.get(id).unwrap().allows_modification());
assert_eq!(s.apply(id, pdfa()), refused);
// Nor inside a batch.
let batch = Edit::Batch { label: "x".into(), edits: vec![Edit::RotatePages { pages: vec![0], degrees: 90 }, pdfa()] };
assert_eq!(s.apply(id, batch), refused);
// Nothing was applied: no undo step, and saving gives back the encrypted original.
assert!(s.get(id).unwrap().can_undo().is_none());
assert_eq!(s.save_bytes(id).unwrap().as_slice(), bytes.as_slice());
// The owner's password allows it, and the result is no longer encrypted.
let id2 = s.open("x.pdf", None, bytes, Some("owner")).unwrap();
s.apply(id2, pdfa()).unwrap();
let saved = s.save_bytes(id2).unwrap();
assert!(pdfcraft_cos::Document::open(saved).unwrap().trailer().get(b"Encrypt").is_none());
// An unencrypted document needs nothing more.
let (mut s, id) = session_with(1);
s.apply(id, pdfa()).unwrap();
}
#[test]
fn combine_source_check_says_why_a_file_cannot_be_combined() {
let (mut s, id) = session_with(1);
+3 -3
View File
@@ -214,7 +214,7 @@ milestone = "M1"
status = "shipped"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/editing.rs::restricted_documents_show_a_notice_and_block_page_changes", "crates/crypt/src/tests.rs::permission_bits_follow_table_22", "crates/crypt/src/tests.rs::perms_limits_what_an_edited_p_grants", "crates/engine/src/tests.rs::combining_protected_files_is_refused_clearly", "crates/engine/src/tests.rs::print_restricted_documents_disable_print_unless_opened_by_the_owner", "crates/ui-egui/tests/it/print.rs::print_restricted_documents_say_so_before_the_dialog"]
evidence = ["crates/ui-egui/tests/it/editing.rs::restricted_documents_show_a_notice_and_block_page_changes", "crates/crypt/src/tests.rs::permission_bits_follow_table_22", "crates/crypt/src/tests.rs::perms_limits_what_an_edited_p_grants", "crates/engine/src/tests.rs::combining_protected_files_is_refused_clearly", "crates/engine/src/tests.rs::print_restricted_documents_disable_print_unless_opened_by_the_owner", "crates/ui-egui/tests/it/print.rs::print_restricted_documents_say_so_before_the_dialog", "crates/engine/src/tests.rs::convert_to_pdfa_needs_the_owners_rights_on_an_encrypted_document"]
notes = ""
[[feature]]
@@ -7817,8 +7817,8 @@ milestone = "M11"
status = "partial"
commands = ["standards.pdfa"]
tools = ["pdfa_convert"]
evidence = ["crates/preflight/src/tests.rs::convert_fixes_all_but_fonts", "crates/preflight/src/tests.rs::convert_does_not_give_cmyk_documents_an_srgb_output_intent", "crates/automation/tests/it/automation.rs::pdfa_through_tools", "crates/ui-egui/tests/it/compare.rs::pdfa_dialog_verifies_and_converts"]
notes = "Fixes XMP, output intent (generated sRGB ICC profile, only for documents without DeviceCMYK; CMYK documents need a user-chosen CMYK output intent, not yet offered), forbidden actions, annotation flags, interpolation, encryption, AFRelationship. Not yet: embedding fonts, converting device CMYK, flattening transparency for PDF/A-1."
evidence = ["crates/preflight/src/tests.rs::convert_fixes_all_but_fonts", "crates/preflight/src/tests.rs::convert_does_not_give_cmyk_documents_an_srgb_output_intent", "crates/automation/tests/it/automation.rs::pdfa_through_tools", "crates/ui-egui/tests/it/compare.rs::pdfa_dialog_verifies_and_converts", "crates/engine/src/tests.rs::convert_to_pdfa_needs_the_owners_rights_on_an_encrypted_document"]
notes = "Fixes XMP, output intent (generated sRGB ICC profile, only for documents without DeviceCMYK; CMYK documents need a user-chosen CMYK output intent, not yet offered), forbidden actions, annotation flags, interpolation, encryption, AFRelationship. Not yet: embedding fonts, converting device CMYK, flattening transparency for PDF/A-1. On an encrypted document it needs the owner password, as Protect and Remove security do, since it removes the encryption (#3921)."
[[feature]]
id = "optimize.pdfa-accessible-levels"