PDFiumPas 透過 SaveAsEncrypted 寫出 ISO/TS 32003 加密:把 Revision 設為 erR7,每一個字串與串流就會以 GCM 模式的 AES-256 保護,這是 PDF 2.0 在 2023 年新增的、具備驗證能力的加密演算法。再把 EnableIntegrityProtection 設為 True,文件還會附帶一個獨立的 PDF MAC 權杖,讀取端可用 ValidatePdfMac 檢查
這是兩種常被混為一談的不同保護機制。GCM 為每一個加密值提供驗證。MAC 則為整份文件提供驗證。兩者你都需要,理由各不相同
GCM 比 CBC 多提供了什麼?
對密文的驗證。ISO 32000-2 中的 AESV3 方案,也就是 CBC 模式的 AES-256,能維持內容機密,但完全不保證內容在傳輸過程中未遭修改。CBC 在特定且已被充分研究的方式上是可延展的:能夠翻轉密文位元的攻擊者,會在下一個區塊的明文中產生可預測的變化,而格式本身對此毫無察覺
GCM 補上了這個缺口。每一個加密值都攜帶一個 16 位元組的驗證標籤,並依 ISO/TS 32003 要求完整序列化;當標籤不符時,解密會直接失敗,而不會回傳被竄改過的明文。以 PDF 的術語來說,AESV4 文件中遭竄改的字串或串流,在使用當下就是一個硬性錯誤,而不會是一個一路傳遞進你應用程式的異常值。加密字典會以 /CFM /AESV4 與 V 6 / R 7 標示這一點,另外還有一個擴充項目宣告 /ExtensionLevel 32003 與 /ExtensionRevision (:2023)
三個修訂版,三種生態圈
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 文件中每一個字串與串流都經過個別驗證,但交互參照表、物件編號與 trailer 屬於結構,不是加密內容。攻擊者無法偽造一個串流,但光靠加密演算法本身,並無法阻止對方重新排列文件所指向的物件,或是從同一份檔案的較早版本拼接物件進來
獨立的 PDF MAC 權杖正是為了解決這個層級的問題。PDFiumPas 用一個記錄在加密字典中、專屬的 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 重複使用的後果是災難性的,遠比靜默溢位更需要避免
如何在實務上於三個修訂版之間做選擇
先寫下誰會開啟這份檔案,再做選擇。若是內部發行、每一位讀者使用的都是你能掌控的當前檢視器,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 元件頁面