HotXLS 會在 Agile 加密的 XLSX 套件中寫入一個合規的 dataIntegrity 區塊,並在開啟檔案時驗證它。HMAC-SHA-512 涵蓋整個 EncryptedPackage 串流,包括其八位元組的 StreamSize 前置欄位,並且是在任何區段被解密之前、針對密文本身進行檢查,因此錯誤密碼或遭修改的套件會被偵測出來,而不會被解密成亂碼
加密而沒有完整性保護,只是答對了一半,而 Office 檔案格式很容易讓人忽略這個缺口,因為從外部看,加密機制顯得如此周全。搞清楚每一層各自承諾了什麼,正是讓安全審查能夠迅速完成的關鍵
一份加密活頁簿到底承諾了什麼?
Agile 加密定義於 [MS-OFFCRYPTO],透過 CBC 模式的 AES 提供機密性,金鑰則是從迭代式 SHA-512 密碼雜湊衍生而來。機密性就是這個結構的全部承諾。CBC 不是一種具備驗證能力的模式:它完全不保證你正在解密的密文,就是當初寫入的密文
實際後果相當具體。翻轉加密套件中的位元,CBC 依然會欣然把它們解密成不同的明文。下游通常會出現 ZIP 剖析錯誤,因為損毀的 deflate 串流很少能倖存,但這句話裡的「通常」承擔了相當大的不確定性,而下游剖析器的錯誤,是得知檔案遭修改的一個很糟糕的方式。dataIntegrity 元素的存在,正是為了在解密之前,用一個涵蓋確切位元組的 MAC,直接回答這個問題
檢查如何進行,順序又是如何
順序才是有趣的部分。HotXLS 先從密碼衍生出中介金鑰,用區塊金鑰衍生出的 IV 解密 dataIntegrity 屬性中的 HMAC 金鑰與 HMAC 值,接著針對儲存狀態下的加密套件計算 HMAC-SHA-512,再進行比對。只有到這一步之後,區段解密才會開始
對密文而言而非明文進行 MAC 檢查,符合標準的「先加密後 MAC」原則,這也正是這項檢查有意義的原因:遭竄改的套件會被拒絕,而不會有任何攻擊者可控的位元組被送進解密與解壓縮路徑。開啟流程中的兩次比對,也就是密碼驗證器雜湊與 HMAC 值,都是以 XOR 與 OR 在整個摘要範圍內累加差異,而不是在第一個不相符的位元組就提早返回,因此都不會透過時間差洩漏出位元組位置
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
// 對純文字、Standard 加密與 Agile 加密的檔案都適用
if Book.OpenEncrypted('incoming.xlsx', PasswordFromVault) = 1 then
ProcessWorkbook(Book)
else
// 密碼錯誤,或套件的 dataIntegrity HMAC 不相符
Quarantine('incoming.xlsx');
except
on E: Exception do
Quarantine('incoming.xlsx: ' + E.Message);
end;
Book.Free;
end;
在寫入端,你的程式碼完全不需要改動。SaveAsEncrypted 會自動產生這個區塊,其中的鹽值、驗證器輸入與 HMAC 金鑰都來自 CryptGenRandom。如果這個呼叫失敗,HotXLS 會直接引發例外,而不是退回較弱的來源。失效即拒絕的 CSPRNG 不是過度謹慎;靜默降級到可預測的亂數來源,會產生看起來已加密、能通過所有功能測試、卻毫無價值的檔案
為什麼沒有這個區塊的檔案仍然能開啟?
因為市面上流通的許多 Agile 加密活頁簿,是由完全省略 dataIntegrity 的產生器寫出的,若一律拒收,破壞的正當工作會遠多於它所保護的部分。HotXLS 只有在兩個屬性都存在且格式正確時,也就是加密後的 HMAC 金鑰與加密後的 HMAC 值,才會視完整性資訊為存在。否則就會略過驗證,讓檔案照常開啟
這是一個帶有安全後果的相容性決策,你應該在自己的威脅模型中明確點出:這個區塊不存在,是無法與攻擊者移除它區分開來的,因為這些屬性本身並不在它們原本會承載的 MAC 涵蓋範圍之內。如果管線的兩端都在你掌控之下,就把區塊缺失視為應用層級的策略失敗。如果你接收的是來自外界的檔案,就把這項檢查當成它原本的樣子:存在時是有價值的訊號,缺失時則完全不算訊號
修改密碼是一種慣例,不是一道邊界
傳統 XLS 活頁簿支援一種常被誤認為加密的獨立機制:寫入保留,也就是 Excel 的「修改密碼」提示。HotXLS 透過 SetModifyPassword 開放這項功能,該方法接受密碼、建議唯讀旗標與保留者使用者名稱,並透過 IsWriteReserved 回報狀態。傳入空密碼即可清除保留設定
實際寫入的是一組 WRITEPROT 與 FILESHARING 記錄,帶有建議唯讀旗標、一組舊式 16 位元密碼雜湊,以及以 BIFF8 Unicode 字串儲存的使用者名稱。那組 16 位元雜湊只是一個檢查碼,不是密碼學摘要,而文件內容完全沒有被加密。任何人用其他工具開啟這份檔案,都能讀到全部內容。這項功能真正的作用是協調:告訴下一個人,某人認為這份檔案是他負責編輯的,性質上與 XLSX 工作表保護與允許選項 所涵蓋的工作表層級控制屬於同一類
var
Book: IXLSWorkbook; // 介面計數:不要呼叫 Free
begin
Book := TXLSWorkbook.Create;
if Book.Open('shared-model.xls') = 1 then
begin
// 建議唯讀,由報表服務保留
Book.SetModifyPassword('edit-me', True, 'Reporting Service');
if Book.IsWriteReserved then
Book.SaveAs('shared-model.xls', xlExcel97);
end;
end;
兩層機制各自發揮所長。真正的機密性來自搭配一個只有目標讀者才知道的密碼所使用的 SaveAsEncrypted,也就是 AES 保護的 XLSX 輸出 所描述的成果。當活頁簿是共用編輯的工件、而你希望 Excel 在有人要覆蓋儲存前先詢問時,寫入保留就疊加在這之上使用
在不受信任的接收路徑上該檢查什麼
完整性驗證保護的是加密後的內容,而不是外層的容器。XLSX 檔案本質上是一個 ZIP 封存檔,而封存結構是在任何加密邏輯執行之前就被剖析的,因此容器層級的驗證應該排在鏈的最前面;具體的失敗模式涵蓋在 不受信任 XLSX 的 ZIP 中央目錄結尾驗證 中。在那之後,把完整性驗證失敗與密碼錯誤視為同一種維運事件,因為從你這一端來看,兩者在設計上本來就無法區分,兩者都代表這份檔案無法被信任為寄件者所以為的那份檔案
記錄哪些檔案帶有 dataIntegrity 區塊,即使只是有無而已。累積幾千份文件之後,這個統計數字會透露出關於你的寄件者所使用工具鏈的有用資訊,把逐檔案的檢查變成一個你能據以行動的全體層級觀察
HotXLS 能讓 Delphi 與 C++Builder 在不安裝 Excel 的情況下讀寫 XLS、XLSX 與 ODS,並以 Pascal 原生實作 [MS-OFFCRYPTO] 的 Standard 與 Agile 加密路徑。加密、保護與活頁簿相關 API 皆記載於 HotXLS Delphi 試算表元件頁面