mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(crypt): /Perms limits what an edited /P allows
AES-256 files (R5 and R6) store the permissions twice: in clear in /P, and in /Perms, encrypted with the file key (ISO 32000-2, Algorithm 13). We wrote /Perms but never read it, so a document opened with the user password got whatever /P said, even when /P no longer matched /Perms. permissions() now decrypts /Perms and, when it holds a valid block (marked "adb"), counts a permission only if both /P and /Perms grant it. Without a readable /Perms, /P applies as before. R2 to R4 need no check: /P is part of their file key, so an edited /P does not open.
This commit is contained in:
+15
-1
@@ -203,8 +203,22 @@ impl SecurityHandler {
|
||||
self.recovered_user.clone()
|
||||
}
|
||||
|
||||
/// For R5/R6, `/P` is stored in clear and again in `/Perms`, encrypted with the file key
|
||||
/// (Algorithm 13). Where the two differ, a permission counts only when both grant it, so an
|
||||
/// edited `/P` grants nothing extra. Without a readable `/Perms`, `/P` applies as before.
|
||||
pub fn permissions(&self) -> Permissions {
|
||||
Permissions { bits: self.dict.p, owner: self.auth == Auth::Owner }
|
||||
let bits = self.perms_p().map_or(self.dict.p, |p| self.dict.p & p);
|
||||
Permissions { bits, owner: self.auth == Auth::Owner }
|
||||
}
|
||||
|
||||
/// `/P` as `/Perms` holds it: AES-256 ECB of one block (= CBC with a zero IV), marked `adb`.
|
||||
fn perms_p(&self) -> Option<i32> {
|
||||
if !matches!(self.dict.r, 5 | 6) {
|
||||
return None;
|
||||
}
|
||||
let plain = aes_cbc_decrypt(&self.key, &[0; 16], self.dict.perms.first_chunk::<16>()?, false);
|
||||
let p = i32::from_le_bytes(*plain.first_chunk::<4>()?);
|
||||
(plain.get(9..12) == Some(&b"adb"[..])).then_some(p)
|
||||
}
|
||||
|
||||
pub fn dict(&self) -> &EncryptDict {
|
||||
|
||||
@@ -212,6 +212,28 @@ fn permission_bits_follow_table_22() {
|
||||
assert!(p.modify(), "owners ignore /P");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perms_limits_what_an_edited_p_grants() {
|
||||
let made = new(Algorithm::Aes256, "user pw", "owner pw", 0b0100); // print only
|
||||
let open = |d: &EncryptDict, pw| SecurityHandler::open(d.clone(), ID, Some(pw)).unwrap().permissions();
|
||||
let mut d = made.dict().clone();
|
||||
assert_eq!(open(&d, "user pw").bits, d.p, "/P and /Perms agree");
|
||||
d.p = -4; // /P edited to allow everything, /Perms as written
|
||||
let p = open(&d, "user pw");
|
||||
assert!(p.print() && !p.modify() && !p.copy() && !p.annotate() && !p.fill_forms() && !p.assemble());
|
||||
assert!(open(&d, "owner pw").modify(), "owners ignore /P");
|
||||
d.p = -3904; // a stricter /P still applies
|
||||
assert!(!open(&d, "user pw").print());
|
||||
// No /Perms, or one that does not decrypt to an `adb` block: /P as before.
|
||||
for perms in [Vec::new(), vec![0; 16], made.dict().perms[..15].to_vec()] {
|
||||
let d = EncryptDict { p: -4, perms, ..made.dict().clone() };
|
||||
assert!(open(&d, "user pw").modify());
|
||||
}
|
||||
// R2 to R4 have no /Perms: /P is part of their file key, so an edited /P does not open.
|
||||
let d = EncryptDict { p: -4, ..new(Algorithm::Aes128, "user pw", "owner pw", 0b0100).dict().clone() };
|
||||
assert_eq!(SecurityHandler::open(d, ID, Some("user pw")).unwrap_err(), CryptError::WrongPassword);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn malformed_dictionaries_never_panic(o in proptest::collection::vec(proptest::num::u8::ANY, 0..60),
|
||||
|
||||
@@ -214,7 +214,7 @@ milestone = "M1"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/editing.rs::restricted_documents_show_a_notice_and_block_page_changes", "crates/crypt/src/tests.rs::permission_bits_follow_table_22", "crates/engine/src/tests.rs::combining_protected_files_is_refused_clearly"]
|
||||
evidence = ["crates/ui-egui/tests/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"]
|
||||
notes = ""
|
||||
|
||||
[[feature]]
|
||||
|
||||
Reference in New Issue
Block a user