From cea192bb861f72a747f08f6cbedac508f514bb97 Mon Sep 17 00:00:00 2001 From: Dhiru Kholia Date: Wed, 30 Sep 2026 11:49:11 +0530 Subject: [PATCH 1/2] pdf2john: fix missing /Length for V4 PDFs using crypt filters --- run/lib/PDF.pm | 19 +++++++++++++++++++ run/pdf2john.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/run/lib/PDF.pm b/run/lib/PDF.pm index 9f18906df9d..da32a5f0890 100644 --- a/run/lib/PDF.pm +++ b/run/lib/PDF.pm @@ -1166,6 +1166,25 @@ sub DecryptInit($$$) } # set "_aesv2" or "_aesv3" flag in %$encrypt hash if AES encryption was used $$encrypt{'_' . lc($1)} = 1 if $cryptMeth =~ /^\/(AESV2|AESV3)$/; + + # V4 PDFs may omit the top-level /Length; derive it from the + # applicable crypt filter. Per the PDF spec, StmF/StrF default + # to Identity (no encryption), so resolve the actual filter name + # rather than assuming StdCF. + # CF /Length is in bytes; top-level /Length is in bits. + unless (defined $$encrypt{Length}) { + my $cf_len; + for my $key ('StmF', 'StrF') { + my $name = $$encrypt{$key} || ''; + $name =~ s{^/}{}; + next unless $name and $name ne 'Identity'; + if (ref $$encrypt{CF}{$name} eq 'HASH') { + $cf_len = $$encrypt{CF}{$name}{Length}; + last if defined $cf_len; + } + } + $$encrypt{Length} = defined $cf_len ? $cf_len * 8 : 40; + } } if ($ver == 5) { # validate OE and UE entries diff --git a/run/pdf2john.py b/run/pdf2john.py index 299714e9946..98f4d7d88c3 100755 --- a/run/pdf2john.py +++ b/run/pdf2john.py @@ -71,10 +71,38 @@ def __init__(self, file_name: str, strict: bool = False): raise RuntimeError("File not encrypted") self.algorithm: int = self.encrypt_dict.get("/V") - self.length: int = self.encrypt_dict.get("/Length", 40) self.permissions: int = self.encrypt_dict["/P"] self.revision: int = self.encrypt_dict["/R"] + if self.algorithm == 4: + # For V4 the key length lives in the applicable crypt filter, + # not in the top-level /Length (which may be absent or written + # in bytes instead of bits by some generators). + # /AESV2 and /AESV3 imply fixed sizes; for /V2 (RC4) we read + # /Length directly, but must handle the PDF 1.7 (bits) vs + # PDF 2.0 (bytes) ambiguity: any value < 40 must be bytes + # because 40-bit is the minimum valid key length. + cf = self.encrypt_dict.get("/CF") or {} + cfm = cf_length = None + for fkey in ("/StmF", "/StrF"): + fname = self.encrypt_dict.get(fkey) + if fname and fname != "/Identity": + fdict = cf.get(fname) or {} + cfm = fdict.get("/CFM") + cf_length = fdict.get("/Length") + break + if cfm == "/AESV2": + self.length: int = 128 + elif cfm == "/AESV3": + self.length: int = 256 + elif cf_length is not None: + self.length: int = cf_length * 8 if cf_length < 40 \ + else cf_length + else: + self.length: int = 40 + else: + self.length: int = self.encrypt_dict.get("/Length") or 40 + @property def document_id(self) -> bytes: return self.pdf.document_id[0] From 05825ac1b05493b6b55d860754e0c3965237cb3c Mon Sep 17 00:00:00 2001 From: Dhiru Kholia <79528+kholia@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:45:07 +0530 Subject: [PATCH 2/2] Update encryption length based on cryptographic method Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- run/lib/PDF.pm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/run/lib/PDF.pm b/run/lib/PDF.pm index da32a5f0890..32d5c8c2d03 100644 --- a/run/lib/PDF.pm +++ b/run/lib/PDF.pm @@ -1183,7 +1183,10 @@ sub DecryptInit($$$) last if defined $cf_len; } } - $$encrypt{Length} = defined $cf_len ? $cf_len * 8 : 40; + $$encrypt{Length} = defined $cf_len ? $cf_len * 8 + : $cryptMeth eq '/AESV2' ? 128 + : $cryptMeth eq '/AESV3' ? 256 + : 40; } } if ($ver == 5) {