How-to
Automating Reporting WorkflowsDeep dive

Password-Protect a PDF Exported from Excel

Encrypt the PDF your Excel report becomes: AES-256 with pypdf after conversion, or encryption set directly in the LibreOffice export filter (and why it is weaker), the difference between open and permissions passwords, one password per recipient, verifying the file really is locked, and what PDF permissions can and cannot stop.

Payslips, customer statements and board packs often leave the building as PDFs converted from Excel, and the people receiving them expect the file to be locked. Python can encrypt the PDF after conversion with pypdf, using AES-256, or ask LibreOffice to encrypt during the export. The two are not equal — LibreOffice's export uses an older 128-bit RC4 scheme — and both distinguish between a password that opens the document and one that guards its permissions. This page walks through both routes, generates a password per recipient, verifies that the result really requires a password, and is honest about what permission flags can stop. Converting the workbook in the first place is covered in Convert Excel to PDF on Linux with LibreOffice. It belongs to Exporting Excel Reports to PDF.

Prerequisites

Bash
pip install pypdf cryptography pandas openpyxl

pypdf reads and writes PDFs; AES encryption needs the cryptography package installed alongside it (pypdf raises an error asking for it otherwise). LibreOffice (soffice) converts report.xlsx, a three-row revenue table, to PDF.

Two passwords, two jobs

A PDF can carry two passwords, and mixing them up is the most common mistake:

  • The user password (or open password) is needed to open the file at all. Without it, the content is encrypted and unreadable.
  • The owner password (or permissions password) unlocks the permission settings — printing, copying text, editing. A reader who opens the file with the user password gets the restricted permissions; one who supplies the owner password gets full control.

A file with only an owner password opens for anyone; it only carries permission flags. For confidential reports you need a user password.

The open password and the permissions password The user password is required to open and decrypt the file. The owner password unlocks the permission settings such as printing and copying. A file with only an owner password opens for anyone and relies on the reader software to respect the flags. user password needed to open the file content encrypted without it what keeps a report private owner password unlocks permission flags print, copy, edit settings flags rely on the reader app

Step 1 — Convert, then encrypt with pypdf (AES-256)

The cleanest pipeline converts the workbook to a plain PDF, then encrypts it with pypdf, which supports AES-256 — the strongest scheme in the PDF standard:

Python
import subprocess
from pypdf import PdfReader, PdfWriter
from pypdf.constants import UserAccessPermissions as Perm

subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", "plain", "report.xlsx"],
               check=True, capture_output=True)

writer = PdfWriter(clone_from="plain/report.pdf")
writer.encrypt(
    user_password="open-7Kp2",
    owner_password="owner-Qz94-long-random",
    algorithm="AES-256",
    permissions_flag=Perm.PRINT | Perm.PRINT_TO_REPRESENTATION,
)
writer.write("report_locked.pdf")

enc = PdfReader("report_locked.pdf").trailer["/Encrypt"].get_object()
print("version", enc["/V"], "revision", enc["/R"])

clone_from copies the whole document, including metadata and any bookmarks, into the writer. Version 5, revision 6 in the encryption dictionary is AES-256 as defined in PDF 2.0. permissions_flag lists what someone with only the user password may do — here, print at full quality but not copy text or edit. Always set a separate, long owner password: if you leave it out, pypdf uses the user password for both, and anyone who can open the file can lift the restrictions too.

Step 2 — Or encrypt in the LibreOffice export

LibreOffice's PDF export filter can encrypt the file itself. Its options are passed as JSON after the filter name:

Python
import json

opts = {
    "EncryptFile": {"type": "boolean", "value": "true"},
    "DocumentOpenPassword": {"type": "string", "value": "open-7Kp2"},
    "RestrictPermissions": {"type": "boolean", "value": "true"},
    "PermissionPassword": {"type": "string", "value": "owner-Qz94-long-random"},
    "EnableCopyingOfContent": {"type": "boolean", "value": "false"},
}
subprocess.run(["soffice", "--headless", "--convert-to", "pdf:calc_pdf_Export:" + json.dumps(opts),
                "--outdir", "lo", "report.xlsx"], check=True, capture_output=True)

enc = PdfReader("lo/report.pdf").trailer["/Encrypt"].get_object()
print("version", enc["/V"], "revision", enc["/R"])

This saves a step, but the file comes out with version 2, revision 3 encryption — 128-bit RC4, the scheme of PDF 1.4. RC4 is considered broken and short passwords protected with it can be recovered far more quickly than with AES-256. Note also that the passwords appear on the soffice command line, where other users of the machine can see them in the process list. Prefer Step 1 for anything sensitive; the LibreOffice route is fine when the PDF only needs to deter casual opening.

Encryption the two routes produce The pypdf route writes version 5 revision 6 encryption, which is AES-256. The LibreOffice export filter writes version 2 revision 3 encryption, which is 128-bit RC4, an older and weaker scheme. pypdf encrypt (AES-256) V5 R6 · AES 256-bit LibreOffice export filter V2 R3 · RC4 128-bit

Step 3 — One password per recipient

When each customer or employee gets their own PDF, each file should have its own password — otherwise one recipient could open everyone else's. Generate a random password per file with secrets and record it for the separate channel you use to deliver it:

Python
import csv
import secrets
import string

ALPHABET = string.ascii_letters + string.digits

def new_password(length=12):
    return "".join(secrets.choice(ALPHABET) for _ in range(length))

recipients = ["north@example.com", "south@example.com", "east@example.com"]
issued = []
for email in recipients:
    pwd = new_password()
    w = PdfWriter(clone_from="plain/report.pdf")
    w.encrypt(user_password=pwd, owner_password=new_password(32), algorithm="AES-256",
              permissions_flag=Perm.PRINT | Perm.PRINT_TO_REPRESENTATION)
    out = f"statement_{email.split('@')[0]}.pdf"
    w.write(out)
    issued.append({"recipient": email, "file": out, "password": pwd})

with open("passwords.csv", "w", newline="") as fh:
    csv.DictWriter(fh, fieldnames=["recipient", "file", "password"]).writerows(issued)
print(len(issued), "files, password length", len(issued[0]["password"]))

secrets uses the operating system's cryptographic random source, unlike random. Twelve characters from 62 letters and digits give about 71 bits of randomness, which is strong for AES-256 files. Never send the password in the same email as the PDF — send it by SMS, a separate message or a portal — and treat passwords.csv as a secret itself: write it somewhere access-controlled and delete it once delivered. A per-recipient sending loop is shown in Send Each Recipient Their Own Excel Report.

Some organisations instead use a password the recipient already knows — the last digits of an account number plus a birth date, say. That avoids a second channel but makes passwords guessable by anyone who knows those facts, and short numeric passwords fall quickly to brute force. Random passwords are the stronger choice.

Step 4 — Verify the PDF is really locked

A test should prove that the file cannot be read without the password, that the right password works, and that the owner password is different:

Python
from pypdf.errors import FileNotDecryptedError
from pypdf import PasswordType

def check_locked(path, user_pw, owner_pw):
    r = PdfReader(path)
    assert r.is_encrypted
    try:
        r.pages[0].extract_text()
        raise AssertionError("readable without a password")
    except FileNotDecryptedError:
        pass
    assert PdfReader(path).decrypt("wrong") == PasswordType.NOT_DECRYPTED
    assert PdfReader(path).decrypt(user_pw) == PasswordType.USER_PASSWORD
    assert PdfReader(path).decrypt(owner_pw) == PasswordType.OWNER_PASSWORD
    unlocked = PdfReader(path)
    unlocked.decrypt(user_pw)
    return unlocked.pages[0].extract_text().split("\n")[0]

print(check_locked("report_locked.pdf", "open-7Kp2", "owner-Qz94-long-random"))
for row in issued:
    print(row["file"], PdfReader(row["file"]).decrypt(row["password"]).name)

Reading a page before decrypting raises FileNotDecryptedError; decrypt() returns which password matched — NOT_DECRYPTED, USER_PASSWORD or OWNER_PASSWORD. After decrypting with the user password the first line of the page reads "Region Revenue", proving the content survived, and each per-recipient file opens with its own password. Running this in the report pipeline's tests catches a file that was accidentally written unencrypted.

A locked-report pipeline The workbook is converted to a plain PDF, the PDF is encrypted with AES-256 and a per-recipient password, a check confirms it cannot be read without the password, and the file and password are sent through separate channels. Convert soffice to plain PDF Encrypt pypdf AES-256 Verify unreadable without password Deliver file and password apart

What permissions can and cannot stop

The user password is real protection: without it, the content is encrypted with AES and cannot be read. Permission flags are different. Once a file is opened with the user password, the content is decrypted on the reader's machine, and the "no copying" or "no editing" flags are only honoured if the reading software chooses to honour them. Mainstream viewers do; libraries and some tools do not — pypdf itself extracted the text above after decrypting with the user password, even though copying was not permitted. Treat permissions as a polite request that stops casual copying, not as a security boundary. If recipients must not be able to extract figures, do not send them the figures. Clean the source workbook of hidden sheets and comments before exporting as well; see Remove Hidden Data Before Sharing an Excel File.

Common pitfalls

  • Only an owner password. The file opens for anyone; set a user password for confidentiality.
  • Owner password equal to the user password. Anyone who opens the file can remove its restrictions.
  • RC4 from the LibreOffice filter. Use pypdf's AES-256 for sensitive files.
  • The password in the same email. Deliver it through a separate channel.
  • Missing cryptography. pypdf needs it for AES; install it with pypdf.

Conclusion

To lock a PDF exported from Excel, convert the workbook to a plain PDF and encrypt it with pypdf's encrypt() using AES-256, a user password to open it and a separate, long owner password for permissions. LibreOffice can encrypt during export, but only with 128-bit RC4 and passwords visible on the command line. Generate a random password per recipient with secrets, deliver it separately, verify in tests that the file cannot be read without it, and remember that permission flags only restrain cooperative software.

Frequently asked questions

How do I password-protect a PDF with Python? Open the PDF with pypdf's PdfWriter using clone_from, call encrypt with a user password, a separate owner password and algorithm AES-256, then write the result. The cryptography package must be installed.

Can LibreOffice encrypt a PDF when converting from Excel? Yes. Pass calc_pdf_Export options such as EncryptFile and DocumentOpenPassword as JSON after the filter name. The result uses 128-bit RC4, weaker than the AES-256 pypdf writes.

What is the difference between the user and owner password? The user password is needed to open and decrypt the file. The owner password unlocks permission settings such as printing and copying.

Do PDF permissions stop people copying text? Only in software that respects them. After a file is opened with the user password, tools can ignore the flags, so permissions deter casual copying but are not a security boundary.

More on pdf export

More on pdf export →