pdf-lib-encrypt
v1.0.3
Published
PDF encryption for pdf-lib: AES-256 (/V5 /R6) password protection and unlock, with validated passwords and RC4-128 legacy compatibility. Pure JS on WebCrypto, zero dependencies.
Downloads
731
Maintainers
Readme
pdf-lib-encrypt
AES-256 password protection for pdf-lib — and unlocking of the files it can safely open.
Writes /V5 /R6 /AESV3 (AES-256, PDF 2.0) and keeps document metadata intact. Reads AES-256 and
RC4; refuses AES-128 and compressed object streams rather than hand back a damaged document —
see Support matrix before choosing it.
Stock pdf-lib ships no encryption support at all — verified against 1.17.1, its current release. People have asked since 2019 (#243, #917, both closed in September 2021 without the feature landing), the encryption PR #1015 has been open since October 2021, and #1680 is still unanswered. This module implements the ISO 32000 standard security handler on pdf-lib's own parsed object graph:
- Write: AES-256 (
/V5 /R6 /AESV3— PDF 2.0, opens in Acrobat X+, Preview, pdf.js and other current viewers). - Read/unlock: AES-256 and RC4 (R2/R3/R4-V2); AES-128 (
/AESV2) is detected and rejected with a clear error instead of producing garbage. - Passwords are validated (user hash, then owner hash) before anything is decrypted — a wrong password throws, it never silently "succeeds" into a corrupted document.
- RC4-128 write stays available behind
{ algo: "rc4" }strictly for very old viewers. RC4 is cryptographically broken — never present it as real protection. It is reached only by that explicit opt-in: without Web Crypto,lock()now throws rather than quietly downgrading AES-256 to RC4.
Pure JavaScript on the Web Crypto API. Runs in browsers, workers and Node 19+. No native modules, no WASM, no dependencies beyond pdf-lib itself.
Install
npm install pdf-lib-encrypt pdf-libUsage
import * as pdfLib from "pdf-lib";
import { configure, lock, unlockInPlace } from "pdf-lib-encrypt";
configure(pdfLib); // once, before anything else
// --- Password-protect (AES-256) ---------------------------------------
const doc = await pdfLib.PDFDocument.create();
doc.addPage([300, 200]).drawText("Confidential", { x: 20, y: 100, size: 14 });
const plain = await doc.save();
const encrypted = await lock(plain, "correct horse battery staple");
// -> Uint8Array of a PDF that every viewer will ask a password for
// --- Unlock a protected PDF you have the password for ------------------
const locked = await pdfLib.PDFDocument.load(encrypted, { ignoreEncryption: true });
await unlockInPlace(locked, "correct horse battery staple"); // throws on a wrong password
const plainAgain = await locked.save({ useObjectStreams: false });
// --- Legacy RC4 for ancient viewers (NOT security) ---------------------
const rc4File = await lock(plain, "pw", { algo: "rc4" });API
| Function | Description |
|---|---|
| configure(pdfLib) | Provide the pdf-lib module once, before anything else. |
| lock(bytes, password, opts?) | Encrypt a saved PDF. opts.algo: "aes256" (default) or "rc4". opts.permissions: raw /P value. Returns Promise<Uint8Array>. |
| unlockInPlace(pdfDoc, password) | Decrypt a PDFDocument loaded with ignoreEncryption: true. Resolves true (unlocked), false (was not encrypted); throws on a wrong password or an unsupported handler. |
How it works
pdf-lib serializes raw streams and hex strings byte-for-byte, so the handler operates on the
parsed object graph directly: save plaintext → reload (every object in its verbatim "raw"
representation) → derive the file key (Algorithm 2.B hash for R6) → encrypt every string and
stream → write the /Encrypt dictionary. Unlock is the inverse, after validating the
password against /U (or /O for the owner password).
The AES-256 path uses the Web Crypto API for SHA-256/384/512 and AES-CBC. Because Web Crypto always applies PKCS#7 padding, the CBC helpers drop the extra block when encrypting and append a synthetic one when decrypting, so the ciphertext matches the PDF specification exactly rather than pdf-lib's or the browser's own conventions.
Support matrix
| PDF encryption | Read / unlock | Write |
|---|---|---|
| AES-256 — /V5 /R6 /AESV3 (PDF 2.0, Acrobat X+) | ✅ | ✅ default |
| AES-256 — /V5 /R5 (deprecated Acrobat 9 extension) | ✅ | — |
| RC4-128 — /V2 /R3 | ✅ | ✅ via { algo: "rc4" } |
| RC4-40 — /V1 /R2 | ✅ | — |
| RC4 inside /V4 crypt filters (/StmF, /StrF, any filter name) | ✅ | — |
| AES-128 — /V4 /AESV2 | ❌ clear error, never garbage | — |
| Any cipher, inside compressed object streams | ❌ clear error (see limitations) | — |
| Public-key handlers (/Adobe.PubSec) | ❌ clear error | — |
Both the user password and the owner password are accepted when unlocking. An unsupported handler throws with an explanatory message instead of silently producing a corrupted document:
| Situation | Error |
|---|---|
| Wrong password | Wrong password for this PDF. |
| AES-128 source | Unsupported encryption: AES-128 (/AESV2) is not implemented. |
| Non-standard handler | Unsupported security handler: only the ISO 32000 Standard handler is implemented. |
| /StmF or /StrF names a filter missing from /CF | Malformed security handler: crypt filter … is not defined in /CF. |
| /EFF encrypts embedded files separately | This PDF encrypts embedded files separately (/EFF), which this module cannot decrypt yet. |
| AES file, no crypto.subtle | Web Crypto is unavailable; AES decryption requires a secure context. |
Known limitations
Compressed object streams are refused. pdf-lib inflates
/ObjStmwhile loading, before this module can decrypt anything, so an encrypted object stream is already unreadable by the timeunlockInPlace()runs and part of the document is gone. Rather than decrypt what survived and report success, it throws. This matters in practice: object streams are the default output of pdf-lib itself and of@cantoo/pdf-lib, so many foreign encrypted files land here. Files this module produces are unaffected —lock()writes without object streams.One password per document.
lock()sets the owner password equal to the user password, so it produces "open password" protection — not a separate permissions password./Pis written but not enforced on unlock. Permission flags are advisory in every PDF reader; treat them as metadata, never as a control.The trailer
/IDis replaced with a fresh random value when locking.
Security notes
- AES-256 is the default and the only strength you should rely on.
{ algo: "rc4" }exists purely so ancient viewers can open a file; RC4 is broken and must never be described to end users as real protection. - A wrong password throws. The user password is validated against
/U, then the owner password against/O, before any object is touched. An implementation that skips this step hands back a document full of garbage bytes that looks like a successful unlock. - Encryption is not redaction. Anyone with the password sees everything in the file. If content must be unreadable without a password, it has to be removed, not merely covered.
- The library never transmits anything. All key derivation and encryption happen in-process.
Requirements
pdf-lib
>= 1.17.0(peer dependency — you install it yourself)Web Crypto (
globalThis.crypto): built in to every modern browser, web worker, and Node 19+. The module throws on import if it is missing entirely (code: "NO_WEBCRYPTO"), so the failure is loud rather than silent.If
cryptoexists butcrypto.subtledoes not — a browser page served over plain HTTP rather than HTTPS —lock()throws (code: "NO_SUBTLE"). Earlier versions silently fell back to RC4-128; they no longer do. Serve the page from a secure context, or pass{ algo: "rc4" }if you genuinely want the legacy cipher.No native modules, no WASM, no bundled binaries. The published package is a single ES module.
ESM only. Use
import.require("pdf-lib-encrypt")works only on Node 22.12+, which addedrequire(esm); on older Node it fails withERR_REQUIRE_ESM. From CommonJS, use a dynamic import instead:const { configure, lock } = await import("pdf-lib-encrypt");
Alternatives
This is a small module in a space that already has bigger players. Worth knowing before you choose this one:
@cantoo/pdf-lib— a maintained fork of pdf-lib (~428k weekly downloads) that does both sides:doc.encrypt({...})beforesave(), andload(bytes, { password })to open. It reads AES-128 and compressed object streams, which this module refuses — so for opening arbitrary protected files it is the more capable choice, and if you can swap your dependency for a fork you should probably use it. Two measured caveats if you are writing protection: it emits/V4 /R4 /AESV2(AES-128, 2008-era), and in our testssetTitle/setAuthordid not survive its encrypt round-trip.@pdfsmaller/pdf-encrypt— also declarespdf-libas a peer dependency, so it too works with the upstream package rather than a fork, and writes the same/V5 /R6 /AESV3.@pdfsmaller/pdf-decryptcovers the other direction. Several other packages (pdf-lib-with-encrypt,pdf-lib-plus-encrypt) target the same need.- qpdf-based tools (
node-qpdf, qpdf-wasm) — a mature C++ implementation, but it means a native binary or a multi-megabyte WASM payload, and it operates on files rather than on aPDFDocumentyou already have in memory.
Where this module is the better pick: you are writing protection and you want the modern
cipher and your metadata intact — it emits AES-256 (/V5 /R6 /AESV3) and preserves
/Info across the round-trip, both pinned by tests. It is also a single ES module with zero
dependencies that works on a PDFDocument you already hold in memory.
Where it is not: opening arbitrary protected PDFs. AES-128 and compressed object streams are the common shapes in the wild and this module refuses both, by design, rather than return a damaged document. If that is your use case, use one of the packages above.
Testing
npm install pdf-lib # peer dep, for the tests
npm testCovers AES-256 header fields, plaintext removal, unlock round-trip, wrong-password rejection,
/P permission passthrough, and the RC4 legacy path.
The RC4-40 fixtures are encrypted by an independent reference implementation built on
node:crypto, not by this library — so the tests cannot pass by agreeing with a bug of their
own. This handler also ships inside DollarFix PDF.
License
MIT — see LICENSE.
