Skip to main content

Encryption and Signatures

Generated PDFs can be password-protected with permissions, and the parser/editor accept passwords for protected input. Detached PKCS#7/CMS digital signatures are supported with caller-supplied signing.

Password protection

import { createDocument, parseDocument } from "@barrierbreak/a11ydocs-pdf";

const doc = createDocument({
encryption: {
algorithm: "aes-256",
userPassword: "reader",
ownerPassword: "owner-secret",
permissions: {
printing: false,
modifying: false,
copying: false,
annotating: false
}
}
});

const bytes = doc.toUint8Array();
const parsed = parseDocument(bytes, { password: "reader" });

Algorithms: "rc4-40" (legacy compatibility), "rc4-128" (default), "aes-128", "aes-256", "aes-256-r6" (ISO 32000-2 / PDF 2.0 hardened key derivation, recommended for new documents).

Browser support

All algorithms work in the browser. When Node's crypto module is unavailable, the library transparently uses a built-in, zero-dependency AES (CBC) and SHA-2 implementation, so AES encryption no longer requires a Node runtime. Output is byte-for-byte compatible with the Node path, so a document encrypted in the browser opens with the same password anywhere.

// Runs identically in Node and the browser.
const doc = createDocument({
encryption: { algorithm: "aes-256-r6", userPassword: "reader", ownerPassword: "owner-secret" }
});
doc.addPage({ size: "A4" }).text("Confidential", { x: 48, y: 780 });
const bytes = doc.toUint8Array();

// Browser download:
const blob = new Blob([bytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);

The only browser requirement is a secure-random source for the AES IV and document key. Modern browsers provide crypto.getRandomValues; the library uses it automatically. The pure-JS AES/SHA-2 fallback is not constant-time — AES PDF encryption is a deterrent, not a strong confidentiality boundary, regardless of runtime.

Metadata-Only Encryption

Encrypt document content but leave metadata (title, author, XMP) in plaintext so search engines and indexers can read it:

const doc = createDocument({
encryption: {
algorithm: "aes-256",
userPassword: "reader",
ownerPassword: "owner-secret",
encryptMetadata: false
}
});

Permissions

FlagMeaning
printingAllow printing
modifyingAllow document modification
copyingAllow text/image copy
annotatingAllow annotation creation
fillingFormsAllow form fill
accessibilityAllow accessibility extraction
assemblingAllow page assembly
highQualityPrintingAllow full-resolution print

Omit permissions to grant all rights.

Complete Permission Bits

All available permission flags used in full:

permissions: {
printing: true,
highQualityPrinting: false,
modifying: true,
copying: true,
annotating: true,
fillingForms: true,
accessibility: true,
assembling: false
}

Cross-Version Encryption Upgrade

Call setEncryption() on an editable document to upgrade legacy RC4 encryption to AES:

const editable = editDocument(rc4Bytes, { password: "reader" });
editable.setEncryption({ algorithm: "aes-256", userPassword: "reader" });
const upgraded = editable.toUint8Array();

Empty Password

An empty string "" is a valid user password—useful when you only want an owner password to restrict permissions:

encryption: {
algorithm: "aes-256",
userPassword: "",
ownerPassword: "admin-secret"
}

Readers can open the PDF without entering a password, but permissions are enforced.

Certificate-Based Encryption

Encrypt for specific recipients using X.509 certificates:

import { readFileSync } from "node:fs";

const cert = readFileSync("recipient.pem");
const doc = createDocument({
encryption: doc.encryptForRecipients([cert], {
permissions: { printing: true, modifying: false }
})
});

Default Permissions for Missing /P

PDFs missing the /P permissions entry are handled gracefully—the library defaults to granting all rights when no permissions dictionary is present.

Editing protected PDFs

editDocument accepts { password } and re-encrypts with the original parameters when serializing:

const editable = editDocument(protectedBytes, { password: "reader" });
editable.updateInfo({ title: "Revised" });
const updated = editable.toUint8Array();

Detached signatures

Generate a /ByteRange and let your signing code produce the CMS blob:

const signed = createDocument({
signature: {
fieldName: "approval",
reason: "Approved for release",
location: "Berlin",
contactInfo: "ops@example.com",
sign: (byteRange) => signDetachedCms(byteRange)
}
});

signed.addPage().text("Print proof", { x: 56, y: 760 });
const bytes = signed.toUint8Array();

sign receives the bytes covered by /ByteRange and must return a DER-encoded CMS SignedData value. Use Node crypto, node-forge, or any PKCS#7 toolkit. The serializer pads /Contents to fit the returned blob.

For the full workflow — incremental signing of existing PDFs, a working node-forge signer, placeholderBytes sizing, visible signature fields, and verification — see Digital Signatures.