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-pythonVerify 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 EncryptionUtilsStep 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) == 16derive_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.