跳到內容

加密:AES-256(CBC)與 AES-256-GCM

Core 依 ISO 32000-2:2020 §7.6 的標準安全處理常式,以 AES-256 加密 PDF。預設模式為 V=5 / R=6 / AESV3(AES-256-CBC)。可選用模式則是 ISO/TS 32003:2023 V=6 / R=7 的 AES-256-GCM 已驗證路徑。本頁說明金鑰衍生、傳輸格式、權限邊界,以及部署時必須了解的限制。

Terminal window
composer require nextpdf/core:^3

預設路徑需要 openssl 擴充功能。AES-256-GCM 路徑會使用 opensslext-sodium。在沒有 AES-NI 硬體的主機上,libsodium 會拒絕 GCM,Core 會退回使用較慢的 OpenSSL 實作,而非降級演算法。

預設處理常式是搭配 AESV3 加密過濾器的 V=5 / R=6 標準安全處理常式。呼叫 setEncryption() 時,Core 會從平台密碼學亂數來源(random_bytes())產生一把隨機的 256 位元檔案金鑰。位元組長度為 32 位元組,與 FIPS 197 的金鑰長度一致。每個物件的內容都以 AES-256-CBC 加密。依 ISO 32000-2:2020 §7.6.4 規定,16 位元組的初始化向量會前置於每段密文。

金鑰衍生依循修訂版 6 的演算法 2.B。依 ISO 32000-2:2020 §7.6.4.3.3 規定,密碼會先以 SASLprep(RFC 4013)正規化,再在字元邊界截斷至 127 個 UTF-8 位元組。衍生雜湊會透過由 AES-128-CBC 步驟驅動的反覆 SHA-256 / SHA-384 / SHA-512 程序計算,以提高離線猜測密碼的成本。使用者、擁有者與每把金鑰的鹽值,會在每個加密器執行個體建立時各產生一次,因此單一執行個體會發出決定性的字典位元組,這是多輪寫入器的前提條件。

useAesGcm() 會開啟可選用的 AES-256-GCM 路徑。它實作 ISO/TS 32003:2023 V=6 / R=7 AESV4 加密過濾器。密碼器為 AES-256-GCM,參數取自 NIST SP 800-38D。每個加密物件的傳輸配置為 12 位元組 IV、密文,接著是 16 位元組的驗證標籤。依 TS 32003 §5.2 設定檔規定,額外驗證資料為空。解密會驗證標籤,並在不符時引發 TamperedDataException;標籤驗證失敗時,絕不會傳回明文。這條路徑加入了預設 CBC 路徑本身無法提供的竄改偵測。

GCM 路徑上的 IV 唯一性紀律依循 NIST SP 800-38D §8。IV 的高位 4 位元組為每個執行個體的固定欄位,於建構時由亂數來源設定。低位 8 位元組是一個大端序計數器,每發出一個 IV 後就遞增。這與 §8.2.1 的決定性建構做法一致,只是固定欄位採隨機化以防跨文件碰撞,而非逐一列舉。第二道防護會把每個發出的 IV 記錄到碰撞集合中,若有值重複就引發 NonceReuseException。計數器溢位同樣會引發 NonceReuseException,因為溢位正是 §8 所警告的 IV 重用失效模式。

GCM 路徑適用兩項長度界限。每個物件的明文上限為 2^39 − 256 位元組,也就是 NIST SP 800-38D §5.2.1.1 推導出的每次呼叫界限。輸入若超過此上限,會引發長度例外,並提示如何跨物件分割。每把金鑰的呼叫安全界限為 2^32 次呼叫。assertWithinSafetyBound() 是一項可選用的檢查,會引發 GcmInvocationLimitExceededException,讓呼叫端在抵達 §8.3 門檻前先輪替文件金鑰。NIST SP 800-57 第 1 部分 §4 將這項金鑰生命週期決策定位為部署責任。

權限旗標屬於建議性質。位元遮罩會寫入加密後的 /Perms 項目與 /P 值,並在讀取時以 validatePerms() 還原;遇到損毀的標記時,會以安全失敗(fail closed)處理。符合規範的讀取器應遵守這些旗標。這些旗標並非由密碼學強制執行:持有解密金鑰並忽略這些位元的處理器,仍可讀取、複製或修改內容。請將權限旗標描述為一種讀取器慣例,而非存取控制。

型別種類主要成員穩定性自版本
Aes256Encryptorclassencrypt(), decrypt(), encryptForObject(), buildEncryptionDictionary(), verifyUserPassword(), verifyOwnerPassword(), validatePerms(), getEncryptionKey()穩定1.0.0
Aes256GcmEncryptorclassencrypt(), decrypt(), encryptStream(), assertWithinSafetyBound(), invocationCount(), isAvailable()穩定2.18.0
KeyMaterialfinal readonly classgenerate(), exposeKey(), fingerprint()穩定2.18.0
EncryptedPayloadSpecfinal readonly classtoDict()穩定2.18.0
CryptoCapabilitiesfinal classhasAesGcm(), detectFipsMode(), assertFipsAvailableForProfile()穩定2.0.0
NonceReuseException例外穩定2.18.0
TamperedDataException例外穩定2.18.0
DecryptionFailedException例外穩定2.18.0
GcmInvocationLimitExceededException例外穩定3.0.0
examples/22-protection.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
// AES-256-CBC, V=5/R=6. Call before addPage().
$doc->setEncryption(
userPassword: 'demo',
ownerPassword: 'admin',
permissions: 4, // printing only; copy/modify denied for a conforming reader
);
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 8, 'Confidential', newLine: true);
$doc->save(__DIR__ . '/output/22-protection.pdf');
examples/security/gcm-authenticated-encryption.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Security\CryptoCapabilities;
use NextPDF\Security\Encryption\Aes256GcmEncryptor;
use NextPDF\Security\Exception\TamperedDataException;
use NextPDF\Security\KeyMaterial;
use Psr\Log\LoggerInterface;
final readonly class AuthenticatedBlobCipher
{
public function __construct(private LoggerInterface $logger) {}
/**
* Seal a payload with AES-256-GCM and return the wire-format bytes.
*
* @param non-empty-string $plaintext The payload to protect.
*
* @return non-empty-string IV(12) || ciphertext || tag(16).
*/
public function seal(string $plaintext, KeyMaterial $key): string
{
if (!CryptoCapabilities::hasAesGcm()) {
throw new \RuntimeException('Host cannot perform AES-256-GCM.');
}
$cipher = new Aes256GcmEncryptor($key);
// Opt-in NIST SP 800-38D §8.3 key-rotation guard.
$cipher->assertWithinSafetyBound();
$wire = $cipher->encrypt($plaintext);
$this->logger->info('Payload sealed', [
'key_fingerprint' => $key->fingerprint(),
'invocations' => $cipher->invocationCount(),
]);
return $wire;
}
/**
* Open a sealed payload; a modified payload raises, never returns plaintext.
*
* @param non-empty-string $wire IV(12) || ciphertext || tag(16).
*/
public function open(string $wire, KeyMaterial $key): string
{
try {
return (new Aes256GcmEncryptor($key))->decrypt($wire);
} catch (TamperedDataException $e) {
$this->logger->warning('Tampered payload rejected', [
'key_fingerprint' => $key->fingerprint(),
]);
throw $e;
}
}
}

此密碼器會檢查主機能力、套用可選用的呼叫防護,且只記錄不可逆的金鑰指紋;偵測到竄改時,會重新拋出例外,而非傳回可疑位元組。

  • 預設的 AES-256-CBC 路徑僅提供機密性。它本身無法偵測被修改過的密文。當你需要竄改偵測時,請改用 AES-256-GCM 路徑。
  • useAesGcm() 在 PDF/A 模式啟用時會引發例外;當 opensslext-sodium 兩者皆未提供 AES-256-GCM 時,也會引發例外。請同時攔截這兩種情況,並呈現一則維運人員可採取行動的訊息。
  • 在沒有 AES-NI 的主機上,libsodium 會拒絕 GCM。Core 會退回使用 OpenSSL GCM,結果正確但較慢;吞吐量會下降,安全性則不變。
  • GCM 每個物件的明文上限為 2^39 − 256 位元組。輸入若超過此上限,會引發長度例外;請以 encryptStream() 將內容分割到多個物件。
  • 每個 KeyMaterial 執行個體必須恰好為 32 位元組;長度錯誤時會在建構時被拒,而不會截斷。
  • 讀取路徑(verifyUserPassword()verifyOwnerPassword()validatePerms())會對密碼學素材使用常數時間比較,並在權限標記損毀時以安全失敗處理。

每個物件的 AES-256-CBC 加密都是一次 OpenSSL 呼叫,相對於物件主體為 O(n)。金鑰衍生會在每個加密器執行個體中執行一次反覆的演算法 2.B 程序;每份文件的成本有界且固定。AES-256-GCM 串流路徑會把輸入分割成 16 MiB 區塊,因此無論輸入總量大小,都能把活躍堆積限制在約 64 MB,遠低於已記載的 64 MB 尖峰預算。每個 GCM 物件會增加 28 位元組的額外負擔(12 位元組 IV 加 16 位元組標籤)。AES-NI 硬體能顯著提升 GCM 吞吐量;缺少它只會降低吞吐量。

這個介面的威脅模型很明確。離線猜測密碼的成本,會因 SASLprep 正規化加上反覆的修訂版 6 金鑰衍生而提高,但弱密碼仍是最主要的殘餘風險。沒有任何衍生方式能消除這項風險。密文遭修改時,在 GCM 路徑上會透過標籤驗證偵測出來,但在預設的 CBC 路徑上並不會被偵測。GCM 路徑上的 IV 重用,由計數器加碰撞集合防止,與 NIST SP 800-38D §8.1 的 IV 紀律一致。計數器溢位會拒絕執行,而非回繞。透過記錄洩漏金鑰的風險,會由 KeyMaterial 遮罩以及密碼上的 #[\SensitiveParameter] 屬性緩解。在平台允許的情況下,衍生出的金鑰素材會在使用後歸零。

這條邊界同樣明確。AES-256 加密的套用方式如 ISO 32000-2:2020 §7.6 所定義,可選用路徑則依 ISO/TS 32003:2023 §5.2;實際的保護效果取決於密碼強度、金鑰管理、部署環境,以及讀取端的讀取器。權限旗標由符合規範的讀取器遵守,並非以密碼學強制執行。用於 /Perms 值的 AES-ECB 步驟,是 ISO 32000-2:2020 §7.6.4.4.10 針對單一 16 位元組區塊所強制要求的。它並不是通用的加密模式。在抵達 2^32 呼叫界限前輪替金鑰,是部署責任;Core 為此提供了一項檢查,但預設並不強制執行。

加密與解密都在程序內執行;不會有任何文件位元組、密碼或金鑰值透過這個介面離開主機。GCM 的 IV 碰撞集合是以不可逆的金鑰指紋為鍵,而非以金鑰位元組。若部署以外部金鑰管理或 PKCS#11 權杖作為金鑰前端,該後端的資料落地問題由該部署負責;OASIS PKCS#11 v3.1 的 C_GenerateKey 是權杖內金鑰產生的契約接點。

請記錄政策名稱與 8 字元的金鑰指紋,絕不要記錄金鑰或密碼。KeyMaterial::__toString()__debugInfo() 會傳回經遮罩的佔位內容。來自這個介面的例外訊息會帶有一個操作標籤與一個指紋,而非金鑰位元組。GCM 呼叫次數是金鑰輪替儀表板上一項安全的遙測訊號。

威脅Core 中的緩解殘餘邊界
離線猜測密碼SASLprep 加上反覆的修訂版 6 衍生弱密碼仍是最主要的風險
密文遭修改GCM 標籤驗證(可選用路徑)CBC 路徑僅提供機密性
IV 重用(GCM)隨機固定欄位加計數器加碰撞集合;溢位即拒絕
GCM 明文過長2^39 − 256 處進行長度檢查;提供分割指引呼叫端必須以串流方式處理大量輸入
金鑰過度使用(GCM)assertWithinSafetyBound()2^32可選用;預設不強制執行
權限旗標遭繞過無 — 旗標屬建議性質不符規範的讀取器會忽略這些旗標
透過記錄洩漏金鑰KeyMaterial 遮罩;#[\SensitiveParameter]記錄 exposeKey() 的呼叫端會破壞這項防護

Core 並非經 FIPS 驗證的密碼學模組,也未取得 FIPS 認證。CryptoCapabilities::detectFipsMode() 是一項盡力而為的探測,會回報啟用、不存在或無法判定;assertFipsAvailableForProfile() 則會在選用 FIPS 設定檔、但主機無法證明具備 FIPS 提供者時,以安全失敗處理。當加密介面是在已載入 FIPS 驗證提供者的主機 OpenSSL 組建上執行時,它會以相容於 FIPS 的模式運作。取得經驗證、經認證的合規狀態,屬於 Enterprise 版的考量範圍。

主張標準條款佐證
每個 GCM IV 都透過決定性的固定欄位加計數器建構,於每次呼叫時保持唯一。NIST SP 800-38D§8.2.1
IV 建構紀律可防止在同一把金鑰的多次呼叫間重用。NIST SP 800-38D§8.1
每個物件的明文上限與每次呼叫的長度界限相符。NIST SP 800-38D§5.2.1.1
金鑰加密週期與輪替屬於部署責任。NIST SP 800-57 第 1 部分修訂版 5§4
AES 檔案金鑰為 256 位元,與標準的金鑰長度相符。FIPS 197§4.2.1
權杖內金鑰產生是外部金鑰儲存區的整合接點。OASIS PKCS#11 v3.1 標準C_GenerateKey(函式)

ISO 32000-2:2020 §7.6 與 ISO/TS 32003:2023 §5.2 是本頁所述處理常式的規範性依據。它們的文字受授權限制。本頁以自己的話改寫這些內容,並以條號引用,未引述其中任何原文。位元組精確金鑰衍生的已驗證執行階段佐證,是演算法 2.B 標準測試,以及頁面佐證結尾所列的外部 oracle(測試載具)夾具。

Core 同時提供預設的 AES-256-CBC 路徑與可選用的 AES-256-GCM 路徑,搭配本機金鑰介面與加密政策 gate。Enterprise 版在相同契約之後,再加上 HSM/PKCS#11 金鑰保管後端,以及 FIPS 模式的加密政策設定檔。公開 API 完全相同,差別在於金鑰保管後端與政策實作。