mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M1: a document that encrypts only its embedded files opens without a password
A crypt filter whose /AuthEvent is /EFOpen needs its password only when an embedded file is accessed (ISO 32000-2 7.6.6, Table 25). A document whose strings and streams are in the clear (/StmF, /StrF /Identity) and whose encrypting filters are all EFOpen ones, /EFF naming one, now opens without a password in cos and in the renderer (hayro-syntax patch 36). Without the password there is no file key: the embedded files' data is held and saved as stored, and Document::embedded_files_locked and reads_as_stored say so. Asking for such an attachment's bytes is an error. A full save that would renumber data whose key mixes in the object number (RC4, AES-128), or change the document's security, is refused. The password still opens the document and its attachments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
1 parent
c3c36f77cf
commit
390b2012e4
10 files changed
+405
-8
No files matched your search
+41
-1
@@ -222,6 +222,42 @@ impl SecurityHandler {
|
||||
Ok(Self::new(dict, key, auth, recovered_user))
|
||||
}
|
||||
|
||||
/// A handler with no file key, for a document opened without a password whose strings and
|
||||
/// streams aren't encrypted and whose encrypted crypt filters are all ones the caller found
|
||||
/// authorised only when an embedded file is opened (`/AuthEvent /EFOpen`, ISO 32000-2 §7.6.6,
|
||||
/// Table 25). Data such a filter encrypts is left as stored, read and written alike, so it
|
||||
/// keeps its encrypted bytes; [`SecurityHandler::decrypts`] tells which. `None` when strings or
|
||||
/// streams are encrypted, which needs the key, or the handler is older than crypt filters (V4).
|
||||
pub fn without_key(dict: EncryptDict) -> Option<Self> {
|
||||
if dict.v < 4 {
|
||||
return None;
|
||||
}
|
||||
let methods = Methods::new(&dict);
|
||||
if methods.string != Method::Identity || methods.stream != Method::Identity {
|
||||
return None;
|
||||
}
|
||||
Some(Self { dict, key: Vec::new(), auth: Auth::User, recovered_user: None, methods })
|
||||
}
|
||||
|
||||
/// Whether the handler has the file key: false for [`SecurityHandler::without_key`].
|
||||
pub fn has_key(&self) -> bool {
|
||||
!self.key.is_empty()
|
||||
}
|
||||
|
||||
/// Whether data of `kind` is encrypted, so that reading it needs the file key.
|
||||
pub fn decrypts(&self, kind: StreamKind) -> bool {
|
||||
self.method(Some(kind)) != Method::Identity
|
||||
}
|
||||
|
||||
/// Whether data left as stored by a handler without the key still decrypts after its object
|
||||
/// is renumbered: only AES-256 doesn't mix the object number into the key (Algorithm 1.A).
|
||||
pub fn survives_renumbering(&self) -> bool {
|
||||
[self.methods.string, self.methods.stream, self.methods.embedded_file]
|
||||
.into_iter()
|
||||
.chain(self.methods.named.values().copied())
|
||||
.all(|m| matches!(m, Method::Identity | Method::Aes256))
|
||||
}
|
||||
|
||||
/// Upgrade to owner access with the owner (permissions) password, as when a restricted
|
||||
/// document is unlocked after opening. The user password, or any other, is `WrongPassword`.
|
||||
/// Both passwords unlock the same file key, so what was decrypted so far stays valid; a
|
||||
@@ -255,7 +291,7 @@ impl SecurityHandler {
|
||||
|
||||
/// `/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) {
|
||||
if !matches!(self.dict.r, 5 | 6) || !self.has_key() {
|
||||
return None;
|
||||
}
|
||||
let plain = aes_cbc_decrypt(&self.key, &[0; 16], self.dict.perms.first_chunk::<16>()?, false);
|
||||
@@ -307,6 +343,10 @@ impl SecurityHandler {
|
||||
}
|
||||
|
||||
fn apply(&self, method: Method, num: u32, generation: u16, data: &[u8], encrypt: bool) -> Vec<u8> {
|
||||
// Without the key, encrypted data stays as stored (see `without_key`).
|
||||
if !self.has_key() {
|
||||
return data.to_vec();
|
||||
}
|
||||
match method {
|
||||
Method::Identity => data.to_vec(),
|
||||
Method::Rc4 => rc4(&self.object_key(method, num, generation), data),
|
||||
|
||||
Reference in new issue
Block a user