高度な暗号化
このページでは、TCPDF-Next Proの内部暗号化実装について説明します。AES-256 AESV3ハンドラ、鍵導出アルゴリズム、パスワード正規化、およびセキュアなパラメータハンドリングをカバーしています。基本的な暗号化の使い方については、AES-256暗号化の例をご覧ください。
AESV3ハンドラによるAES-256
TCPDF-Next Proは、すべてのストリームおよび文字列暗号化にAES-256-CBCを義務付けるISO 32000-2(PDF 2.0)標準セキュリティハンドラリビジョン6を実装しています。ハンドラは暗号化ディクショナリの/V 5および/R 6で識別されます。
use Yeeefang\TcpdfNext\Pro\Security\Aes256Encryptor;
$encryptor = new Aes256Encryptor(
ownerPassword: 'Str0ng!OwnerP@ss',
userPassword: 'reader2026',
);RC4やAES-128がない理由
TCPDF-Next Proは、RC4(40ビットおよび128ビット)とAES-128を意図的に除外しています:
| アルゴリズム | 除外理由 |
|---|---|
| RC4-40 | 1995年以降に破られた、容易に攻撃可能 |
| RC4-128 | キーストリームにバイアスあり、PDF 2.0で禁止 |
| AES-128 | リビジョン6でAES-256に取って代わられた、前方互換性なし |
PDF 2.0(ISO 32000-2:2020)は新規ドキュメントにAESV3を要求しています。弱いアルゴリズムのサポートはセキュリティ態勢を損ない、仕様に違反します。
アルゴリズム2.B:鍵導出
ファイル暗号化キーは、アルゴリズム2.B(ISO 32000-2、条項7.6.4.3.4)を使用してパスワードから導出されます。これはSHA-256、SHA-384、SHA-512をチェーンする反復プロセスです:
function computeHash(password, salt, userKey = ''):
K = SHA-256(password || salt || userKey)
round = 0
lastByte = 0
while round < 64 OR lastByte > round - 32:
K1 = (password || K || userKey) repeated 64 times
E = AES-128-CBC(key = K[0..15], iv = K[16..31], data = K1)
mod3 = (sum of all bytes in E) mod 3
if mod3 == 0: K = SHA-256(E)
elif mod3 == 1: K = SHA-384(E)
else: K = SHA-512(E)
lastByte = E[len(E) - 1]
round += 1
return K[0..31] // 32バイトのファイル暗号化キーこの反復ハッシュにより、正当な使用には十分な速度を維持しながら、ブルートフォース攻撃を計算コスト的に困難にします。
暗号化ディクショナリのキーコンポーネント
| エントリ | 長さ | 用途 |
|---|---|---|
/O | 48バイト | オーナーパスワード検証(ハッシュ + 検証ソルト) |
/U | 48バイト | ユーザーパスワード検証(ハッシュ + 検証ソルト) |
/OE | 32バイト | オーナー暗号化ファイル暗号化キー |
/UE | 32バイト | ユーザー暗号化ファイル暗号化キー |
/Perms | 16バイト | AES-256暗号化された権限フラグ |
SASLprepパスワード正規化
暗号操作の前に、パスワードはSASLprep(RFC 4013)を使用して正規化されます。これはstringprep(RFC 3454)のプロファイルです。オペレーティングシステムや入力方式で使用されるUnicode正規化形式に関係なく、一貫したパスワード処理を保証します。
use Yeeefang\TcpdfNext\Pro\Security\SaslPrep;
$normalized = SaslPrep::prepare('P\u{00E4}ssw\u{00F6}rd');
// 合成/分解形式を正規化し、特定の文字をマッピングし、
// 禁止コードポイントを拒否します。SASLprepの機能
- マッピング -- 一般的にnothingにマッピングされる文字(例:ソフトハイフン
U+00AD)が除去されます。 - 正規化 -- 文字列がUnicode NFC(正規分解後の正規合成)に変換されます。
- 禁止 -- RFC 3454のテーブルC.1.2からC.9の文字(制御文字、私用文字、サロゲート、非文字など)が拒否されます。
- 双方向チェック -- 左から右と右から左の両方の文字を含む文字列がRFC 3454条項6に従って検証されます。
これにより、U+00FC(LATIN SMALL LETTER U WITH DIAERESIS)を入力するユーザーとU+0075 U+0308(LATIN SMALL LETTER U + COMBINING DIAERESIS)を入力するユーザーが同じ暗号化キーを生成します。
権限エンコーディング
権限は/Permsエントリに16バイトのAES-256-ECB暗号化ブロックとして格納されます。平文のレイアウト:
Bytes 0-3: 権限フラグ(リトルエンディアンint32)
Bytes 4-7: 0xFFFFFFFF
Byte 8: EncryptMetadataがtrueなら'T'、それ以外は'F'
Bytes 9-11: 'adb'
Bytes 12-15: ランダムパディング権限フラグはISO 32000-2 テーブル22で定義されたものと同じビットレイアウトに従います。
暗号化ストリームの処理
PDFのすべてのコンテンツストリームと文字列は個別に暗号化されます:
- ストリーム/文字列ごとに一意の16バイト初期化ベクトル(IV)が
random_bytes(16)を使用して生成されます。 - IVは暗号文の先頭に付加されます。
- 暗号化前にPKCS#7パディングが適用されます。
- すべてのストリームデコードパラメータに
/AESV3付きの/Cryptフィルタが設定されます。
// 内部処理 -- ライターによって自動的に処理されます
$iv = random_bytes(16);
$padded = pkcs7_pad($plaintext, blockSize: 16);
$encrypted = openssl_encrypt($padded, 'aes-256-cbc', $fileKey, OPENSSL_RAW_DATA, $iv);
$output = $iv . $encrypted;WARNING
TCPDF-Next Proは常にストリームデータを暗号化します。/EncryptMetadataフラグはデフォルトでtrueです。falseに設定すると、XMPメタデータストリームは暗号化されません(検索インデックス作成に有用)が、他のすべてのストリームは暗号化されます。
センシティブパラメータの処理
パスワードを受け取るすべてのメソッドには、PHP 8.2の#[\SensitiveParameter]属性が付加されています。これにより、パスワードがスタックトレース、デバッグ出力、エラーログに表示されることを防ぎます:
public function setOwnerPassword(
#[\SensitiveParameter] string $password,
): self {
$this->ownerPassword = SaslPrep::prepare($password);
return $this;
}例外が発生した場合、スタックトレースには実際のパスワード文字列ではなくObject(SensitiveParameterValue)が表示されます。
完全なサンプル
use Yeeefang\TcpdfNext\Core\Document;
use Yeeefang\TcpdfNext\Pro\Security\Aes256Encryptor;
use Yeeefang\TcpdfNext\Pro\Security\Permissions;
$pdf = Document::create()
->setTitle('Confidential Report')
->addPage()
->setFont('Helvetica', size: 12)
->multiCell(0, 6, 'This document is protected with AES-256 encryption.');
$encryptor = new Aes256Encryptor(
ownerPassword: 'Str0ng!OwnerP@ss',
userPassword: 'reader2026',
permissions: new Permissions(
print: true,
printHighQuality: false,
copy: false,
modify: false,
annotate: true,
fillForms: true,
extractForAccess: true,
assemble: false,
),
);
$pdf->encrypt($encryptor)
->save(__DIR__ . '/encrypted.pdf');