From 6765ae0b5c6e4e017de6ee179de5c765e842328b Mon Sep 17 00:00:00 2001 From: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:12:14 +0530 Subject: [PATCH] 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 --- crates/engine/src/lib.rs | 10 +++++++++- crates/engine/src/tests.rs | 28 ++++++++++++++++++++++++++++ parity/acrobat-features.toml | 6 +++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index f257865d3..835b5888b 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -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 { .. } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index c8f0eb886..ec774b65b 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index fb1f2e0e2..9329c2b35 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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"