跳到內容

加密 PDF 並限制權限

這篇 recipe(範例)會以 AES-256 標準安全處理常式加密一份文件。它會設定使用者密碼(開啟文件時必須提供)與擁有者密碼(完整存取權),並透過權限位元遮罩限制可執行的操作。這個範例刻意強調這些權限仰賴閱讀器配合的特性:加密提供的是機密性,而非完整性,而且權限位元只有配合的軟體才會遵守。本範例對應 examples/22-protection.php

信任邊界(每次提到權限時都請記住)。 PDF 加密保護內容的機密性,讓沒有密碼的一方無法讀取內容(ISO 32000-2 §7.6)。 它並不保護完整性:不會偵測或防止內容遭到修改。權限 P 項目是一組 32 位元的無號旗標, 用來請求符合規範的閱讀器遵守;它們並不是一種存取控制。不符合規範的工具,或任何以擁有者密碼操作的工具, 都能執行每一項被「拒絕」的操作。請勿將加密 PDF 描述為 「安全」、「防竄改」或「防複製」。

Terminal window
composer require nextpdf/core:^3

請啟用 openssl PHP 擴充。AES-256 加密器會用它進行加密運算與金鑰衍生。

標準安全處理常式由加密字典的 V/R 代碼選定(ISO 32000-2 §7.6)。NextPDF 的 Aes256Encryptor 在安全處理常式修訂版 6V=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 位元無號數值。

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):

位元操作
34列印文件
48修改文件內容
516複製/擷取文字與圖形
632新增或修改註解,以及填寫表單欄位
<?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。

加密會發出一個 EncryptionAppliedEvent,事件只攜帶演算法名稱(AES-256),以及三個布林值,用來摘要是否允許 print/copy/修改——任何密碼、金鑰、鹽或 IV 都絕不會放入這個事件src/Event/Security/EncryptionAppliedEvent.php)。OpenTelemetry 路徑會把 span 屬性送進一個採白名單的清理器(src/Telemetry/AttributeSanitizer.php),它會無條件拒絕密碼與檔案路徑;只有列在白名單、且值為純量的鍵能保留下來。請勿在你自己的整合程式碼中,把密碼或金鑰素材加進 span、記錄檔或例外訊息——#[SensitiveParameter] 標記保護的是堆疊追蹤,而非你自行組出的字串。

在範圍內:取得加密檔案但沒有密碼的對手——他們無法讀取內容(視密碼強度而定),且檔案不會洩漏明文。在範圍外:持有使用者或擁有者密碼的對手;忽略權限位元、不符合規範的閱讀器;對弱密碼的離線暴力破解;竄改偵測(加密提供的是機密性,而非完整性);主機 OpenSSL 組建中的旁路通道;以及金鑰保管,這完全是整合者的責任。列出這些威脅,並不代表主張不存在弱點。

密碼學基本元件由主機的 OpenSSL 組建提供,因此 FIPS 狀態是主機的屬性,而非函式庫的設定。CryptoCapabilities::detectFipsMode() 會回傳三態的 FipsModeDetectionsrc/Security/FipsModeDetection.php):FIPS_ACTIVEFIPS_ABSENTINDETERMINATE。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 驗證,或任何法律或契約上的機密性保證。「支援標準安全處理常式」並不是對你的部署安全性的認證——這取決於函式庫之外的密碼保管與驗證者政策。