Skip to content

Security

ABCrimson edited this page Mar 1, 2026 · 5 revisions

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.


Encryption

Supported algorithms

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

Encrypting a document

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.

Permission flags

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

Decrypting a document

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.

Internal implementation

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.


Digital Signatures

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.

Signing a document (invisible)

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',
});

Signing a document (visible)

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:

  1. Reserve space — a placeholder ByteRange is written to the PDF with space reserved for the PKCS#7 signature blob (default: 8 KB).
  2. 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().
  3. Build PKCS#7 — a SignedData CMS structure is assembled containing the certificate, signing time, message digest, and the RSA signature over the signed attributes computed with SubtleCrypto.sign().
  4. Embed signature — the PKCS#7 DER bytes are hex-encoded and written into the reserved placeholder.

Verifying signatures

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.

Signature hash algorithms

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.

Timestamp embedding

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

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.

Two-step process

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 };
}

Security considerations

  • Always use the two-step process (markForRedaction + applyRedactions). Saving after markForRedaction only 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.

Clone this wiki locally