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.

Three encryption errors, three different fixes Three lanes. DependencyError PyCryptodome required maps to a missing AES backend, fixed by installing pypdf with the crypto extra. NotImplementedError algorithm code five maps to an old pypdf that predates AES-256 support, fixed by upgrading pypdf. PdfReadError file has not been decrypted maps to a missing decrypt call, fixed by calling decrypt with an empty string before reading pages. All three converge on a readable document. Error raised What it means Fix DependencyError PyCryptodome required AES backend absent optional extra not installed pip install "pypdf[crypto]" NotImplementedError algorithm code 5 Library predates AES-256 revision 5 or 6 file upgrade pypdf to 5.x PdfReadError not been decrypted decrypt() never called even for an empty password reader.decrypt("") reader.pages readable

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.

Which encryption scheme to choose Four schemes compared. RC4 40-bit is supported by pure Python pypdf, needs no backend, opens everywhere, but is cryptographically broken. RC4 128-bit is the same but only marginally stronger. AES-128 needs a crypto backend, opens in readers from 2005 onwards, and is the safe compatible default. AES-256 needs a backend and pypdf 5, opens only in modern readers, and is the strongest option. Scheme Backend needed Reader support Verdict RC4 40-bit none universal broken, do not use RC4 128-bit none universal legacy reads only AES-128 yes 2005 onwards compatible default AES-256 yes, plus pypdf 5 modern readers strongest choice Pick AES-256 for internal archives; AES-128 when the recipient's reader is unknown What the crypto extra actually installs Three stacked layers. The top layer is your code calling reader dot decrypt. Below it sits pypdf, which implements RC4 in pure Python but delegates AES. Below that sits the optional backend, pycryptodome or cryptography, installed only by the crypto extra. Without the bottom layer, AES files raise DependencyError while RC4 files still open, which is why some protected PDFs work and others do not. your code: reader.decrypt(password) pypdf RC4 in pure Python · AES delegated crypto backend — optional extra absent here means every AES file fails

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.

Part of Watermarking and Securing PDFs.