How to Work with the PDF Encryption Engine in Python

How to Work with the PDF Encryption Engine in Python

Aspose.PDF FOSS for Python (import aspose_pdf) implements PDF password protection at the byte level through a small internal engine class, EncryptionUtils, in aspose_pdf.engine.encryption. This is where the AES-CBC cipher and the PDF Standard Security Handler’s key-derivation algorithms (ISO 32000 Revisions 2 through 6) actually live, independent of the high-level Document API. This guide installs the package and walks through three engine tasks: encrypting and decrypting raw bytes with AES-CBC, deriving Standard Security Handler keys for a password (Revisions 2-4), and deriving AES-256 keys for Revision 5/6 documents.

Step-by-Step Guide

Step 1: Install the Package

pip install aspose-pdf-foss-for-python

Verify the install by importing the package and constructing an empty Document:

from aspose_pdf import Document

document = Document()
print("Aspose.PDF FOSS for Python installed OK — pages:", document.page_count)

Step 2: Import Required Classes

EncryptionUtils is not re-exported from the top-level aspose_pdf package — import it directly from the engine.encryption submodule:

import os
from aspose_pdf.engine.encryption import EncryptionUtils

Step 3: Encrypt and Decrypt Data with AES-CBC

encrypt_aes_cbc and decrypt_aes_cbc work directly on any 16-, 24-, or 32-byte key (AES-128, AES-192, or AES-256) without going through password-based key derivation. encrypt_aes_cbc applies PKCS7 padding internally, and decrypt_aes_cbc removes it.

key = os.urandom(32)  # AES-256; 16- and 24-byte keys are also accepted
plaintext = b"Confidential PDF content"

ciphertext = EncryptionUtils.encrypt_aes_cbc(key, plaintext)
decrypted = EncryptionUtils.decrypt_aes_cbc(key, ciphertext)

assert decrypted == plaintext
print("Roundtrip OK, ciphertext length:", len(ciphertext))

Step 4: Derive Standard Security Handler Keys for a Password (Revisions 2-4)

compute_owner_key_v4 computes the Owner password value (O) per Algorithm 3.3, and compute_user_key_v4 computes the User password value (U) and the file encryption key per Algorithm 3.4/3.5. verify_password_v4 re-derives the key from a candidate password so you can confirm it opens the document before trusting it.

owner_password = "owner-secret"
user_password = "user-secret"
key_length = 16   # 128-bit (AES/RC4); Revision 2 uses 5 (40-bit)
revision = 4
file_id = os.urandom(16)
permissions = -4  # standard PDF permissions value

o_value = EncryptionUtils.compute_owner_key_v4(
    owner_password, user_password, key_length, revision
)

u_value, encryption_key = EncryptionUtils.compute_user_key_v4(
    user_password, o_value, permissions, file_id, key_length, revision
)

verified_key = EncryptionUtils.verify_password_v4(
    user_password, u_value, o_value, permissions, file_id, key_length, revision
)
assert verified_key == encryption_key

ciphertext = EncryptionUtils.encrypt_aes_cbc(verified_key, b"Confidential PDF content")
decrypted = EncryptionUtils.decrypt_aes_cbc(verified_key, ciphertext)
assert decrypted == b"Confidential PDF content"

o_value and u_value are always 32 bytes regardless of key_length. verify_password_v4 returns None — not an exception — when the password does not match.


Step 5: Derive AES-256 Keys for Revision 5/6 Documents

Revision 5 and 6 security handlers (AES-256) do not use compute_owner_key_v4/compute_user_key_v4 — they hash the password with compute_hash_v5, which implements ISO 32000-2’s “Algorithm 2.B”. Use generate_file_id for a fresh file identifier, and derive_object_key to turn a file encryption key into a key for one specific indirect object.

password = b"SecurePassword123"
salt = os.urandom(8)

password_hash = EncryptionUtils.compute_hash_v5(password, salt)
assert len(password_hash) == 32

file_id = EncryptionUtils.generate_file_id()
assert len(file_id) == 16

file_key = os.urandom(32)
object_key = EncryptionUtils.derive_object_key(file_key, 12, 0, use_aes=True)
assert len(object_key) == 16

derive_object_key mixes the object number and generation number into the file key, so every indirect object gets its own key even though the file key is shared.


Common Issues and Fixes

encrypt_aes_cbc/decrypt_aes_cbc raises “AES key must be 16, 24, or 32 bytes”. The key argument must be a real AES key, not a raw password string. Generate one with os.urandom(16 | 24 | 32), or derive one first with compute_user_key_v4 (Revisions 2-4) or compute_hash_v5 (Revisions 5/6).

verify_password_v4 returns None instead of raising. This means the password did not match — it is not an error condition to catch, just a result to check. Also confirm that permissions, file_id, key_length, and revision are the exact same values used when the document’s o_value and u_value were originally computed; a mismatch on any one of them silently produces the wrong key instead of failing loudly.

compute_owner_key_v4 with an empty owner password. Passing "" for owner_password is spec-defined behavior, not a bug — the algorithm falls back to using the user password, so the owner and user passwords become equivalent.

Keys derived for the wrong revision or key length. Revision 2 uses 40-bit (5-byte) RC4 keys, Revision 3/4 use 128-bit (16-byte) RC4/AES keys, and Revision 5/6 use compute_hash_v5 instead of compute_owner_key_v4/compute_user_key_v4 entirely. Mixing these does not raise an exception — it just produces a key that will not open the document.

compute_hash_v5 output changes unexpectedly. The third argument (user_key) is mixed into the hash. Pass b"" explicitly when you want a plain password-validation hash, since omitting it is not the same as passing an empty value in every call site.


Frequently Asked Questions

Why isn’t EncryptionUtils importable from aspose_pdf directly?

It lives in the engine subpackage because it is an internal implementation detail of document-level password protection, not part of the high-level public API surface. Import it explicitly from aspose_pdf.engine.encryption.

Is compute_hash_v5 the same as a single SHA-256 call?

No. It implements ISO 32000-2’s Algorithm 2.B, which performs multiple rounds using different hash algorithms selected by intermediate values — deliberately more expensive than a single hash call.

Does generate_file_id() return the same bytes every time?

No — it returns a fresh, random 16-byte value on every call. A different file identifier changes every key derived from it, even when the password is identical.

Do all indirect objects in an encrypted PDF share one key?

No. derive_object_key combines the object number and generation number with the file encryption key, so each indirect object gets a distinct per-object key.

Should application code call EncryptionUtils directly to password-protect a PDF?

Usually not. EncryptionUtils is the low-level engine that document-level password APIs are built on. Most applications should protect PDFs through the document-level API and reach for EncryptionUtils only for custom tooling or forensic inspection of encrypted files.


See Also