PDFiumPas 通过 SaveAsEncrypted 写出 ISO/TS 32003 加密:把 Revision 设为 erR7,每个字符串和流都会用 GCM 模式的 AES-256 保护,这是 PDF 2.0 在 2023 年获得的认证加密算法。再设置 EnableIntegrityProtection,文档还会携带一个独立的 PDF MAC 令牌,读取端由 ValidatePdfMac 负责校验
这是两种经常被混为一谈的不同保护机制。GCM 认证的是每一个加密值。MAC 认证的是文档整体。出于不同的理由,你两者都需要
GCM 提供了 CBC 从未提供过的什么能力?
是对密文的认证。CBC 模式的 AES-256,也就是 ISO 32000-2 中的 AESV3 方案,能保证内容的保密性,但完全不能说明内容到达时是否被修改过。CBC 具有特定的、已被充分研究过的可延展性:能够翻转密文比特位的攻击者,会在下一个块的明文中产生可预测的变化,而格式本身对此毫无察觉
GCM 补上了这个缺口。每个加密值都携带一个 16 字节的认证标签,按 ISO/TS 32003 的要求完整序列化,标签不匹配时解密会直接失败,而不是返回被篡改过的明文。用 PDF 的话说,AESV4 文档中被篡改的字符串或流,会在使用点直接触发硬错误,而不是变成一个悄悄传播进你应用程序的奇怪值。Encrypt 字典用 /CFM /AESV4 和 V 6 / R 7 标记这一点,还有一个扩展条目声明 /ExtensionLevel 32003 和 /ExtensionRevision (:2023)
三个 revision,三种生态
TPdfEncryptionRevision 提供 erR5、erR6 和 erR7 三个取值,这个选择更多是兼容性决策,而不是密码学决策。R5 是最初以 PDF 1.7 扩展形式发布的 AES-256 方案,只用一次 SHA-256 密码哈希,过去十五年里的几乎任何软件都能打开它。R6 是 ISO 32000-2 中标准化的加固版密钥派生,使用算法 2.B 的迭代 SHA-256/384/512 构造,是当前 PDF 2.0 或 PDF/A-4 工作流所期望的方案。R7 就是 ISO/TS 32003,使用同样的 2.B 派生,只是把密码换成了 AES-GCM
阅读器的支持程度恰好就是按这个顺序排列的,R7 的支持在主流查看器之外仍然相当有限。这和任何 PDF 2.0 特性面临的权衡是一样的:最新的选项工程上最好,受众却最窄。应该根据谁必须打开这份文件来决定,如果答案是"一套自 2019 年起就没人更新过的档案系统",那不管安全策略更偏好哪个,答案都是 R5
uses
PDFium, FPdfEncrypt;
var
Pdf: TPdf;
Opts: TPdfEncryptOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'quarterly-report.pdf';
Pdf.LoadDocument;
Opts := TPdfEncryptOptions.Default;
Opts.UserPassword := 'open-secret';
Opts.OwnerPassword := 'admin-secret';
Opts.EncryptMetadata := True;
Opts.Revision := erR7; // ISO/TS 32003 AESV4-GCM
Opts.EnableIntegrityProtection := True; // 独立的 PDF MAC 令牌
if not Pdf.SaveAsEncrypted('quarterly-report.enc.pdf', Opts) then
raise Exception.Create('Encrypted save failed');
finally
Pdf.Free;
end;
end;
为什么在认证加密之上还需要文档级 MAC?
因为 GCM 标签保护的是值本身,而不是这些值的排列方式。AESV4 文档中的每个字符串和流都被单独认证,但交叉引用表、对象编号和尾部字典属于结构,不是加密内容。攻击者伪造不了一个流,但仅凭密码本身,什么都阻止不了他们重新排布文档指向哪些对象,或者从同一份文件更早的版本里拼接对象进来
独立的 PDF MAC 令牌解决的正是这一层问题。PDFiumPas 用文件加密密钥、配合 Encrypt 字典中记录的一个专用 32 字节 /KDFSalt 来派生这个令牌,因此只有持有密码,阅读器才能确认这个令牌。最终得到的是对单一问题的单一答案:这份文档整体上,是不是当初写出的那份文档
var
Pdf: TPdf;
Mac: TPdfMacValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'quarterly-report.enc.pdf';
Pdf.LoadDocument;
Mac := Pdf.ValidatePdfMac('open-secret');
case Mac.Status of
pmvsValid: ProcessDocument(Pdf);
pmvsNotPresent: ProcessWithWarning(Pdf); // 这份文件里没有令牌
pmvsInvalid: Quarantine(Mac.MessageText); // 被篡改或被截断
pmvsUnsupported: RouteForManualReview(Mac.MessageText);
end;
finally
Pdf.Free;
end;
end;
这四种状态值需要四种不同的应对方式,把它们压缩成一个布尔值,会丢掉真正要紧的区别。pmvsNotPresent 表示文件根本没有令牌,这描述了几乎所有 2024 年之前写出的加密 PDF,本身不构成任何证据。pmvsInvalid 表示令牌存在但校验不通过,这是一项真实的发现,应该中止后续处理。pmvsUnsupported 表示令牌以当前构建版本尚未实现的形式存在,这是兼容性缺口,不是攻击。如果把"不存在"当成"无效"来处理,第一天就会把你的整个历史存档全部隔离掉
加密仍然做不到的事
权限标志一直以来的性质都没有变:它是对合规软件的一个请求,不是一道控制手段。ISO 32000-1 表 22 中那些禁止打印或提取的 /P 位,会被守规矩的查看器遵守,被其余一切软件无视,而任何持有用户密码的人本来就已经拿到了解密后的内容。加密才是边界;权限只是在这道边界之内描述意图
有两个运维层面的细节值得提前规划。第一,加密和后续修改会相互影响:给加密文档追加增量更新有它自己的一套规则,在加密 PDF 上的增量更新一文中有说明,而 MAC 令牌是一个文档级别的声明,粗心的追加操作会使它失效。第二,GCM 构造使用确定性的 IV 计数器,如果计数器空间被耗尽,PDFiumPas 会直接抛出异常,而不是重用某个计数器值,因为 GCM 中 nonce 重用是灾难性的,静默回绕反而会把这种灾难掩盖起来
在实践中如何在三个 revision 之间做选择
先写清楚谁会打开这份文件,再做选择。对于内部分发场景,每个阅读者用的都是你掌控之下的当前版本查看器,带完整性保护的 R7 是能拿到的最强选项,没有理由不用它。对于要离开组织的文档,R6 是站得住脚的默认选择:它标准化在 ISO 32000-2 本身,而不是它之上的一份技术规范里,支持范围也很广。对于档案和遗留消费端,R5 是唯一能可靠打开的选择,你应该把理由记录在保存留存策略其余部分的同一个地方
不管选哪一个,都要校验实际输出,而不是相信调用成功就万事大吉。重新打开加密文件,检查 ValidatePdfMac,并用精确 PDF 版本合规性一文中的版本合规检查,确认声明的版本符合预期。面向不受信任文档的更全面接入检查清单,见审计 PDF 安全风险
PDFiumPas 是基于 PDFium 引擎的 Delphi 和 Lazarus 组件,PDF 2.0 加密栈完全用 Pascal 原生实现,因此 AES-256、GCM 和 MAC 令牌都不需要任何外部加密 DLL。加密 API 记录在 PDFium Delphi 组件页面