Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions run/lib/PDF.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,28 @@ 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
: $cryptMeth eq '/AESV2' ? 128
: $cryptMeth eq '/AESV3' ? 256
: 40;
}
}
if ($ver == 5) {
# validate OE and UE entries
Expand Down
30 changes: 29 additions & 1 deletion run/pdf2john.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading