如何在 Python 中使用 PDF 加密引擎
Aspose.PDF FOSS for Python(import aspose_pdf)在字节层面通过 aspose_pdf.engine.encryption 中的一个小型内部引擎类 EncryptionUtils 实现 PDF 密码保护。这里正是 AES-CBC 密码以及 PDF 标准安全处理器的密钥派生算法(ISO 32000 第 2 至第 6 版)所在的位置,且独立于高级的 Document API。本指南将安装该包并演示三个引擎任务:使用 AES-CBC 对原始字节进行加密和解密、为密码(第 2-4 版)派生标准安全处理器密钥,以及为第 5/6 版文档派生 AES-256 密钥。
分步指南
步骤 2:导入所需类
EncryptionUtils 未从顶层 aspose_pdf 包重新导出 — 请直接从 engine.encryption 子模块导入:
import os
from aspose_pdf.engine.encryption import EncryptionUtils步骤 3:使用 AES-CBC 加密和解密数据
encrypt_aes_cbc 和 decrypt_aes_cbc 可直接使用任意 16、24 或 32 字节的密钥(AES-128、AES-192 或 AES-256),无需经过基于密码的密钥派生。encrypt_aes_cbc 在内部应用 PKCS7 填充,而 decrypt_aes_cbc 则将其移除。
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))步骤 4:为密码派生标准安全处理程序密钥(修订版 2-4)
compute_owner_key_v4 根据算法 3.3 计算所有者密码值 (O),compute_user_key_v4 根据算法 3.4/3.5 计算用户密码值 (U) 和文件加密密钥。verify_password_v4 从候选密码重新派生密钥,以便您可以在信任之前确认它能打开文档。
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 和 u_value 始终为 32 字节,无论 key_length 为何。verify_password_v4 在密码不匹配时返回 None —— 这不是异常。
步骤 5:为修订版 5/6 文档派生 AES-256 密钥
修订版 5 和 6 的安全处理程序(AES-256)不使用 compute_owner_key_v4/compute_user_key_v4 —— 它们使用 compute_hash_v5 对密码进行哈希,该实现符合 ISO 32000-2 的 “Algorithm 2.B”。使用 generate_file_id 获取全新的文件标识符,并使用 derive_object_key 将文件加密密钥转换为特定间接对象的密钥。
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 将对象号和代号混入文件密钥中,因此每个间接对象都有自己的密钥,尽管文件密钥是共享的。
常见问题及解决方案
encrypt_aes_cbc/decrypt_aes_cbc 抛出 “AES 密钥必须为 16、24 或 32 字节”。 密钥参数必须是实际的 AES 密钥,而不是原始密码字符串。使用以下方式生成: os.urandom(16 | 24 | 32),或者先使用以下方式派生: compute_user_key_v4 (修订 2-4)或 compute_hash_v5 (修订 5/6)。
verify_password_v4 returns None 而不是抛出。 这意味着密码不匹配——这不是需要捕获的错误情况,而是一个需要检查的结果。同时确认 permissions, file_id, key_length,并 revision 与文档在…时使用的完全相同的值 o_value and u_value 最初被计算的;其中任意一个不匹配时会悄悄产生错误的密钥,而不是明确报错。
compute_owner_key_v4 使用空的所有者密码。 Passing "" for owner_password 是规范定义的行为,而非 bug —— 算法会回退使用用户密码,因此所有者密码和用户密码等价。
为错误的修订版或密钥长度派生的密钥。 修订版 2 使用 40 位(5 字节)RC4 密钥,修订版 3/4 使用 128 位(16 字节)RC4/AES 密钥,而修订版 5/6 使用 compute_hash_v5 而不是 compute_owner_key_v4/compute_user_key_v4 完全如此。混合使用这些不会抛出异常 —— 只会生成一个无法打开文档的密钥。
compute_hash_v5 输出意外改变。 第三个参数(user_key)被混入哈希中。传入 b"" 显式传入,当你想要一个纯粹的密码验证哈希时,因为省略它并不等同于在每个调用点传入空值。
常见问题
为什么不能直接从 aspose_pdf 导入 EncryptionUtils?
它位于 engine 子包中,因为它是文档级密码保护的内部实现细节,并不属于高层公共的 API 接口。请从 aspose_pdf.engine.encryption 显式导入它。
compute_hash_v5 与单次 SHA-256 调用相同吗?
不。它实现了 ISO 32000-2 的算法 2.B,该算法使用不同的哈希算法进行多轮运算,算法由中间值选择——故意比单次哈希调用更耗费资源。
generate_file_id() 每次都会返回相同的字节吗?
不——它在每次调用时返回一个全新的、随机的 16 字节值。不同的文件标识符会导致从中派生的每个密钥都发生变化,即使密码相同。
加密的 PDF 中的所有间接对象是否共享同一个密钥?
不。derive_object_key 将对象编号和代数与文件加密密钥结合,因此每个间接对象都会获得唯一的对象级密钥。
应用代码是否应直接调用 EncryptionUtils 来对 PDF 设置密码保护?
通常不建议。EncryptionUtils 是文档级密码 API 所基于的底层引擎。大多数应用应通过文档级 API 来保护 PDF,仅在需要自定义工具或对加密文件进行取证检查时才使用 EncryptionUtils。