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 |
|---|---|
|
Print the document, at full quality. |
|
Print at low resolution only. |
|
Modify the page content. |
|
Copy or extract text and graphics. |
|
Add or change annotations and fill in form fields. |
|
Fill in existing form fields, even when annotations may not be modified. |
|
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. |
|
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 asbyte[]. 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 thatENCRYPTION_AES_256in 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
PdfDocumentis closed, which here is the end of thetry-with-resources block. The bytes are only complete after that point, so thetoByteArray()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 |
|---|---|---|---|
|
RC4, 40-bit |
1.1 to 1.3 |
Broken. Do not use. |
|
RC4, 128-bit |
1.4 and 1.5 |
Broken. Do not use. |
|
AES-128-CBC |
1.6 and 1.7 |
Acceptable for old consumers only. |
|
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. |
|
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 theEncryptionConstantsvalues; and -
a wrong or missing password throws
BadPasswordException, whose message namesReaderProperties#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.
-
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.
-
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. -
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.
-
It loads the keystore and takes the certificate and the private key under the alias.
-
setPublicKeyEncryption(certificates, permissions, algorithm)takes three arguments: an array of recipients'Certificateobjects, anint[]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. -
The output declares PDF 2.0 and contains the
Adobe.PubSecfilter instead ofStandard, with one CMS recipient entry per certificate, which is how the inspected file looks. -
To open it,
ReaderProperties#setPublicKeySecurityParamsgets 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 anIExternalDecryptionProcess: passnullwhen 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
-
PdfWriter.setEncryption(...)andPdfStamper.setEncryption(...)withPdfWriter.STRENGTH40BITS,STRENGTH128BITSandALLOW_*constants of the oldPdfWriter: replaced byWriterProperties.setStandardEncryptionand the constants ofcom.itextpdf.kernel.pdf.EncryptionConstants. -
RC4 40 and 128 bits: still accepted for reading old files, but never write them.
-
AES-256 declared as PDF 1.7: valid, but it writes the weaker revision 5; declare PDF 2.0.
-
Treating the permission flags as DRM: they are a courtesy, not a barrier.
-
Tutorials that open a protected file without telling the reader the password: the constructor
new PdfReader(path)throwsBadPasswordExceptionfor a file with a non-empty user password. -
Hard-coded password literals in the example code you copy from tutorials: read them from the environment or a secret manager.
References
-
iText Knowledge Base, Encrypting / Decrypting PDFs.
-
iText Knowledge Base, How does iText handle PDF encryption?.
-
iText Knowledge Base, Certificate encryption.
-
iText Knowledge Base, AES-GCM Encryption Support (ISO/TS 32003).
-
iText Knowledge Base, Support for Message Authentication Code (MAC) integrity protection (ISO/TS 32004).
-
iText Knowledge Base, Breaking changes for iText 9.
-
iText Knowledge Base, Installing iText for Java.
-
iText Java API, WriterProperties, ReaderProperties, EncryptionConstants, PdfReader and MacProperties.
-
iText licensing, AGPLv3 licence.
-
ISO 32000-2:2020, PDF 2.0, iso.org catalogue entry (the site returns an error to command line tools, open it in a browser).
-
Bouncy Castle, project site.
-
Oracle, keytool in JDK 21.
-
In this section: How a PDF File Works, PDF Standards & Conformance, Document Lifecycle, Manipulating Existing PDFs and Getting Started with Spring Boot.