Merge PR #2702: Remove security also from files with /Encrypt inline in the trailer

This commit is contained in:
aiongg
2026-10-16 14:02:08 +00:00
3 changed files with 57 additions and 2 deletions
+3 -2
View File
@@ -851,9 +851,10 @@ impl Document {
self.repair_log = Arc::new(log);
}
/// `true` when there are unsaved edits.
/// `true` when there are unsaved edits. Removing an `/Encrypt` dictionary written inline in
/// the trailer (as MuPDF writes it) changes no object, and is an edit all the same.
pub fn is_modified(&self) -> bool {
!self.overlay.is_empty()
!self.overlay.is_empty() || self.encryption_changed
}
/// Object numbers changed since the document was opened (or last saved).
+38
View File
@@ -336,3 +336,41 @@ fn an_earlier_revision_opens_with_the_documents_own_credentials() {
assert_eq!(content(&current.open_with_same_credentials(Arc::new(fixture())).unwrap()), SECRET, "{alg:?}");
}
}
/// `protect`, with the `/Encrypt` dictionary written inline in the trailer, as MuPDF writes it,
/// instead of as an indirect object.
fn protect_inline(alg: Algorithm, user: &str, owner: &str) -> Vec<u8> {
let mut doc = Document::open(Arc::new(fixture())).unwrap();
doc.set_encryption(&NewEncryption {
algorithm: alg,
user_password: user,
owner_password: owner,
permissions: -1,
encrypt_metadata: true,
seed: [9; 32],
})
.unwrap();
let dict = doc.get(doc.trailer().reference(b"Encrypt").unwrap()).as_dict().cloned().unwrap();
doc.trailer_mut().set(b"Encrypt".to_vec(), Object::Dict(dict));
write_full(&doc, &SaveOptions::default()).unwrap()
}
/// MuPDF writes `/Encrypt` inline in the trailer, so removing it changes no object. It is still
/// an unsaved change, and saving must write a file that opens without a password (#2661).
#[test]
fn removing_an_inline_encrypt_dictionary_leaves_an_unencrypted_file() {
for alg in ALL {
let bytes = Arc::new(protect_inline(alg, "user", "owner"));
assert!(matches!(Document::open(bytes.clone()), Err(CosError::NeedsPassword)), "{alg:?}");
let mut doc = Document::open_with_password(bytes, Some("owner")).unwrap();
assert!(matches!(doc.trailer().get(b"Encrypt"), Some(Object::Dict(_))), "{alg:?}: inline");
assert!(!doc.is_modified(), "{alg:?}");
doc.remove_encryption();
assert!(doc.is_modified(), "{alg:?}: removing security is an unsaved change");
let plain = write_incremental(&doc, &SaveOptions::default()).unwrap();
let again = Document::open(Arc::new(plain.clone())).unwrap_or_else(|e| panic!("{alg:?}: {e:?}"));
assert!(again.security().is_none() && !again.trailer().contains(b"Encrypt"), "{alg:?}");
assert_eq!(content(&again), SECRET, "{alg:?}");
assert!(hayro_syntax::Pdf::new(plain).is_ok(), "{alg:?}");
}
}
+16
View File
@@ -1186,6 +1186,22 @@ fn the_permissions_password_unlocks_an_open_document() {
assert!(pdfcraft_cos::Document::open(plain).unwrap().security().is_none(), "security removed");
}
#[test]
fn remove_security_from_a_file_with_its_encrypt_dictionary_in_the_trailer() {
// MuPDF writes /Encrypt inline in the trailer rather than as an object (#2661).
let mut doc = pdfcraft_cos::Document::open_with_password(protected("user", "owner", -1), Some("owner")).unwrap();
let r = doc.trailer().reference(b"Encrypt").unwrap();
let dict = doc.get(r).as_dict().cloned().unwrap();
doc.trailer_mut().set(b"Encrypt".to_vec(), pdfcraft_cos::Object::Dict(dict));
let bytes = Arc::new(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap());
let mut s = Session::new();
let id = s.open("m.pdf", None, bytes, Some("owner")).unwrap();
s.apply(id, Edit::RemoveProtection).unwrap();
assert!(!s.get(id).unwrap().info.encrypted);
let plain = s.save_bytes(id).unwrap();
assert!(pdfcraft_cos::Document::open(plain).unwrap().security().is_none(), "security removed");
}
#[test]
fn unlocking_needs_the_permissions_password_not_the_open_password() {
let mut s = Session::new();