Encryption & Permissions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

A PDF can be protected in two different ways: it can require a secret to be opened at all, or it can open for everybody while asking the viewer to refrain from printing, copying or editing. Both are implemented by the same mechanism, the encryption dictionary in the file trailer (see How a PDF File Works), and both are configured in iText 9 through WriterProperties when the file is written and through ReaderProperties when it is read. This page shows how to encrypt the Bookshelf Billing invoice with passwords and with a recipient’s certificate, which algorithm to choose, how to open and decrypt a protected file, how to change its protection, and how far the permission flags can be trusted (not very far). Versions are stated as of 2026-10-09: iText Core 9.8.0 on Java 21. Every example on this page was compiled and run against exactly that version, and the resulting files were inspected for the encryption dictionary they contain.

The Spring Boot plumbing that returns the bytes to a client is in Getting Started with Spring Boot. How the layout Document and the PdfDocument underneath it are opened and closed is in Document Lifecycle, and opening an existing file in stamping or append mode is in Manipulating Existing PDFs.

What changed since RC4 and the old PdfStamper

Books written for iText 1 and 2, including the 2006 text iText in Action that was consulted for the concept order of this page and is not reproduced, protect a document with PdfWriter.setEncryption(...) or PdfStamper.setEncryption(...) and show the 40-bit and 128-bit RC4 ciphers as the normal choice, because Acrobat 3 to 5 could not read anything stronger. That is outdated in two ways (finding O4 of this section’s source analysis). First, RC4 is considered broken: the iText Knowledge Base article How does iText handle PDF encryption? states that the PDF specification deprecated it as of PDF 1.7, and that iText keeps STANDARD_ENCRYPTION_40 and STANDARD_ENCRYPTION_128 only for backwards compatibility and strongly advises against them. Second, the API moved. In iText 7 and later the whole protection is described on the writer: a WriterProperties object is built, passed to the PdfWriter constructor, and the PdfWriter is passed to the PdfDocument. There is no PdfStamper; encrypting an existing file means opening it with a reader and a writer that carries the new properties (see the section on changing the protection below).

The current approach is AES-256, selected with EncryptionConstants.ENCRYPTION_AES_256, written into a file that is declared as PDF 2.0. In iText 9 you can go one step further and use AES in Galois/Counter Mode, EncryptionConstants.ENCRYPTION_AES_GCM, which was specified in ISO/TS 32003 as an extension of PDF 2.0, together with the optional MAC integrity protection of ISO/TS 32004. Both are described below, with the interoperability caveat that goes with such young specifications. The licence is also different from the old library: iText 7 and later is released under the AGPLv3 or under a commercial licence, while iText 1 and 2 used the MPL and LGPL, which matters if you are porting code. The LGPL and MPL fork of the old API is OpenPDF, which keeps the old setEncryption style.

The examples use Java 21 idioms where they make the code shorter: try-with-resources for every PdfDocument, Document and PdfReader, a record with a compact constructor for validated settings, and Set.copyOf for an immutable set of permissions.

Two passwords and a set of permission flags

The standard security handler knows two passwords, and understanding them avoids most of the mistakes made with PDF protection.

  • The user password (also called the document open password) is what a viewer asks for before it shows anything. Whoever knows it can open the file, but only with the permissions that were granted.

  • The owner password identifies the creator of the file. Whoever supplies it opens the file with all permissions, can change them and can remove the encryption. The owner password is a master key, not a second user password.

  • The permission flags are a 32-bit integer stored in the encryption dictionary. Each bit grants one right to the user: printing, high-quality printing, modifying contents, copying text and graphics, modifying annotations, filling in forms, extracting text for accessibility, and assembling the document (inserting, rotating or deleting pages).

Two combinations are useful in practice. A confidential document has a non-empty user password. A restricted document has an empty user password and a non-empty owner password: any viewer opens it without a prompt, and the permissions are what the viewer is asked to respect. Passwords are limited to 127 UTF-8 bytes in AES-256; older algorithms allowed 32 Latin-1 characters only, according to the iText Knowledge Base.

iText exposes the permission bits as constants of EncryptionConstants, which are combined with the bitwise or operator:

Constant Meaning

ALLOW_PRINTING

Print the document, at full quality.

ALLOW_DEGRADED_PRINTING

Print at low resolution only.

ALLOW_MODIFY_CONTENTS

Modify the page content.

ALLOW_COPY

Copy or extract text and graphics.

ALLOW_MODIFY_ANNOTATIONS

Add or change annotations and fill in form fields.

ALLOW_FILL_IN

Fill in existing form fields, even when annotations may not be modified.

ALLOW_SCREENREADERS

Extract text for accessibility. Leave it set on every document that people with disabilities may read, and on every PDF/UA document, as the accessibility standards expect.

ALLOW_ASSEMBLY

Insert, delete and rotate pages and create bookmarks and thumbnails.

In the table, a constant that is not set means that the right is denied. Passing 0 therefore denies everything, and the iText Knowledge Base uses exactly that to create a fully locked file in its MAC example.

Encrypting with passwords: AES-256

The first example encrypts the invoice with both passwords and grants printing and screen-reader access. It needs two Maven artifacts on the classpath: iText Core as a POM aggregator, and exactly one Bouncy Castle adapter (the adapter is required for encryption and signing, and is restated here so the coordinates are complete on this page). iText is licensed under the AGPLv3 or under a commercial licence. The tested Bouncy Castle version pulled in by the adapter is 1.86.

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>9.8.0</version>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>9.8.0</version>
</dependency>

The passwords come from environment variables, which in a real service are populated from the secret store of the platform (see Secrets Manager and KMS, Key Vault and Secrets or Secret Manager and KMS). The values in this page’s commands are obvious placeholders and the code never contains a password literal.

package com.example.billing;

import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class InvoiceEncryptor {

    public static byte[] encryptedInvoice(String userPassword, String ownerPassword) throws IOException {
        int permissions = EncryptionConstants.ALLOW_PRINTING
                | EncryptionConstants.ALLOW_SCREENREADERS;
        int algorithm = EncryptionConstants.ENCRYPTION_AES_256;

        WriterProperties properties = new WriterProperties()
                .setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(
                        userPassword.getBytes(StandardCharsets.UTF_8),
                        ownerPassword.getBytes(StandardCharsets.UTF_8),
                        permissions,
                        algorithm);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (PdfDocument pdf = new PdfDocument(new PdfWriter(out, properties));
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Bookshelf Billing - invoice INV-2026-0001"));
            document.add(new Paragraph("Total: 42.50 EUR"));
        }
        return out.toByteArray();
    }

    public static void main(String[] args) throws IOException {
        String user = requiredEnv("BILLING_PDF_USER_PASSWORD");
        String owner = requiredEnv("BILLING_PDF_OWNER_PASSWORD");
        Path target = Path.of(args.length > 0 ? args[0] : "invoice-encrypted.pdf");
        Files.write(target, encryptedInvoice(user, owner));
        System.out.println("Wrote " + target);
    }

    static String requiredEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException("Environment variable " + name + " is not set");
        }
        return value;
    }
}

Run it with the two placeholder variables exported, for example BILLING_PDF_USER_PASSWORD=user-placeholder and BILLING_PDF_OWNER_PASSWORD=owner-placeholder. Four details are worth noticing.

  • setStandardEncryption(userPassword, ownerPassword, permissions, algorithm) takes the passwords as byte[]. UTF-8 is the encoding that AES-256 uses, so encode explicitly instead of relying on the platform default charset.

  • The call to setPdfVersion(PdfVersion.PDF_2_0) is not decoration. Inspecting the output of iText 9.8.0 shows that ENCRYPTION_AES_256 in a file declared as PDF 1.7 writes revision 5 of the security handler, the Adobe Extension Level 3 variant. The iText Knowledge Base describes that variant as AES-256 with shortcomings in password handling, deprecated in PDF 2.0. In a PDF 2.0 file the same constant writes revision 6 (/R 6, /V 5), the variant with improved password handling that ISO 32000-2 standardises. Declare PDF 2.0 whenever you choose AES-256, unless you have a consumer that cannot read it.

  • The permission value is combined with the bitwise or. Using + also works for distinct bits but does not document the intent as well.

  • The writer closes the underlying stream when the PdfDocument is closed, which here is the end of the try-with-resources block. The bytes are only complete after that point, so the toByteArray() call is placed after the block.

The other algorithm constants are ENCRYPTION_AES_128, which writes the PDF 1.6 AESV2 handler and which the Knowledge Base recommends replacing with AES-256, and the RC4 constants mentioned above. Additional flags can be added to the algorithm argument with the or operator: DO_NOT_ENCRYPT_METADATA leaves the XMP metadata stream readable so that search indexers can classify an otherwise protected file, and EMBEDDED_FILES_ONLY encrypts only the embedded files. Both come from the same EncryptionConstants class and are listed in its API documentation. Remember that the PDF/A parts forbid encryption altogether, see PDF Standards & Conformance.

This example derives from the Knowledge Base articles Encrypting / Decrypting PDFs and How does iText handle PDF encryption?, and from the API pages for WriterProperties and EncryptionConstants. Note that the Knowledge Base pages show the older getBytes() form without a charset, and that the PDF version is not set in some of their snippets; the form above is the one that was compiled and inspected against 9.8.0.

Choosing the algorithm

The following table summarises the choices that iText 9.8.0 offers, together with the PDF version in which each appears, following the overview in the iText Knowledge Base article on AES-GCM.

Constant Cipher PDF version Recommendation

STANDARD_ENCRYPTION_40

RC4, 40-bit

1.1 to 1.3

Broken. Do not use.

STANDARD_ENCRYPTION_128

RC4, 128-bit

1.4 and 1.5

Broken. Do not use.

ENCRYPTION_AES_128

AES-128-CBC

1.6 and 1.7

Acceptable for old consumers only.

ENCRYPTION_AES_256

AES-256-CBC

1.7 Extension Level 3 (weak password hashing), 1.7 Extension Level 8 and 2.0

Default choice, in a PDF 2.0 file.

ENCRYPTION_AES_GCM

AES-256-GCM

2.0 with ISO/TS 32003

Most secure; check that your readers support it.

Interoperability decides the last two rows. The Knowledge Base notes that many PDF consumers, Acrobat among them, did not support GCM at the time of writing. The practical rule is to use AES-256 in PDF 2.0 when files go to people, and AES-GCM when both ends are under your control, for example between two services of the same platform or into an archive that only your own tools read. Whichever you choose, test the file with the viewers your recipients really use.

AES-GCM and the PDF MAC

AES-GCM is an authenticated cipher: besides hiding the content it detects modifications of each encrypted object, and it is faster than the CBC mode. In iText 9 it is requested exactly like the other algorithms, with EncryptionConstants.ENCRYPTION_AES_GCM, and it needs a PDF 2.0 writer. The example below adds the second feature of PDF 2.0 that is related to encryption, the message authentication code, specified in ISO/TS 32004. A MAC protects the integrity of the whole file: it is computed with a key derived from the encryption key, so it can only be created and verified by somebody who can decrypt the document, and it does not need certificates or a trust chain the way a digital signature does. It complements signatures and does not replace them (a signature identifies the signer and is verifiable by anyone; a MAC is a shared-secret check). MacProperties selects the digest, with SHA_256, SHA_384, SHA_512 and the three SHA-3 variants available in 9.8.0, and its three-argument constructor also takes the MAC algorithm (HMAC_WITH_SHA_256) and the key wrapping algorithm (AES_256_NO_PADD).

package com.example.billing;

import com.itextpdf.kernel.mac.MacProperties;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class GcmInvoice {

    public static void main(String[] args) throws IOException {
        byte[] user = System.getenv("BILLING_PDF_USER_PASSWORD").getBytes(StandardCharsets.UTF_8);
        byte[] owner = System.getenv("BILLING_PDF_OWNER_PASSWORD").getBytes(StandardCharsets.UTF_8);

        WriterProperties properties = new WriterProperties()
                .setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(user, owner,
                        EncryptionConstants.ALLOW_PRINTING,
                        EncryptionConstants.ENCRYPTION_AES_GCM,
                        new MacProperties(MacProperties.MacDigestAlgorithm.SHA_256));

        try (PdfDocument pdf = new PdfDocument(new PdfWriter("invoice-gcm.pdf", properties));
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Bookshelf Billing - AES-GCM invoice with PDF MAC"));
        }

        try (PdfReader reader = new PdfReader("invoice-gcm.pdf",
                new ReaderProperties().setPassword(user));
             PdfDocument pdf = new PdfDocument(reader)) {
            System.out.println("crypto mode " + reader.getCryptoMode() + ", version " + pdf.getPdfVersion());
        }
    }
}

The file written by this class declares %PDF-2.0, uses crypt filter method AESV4 and contains a MAC dictionary in the /MAC entry with the location Standalone. The Standalone form is the one iText writes for an encrypted file; a MAC can also be embedded in a signature container, a use that belongs to signing and is outside this page. When the reader opens the file, getCryptoMode() reports 4, the value of ENCRYPTION_AES_GCM.

The setStandardEncryption overload without MacProperties also accepts ENCRYPTION_AES_GCM; in a test with 9.8.0 the resulting file still contained a standalone MAC, so a GCM file written by iText carries one with or without the extra argument, whereas an AES-256 file written without MacProperties has none. A matching setPublicKeyEncryption overload takes MacProperties for certificate encryption. The kernel contains a MacValidationException for the case where the check fails when a document is read.

This example derives from the Knowledge Base pages AES-GCM Encryption Support and Support for Message Authentication Code (MAC) integrity protection, and from the API page for MacProperties. The Knowledge Base snippets register the Bouncy Castle provider with Security.addProvider(...) before writing; the example above ran without that call with the bouncy-castle-adapter on the classpath, but if you hit a missing-provider error in your own environment, that line is the first thing to add.

Reading, inspecting and decrypting

Opening an encrypted file needs the same two classes in reverse: a ReaderProperties carrying the password, passed to the PdfReader. The reader works out for itself whether the password was the user or the owner one, and reports the result:

  • isEncrypted() is true when the file has an encryption dictionary;

  • isOpenedWithFullPermission() is true when the owner password (or the recipient’s private key) was supplied;

  • getPermissions() returns the permission integer from the file, whichever password opened it;

  • getCryptoMode() returns the algorithm as one of the EncryptionConstants values; and

  • a wrong or missing password throws BadPasswordException, whose message names ReaderProperties#setPassword().

package com.example.billing;

import com.itextpdf.kernel.exceptions.BadPasswordException;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;

public class InvoiceReader {

    public static void inspect(Path file, String password) throws IOException {
        ReaderProperties properties = new ReaderProperties()
                .setPassword(password.getBytes(StandardCharsets.UTF_8));
        try (PdfReader reader = new PdfReader(file.toString(), properties);
             PdfDocument pdf = new PdfDocument(reader)) {
            long permissions = reader.getPermissions();
            System.out.println("encrypted:        " + reader.isEncrypted());
            System.out.println("full permission:  " + reader.isOpenedWithFullPermission());
            System.out.println("crypto mode:      " + reader.getCryptoMode());
            System.out.println("may print:        " + ((permissions & EncryptionConstants.ALLOW_PRINTING) == EncryptionConstants.ALLOW_PRINTING));
            System.out.println("may copy text:    " + ((permissions & EncryptionConstants.ALLOW_COPY) != 0));
            System.out.println("pages:            " + pdf.getNumberOfPages());
        } catch (BadPasswordException e) {
            System.out.println("Wrong password: " + e.getMessage());
        }
    }

    public static void decrypt(Path encrypted, String ownerPassword, Path plain) throws IOException {
        ReaderProperties properties = new ReaderProperties()
                .setPassword(ownerPassword.getBytes(StandardCharsets.UTF_8));
        try (PdfReader reader = new PdfReader(encrypted.toString(), properties);
             PdfDocument pdf = new PdfDocument(reader, new PdfWriter(plain.toString()))) {
            // Writing through a PdfWriter without encryption properties removes the encryption.
        }
    }

    public static void main(String[] args) throws IOException {
        Path file = Path.of(args[0]);
        inspect(file, System.getenv("BILLING_PDF_USER_PASSWORD"));
        inspect(file, System.getenv("BILLING_PDF_OWNER_PASSWORD"));
        inspect(file, "wrong");
        decrypt(file, System.getenv("BILLING_PDF_OWNER_PASSWORD"), Path.of("invoice-plain.pdf"));
        try (PdfReader r = new PdfReader("invoice-plain.pdf"); PdfDocument d = new PdfDocument(r)) {
            System.out.println("plain encrypted: " + r.isEncrypted());
        }
    }
}

The output of the first call, with the user password, is full permission: false and may copy text: false: the user may print but not copy. The second call, with the owner password, reports full permission, while getPermissions() still shows the stored flags. The third call, with a deliberately wrong password, prints the exception message instead of throwing, and the last step shows that decrypt produced a file with isEncrypted() equal to false.

How does decryption work? iText has no decrypt method. A document opened with a reader that has the owner password and written with a PdfWriter that carries no encryption properties is written without encryption. The same class therefore removes the protection of a file that you own, which is exactly the capability that makes the owner password a master key. The opposite case, rewriting an encrypted file in full (not in append mode) while keeping its existing encryption, is what new StampingProperties().preserveEncryption() passed to the PdfDocument constructor is for, as described in the StampingProperties API. It also explains why the Knowledge Base article describes encryption and decryption together: they are two configurations of the same reader and writer pair.

This example derives from the Knowledge Base article Encrypting / Decrypting PDFs and the API pages for ReaderProperties and PdfReader.

Changing or rotating the protection

Two operations are common in a billing back office: editing a protected file without breaking its protection, and rotating passwords when a secret has leaked or an employee has left. Both are variants of the stamping mode described in Manipulating Existing PDFs. In append mode the file is extended by an incremental update and keeps its encryption dictionary, and with new WriterProperties the file is rewritten with the new settings.

package com.example.billing;

import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.kernel.pdf.WriterProperties;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ChangeProtection {

    public static void main(String[] args) throws IOException {
        byte[] owner = System.getenv("BILLING_PDF_OWNER_PASSWORD").getBytes(StandardCharsets.UTF_8);
        byte[] newUser = System.getenv("BILLING_PDF_NEW_USER_PASSWORD").getBytes(StandardCharsets.UTF_8);
        byte[] newOwner = System.getenv("BILLING_PDF_NEW_OWNER_PASSWORD").getBytes(StandardCharsets.UTF_8);

        // 1. Edit the encrypted file in append mode: the existing protection is kept.
        try (PdfReader reader = new PdfReader("invoice-encrypted.pdf", new ReaderProperties().setPassword(owner));
             PdfDocument pdf = new PdfDocument(reader, new PdfWriter("invoice-updated.pdf"),
                     new StampingProperties().useAppendMode())) {
            pdf.getDocumentInfo().setTitle("Invoice INV-2026-0001 (updated)");
        }

        // 2. Re-protect with new passwords: new WriterProperties plus the old owner password to read.
        WriterProperties rotated = new WriterProperties()
                .setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(newUser, newOwner,
                        EncryptionConstants.ALLOW_PRINTING, EncryptionConstants.ENCRYPTION_AES_256);
        try (PdfReader reader = new PdfReader("invoice-encrypted.pdf", new ReaderProperties().setPassword(owner));
             PdfDocument pdf = new PdfDocument(reader, new PdfWriter("invoice-rotated.pdf", rotated))) {
            // Opening and closing rewrites the file with the new protection.
        }
    }
}

Both variants were run against the file produced by the first example: the appended file reports /R 6 and opens with the original user password, and the rotated file opens only with the new ones. The reader must hold the owner password in both cases, because writing needs the keys that the owner password unlocks. Two warnings apply. A file that has been distributed cannot be recalled: rotating passwords protects new copies only, so the passwords that unlock old copies must be treated as compromised for those copies. And changing a file that carries a digital signature invalidates the signature unless the change is made in append mode and is allowed by the signature’s certification level, a subject covered in the signature pages of this section.

A policy object with a record

Real services protect many kinds of documents with different rules: invoices are printable, internal reports are not. Collecting the rules in a validated value object keeps the encryption call out of the business code. The record below validates its input in the compact constructor, copies the permission set to an immutable one and derives the bit mask and the WriterProperties from it. The main method also demonstrates a restricted-only file.

package com.example.billing;

import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.Set;

public class EncryptionPolicy {

    public enum Permission {
        PRINT(EncryptionConstants.ALLOW_PRINTING),
        COPY(EncryptionConstants.ALLOW_COPY),
        SCREEN_READER(EncryptionConstants.ALLOW_SCREENREADERS),
        FILL_IN(EncryptionConstants.ALLOW_FILL_IN);

        private final int flag;

        Permission(int flag) {
            this.flag = flag;
        }

        int flag() {
            return flag;
        }
    }

    public record Settings(String userPassword, String ownerPassword, Set<Permission> allowed) {
        public Settings {
            if (ownerPassword == null || ownerPassword.isBlank()) {
                throw new IllegalArgumentException("An owner password is required");
            }
            userPassword = userPassword == null ? "" : userPassword;
            allowed = Set.copyOf(allowed);
        }

        int permissionMask() {
            return allowed.stream().mapToInt(Permission::flag).reduce(0, (a, b) -> a | b);
        }

        WriterProperties toWriterProperties() {
            return new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0).setStandardEncryption(
                    userPassword.getBytes(StandardCharsets.UTF_8),
                    ownerPassword.getBytes(StandardCharsets.UTF_8),
                    permissionMask(),
                    EncryptionConstants.ENCRYPTION_AES_256);
        }
    }

    public static void main(String[] args) throws IOException {
        // Restrictions only: empty user password, so any viewer opens it without a prompt.
        Settings restrictedOnly = new Settings("", System.getenv("BILLING_PDF_OWNER_PASSWORD"),
                EnumSet.of(Permission.PRINT, Permission.SCREEN_READER));

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (PdfDocument pdf = new PdfDocument(new PdfWriter(out, restrictedOnly.toWriterProperties()));
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Restricted but not secret"));
        }

        try (PdfReader reader = new PdfReader(new ByteArrayInputStream(out.toByteArray()));
             PdfDocument pdf = new PdfDocument(reader)) {
            System.out.println("opened without a password, full permission = " + reader.isOpenedWithFullPermission());
        }

        try (PdfReader reader = new PdfReader(new ByteArrayInputStream(out.toByteArray()))
                .setUnethicalReading(true);
             PdfDocument pdf = new PdfDocument(reader)) {
            System.out.println("unethical reading, full permission = " + reader.isOpenedWithFullPermission());
        }
    }
}

The output is opened without a password, full permission = false, which shows that the restricted-only file opened without any password and that its permissions were applied, followed by unethical reading, full permission = true, which leads to the next section.

Why permissions are advisory

The permission flags are not a security mechanism, and an honest design must not treat them as one. There are three reasons, all visible in the example above.

  1. The cipher protects the bytes, not the policy. In a restricted-only file the content is encrypted with a key that is derived from the empty user password, and everybody knows it. A viewer that opens the file has the key and therefore can do whatever it likes with the content; honouring the flags is a courtesy of the viewer.

  2. Libraries can ignore the flags. iText itself will do it on request: PdfReader#setUnethicalReading(true), whose name documents the intent, makes the reader behave as if it had been opened with full permission, as the last line of the previous example shows. Other libraries and command line tools offer the same, and a printer-driver or a screenshot bypasses the flags without any PDF library at all.

  3. An open document can be re-saved. Copying is only one of the rights. Once a viewer shows the text, nothing technical prevents a person from retyping it or from photographing the screen.

What the flags do achieve is to make the intent explicit and to stop casual use, for example the copy button in a mainstream viewer. What actually protects confidentiality is the user password (or a recipient certificate) combined with a strong algorithm and a long, random password, and what protects authenticity is a digital signature. Use the permission flags to express policy, never to enforce it, and never put a secret in a restricted-only file in the belief that "no copy" keeps it safe. The same holds for redaction: covering text with a black rectangle, or denying the copy right, does not remove the text; real removal needs a redaction tool, such as the pdfSweep add-on mentioned in the add-ons page of this section.

A related practical point is that a protected file can no longer be inspected by the tools that support operations: text extraction, search indexing and conversion all need the password. If a file must be stored in an archive for decades, protect it with the storage layer (encryption at rest, object-store keys) instead of in the file, because PDF/A forbids encryption in the file itself, as the standards page explains.

Certificate (public-key) encryption

Passwords have to be shared with the people who read the file. Public-key encryption avoids that: the file is encrypted for one or more recipients, identified by their X.509 certificates, and each recipient opens it with the private key that belongs to its certificate. The sender needs only the public certificates, and nobody has to transmit a secret. Permissions are granted per recipient, so the writer receives three parallel arguments: an array of certificates, an array of permission masks of the same length, and the algorithm.

The example uses a throwaway test certificate generated on this page with keytool, a tool that ships with the JDK. The command creates a PKCS#12 keystore with a self-signed RSA key pair valid for 30 days. It is a test fixture: the subject names are invented, the store password is typed interactively and kept only in an environment variable, and the file must never be committed or reused for anything else.

read -rs -p 'Test keystore password: ' BILLING_KEYSTORE_PASSWORD && export BILLING_KEYSTORE_PASSWORD

keytool -genkeypair -alias billing-test -keyalg RSA -keysize 3072 -sigalg SHA256withRSA \
  -validity 30 -dname "CN=Bookshelf Billing Test Recipient,O=Bookshelf Test,C=ES" \
  -keystore billing-test.p12 -storetype PKCS12 -storepass "$BILLING_KEYSTORE_PASSWORD"

In production the recipient’s certificate would come from the recipient (a public file) and the private key would stay in a hardware token, a keystore of the recipient’s organisation, or a key management service. The sender never needs the private key. The class below plays both roles for the sake of the demonstration: it reads the certificate from the keystore to encrypt, and then reads the private key to prove that the file can be opened.

package com.example.billing;

import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
import com.itextpdf.commons.bouncycastle.IBouncyCastleFactory;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.Certificate;

public class CertificateInvoice {

    private static final IBouncyCastleFactory BC_FACTORY = BouncyCastleFactoryCreator.getFactory();

    public static void main(String[] args) throws IOException, GeneralSecurityException {
        char[] storePassword = System.getenv("BILLING_KEYSTORE_PASSWORD").toCharArray();
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (InputStream in = Files.newInputStream(Path.of("billing-test.p12"))) {
            keyStore.load(in, storePassword);
        }
        Certificate recipient = keyStore.getCertificate("billing-test");
        PrivateKey privateKey = (PrivateKey) keyStore.getKey("billing-test", storePassword);

        WriterProperties properties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0).setPublicKeyEncryption(
                new Certificate[] {recipient},
                new int[] {EncryptionConstants.ALLOW_PRINTING},
                EncryptionConstants.ENCRYPTION_AES_256);

        try (PdfDocument pdf = new PdfDocument(new PdfWriter("invoice-recipient.pdf", properties));
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Bookshelf Billing - invoice for a certificate holder"));
        }

        Provider provider = BC_FACTORY.getProvider();
        ReaderProperties readerProperties = new ReaderProperties()
                .setPublicKeySecurityParams(recipient, privateKey, provider.getName(), null);
        try (PdfReader reader = new PdfReader("invoice-recipient.pdf", readerProperties);
             PdfDocument pdf = new PdfDocument(reader)) {
            System.out.println("opened, pages=" + pdf.getNumberOfPages()
                    + ", full=" + reader.isOpenedWithFullPermission());
        }
    }
}

The class works in four steps.

  1. It loads the keystore and takes the certificate and the private key under the alias.

  2. setPublicKeyEncryption(certificates, permissions, algorithm) takes three arguments: an array of recipients' Certificate objects, an int[] of the same length with one permission mask per recipient, and the algorithm. With two recipients you could grant one of them printing and the other nothing.

  3. The output declares PDF 2.0 and contains the Adobe.PubSec filter instead of Standard, with one CMS recipient entry per certificate, which is how the inspected file looks.

  4. To open it, ReaderProperties#setPublicKeySecurityParams gets the certificate, the private key and the name of the security provider that can perform the RSA decryption (Bouncy Castle here, taken from the factory that the connector creates). The last argument is an IExternalDecryptionProcess: pass null when the key is in memory, or an implementation that delegates the decryption to a smart card, a token or a key management service when it is not.

A reader opened with a private key always reports isOpenedWithFullPermission() as true, whatever the recipient’s permission mask says, so the example prints full=true even though the mask grants only printing: in iText 9 the public-key security handler has no owner/user distinction to test. Permissions in public-key encryption are as advisory as in the password case, as explained in the section above. In a real PKI the recipient’s certificate must also be suitable for key encipherment. The sender should check the certificate chain, the validity dates and the revocation status of a recipient before encrypting for it, which is the same trust-validation work that signature verification needs.

This example derives from the Knowledge Base article Certificate encryption, which links to the iText sandbox example EncryptWithCertificate; the API pages are WriterProperties and ReaderProperties. The keytool options are listed in the JDK 21 keytool reference.

Returning an encrypted PDF from Spring Boot

A Spring Boot 4 service reads the passwords from configuration, which in turn comes from the environment, a Kubernetes secret or a secret manager, as described in Spring Boot configuration and profiles. A @ConfigurationProperties record keeps the two secrets out of the controller, and the endpoint only adds Content-Disposition so that the browser saves the file. The record binds the properties billing.pdf.user-password and billing.pdf.owner-password, which the deployment fills from BILLING_PDF_USER_PASSWORD and BILLING_PDF_OWNER_PASSWORD. The class below reuses the InvoiceEncryptor from the first example and was compiled against Spring Framework 7.0.9 and Spring Boot 4.1.1; register the record with @ConfigurationPropertiesScan or @EnableConfigurationProperties.

package com.example.billing;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@ConfigurationProperties(prefix = "billing.pdf")
record PdfSecurityProperties(String userPassword, String ownerPassword) { }

@RestController
class EncryptedInvoiceController {

    private final PdfSecurityProperties security;

    EncryptedInvoiceController(PdfSecurityProperties security) {
        this.security = security;
    }

    @GetMapping("/invoices/{number}.pdf")
    ResponseEntity<byte[]> invoice(@PathVariable String number) throws IOException {
        byte[] pdf = InvoiceEncryptor.encryptedInvoice(security.userPassword(), security.ownerPassword());
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_PDF)
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        ContentDisposition.attachment().filename(number + ".pdf").build().toString())
                .body(pdf);
    }
}

The user password is the one the recipient needs. If the invoice is mailed to the customer, send the password through a different channel from the file, or use certificate encryption so that no password is transmitted at all. If the file is only meant to be stored, think about whether the storage layer is the better place for the protection. The streaming and Content-Disposition details are in Getting Started with Spring Boot.

Operational advice

  • Never log passwords or keys, and never put them in the PDF’s metadata or file name. Do not build the user password from customer data that others know, such as a tax number or a birth date; a customer-specific secret that is easy to guess is only a nuisance for an attacker.

  • Use long random passwords for the owner password and keep it in the secret store. It is the master key for every document that was encrypted with it, and losing it means that you can no longer change or decrypt those documents.

  • Mind the order with signatures. A digital signature covers the bytes of the file, so decide early whether a document is signed, encrypted or both, and test the combination you ship, because changing a signed file invalidates the signature. The signature pages of this section explain how to sign.

  • Know what you are protecting from. Encryption in the file protects against someone who obtains the file. It does not protect against the recipient, who can always read the content, nor against a compromised service that holds the password. Pair it with transport security and access control on the endpoint.

  • Test with the viewers of your recipients. Open the output in Acrobat, a browser viewer and a mobile viewer. This is the only way to learn whether PDF 2.0, AES-GCM or a MAC is accepted.

  • Check the licence of any add-on that reads protected files. The kernel handles encryption in iText Core, which is AGPLv3 or commercial. The pdfSweep, pdfOCR and other add-ons are covered in the add-ons page of this section, with their own licences.

Outdated points to avoid

  1. PdfWriter.setEncryption(...) and PdfStamper.setEncryption(...) with PdfWriter.STRENGTH40BITS, STRENGTH128BITS and ALLOW_* constants of the old PdfWriter: replaced by WriterProperties.setStandardEncryption and the constants of com.itextpdf.kernel.pdf.EncryptionConstants.

  2. RC4 40 and 128 bits: still accepted for reading old files, but never write them.

  3. AES-256 declared as PDF 1.7: valid, but it writes the weaker revision 5; declare PDF 2.0.

  4. Treating the permission flags as DRM: they are a courtesy, not a barrier.

  5. Tutorials that open a protected file without telling the reader the password: the constructor new PdfReader(path) throws BadPasswordException for a file with a non-empty user password.

  6. Hard-coded password literals in the example code you copy from tutorials: read them from the environment or a secret manager.

References