-
Notifications
You must be signed in to change notification settings - Fork 0
Security
modern-pdf-lib v0.15.1
modern-pdf-lib provides three distinct security features: encryption of PDF
content, digital signatures using PKCS#7, and content redaction. All
cryptographic operations use the Web Crypto API (SubtleCrypto) and are
therefore available in every supported runtime without native add-ons.
| Identifier | Algorithm | Key length | PDF revision |
|---|---|---|---|
'rc4-40' |
RC4 | 40-bit | V=1, R=2 |
'rc4-128' |
RC4 | 128-bit | V=2, R=3 |
'aes-128' |
AES-CBC | 128-bit | V=4, R=4 |
'aes-256' |
AES-CBC | 256-bit | V=5, R=6 (ISO 32000-2) |
The default algorithm is 'aes-128'. For maximum compatibility with older
viewers use 'rc4-128'; for maximum security use 'aes-256'.
Encryption is applied at save time via PdfSaveOptions:
import { createPdf } from 'modern-pdf-lib';
const doc = createPdf();
const page = doc.addPage();
page.drawText('This document is encrypted.', { x: 50, y: 700, size: 14 });
const bytes = await doc.save({
userPassword: 'reader', // password required to open
ownerPassword: 'admin-secret', // password required to change permissions
algorithm: 'aes-256', // optional, defaults to 'aes-128'
permissions: {
printing: true, // allow printing
modifying: false, // disallow modification
copying: false, // disallow text copy
annotating: true, // allow annotations
fillingForms: true, // allow form filling
contentAccessibility: true, // allow screen readers
documentAssembly: false, // disallow page manipulation
},
});An empty string '' for userPassword means the document opens without
prompting — the encryption still protects against modification by users
who do not know the owner password.
The /P integer in the PDF /Encrypt dictionary encodes permission flags
per ISO 32000-1 Table 22. The permissions object maps to these bits:
| Permission key | Bit | Description |
|---|---|---|
printing |
3 | Low-resolution or high-resolution printing |
modifying |
4 | Modify document content |
copying |
5 | Copy or extract text/graphics |
annotating |
6 | Add/modify annotations, fill forms |
fillingForms |
8 | Fill interactive form fields |
contentAccessibility |
9 | Extract content for accessibility |
documentAssembly |
10 | Insert/delete/rotate pages |
Pass the password to loadPdf(). The library auto-detects the encryption
algorithm from the /Encrypt dictionary:
import { loadPdf } from 'modern-pdf-lib';
// Open with user password
const doc = await loadPdf(encryptedBytes, { password: 'reader' });
// Open with owner password (full permissions)
const docOwner = await loadPdf(encryptedBytes, { password: 'admin-secret' });
// Document with no user password (opens freely)
const docNoPass = await loadPdf(encryptedBytes);If the wrong password is supplied an EncryptedPdfError is thrown. If the
document is encrypted but no password is provided and the document has
a non-empty user password, an EncryptedPdfError is thrown.
The PdfEncryptionHandler class in src/crypto/encryptionHandler.ts
manages per-object key derivation and encryption:
-
V=1/V=2 (RC4): The file encryption key is derived using the MD5-based algorithm from the PDF 1.7 specification (§7.6.3.3). Each object's encryption key is derived from the file key, object number, and generation number. Strings and streams are encrypted with RC4.
-
V=4 (AES-128): The same key derivation as V=2 but with AES-CBC encryption. Each encrypted string/stream is prefixed with a 16-byte random IV.
-
V=5/R=6 (AES-256): Uses SHA-256 and SHA-512 for key derivation per ISO 32000-2 §7.6.4. The file encryption key is a 32-byte random value encrypted with owner/user keys derived from passwords via the Validation Salt / Key Salt scheme. Passwords are normalised with SASLprep (RFC 4013) before hashing, as required by the specification.
modern-pdf-lib supports creating and verifying PDF digital signatures
conforming to ISO 32000-1 §12.8 and the ETSI PDF signature profile. Both
invisible and visible signatures are supported.
import { signPdf } from 'modern-pdf-lib';
// DER-encoded X.509 certificate and PKCS#8 private key
const certificate = await readFile('signer.crt.der');
const privateKey = await readFile('signer.key.der');
const signedBytes = await signPdf(pdfBytes, 'Signature1', {
certDer: certificate,
keyDer: privateKey,
hashAlgorithm: 'SHA-256', // 'SHA-256' | 'SHA-384' | 'SHA-512'
reason: 'Document approval',
location: 'London, UK',
contactInfo: 'signer@example.com',
// Optional: RFC 3161 timestamp authority URL
timestampUrl: 'https://freetsa.org/tsr',
});Visible signatures render on a page with a customisable appearance:
const signedBytes = await signPdf(pdfBytes, 'Signature1', {
certDer: certificate,
keyDer: privateKey,
hashAlgorithm: 'SHA-256',
reason: 'Approved',
appearance: {
pageIndex: 0,
rect: [50, 50, 250, 120],
label: 'Signed by Alice',
},
});The signing process:
-
Reserve space — a placeholder
ByteRangeis written to the PDF with space reserved for the PKCS#7 signature blob (default: 8 KB). -
Compute hash — the two byte ranges covered by the signature (the PDF
bytes before and after the signature placeholder) are hashed with the
specified algorithm using
SubtleCrypto.digest(). -
Build PKCS#7 — a
SignedDataCMS structure is assembled containing the certificate, signing time, message digest, and the RSA signature over the signed attributes computed withSubtleCrypto.sign(). - Embed signature — the PKCS#7 DER bytes are hex-encoded and written into the reserved placeholder.
import { verifySignatures } from 'modern-pdf-lib';
const results = await verifySignatures(signedPdfBytes);
for (const result of results) {
console.log(`Field: ${result.fieldName}`);
console.log(`Signed by: ${result.signedBy}`);
console.log(`Integrity: ${result.integrityValid}`); // hash matches
console.log(`Cert: ${result.certificateValid}`); // signature math valid
console.log(`Valid: ${result.valid}`); // both true
console.log(`Date: ${result.signingDate}`);
console.log(`Reason: ${result.reason}`);
}SignatureVerificationResult fields:
| Field | Type | Description |
|---|---|---|
fieldName |
string |
The /T entry of the signature field |
signedBy |
string |
Subject CN from the certificate |
valid |
boolean |
integrityValid && certificateValid |
integrityValid |
boolean |
ByteRange hash matches the PKCS#7 message digest |
certificateValid |
boolean |
RSA/ECDSA signature math is valid |
signingDate |
Date | undefined |
Signing time from /M entry |
reason |
string | undefined |
Signing reason from /Reason entry |
Limitations: Certificate chain validation (CA trust) and CRL/OCSP
revocation checking are not performed. The Web Crypto API does not expose
chain validation APIs. Only the mathematical validity of the signature is
checked against the certificate embedded in the PKCS#7 SignedData.
hashAlgorithm |
Web Crypto algorithm | OID |
|---|---|---|
'SHA-256' (default) |
SHA-256 |
2.16.840.1.101.3.4.2.1 |
'SHA-384' |
SHA-384 |
2.16.840.1.101.3.4.2.2 |
'SHA-512' |
SHA-512 |
2.16.840.1.101.3.4.2.3 |
SHA-1 is not supported.
When a timestampUrl is provided, the signer sends the hash to the TSA
(Time Stamp Authority) using the RFC 3161 protocol and embeds the response
token as an unsigned attribute in the PKCS#7 SignerInfo. This proves that
the signature existed at a particular point in time even after the signing
certificate expires.
Redaction permanently removes sensitive content from a PDF. modern-pdf-lib
implements a visual redaction: an opaque rectangle is drawn over the target
region and the content-stream operators underneath are removed.
import { markForRedaction, applyRedactions } from 'modern-pdf-lib';
const doc = await loadPdf(originalBytes);
const page = doc.getPage(0);
// Step 1: Mark regions
markForRedaction(page, {
rect: [100, 600, 300, 630], // [x, y, width, height] in points
overlayText: 'REDACTED', // optional overlay label
color: { r: 0, g: 0, b: 0 }, // fill colour (default: black)
});
// Step 2: Apply — draws rectangles and removes underlying content
applyRedactions(doc);
const cleanBytes = await doc.save();RedactionOptions:
interface RedactionOptions {
/** Rectangle to redact: [x, y, width, height] in points. */
rect: [number, number, number, number];
/** Optional text to draw on top of the black rectangle. */
overlayText?: string;
/** Fill colour. Default: { r: 0, g: 0, b: 0 } (black). */
color?: { r: number; g: number; b: number };
}- Always use the two-step process (
markForRedaction+applyRedactions). Saving aftermarkForRedactiononly produces a PDF with redaction annotation marks — the underlying content is still present. - Redacted PDFs should be verified with a PDF viewer before distribution. Content inside embedded images is not currently removed by the content-stream processor; a separate image cropping step is needed if images contain sensitive data.
- Do not rely on visual redaction alone (overlaying a white rectangle) — the content stream operators remain readable in such files.