Skip to main content

Encryption and permissions

A PDF can be encrypted so that it opens only with a password, and it can carry permission flags that ask a reader not to print, copy or change it. PdfPinata writes RC4-encrypted documents and reads documents encrypted with RC4 or AES. Everything on this page is in the core PdfPinata package, in namespaces PdfPinata.Pdf.Security and PdfPinata.Pdf.IO.

Use encryption to keep a document from casual readers, or to state how its author wants it used. Do not rely on it to protect secrets: see Things to know.

Two passwords

A protected PDF has two passwords, and they do different jobs:

  • The user password opens the document. A reader asks for it before it shows anything.
  • The owner password lifts the restrictions. A reader asks for it before it lets someone change the permissions.

You can set either or both. A document with only an owner password opens for anyone, and the permissions still apply. Most "protected" PDFs you meet are like this: they open without a password and refuse to print.

Protect a document

Set the passwords and permissions on document.SecuritySettings, then save. The settings are read when the document is saved, so you can set them at any point before that:

src/SampleApp/Demos/ProtectDemo.cs
var sample = new PdfDocument();
_ = sample.AddPage();
sample.SecuritySettings.DocumentSecurityLevel = PdfDocumentSecurityLevel.Encrypted128Bit;
sample.SecuritySettings.UserPassword = ReaderPassword;
sample.SecuritySettings.OwnerPassword = OwnerPassword;
sample.SecuritySettings.PermitExtractContent = false;
sample.SecuritySettings.PermitModifyDocument = false;
sample.Save(buffer, false);

Setting either password turns encryption on. If DocumentSecurityLevel is still None, it becomes Encrypted128Bit. If you set a security level but no password, the save fails.

PdfDocumentSecurityLevel has three values:

ValueMeaning
NoneNot encrypted. The default.
Encrypted40Bit40-bit RC4. Use it only for a reader that cannot open anything newer.
Encrypted128Bit128-bit RC4.

PdfPinata does not write AES encryption.

Permission flags

Each flag is a bool on PdfSecuritySettings. All eight are allowed by default.

PropertyAllows the reader to
PermitPrintprint the document
PermitFullQualityPrintprint at full resolution rather than as a low-quality draft
PermitExtractContentcopy text and graphics
PermitAccessibilityExtractContentextract text for a screen reader or other assistive tool
PermitModifyDocumentchange the content
PermitAssembleDocumentinsert, rotate or delete pages
PermitAnnotationsadd or change comments and markup
PermitFormsFillfill in form fields

Leave PermitAccessibilityExtractContent on unless you have a reason. Turning it off stops screen readers from reading the document.

Open a protected document

Pass the password to PdfReader.Open. A wrong password throws PdfReaderException. A password given for a document that is not encrypted is ignored.

Which password you give decides what you can do:

  • The owner password opens the document in any mode, including PdfDocumentOpenMode.Modify.
  • The user password opens it in ReadOnly, Import and Append mode. Opening it with PdfDocumentOpenMode.Modify throws PdfReaderException, which says the owner password is required.
src/SampleApp/Demos/ProtectDemo.cs
// The user password opens the file to be read. It does not open it to be changed:
// PdfDocumentOpenMode.Modify with it is refused, by name, which is the library
// enforcing the distinction between the two passwords rather than merely recording it.
string refusal;
try
{
buffer.Position = 0;
using var _ = PdfReader.Open(buffer, ReaderPassword, PdfDocumentOpenMode.Modify);
refusal = "Modify with the user password was allowed";
}
catch (PdfReaderException exception)
{
refusal = exception.Message;
}

buffer.Position = 0;
using var asReader = PdfReader.Open(buffer, ReaderPassword, PdfDocumentOpenMode.ReadOnly);

buffer.Position = 0;
using var asOwner = PdfReader.Open(buffer, OwnerPassword, PdfDocumentOpenMode.Modify);

PdfReader.Open(path, password) without a mode opens for Modify, so with a user password you must also pass another mode.

SecuritySettings.HasOwnerPermissions tells you which password opened the document: true for the owner password or an unencrypted document, false for the user password. PdfPinata enforces the open mode; the permission flags are yours to honour. If your program shows or processes documents for other people, check the flags and HasOwnerPermissions before you print or copy.

Ask for the password only when it is needed

If you do not know in advance whether a file is encrypted, pass a PdfPasswordProvider callback. PdfPinata calls it only when the document needs a password, which is where an application shows a password dialog. Set args.Password to try a password, or set args.Abort to true to give up, in which case Open returns null:

src/SampleApp/Demos/ProtectDemo.cs
buffer.Position = 0;
using var reopened = PdfReader.Open(buffer, PdfDocumentOpenMode.Modify,
args =>
{
timesAsked++;
args.Password = ReaderPassword;
});

If the password the callback gives is wrong, or is only the user password when you asked for Modify, PdfPinata calls it again.

Remove protection

If you have the owner password, you can save an unprotected copy:

PdfDocument document = PdfReader.Open("protected.pdf", "owner-password", PdfDocumentOpenMode.Modify);
document.Save("unprotected.pdf");

A document read from an encrypted file is saved without encryption unless you set a password again. The old passwords are not reused. To keep the file protected after changing it, set UserPassword, OwnerPassword and the permissions again before you save.

PdfPinata does not help to open a document without its password.

Things to know

  • A permission is a request, not a lock. The flags travel inside the file, and a well-behaved reader honours them. A program that has the user password can ignore them. Only the encryption itself stops someone reading the content.
  • RC4 is a broken cipher. A determined attacker can recover the content of an RC4-encrypted PDF. Treat PdfPinata's encryption as a statement to honest readers, not as protection for sensitive data. If you need strong protection, encrypt the file with another tool after PdfPinata writes it.
  • AES is read, not written. Documents encrypted by other tools with AES-128 or AES-256 open normally when you give the password.
  • PDF/A forbids encryption. A document that claims a PDF/A level and has a password will not save.
  • Saving rewrites the whole file. If you open a signed document and save it again, with or without new passwords, its signatures break. See Digital signatures.
  • The demo's passwords are public. The Protect demo's PDF opens with the user password open-me. Its owner password is owner-only. Both are printed on its first page.

See it in action

The Protect demo writes an encrypted document with a mixed set of permissions. It then reads a protected document back with each password and shows which open modes each allows.

The full Protect demo
src/SampleApp/Demos/ProtectDemo.cs
var document = new PdfDocument();
document.Info.Title = "Protect";

var heading = new XFont("Liberation Sans", 16, XFontStyle.Bold);
var label = new XFont("Liberation Sans", 9, XFontStyle.Bold);
var body = new XFont("Liberation Sans", 9);
var mono = new XFont("Source Code Pro", 8.5);

// ----- page 1: what was asked for -----

var page1 = document.AddPage();
var gfx1 = XGraphics.FromPdfPage(page1);
var prose1 = new XTextFormatter(gfx1);

gfx1.DrawString("This document is encrypted", heading, XBrushes.Black, new XPoint(50, 60));

prose1.DrawString(
"Two passwords, and they are not interchangeable. The user password is what a reader "
+ "asks for before it will open the file at all. The owner password lifts every "
+ "restriction below - it is the one a viewer wants before it will let somebody change "
+ "the permissions - and a document that has one but no user password opens for anybody "
+ "and is still restricted.",
body, XBrushes.Black, new XRect(50, 80, 495, 70));

gfx1.DrawString("User password", label, XBrushes.Black, new XPoint(50, 165));
gfx1.DrawString(ReaderPassword, mono, XBrushes.Firebrick, new XPoint(180, 165));
gfx1.DrawString("Owner password", label, XBrushes.Black, new XPoint(50, 182));
gfx1.DrawString(OwnerPassword, mono, XBrushes.Firebrick, new XPoint(180, 182));

// The eight flags, deliberately mixed rather than all on or all off, so that a reader's
// security dialog has something to disagree about.
(string Name, bool Allowed, string What)[] permissions =
{
("PermitPrint", true, "Print the document at all"),
("PermitFullQualityPrint", false, "Print it at full resolution rather than a draft"),
("PermitExtractContent", false, "Copy text and graphics out of it"),
("PermitAccessibilityExtractContent", true, "Extract for a screen reader, which is not the same permission"),
("PermitModifyDocument", false, "Change the content"),
("PermitAssembleDocument", true, "Insert, rotate or delete pages without changing them"),
("PermitAnnotations", true, "Add notes and markup"),
("PermitFormsFill", true, "Fill in form fields")
};

gfx1.DrawString("Permissions", label, XBrushes.Black, new XPoint(50, 215));

double y = 235;
foreach (var permission in permissions)
{
gfx1.DrawString(permission.Allowed ? "allowed" : "refused", body,
permission.Allowed ? XBrushes.SeaGreen : XBrushes.Firebrick, new XPoint(50, y));
gfx1.DrawString(permission.Name, mono, XBrushes.Black, new XPoint(105, y));
gfx1.DrawString(permission.What, body, XBrushes.DimGray, new XPoint(300, y));
y += 16;
}

prose1.DrawString(
"A permission is a request, not a lock. The flags travel in the encrypted document and "
+ "a conformant reader honours them; nothing stops a program that has the password from "
+ "ignoring them entirely. Encryption is what keeps the content from being read at all, "
+ "and that part is arithmetic rather than good manners.",
body, XBrushes.Black, new XRect(50, y + 14, 495, 60));

gfx1.DrawString("What this library writes", label, XBrushes.Black, new XPoint(50, y + 90));
prose1.DrawString(
"PdfDocumentSecurityLevel offers None, Encrypted40Bit and Encrypted128Bit, and 128-bit "
+ "RC4 is what this document uses. AES is implemented for reading - EncryptorFactory "
+ "answers an /AESV2 or /AESV3 crypt filter with an AES decryptor - so a document "
+ "somebody else encrypted with it opens here. Nothing writes AES yet.",
body, XBrushes.Black, new XRect(50, y + 100, 495, 60));

// ----- page 2: reading one back -----

// A separate document, built with the same settings, saved to memory and read back. It is
// the only way to show the reading half of the API on the same page as the writing half:
// the document being built cannot be reopened until it has been saved, and by then the
// demo has handed it over.
string report;
using (var buffer = new MemoryStream())
{
var sample = new PdfDocument();
_ = sample.AddPage();
sample.SecuritySettings.DocumentSecurityLevel = PdfDocumentSecurityLevel.Encrypted128Bit;
sample.SecuritySettings.UserPassword = ReaderPassword;
sample.SecuritySettings.OwnerPassword = OwnerPassword;
sample.SecuritySettings.PermitExtractContent = false;
sample.SecuritySettings.PermitModifyDocument = false;
sample.Save(buffer, false);

// The user password opens the file to be read. It does not open it to be changed:
// PdfDocumentOpenMode.Modify with it is refused, by name, which is the library
// enforcing the distinction between the two passwords rather than merely recording it.
string refusal;
try
{
buffer.Position = 0;
using var _ = PdfReader.Open(buffer, ReaderPassword, PdfDocumentOpenMode.Modify);
refusal = "Modify with the user password was allowed";
}
catch (PdfReaderException exception)
{
refusal = exception.Message;
}

buffer.Position = 0;
using var asReader = PdfReader.Open(buffer, ReaderPassword, PdfDocumentOpenMode.ReadOnly);

buffer.Position = 0;
using var asOwner = PdfReader.Open(buffer, OwnerPassword, PdfDocumentOpenMode.Modify);

// HasOwnerPermissions is how a program finds out which password it was let in with,
// and therefore whether it is entitled to change anything.
report =
$"User password, ReadOnly: HasOwnerPermissions = {asReader.SecuritySettings.HasOwnerPermissions}\n"
+ $"Owner password, Modify: HasOwnerPermissions = {asOwner.SecuritySettings.HasOwnerPermissions}\n"
+ $"User password, Modify: refused - \"{refusal}\"\n"
+ $"PermitExtractContent read back as {asOwner.SecuritySettings.PermitExtractContent}\n"
+ $"PermitModifyDocument read back as {asOwner.SecuritySettings.PermitModifyDocument}\n"
+ $"PermitPrint read back as {asOwner.SecuritySettings.PermitPrint}";
}

var page2 = document.AddPage();
var gfx2 = XGraphics.FromPdfPage(page2);
var prose2 = new XTextFormatter(gfx2);

gfx2.DrawString("Reading a protected document", heading, XBrushes.Black, new XPoint(50, 60));

prose2.DrawString(
"There are two ways in. PdfReader.Open takes the password directly when the caller "
+ "already has it, and takes a PdfPasswordProvider when it does not - the provider is "
+ "called only if the document turns out to need one, which is how a viewer knows to "
+ "put a dialog up. A wrong password throws PdfReaderException rather than returning "
+ "an empty document.",
body, XBrushes.Black, new XRect(50, 80, 495, 70));

gfx2.DrawString("What came back", label, XBrushes.Black, new XPoint(50, 165));

double line = 185;
foreach (var row in report.Split('\n'))
{
gfx2.DrawString(row, mono, XBrushes.Black, new XPoint(50, line));
line += 15;
}

gfx2.DrawString("The password provider", label, XBrushes.Black, new XPoint(50, line + 20));

// Demonstrated rather than described: the provider is asked for a password only because
// the document has one, and args.Abort is how a caller says the user gave up.
var timesAsked = 0;
using (var buffer = new MemoryStream())
{
var sample = new PdfDocument();
_ = sample.AddPage();
sample.SecuritySettings.UserPassword = ReaderPassword;
sample.Save(buffer, false);

buffer.Position = 0;
using var reopened = PdfReader.Open(buffer, PdfDocumentOpenMode.Modify,
args =>
{
timesAsked++;
args.Password = ReaderPassword;
});

gfx2.DrawString(
$"The provider was called {timesAsked} time(s), and the document opened with "
+ $"{reopened.PageCount} page(s).",
body, XBrushes.Black, new XPoint(50, line + 40));
}

prose2.DrawString(
"Setting a user password is what makes a document encrypted; DocumentSecurityLevel "
+ "follows from it. Setting an owner password alone leaves the file readable by anyone "
+ "and still restricted, which is the arrangement most 'protected' PDFs in the world "
+ "actually use - and the reason so many of them can be opened without a password and "
+ "still refuse to print.",
body, XBrushes.Black, new XRect(50, line + 70, 495, 70));

// Set last, on the document that is about to be handed back and saved. The settings are
// read at save time, so where in the build they are set makes no difference - but keeping
// them next to each other is what makes them readable.
document.SecuritySettings.DocumentSecurityLevel = PdfDocumentSecurityLevel.Encrypted128Bit;
document.SecuritySettings.UserPassword = ReaderPassword;
document.SecuritySettings.OwnerPassword = OwnerPassword;

foreach (var permission in permissions)
{
switch (permission.Name)
{
case "PermitPrint":
document.SecuritySettings.PermitPrint = permission.Allowed;
break;
case "PermitFullQualityPrint":
document.SecuritySettings.PermitFullQualityPrint = permission.Allowed;
break;
case "PermitExtractContent":
document.SecuritySettings.PermitExtractContent = permission.Allowed;
break;
case "PermitAccessibilityExtractContent":
document.SecuritySettings.PermitAccessibilityExtractContent = permission.Allowed;
break;
case "PermitModifyDocument":
document.SecuritySettings.PermitModifyDocument = permission.Allowed;
break;
case "PermitAssembleDocument":
document.SecuritySettings.PermitAssembleDocument = permission.Allowed;
break;
case "PermitAnnotations":
document.SecuritySettings.PermitAnnotations = permission.Allowed;
break;
case "PermitFormsFill":
document.SecuritySettings.PermitFormsFill = permission.Allowed;
break;
default:
throw new InvalidOperationException($"No setter for {permission.Name}.");
}
}