加密 PDF 並限制權限
這篇 recipe(範例)會以 AES-256 標準安全處理常式加密一份文件。它會設定使用者密碼(開啟文件時必須提供)與擁有者密碼(完整存取權),並透過權限位元遮罩限制可執行的操作。這個範例刻意強調這些權限仰賴閱讀器配合的特性:加密提供的是機密性,而非完整性,而且權限位元只有配合的軟體才會遵守。本範例對應 examples/22-protection.php。
信任邊界(每次提到權限時都請記住)。 PDF 加密保護內容的機密性,讓沒有密碼的一方無法讀取內容(ISO 32000-2 §7.6)。 它並不保護完整性:不會偵測或防止內容遭到修改。權限
P項目是一組 32 位元的無號旗標, 用來請求符合規範的閱讀器遵守;它們並不是一種存取控制。不符合規範的工具,或任何以擁有者密碼操作的工具, 都能執行每一項被「拒絕」的操作。請勿將加密 PDF 描述為 「安全」、「防竄改」或「防複製」。
composer require nextpdf/core:^3請啟用 openssl PHP 擴充。AES-256 加密器會用它進行加密運算與金鑰衍生。
概念總覽
標題為「概念總覽」的區段標準安全處理常式由加密字典的 V/R 代碼選定(ISO 32000-2 §7.6)。NextPDF 的 Aes256Encryptor 在安全處理常式修訂版 6(V=5/R=6)中實作 AESV3 加密過濾器:使用隨機產生的 256 位元檔案加密金鑰、加鹽的迭代雜湊金鑰衍生(演算法 2.B),以及逐物件、搭配隨機初始化向量的 AES-256-CBC 加密。CBC 是一種機密性模式(NIST SP 800-38A)。它的 IV 必須是不可預測的。
IV 會針對每個物件、每次執行重新產生,因此原始位元組會隨每次執行而不同。因此可重現性類別為 structural。在比較兩次執行結果之前,這個測試載具(harness)會將加密 IV、物件順序與尾段的 /ID 正規化。這個類別比省略加密的 recipe 所屬類別更嚴格。
權限位元遮罩會設定 P 項目。位元 3 授予列印權,位元 6 授予 annotation/form-fill;其值是文件規定的 32 位元無號數值。
API 介面
標題為「API 介面」的區段NextPDF\Core\Concerns\HasSecurity(混入 Document):
setEncryption(#[SensitiveParameter] string $userPassword, #[SensitiveParameter] string $ownerPassword = '', int $permissions = -1): static— 設定 AES-256 標準處理常式加密。permissions = -1會授予所有權限。當ownerPassword為空時,會把使用者密碼重複當作擁有者密碼。請在addPage()之前呼叫。getEncryptor(): ?Aes256Encryptor— 取得已設定的加密器,若未設定則為null。useAesGcm(?bool $enabled = true): static— 選擇啟用 ISO/TS 32003 的 AES-256-GCM;若主機的 OpenSSL/libsodium 缺少該加密法,則會丟出例外。
兩個密碼參數都標有 #[SensitiveParameter],因此 PHP 會在堆疊追蹤中將它們遮蔽。
權限位元(P 項目,常用的是低位元 3–6):
| 位元 | 值 | 操作 |
|---|---|---|
| 3 | 4 | 列印文件 |
| 4 | 8 | 修改文件內容 |
| 5 | 16 | 複製/擷取文字與圖形 |
| 6 | 32 | 新增或修改註解,以及填寫表單欄位 |
程式碼範例 — 快速上手
標題為「程式碼範例 — 快速上手」的區段<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('Confidential Memo');
// Grant printing only (bit 3 = 4). MUST run before addPage().$doc->setEncryption( userPassword: 'open-me', ownerPassword: 'owner-secret', permissions: 4,);
$doc->addPage();$doc->setFont('helvetica', '', 12);$doc->cell(0, 10, 'Encrypted with AES-256; printing allowed only.', newLine: true);
$doc->save(__DIR__ . '/confidential.pdf');echo "Wrote confidential.pdf\n";程式碼範例 — 正式環境
標題為「程式碼範例 — 正式環境」的區段以下完整範例對應 examples/22-protection.php,並為測試載具寫入 NEXTPDF_COOKBOOK_OUTPUT。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$userPassword = 'demo';$ownerPassword = 'admin';
// Grant ONLY printing (bit 3 = 4); deny copy/modify/annotate.$permissions = 4;
$doc = Document::createStandalone();$doc->setTitle('Encrypted Document — Restricted Permissions');$doc->setAuthor('NextPDF Example');
// setEncryption() MUST be called before addPage().$doc->setEncryption( userPassword: $userPassword, ownerPassword: $ownerPassword, permissions: $permissions,);
$doc->addPage();$doc->setFont('helvetica', 'B', 20);$doc->cell(0, 14, 'Encrypted PDF Document', newLine: true);$doc->ln(8);
$doc->setFont('helvetica', '', 11);$doc->multiCell(0, 7, 'This document is protected with AES-256 encryption ' . '(standard security handler, revision 6). The user password is required ' . 'to open it; the owner password grants full access. The permission ' . 'bits below are honoured by conforming readers only.');$doc->ln(5);
$permissionTable = [ ['Bit 3 (4)', 'Printing', 'ALLOWED'], ['Bit 4 (8)', 'Content modification', 'DENIED'], ['Bit 5 (16)', 'Text copying / extraction', 'DENIED'], ['Bit 6 (32)', 'Annotations / form fields', 'DENIED'],];$doc->setFont('helvetica', 'B', 10);$doc->cell(30, 7, 'Flag');$doc->cell(60, 7, 'Operation');$doc->cell(0, 7, 'Status', newLine: true);foreach ($permissionTable as [$bit, $operation, $status]) { $doc->setFont('courier', '', 9); $doc->cell(30, 7, $bit); $doc->setFont('helvetica', '', 10); $doc->cell(60, 7, $operation); $doc->setFont('helvetica', 'B', 10); $doc->cell(0, 7, $status, newLine: true);}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$doc->save($out !== false ? $out : __DIR__ . '/encrypted.pdf');
echo "Wrote encrypted PDF (AES-256, printing only)\n";預期輸出:
Wrote encrypted PDF (AES-256, printing only)開啟這個檔案時會提示輸入密碼。使用者密碼會以受限的權限集合開啟檔案;擁有者密碼則會以完整存取權開啟檔案。
邊界情況與注意事項
標題為「邊界情況與注意事項」的區段- 呼叫順序。
setEncryption()若在addPage()之後才呼叫,不會回溯加密先前的內容。請務必先設定加密;引擎會在每個物件本體寫出時就將其加密。 - 擁有者密碼預設值。 空的擁有者密碼會讓引擎把使用者密碼重複當作擁有者密碼,這樣實際上就不再有特權角色。當兩種角色必須有所區別時,請設定不同的密碼。
- 權限語意僅供參考。 只有符合規範的閱讀器才會遵守這些位元。它們並非以密碼學強制執行:不符合規範的工具,或任何以擁有者密碼操作的工具,都能執行受限的操作。請把權限視為給配合軟體的政策訊號,絕不要當成能抵擋有心人士的存取控制。
- 沒有完整性保證。 加密提供的是機密性,而非完整性。沒有密碼的攻擊者無法讀取內容,但格式本身並不會偵測竄改。若要保護完整性,則需要另一套機制(數位簽章,或 ISO/TS 32004 的文件 MAC)。
- 與 PDF/A 衝突。 PDF/A 禁用
Encrypt這個尾段鍵。對 PDF/A 文件呼叫setEncryption(),無論順序為何,都會丟出不相容例外。 - 選擇啟用 AES-256-GCM。 當主機的 OpenSSL 或 libsodium 提供時,
useAesGcm()會選用 ISO/TS 32003 的 GCM 大量加密;否則會丟出InvalidConfigException。基於同樣的原因,它也與 PDF/A 不相容。 - 公開金鑰加密尚未接通。
setPublicKeyEncryption()凍結了 API 介面,但save()在寫入器完成接線之前都會丟出例外(這是已知缺陷);請勿在 Core 上的正式環境使用它。
每份文件都會為金鑰衍生執行一次演算法 2.B 的迭代雜湊。逐物件的 AES-256-CBC 與物件本體大小呈線性關係。對於一般文件,其成本通常仍落在 1500 ms/64 MB 的預算之內。非常大的文件則會產生逐物件的 AES 吞吐量成本。在具備 AES-NI 的主機上,GCM 會更快。
安全性注意事項
標題為「安全性注意事項」的區段- 僅有機密性。 重申信任邊界:加密讓沒有密碼的一方無法取得內容;它不證明檔案未遭更動,而且權限位元仰賴閱讀器配合。
- 密碼強度由你負責。 這個處理常式的強度取決於密碼強度。一旦取得檔案,強度不足的使用者密碼便能被離線暴力破解;格式本身無法限制嘗試速率。
- 擁有者密碼是主金鑰。 任何持有擁有者密碼的人都能繞過每一項限制。請把它當成 root 憑證看待;絕不要連同文件一起散布,也不要記錄它。
#[SensitiveParameter]是縱深防禦。 它會把密碼從 PHP 堆疊追蹤中遮蔽,但你仍必須避免讓它們出現在自己的記錄檔、例外訊息與當機報告中。
資料落地與 PII 緩解措施
標題為「資料落地與 PII 緩解措施」的區段此函式庫在行程內完成加密。它不會把文件或密碼傳輸到任何地方。除了你儲存的加密輸出之外,引擎不會把任何密碼、金鑰或文件位元組寫入磁碟。輸出檔案的存放位置與密碼保管,都是整合者負責的部署層面考量。此函式庫不提供任何資料落地保證。若明文文件含有個人資料,該資料受到的保護程度,僅取決於最弱的密碼,以及上述仰賴閱讀器配合的限制。加密無法取代盡量減少放進文件的 PII。
安全遙測與記錄檔清理
標題為「安全遙測與記錄檔清理」的區段加密會發出一個 EncryptionAppliedEvent,事件只攜帶演算法名稱(AES-256),以及三個布林值,用來摘要是否允許 print/copy/修改——任何密碼、金鑰、鹽或 IV 都絕不會放入這個事件(src/Event/Security/EncryptionAppliedEvent.php)。OpenTelemetry 路徑會把 span 屬性送進一個採白名單的清理器(src/Telemetry/AttributeSanitizer.php),它會無條件拒絕密碼與檔案路徑;只有列在白名單、且值為純量的鍵能保留下來。請勿在你自己的整合程式碼中,把密碼或金鑰素材加進 span、記錄檔或例外訊息——#[SensitiveParameter] 標記保護的是堆疊追蹤,而非你自行組出的字串。
威脅模型
標題為「威脅模型」的區段在範圍內:取得加密檔案但沒有密碼的對手——他們無法讀取內容(視密碼強度而定),且檔案不會洩漏明文。在範圍外:持有使用者或擁有者密碼的對手;忽略權限位元、不符合規範的閱讀器;對弱密碼的離線暴力破解;竄改偵測(加密提供的是機密性,而非完整性);主機 OpenSSL 組建中的旁路通道;以及金鑰保管,這完全是整合者的責任。列出這些威脅,並不代表主張不存在弱點。
FIPS 模式行為
標題為「FIPS 模式行為」的區段密碼學基本元件由主機的 OpenSSL 組建提供,因此 FIPS 狀態是主機的屬性,而非函式庫的設定。CryptoCapabilities::detectFipsMode() 會回傳三態的 FipsModeDetection(src/Security/FipsModeDetection.php):FIPS_ACTIVE、FIPS_ABSENT 或 INDETERMINATE。PHP openssl 擴充並未公開 OpenSSL 3 provider 模型的任何繫結,所以探測只能盡力而為;INDETERMINATE 會被視為「FIPS 未獲證明」(fail-closed),並可在維運者可據以採取行動的遙測中區分出來。NextPDF 並不宣稱通過 FIPS 140 驗證;在通過 FIPS 驗證的 OpenSSL 上執行是維運者的責任,而偵測結果僅供參考。
符合性
標題為「符合性」的區段| 陳述 | 規格 | 條款 | 參考 ID |
|---|---|---|---|
加密字典的 V 代碼會選定加密演算法。 | ISO 32000-2 | §7.6 | |
AESV3 加密過濾器方法是由 CFM 項目命名的。 | ISO 32000-2 | §7.6 | |
P 項目是一個 32 位元的無號存取權限數值。 | ISO 32000-2 | §7.6 | |
| 權限位元 3 控制列印。 | ISO 32000-2 | §7.6 | |
| 權限位元 6 控制註解/表單填寫。 | ISO 32000-2 | §7.6 | |
| 加密保護內容免於未經授權的存取(機密性)。 | ISO 32000-2 | §7.6 | |
| 修訂版 6 的金鑰衍生採用加鹽的迭代雜湊(演算法 2.B)。 | ISO 32000-2 | §7.6 | |
| CBC 是一種機密性模式(而非完整性模式)。 | NIST SP 800-38A | §6.2 | |
| CBC 初始化向量必須是不可預測的。 | NIST SP 800-38A | 附錄 C |
NextPDF 已實作上述引用條款;它並不主張全面符合 ISO 32000-2、通過 FIPS 140 驗證,或任何法律或契約上的機密性保證。「支援標準安全處理常式」並不是對你的部署安全性的認證——這取決於函式庫之外的密碼保管與驗證者政策。