Fix pypdf Encryption Not Supported Error
Opening a protected PDF stops with one of these:
NotImplementedError: only algorithm code 1 and 2 are supported. This PDF uses code 5
DependencyError: PyCryptodome is required for AES algorithm
PdfReadError: file has not been decrypted
All three come from the same place — the file is encrypted with a scheme that the installed pypdf cannot process — but they need different fixes: a missing dependency, an outdated library, or a decrypt call that was never made.
Root Cause
PDF encryption is a family of schemes, not one algorithm. The /Encrypt dictionary records a revision (/R) and a version (/V) that together identify the scheme: RC4 40-bit and 128-bit are revisions 2 and 3, AES-128 is revision 4, and AES-256 is revisions 5 and 6. Pure-Python pypdf implements RC4 itself, but AES requires a cryptographic backend — cryptography or pycryptodome — which is an optional extra, not a hard dependency. Install pypdf without it and every AES file raises DependencyError even though the library "supports" AES.
Separately, a file that opens with an empty user password still counts as encrypted. Reading it without calling decrypt("") first raises PdfReadError: file has not been decrypted, which reads like a permissions problem and is really a missing method call.
Minimal Diagnostic
Read the /Encrypt dictionary without decrypting. This works even when opening the pages fails, because the trailer is plaintext.
# pip install "pypdf>=4.2,<6"
from pathlib import Path
from pypdf import PdfReader
PDF = Path("data/protected.pdf")
REVISIONS = {
2: "RC4 40-bit",
3: "RC4 128-bit",
4: "AES-128 (or RC4-128 with crypt filters)",
5: "AES-256 (deprecated draft)",
6: "AES-256 (PDF 2.0)",
}
def encryption_profile(pdf_path: Path) -> dict:
"""Identify the encryption scheme and whether a backend is present."""
if not pdf_path.exists():
raise FileNotFoundError(f"No such file: {pdf_path}")
reader = PdfReader(str(pdf_path))
if not reader.is_encrypted:
return {"encrypted": False}
enc = reader.trailer["/Encrypt"].get_object()
revision = int(enc.get("/R", 0))
try:
import Crypto # noqa: F401 (pycryptodome)
backend = "pycryptodome"
except ImportError:
try:
import cryptography # noqa: F401
backend = "cryptography"
except ImportError:
backend = None
return {
"encrypted": True,
"revision": revision,
"scheme": REVISIONS.get(revision, f"unknown revision {revision}"),
"key_length_bits": int(enc.get("/Length", 40)),
"crypto_backend": backend,
}
if __name__ == "__main__":
print(encryption_profile(PDF))
{'encrypted': True, 'revision': 6, 'scheme': 'AES-256 (PDF 2.0)',
'key_length_bits': 256, 'crypto_backend': None} # -> install the backend
Fix: Install the Crypto Backend
# The extra pulls in a maintained AES implementation
pip install "pypdf[crypto]>=5,<6"
# Confirm the backend is importable from the same interpreter that runs your script
python -c "import Crypto; print('pycryptodome', Crypto.__version__)"
Then decrypt explicitly before touching pages — including when the password is empty:
# pip install "pypdf[crypto]>=5,<6"
from pathlib import Path
from pypdf import PdfReader
def open_protected(pdf_path: Path, password: str = "") -> PdfReader:
"""Open an encrypted PDF, raising a clear error when the password is wrong."""
reader = PdfReader(str(pdf_path))
if not reader.is_encrypted:
return reader
try:
result = reader.decrypt(password)
except NotImplementedError as exc:
raise RuntimeError(
f"{pdf_path.name}: unsupported encryption — upgrade pypdf ({exc})"
) from exc
# 0 = failed, 1 = opened with the user password, 2 = opened with the owner password
if result == 0:
raise ValueError(f"{pdf_path.name}: wrong password")
return reader
if __name__ == "__main__":
reader = open_protected(Path("data/protected.pdf"), password="")
print(f"{len(reader.pages)} page(s) readable")
The return code is the part worth keeping. decrypt returning 0 is not an exception — code that ignores it goes on to read pages and gets an empty or garbled document, which is far harder to debug than a raised error.
Variant Fix 1: Owner Password Only
Many documents have an empty user password (anyone can open them) and an owner password that restricts printing or copying. pypdf opens those with decrypt("") and reports code 1; the restrictions are advisory metadata, not cryptography, so extraction works normally.
# pip install "pypdf[crypto]>=5,<6"
from pypdf import PdfReader
reader = PdfReader("data/restricted.pdf")
code = reader.decrypt("")
print({0: "wrong password", 1: "opened as user", 2: "opened as owner"}[code])
print("permissions:", reader.trailer["/Encrypt"].get_object().get("/P"))
Removing those restrictions on a document you own means writing an unencrypted copy; the workflow, and the important caveat about only doing it to your own files, is covered in remove password from PDF with Python.
Variant Fix 2: Writing Encrypted Output
When the error appears on write rather than read, the cause is usually an algorithm string the installed version does not know:
# pip install "pypdf[crypto]>=5,<6"
from pathlib import Path
from pypdf import PdfReader, PdfWriter
def protect(src: Path, dest: Path, user_pw: str, owner_pw: str) -> Path:
"""Write an AES-256 encrypted copy with an explicit algorithm."""
writer = PdfWriter(clone_from=PdfReader(str(src)))
try:
writer.encrypt(
user_password=user_pw,
owner_password=owner_pw,
algorithm="AES-256", # "RC4-40" and "RC4-128" exist but should not be used
)
except NotImplementedError as exc:
raise RuntimeError(f"This pypdf build cannot write AES-256: {exc}") from exc
dest.parent.mkdir(parents=True, exist_ok=True)
with dest.open("wb") as fh:
writer.write(fh)
return dest
Older readers cannot open AES-256. If the recipient's software is unknown, AES-128 is the compatible choice — the trade-off table is in add password protection to PDF files.
Variant Fix 3: Batch Jobs Where Some Files Are Protected
In a mixed directory, one encrypted file should not kill the run. Classify first, then handle each class:
# pip install "pypdf[crypto]>=5,<6"
from pathlib import Path
from pypdf import PdfReader
def triage(folder: Path, password: str = "") -> dict[str, list[str]]:
"""Split a folder into readable, unlocked-by-password and blocked files."""
result = {"plain": [], "unlocked": [], "blocked": []}
for path in sorted(folder.glob("*.pdf")):
try:
reader = PdfReader(str(path))
if not reader.is_encrypted:
result["plain"].append(path.name)
elif reader.decrypt(password) != 0:
result["unlocked"].append(path.name)
else:
result["blocked"].append(path.name)
except Exception as exc: # unsupported scheme, corrupt file
result["blocked"].append(f"{path.name} ({type(exc).__name__})")
return result
if __name__ == "__main__":
for bucket, names in triage(Path("data/inbox")).items():
print(f"{bucket}: {len(names)}")
Log the blocked list rather than raising: a document-processing job that stops on the first protected attachment fails the whole batch for one bad input, which is the anti-pattern that scheduling and logging automation jobs exists to prevent.
Verification
After installing the backend, prove both directions work — reading a protected file and writing one — inside the environment that actually runs the job.
# pip install "pypdf[crypto]>=5,<6"
from pathlib import Path
from pypdf import PdfReader, PdfWriter
def crypto_smoke_test(sample: Path, tmp: Path) -> None:
"""Round-trip a document through AES-256 to prove the backend is wired up."""
writer = PdfWriter(clone_from=PdfReader(str(sample)))
writer.encrypt(user_password="pw", owner_password="ownerpw", algorithm="AES-256")
tmp.parent.mkdir(parents=True, exist_ok=True)
with tmp.open("wb") as fh:
writer.write(fh)
reader = PdfReader(str(tmp))
assert reader.is_encrypted, "output was not encrypted"
assert reader.decrypt("pw") != 0, "correct password rejected"
assert len(reader.pages) == len(PdfReader(str(sample)).pages), "page count changed"
assert PdfReader(str(tmp)).decrypt("wrong") == 0, "wrong password accepted"
print("AES-256 round trip OK")
if __name__ == "__main__":
crypto_smoke_test(Path("data/sample.pdf"), Path("out/encrypted.pdf"))
Run that in CI on the same image the job uses. The most common regression is not a code change at all: a slimmed container drops the optional extra, and every protected file starts failing in production while passing locally.
FAQ
Which backend should I install, cryptography or pycryptodome?
Either satisfies pypdf. pip install "pypdf[crypto]" picks a supported one for you. Choose explicitly only when another dependency already pins one of them, to avoid two crypto libraries in the same environment.
Why does decrypt("") succeed on a file I was told is password-protected?
Because it has an owner password but no user password. That configuration restricts actions like printing while leaving the document open to anyone — the protection is a viewer convention, not encryption of the content.
Can pypdf open a PDF whose password I do not know? No, and neither can anything else without brute force. If the file is yours, recover the password from the system that produced it; if it is not, you have no business opening it.
Is NotImplementedError ever a corrupt-file problem rather than a version problem?
Occasionally — a truncated /Encrypt dictionary can present as an unknown revision. Print the profile from the diagnostic: a revision outside 2–6 means the file is damaged rather than merely modern.
Does encrypting a PDF stop it being indexed or extracted? Only for parties without the password. Once decrypted in memory, extraction proceeds exactly as for a plain file, which is why encryption is a transport control rather than a data-loss control.
Checking the Environment Rather Than the Code
This error is almost always environmental, which makes it a good candidate for a startup check rather than a runtime surprise. A three-line probe at import time — attempt an AES round trip on a two-page in-memory document and fail loudly if it raises — turns a mid-batch DependencyError into a container that refuses to start.
That matters more than it sounds. The failure otherwise appears only when a protected file happens to arrive, which may be weeks after the image was rebuilt without the optional extra, and by then nobody connects the two events.
Related
- Watermarking and Securing PDFs — the full protection workflow
- Add Password Protection to PDF Files — choosing algorithms and permissions
- Remove Password from PDF with Python — writing an unlocked copy of your own file
- Fix PdfFileReader Deprecation Error in pypdf — the API rename that changed
encrypt() - Merging and Splitting PDF Documents — handling protected inputs in a batch
Part of Watermarking and Securing PDFs.