What happens when you password-protect a PDF
“Set a password” is one checkbox in most PDF tools, which makes it sound like one thing. It is not. The PDF specification has carried several incompatible encryption schemes over the years, one of them was withdrawn after publication, and picking the wrong one produces a file that some readers simply refuse to open. Here is what we ran into implementing it in a browser.
The revisions, briefly
PDF encryption is versioned by a number the file carries in its security dictionary,
written /R. What matters in practice:
| Revision | Cipher | Status |
|---|---|---|
| R2 / R3 | RC4, 40–128 bit | Obsolete. Broken in the sense that matters: recovering the content is a solved problem. |
| R4 | RC4 or AES-128 | Legacy. Widely readable, no longer a reasonable choice for new files. |
| R5 | AES-256 | Withdrawn. Published by Adobe as an extension, then retracted — its key derivation was a single SHA-256 pass and fell to brute force far faster than intended. Some readers refuse R5 files. |
| R6 | AES-256 | Current. Standardised in ISO 32000-2 (PDF 2.0). Same cipher as R5, hardened key derivation. |
The gap between R5 and R6 is entirely in how the password becomes a key. R5 hashes once. R6 runs Algorithm 2.B, which iterates at least 64 rounds, alternating hash functions and mixing in the previous round's output, with a termination condition that depends on the data itself so the round count cannot be predicted from the password. Same AES underneath; vastly more expensive to attack by guessing.
Why we had to write this ourselves
The library we use to write PDFs can produce AES-256 — but it chooses the revision
from the PDF header string and writes R = 5 in that branch. That is the
withdrawn one. We were producing files that were fine in some readers and rejected in
others, which is the worst possible failure mode for a document you are about to email
to someone.
Patching the library was the obvious fix and the wrong one: mousePDF has no build step, deliberately, and a patched vendor bundle is a maintenance liability that only reveals itself at the next upgrade. Instead the security handler is attached from the outside, through the library's own public interface. The bundle stays untouched.
An awkward constraint. The writer asks the security handler to
encrypt each object synchronously. The browser's own crypto API,
crypto.subtle, is asynchronous throughout — so it cannot be used for the
encryption step at all. A crypto library was already inside the vendor bundle, but not
exported; we checked all 321 exported names for anything matching crypt, hash, sha or
aes, and none of them was reachable. That left writing AES itself: the encryption
direction only, table-driven, synchronous.
The hashing is a different story. Key derivation happens once, up front, where asynchronous code is fine — so that part uses the browser's own implementation, as it should. Writing our own SHA-512 would have been code for its own sake.
The limitation we are not going to paper over
The writing library encrypts streams — page content, images, fonts. It does not encrypt strings sitting directly in dictionaries. In practice those strings end up inside object streams when the file is saved, and those streams are encrypted, so the content is protected. But it is protected as a consequence of how the file is assembled, not because each string was individually encrypted. We measured this rather than assumed it, and it is the same under R5 and R6 — the revision changes nothing here.
We mention this because “encrypted” is the kind of word people reasonably take literally. If your threat model is a lost laptop or a misdirected email, this is not a concern. If it is a determined adversary with the file and time, you want a container built for that job, not a PDF.
How you know it actually worked
There is no server to validate the output against, and a wrong implementation does not raise an error — it produces a file that opens nowhere. So the only test that means anything is opening the result in readers we did not write. Every change to this code path is checked against Acrobat, macOS Preview and several mobile readers before it ships. If the password is accepted in all of them and refused when wrong, it works.
What this means for you, practically
- A password set in mousePDF is AES-256 at revision 6 — the current standard, not the withdrawn one.
- The password is processed in your browser. It is never transmitted, because there is nowhere for it to be transmitted to. We could not recover your document if you forgot it, and neither could anyone else.
- Choose a long password rather than a clever one. Algorithm 2.B makes each guess expensive; length is what turns “expensive” into “not worth attempting”.
- Removing a password requires knowing it. mousePDF will not open a document you cannot open yourself.
The specification, if you want the primary source, is ISO 32000-2 — algorithms 2.A and 2.B for key derivation, 8 and 9 for the user and owner entries, 10 for the permissions block. AES itself is FIPS-197.
Related: building a PDF editor with no server at all, and the password tool itself.