pypdf 全面指南:使用 Python 对 PDF 进行加密与解密(RC4 / AES 全算法实战)
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
导读
本文围绕 pypdf 的加密与解密能力展开,系统讲解如何用PdfWriter.encrypt()为 PDF 添加密码保护、如何用PdfReader.decrypt()打开受保护的文档,并深入到仓库源码层面剖析 RC4-40、RC4-128、AES-128、AES-256-R5、AES-256 五种算法的底层实现、可选加密依赖的安装方式以及用户权限位的控制方法。读完本文,你将能够独立完成"加密 PDF → 分发 → 密码解密"的完整工作流,并理解这些操作在 PDF 标准安全处理器中的真实原理。
PDF 加密的基础:算法与标准支持
PDF 的加密机制基于对称加密算法。pypdf 支持 RC4 与 AES 两类算法,且覆盖不同的密钥长度:
- RC4:历史最悠久的流密码,PDF 1.1 引入,密钥长度可为 40 位(RC4-40)或 128 位(RC4-128)。
- AES:分组密码,PDF 1.5 起引入 AES-128,PDF 2.0 引入 AES-256(含 Revision 5 与 Revision 6 两个变体)。
根据 加密解密官方文档,pypdf 完整支持以上所有算法直至最新的PDF-2.0标准。在源码中,这一能力体现在 pypdf/_encryption.py 的EncryptAlgorithm枚举里,每个算法用(V, R, Length)三元组表示——V是加密字典中的算法代码,R是安全处理器修订版本号,Length是密钥位数:
class EncryptAlgorithm(tuple, Enum): # V, R, Length RC4_40 = (1, 2, 40) RC4_128 = (2, 3, 128) AES_128 = (4, 4, 128) AES_256_R5 = (5, 5, 256) AES_256 = (5, 6, 256)其中AES-256-R5对应修订版本 R5,AES-256对应 R6(两者算法强度相同,R6 增加了更强的密钥派生过程)。
环境准备:AES 需要额外依赖
一个重要的前提:pypdf 对 RC4 的支持开箱即用,但对 AES 的加解密依赖第三方加密库。官方文档明确推荐使用pyca/cryptography,备选方案是pycryptodome。
在仓库的加密提供者模块 pypdf/_crypt_providers/init.py 中可以看到清晰的依赖解析逻辑:pypdf 会按顺序尝试导入_cryptography→_pycryptodome→_fallback三个实现,只要装上了cryptography或pycryptodome其中之一,AES 相关函数(aes_cbc_encrypt、aes_cbc_decrypt、aes_ecb_encrypt、aes_ecb_decrypt)即可正常工作。
安装方式(详见 安装指南):
# 安装全部可选依赖(加密 + 图像等) pip install pypdf[full] # 仅安装加密所需依赖(处理 AES 加密的 PDF 时必需) pip install pypdf[crypto] # 仅安装基础版本(仅支持 RC4 加解密) pip install pypdf提示:如果你只处理 RC4 加密的 PDF,普通安装即可;一旦需要加密或解密 AES 类的 PDF,务必先执行
pip install pypdf[crypto]。
实战一:加密 PDF
加密的入口是PdfWriter.encrypt()。最简流程是先用PdfReader读取原文件,再用PdfWriter(clone_from=reader)克隆内容,加密后写出。完整示例(取自 官方文档):
from pypdf import PdfReader, PdfWriter reader = PdfReader("example.pdf") writer = PdfWriter(clone_from=reader) # Add a password to the new PDF writer.encrypt("my-secret-password", algorithm="AES-256") # Save the new PDF to a file writer.write("out-encrypt.pdf")algorithm 参数的五个可选值
encrypt()的algorithm参数决定加密强度,官方文档给出的可选值如下:
| algorithm 取值 | 对应枚举 | 说明 |
|---|---|---|
"RC4-40" | RC4_40 | 40 位密钥 RC4,兼容老版本阅读器,最不安全 |
"RC4-128" | RC4_128 | 128 位密钥 RC4 |
"AES-128" | AES_128 | 128 位密钥 AES |
"AES-256-R5" | AES_256_R5 | 256 位密钥 AES(Revision 5),官方推荐 |
"AES-256" | AES_256 | 256 位密钥 AES(Revision 6) |
官方文档明确建议优先使用AES-256-R5。同时文档给出了一个安全警告:
⚠️ pypdf 在省略
algorithm参数时,默认使用RC4-128(以兼容旧版工具)。由于 RC4 已被认为不安全,实际使用中应始终显式指定 AES 算法。
这一点在源码 pypdf/_writer.py 中得到印证:当algorithm为None时,pypdf 回退到EncryptAlgorithm.RC4_128;只有当use_128bit=False时才降级为RC4_40。因此切勿省略 algorithm 参数。
encrypt() 的完整参数签名
结合 pypdf/_writer.py 的源码,encrypt()的完整签名与含义如下:
writer.encrypt( user_password: str, # 用户密码:打开文档需提供的密码(受权限限制) owner_password: Optional[str] = None, # 所有者密码:无限制访问;默认与 user_password 相同 use_128bit: bool = True, # 兼容旧接口;当 algorithm 有效时被忽略 permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS, algorithm: Optional[str] = None, # "RC4-40" / "RC4-128" / "AES-128" / "AES-256-R5" / "AES-256" ) -> None其中owner_password允许你设置"管理者口令"——持有它的人可以不受权限限制地打开文档;若不提供,则默认与用户密码相同。algorithm一旦合法指定,use_128bit参数即被忽略。
需要留意的一个限制(源码 pypdf/_writer.py):对增量更新的 PDF(incremental=True)调用encrypt()会抛出NotImplementedError,加密操作需在普通模式下进行。
实战二:解密 PDF
解密使用PdfReader.decrypt(),流程是先判断is_encrypted,再传入正确的密码(用户密码或所有者密码均可),最后克隆写出。完整示例(取自 官方文档):
from pypdf import PdfReader, PdfWriter reader = PdfReader("encrypted-file.pdf") if reader.is_encrypted: reader.decrypt("test") # secret password writer = PdfWriter(clone_from=reader) # Save the new PDF to a file writer.write("out-decrypt.pdf")源码层面的行为细节
从 pypdf/_reader.py 的源码可以看出以下关键行为:
PdfReader.decrypt(password)会依次核对用户密码与所有者密码(先验证所有者密码,见 pypdf/_encryption.py 的verify_v4与verify_v5),只要其中一个匹配即获得解密密钥;返回值是PasswordType枚举(NOT_DECRYPTED/USER_PASSWORD/OWNER_PASSWORD),可用来判断命中的是哪种密码。测试用例 tests/test_encryption.py 中也对此做了断言:reader.decrypt(owner_password) == PasswordType.OWNER_PASSWORD。is_encrypted是一个只读属性,它检查文件 trailer 中是否包含/Encrypt条目;即使调用decrypt()成功,该属性依然保持True(这是源码 docstring 中特别说明的行为)。- 如果文件本身未加密却调用
decrypt(),会抛出PdfReadError("Not encrypted file")。 - 密码错误时不会抛出异常,而是返回
PasswordType.NOT_DECRYPTED,后续读取页面内容才会因无法解密而失败,因此实践中建议检查decrypt()的返回值。
解密后"另存为新文件"即可去除密码
上面的示例本质上是通过"解密读取 → 克隆写入"生成一份新的未加密 PDF。这也是社区中"去除 PDF 密码"的常规做法:解密成功后用PdfWriter(clone_from=reader)写出新文件,新文件将不再包含加密字典。
原理纵深:pypdf 是如何加解密 PDF 的
1. 加密字典与 CryptFilter
PDF 的加密信息存放在 trailer 的加密字典(Encrypt dictionary)中,包含/V(算法版本)、/R(修订号)、/Length(密钥位数)、/P(权限位)、/O、/U、/OE、/UE、/Perms等条目。pypdf 在 pypdf/_encryption.py 的Encryption类中统一管理这些参数。
在Encryption内部,_make_crypt_filter()(pypdf/_encryption.py)实现了 PDF 标准 Algorithm 1 / Algorithm 3.1a 的密钥派生逻辑:
- 对 V ≤ 4 的算法(RC4、AES-128):将对象号(3 字节小端)与代次号(2 字节小端)拼接到文件加密密钥之后,再做 MD5 哈希取前 n+5 字节;AES-128 额外拼接
"sAlT"四个字节。 - 对 V ≥ 5 的算法(AES-256):不再使用 MD5 密钥派生,直接用 32 字节文件加密密钥,因此 AES-256 加密的文件可以在禁用了 MD5 的 FIPS 环境中正常读取(这是源码注释中明确指出的优势)。
2. V4(RC4 / AES-128)与 V5(AES-256)两套实现
源码 pypdf/_encryption.py 按 PDF 标准将算法分为两个实现类:
AlgV4(pypdf/_encryption.py):对应 RC4-40 / RC4-128 / AES-128,实现了 PDF 规范中的 Algorithm 2~7,包括用固定 32 字节填充串对密码补齐、MD5 哈希 50 轮迭代、O/U 值的 RC4 加密计算与口令验证等。AlgV5(pypdf/_encryption.py):对应 AES-256-R5 / AES-256,实现了 Algorithm 3.2a~3.10,核心是 SHA-256 哈希 + AES-256-CBC / ECB 模式:通过 Validation Salt 验证口令、通过 Key Salt 派生中间密钥、再用中间密钥解出 32 字节文件加密密钥,最后用 ECB 模式解密/Perms校验权限是否被篡改。
3. V5 口令的 SASLprep 规范化
PDF 2.0(AES-256)要求口令先经过SASLprep(RFC 4013)规范化再按 UTF-8 编码。pypdf 在 pypdf/_encryption.py 的_saslprep()中实现了完整的映射、NFKC 归一化、禁用字符检查与双向文本(RandALCat/LCat)检查;若口令包含规范禁止的字符,在严格模式下会抛出ValueError,否则回退为普通 UTF-8 编码并记录警告。测试文件 tests/test_encryption.py 对_saslprep有专门覆盖。
4. 加密提供者的降级机制
pypdf/_crypt_providers/init.py 展示了三层降级链:优先cryptography,其次pycryptodome,最后是仅含 RC4 的纯 Python 回退实现_fallback。因此即使两个 AES 依赖都没安装,pypdf 仍能完成 RC4 加解密——这也解释了为什么文档说"RC4 使用普通安装即可"。
进阶:加密时控制文档权限
encrypt()的permissions_flag参数允许在加密的同时限制用户密码持有者的操作范围。pypdf 在 pypdf/constants.py 中定义了UserAccessPermissions位标志(对应 PDF 1.7 规范 Table 3.20):
| 权限标志 | 位值 | 含义 |
|---|---|---|
PRINT | 4 | 允许打印 |
MODIFY | 8 | 允许修改内容 |
EXTRACT | 16 | 允许提取文本与图形 |
ADD_OR_MODIFY | 32 | 允许添加或修改批注、填写表单 |
FILL_FORM_FIELDS | 256 | 允许填写表单字段 |
EXTRACT_TEXT_AND_GRAPHICS | 512 | 允许提取文本和图形(辅助功能场景) |
ASSEMBLE_DOC | 1024 | 允许组装文档(插入、旋转、删除页面) |
PRINT_TO_REPRESENTATION | 2048 | 允许以低分辨率表示形式打印 |
这些标志可以按位或(|)组合使用。注意:权限只约束"用户密码"持有者,持有所有者密码(owner password)的用户始终拥有全部权限;并且权限标志在 PDF 标准中属于软性限制,多数阅读器会尊重,但并非强制的 DRM 手段。权限位实际写入加密字典的/P条目(见 pypdf/_encryption.py 的write_entry),并在解密时通过/Perms校验其完整性。
注意事项小结
- 默认算法是 RC4-128:省略
algorithm参数即回退 RC4,出于安全考虑请始终显式传入"AES-256-R5"或"AES-256"。 - AES 依赖必须安装:处理 AES 前先执行
pip install pypdf[crypto]。 is_encrypted在解密后仍为True:它只反映文件原本是否带加密字典,不代表当前解密状态。- 密码错误不抛异常:
decrypt()返回PasswordType.NOT_DECRYPTED表示失败,建议检查返回值。 - 增量更新的 PDF 不能加密:
incremental=True时调用encrypt()会直接报错。 - 非 ASCII 口令注意编码:V4 算法优先按 Latin-1 编码口令,失败才退回 UTF-8;V5 算法则严格走 SASLprep + UTF-8(pypdf/_encryption.py),若口令含特殊字符建议使用 ASCII 字符以确保跨阅读器兼容。
通过本文的加密、解密实战与源码级剖析,你可以放心地在项目中引入 pypdf 处理受保护 PDF:加密时选对算法与权限位,解密时正确处理返回值,遇到 AES 文件先装好crypto可选依赖——这三步足以覆盖绝大多数生产场景。
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考