npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-lib

Usage

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 /ObjStm while loading, before this module can decrypt anything, so an encrypted object stream is already unreadable by the time unlockInPlace() 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.

  • /P is written but not enforced on unlock. Permission flags are advisory in every PDF reader; treat them as metadata, never as a control.

  • The trailer /ID is 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 crypto exists but crypto.subtle does 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 added require(esm); on older Node it fails with ERR_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({...}) before save(), and load(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 tests setTitle/setAuthor did not survive its encrypt round-trip.
  • @pdfsmaller/pdf-encrypt — also declares pdf-lib as a peer dependency, so it too works with the upstream package rather than a fork, and writes the same /V5 /R6 /AESV3. @pdfsmaller/pdf-decrypt covers 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 a PDFDocument you 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 test

Covers 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.