How PDF password hashes are extracted
A precise look at how the $pdf$ hash is built from an encrypted PDF's /Encrypt dictionary, and how to crack it with hashcat or John the Ripper.
Encrypted PDFs do not store the user's password anywhere in the file. Instead, the PDF Standard Security Handler stores a password-derived check value that a cracker can reproduce offline. Hash Extractor pulls that check value out of the file entirely in the browser — no upload, no server — and formats it as a $pdf$ hash ready for hashcat or John the Ripper.
This post walks through exactly what is in the file, how the parser finds it, and what to do with the resulting hash.
Where the secret lives
The PDF specification's Standard Security Handler stores everything a cracker needs inside the /Encrypt dictionary. That dictionary lives either inline in the document trailer or as an indirect object referenced from the trailer. The fields that matter are:
| Field | Role |
|---|---|
/V | Algorithm version (2 = RC4-128, 4 = AESV2-128, 5 = AES-256) |
/R | Revision (2–6); drives the hashcat mode |
/Length | Key length in bits (defaults to 40 if absent) |
/P | Permission flags as a signed 32-bit integer |
/EncryptMetadata | Whether metadata is encrypted (false → 0, otherwise 1) |
/O | Owner password check value (32 bytes for V≤4, 48 bytes for V=5) |
/U | User password check value (same sizes) |
/OE | Owner key encrypted with owner password (32 bytes, V=5 only) |
/UE | User key encrypted with user password (32 bytes, V=5 only) |
The document /ID — a 16-byte array in the trailer — is also required for RC4-based revisions. The password itself is never stored; cracking works by re-deriving the key from a candidate password and checking whether it reproduces the stored /O or /U value.
What the parser does
Finding the /Encrypt dictionary
The parser scans for every /Encrypt occurrence in the file and keeps the last one (incremental updates can shadow earlier trailers). After finding /Encrypt, it looks at the next non-whitespace bytes:
- Inline dict — if the next bytes are
<<, it reads the dictionary directly by tracking nested<</>>pairs to find the balanced closing>>. - Indirect reference — if the bytes are
N G R(object number, generation, the letterR), the parser searches for theN G objmarker elsewhere in the file and extracts the dictionary from that object.
The parser also verifies that /Standard appears inside the dictionary. Non-standard security handlers (certificate-based, etc.) are rejected immediately — they are not crackable this way.
Reading fields from the dictionary
Integer fields like /V, /R, /Length, and /P are read with a prefix-safe key search: /O will not accidentally match /OE because the parser checks that the character immediately following the key name is not alphanumeric.
The /P value receives special treatment. Some writers store it as an unsigned integer even though the spec defines it as signed. The parser normalises it with a u32 → i32 cast so the sign is always correct in the output.
Parsing PDF strings
/O, /U, /OE, and /UE are PDF strings, which appear in two forms:
- Hex string —
<4f4f4f...>: hex digits are collected, any whitespace inside is ignored, and an odd number of digits is right-padded with0per spec. - Literal string —
(...): the parser handles escape sequences (\n,\r,\t,\b,\f,\(,\),\\, line continuations, and octal escapes up to three digits), and tracks nested parentheses so an unescaped(inside a string does not end the parse early.
Reading the document /ID
The trailer's /ID field is an array of two strings. The parser takes the first element, again keeping the last /ID occurrence to handle incremental updates. If /ID is absent the field is left empty.
The $pdf$ hash format
The output format is:
$pdf$<V>*<R>*<length>*<P>*<encmeta>*<idlen>*<idhex>*<passwords>
The <passwords> sub-field differs by version.
V≤4 (RC4-40, RC4-128, AESV2-128):
<ulen>*<uhex>*<olen>*<ohex>
U and O are truncated to 32 bytes before hex-encoding.
V=5 (AES-256):
<ulen>*<uhex>*<olen>*<ohex>*<uelen>*<uehex>*<oelen>*<oehex>
U and O are truncated to 48 bytes; UE and OE are truncated to 32 bytes.
Real example: RC4-128, R3
From the rc4_v2_r3_inline test — O is 32 bytes of 0x4f, U is 32 bytes of 0x55, document ID is 0102030405060708090a0b0c0d0e0f10:
$pdf$2*3*128*-1028*1*16*0102030405060708090a0b0c0d0e0f10*32*5555...55*32*4f4f...4f
Fields decoded: V=2, R=3, key length 128 bits, permissions -1028, metadata encrypted, 16-byte ID, then the 32-byte U value followed by the 32-byte O value.
Real example: AES-256, R6
From the aes256_v5_r6 test — O is 48 bytes of 0xa0, U is 48 bytes of 0xb0, OE is 32 bytes of 0xc0, UE is 32 bytes of 0xd0, document ID is 11223344556677881122334455667788:
$pdf$5*6*256*-1028*1*16*11223344556677881122334455667788*48*b0b0...b0*48*a0a0...a0*32*d0d0...d0*32*c0c0...c0
Cracking the hash
Hashcat mode by revision
| Revision | Cipher | hashcat -m | Description |
|---|---|---|---|
| R2 | RC4-40 | 10400 | PDF 1.1–1.3 (Acrobat 2–4) |
| R3 or R4 | RC4-128 / AESV2-128 | 10500 | PDF 1.4–1.6 (Acrobat 5–8) |
| R5 | AES-256 (legacy) | 10600 | PDF 1.7 Level 3 (Acrobat 9) |
| R6 | AES-256 | 10700 | PDF 1.7 Level 8 (Acrobat 10–11) |
hashcat one-liner (R3/R4)
echo '$pdf$2*3*128*-1028*1*16*0102030405060708090a0b0c0d0e0f10*32*5555555555555555555555555555555555555555555555555555555555555555*32*4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f' > hash.txt
hashcat -m 10500 hash.txt /usr/share/wordlists/rockyou.txt
For AES-256 R6 change -m 10700; for legacy RC4-40 R2 use -m 10400.
John the Ripper
john --format=PDF hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
pdf2john produces the same $pdf$ format — hashes extracted by Hash Extractor drop straight in.
Client-side, no upload
The entire extraction runs in a Rust library compiled to WebAssembly and executed in your browser. The file is read locally, parsed locally, and the hash is produced locally. Nothing leaves your machine. This mirrors what pdf2john does on the command line, but without requiring a local Perl or Python install.
Drop any encrypted PDF into Hash Extractor and the $pdf$ hash — along with the correct hashcat mode — is ready to copy in under a second.