# 暗号化 PDFのstringとstreamを暗号化することができる。 [トレイラー](PDFの構造#トレイラー)にEncryptを追加してV(バージョン)とR(リビジョン)、CFMの組み合わせで暗号化方法を指定する。 使用できる鍵長やアルゴリズムの組み合わせは複数あるが、現在新たにPDFを作成するのであれば、おおむね下記のどちらかであろう。 | V | R | CFM | 対応状況 | 説明 | |---|---|-------|------------------------------------|----------------------| | 4 | 4 | AESV2 | PDF1.5より使用可、PDF2.0より非推奨 | 鍵長128ビット長のAES | | 5 | 6 | AESV3 | PDF2.0より使用可 | 鍵長256ビット長のAES | 以降はCFMの名称で説明する。 ## AESV2 鍵長128ビットの[AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)による暗号化を行う。 ユーザーパスワードとオーナーパスワードとトレイラーにIDが必要になる。 Encrypt辞書には次の設定を行う。 | キー | 型 | 説明 | |--------|------------|------------------------------------------------------------------------| | Filter | name | 必須、/Standardが組み込みのセキュリティハンドラ | | P | integer | 必須、パーミッション | | V | number | 推奨、4固定 | | R | number | 必須、4固定 | | CF | dictionary | 必須、後述のCF辞書参照 | | O | string | 必須、オーナーパスワード暗号化キー、32バイト | | U | string | 必須、ユーザーパスワード暗号化キー、32バイト(後半16バイトはパディング) | | StmF | name | 推奨、streamの暗号化要否 | | StrF | name | 推奨、stringの暗号化要否 | | EFF | name | 推奨、埋め込みファイルの暗号化要否 | CF辞書はStmF、StrF、EFFから参照される暗号化方法である。 全てに下記の暗号化方法(128ビット長AESV2方式、開いた際にパスワード要求)が指定されているものとする。 ``` << /StdCF << /CFM /AESV2 /AuthEvent /DocOpen /Length 128 >> >> ``` パスワードが32バイト以下の場合[^AESV2-PasswordEncoding]、パスワードの後ろに下記をつけて32バイトにする。(パディング) パスワード長が32バイトを超えていても32バイトしか使用しない。 [^AESV2-PasswordEncoding]: PDF1.7の仕様ではパスワードの文字コードについて定めがない。
PDF1.7ではパスワードの文字コードをUTF-8としておくのが無難であろう。
PDF2.0の仕様よりUTF-8と記述が追加された。 ``` 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A, ``` ### AESV2オーナーパスワード暗号化キー オーナーパスワード暗号化キーの決定は次の通りとなる。 * オーナーパスワードを32バイトパディングし、51回MD5ハッシュ値をとる * ユーザーパスワードを32バイトパディングし、20回RC4暗号化を行う RC4暗号化キーにはハッシュ値とループカウンタをXORしたものを使用する ```cs var hash = MD5(Padding(オーナーパスワード)); for(var i = 0; i < 50; i++) hash = MD5(hash); var オーナーパスワード暗号化キー = Padding(ユーザーパスワード); var key = byte[16]; // hashがMD5(128ビット)のため16バイト固定 for(var i = 0; i < 20; i++) { for (var j = 0; j < 16; j++) key[j] = hash[j] ^ i; オーナーパスワード暗号化キー = RC4(key, オーナーパスワード暗号化キー); } ``` ### AESV2暗号化キー 暗号化キーの決定は次の通りとなる。 * 次のものを51回MD5ハッシュ値をとる ユーザーパスワードを32バイトパディングしたもの + オーナーパスワードを32バイトパディングしたもの + パーミッション値を4バイト配列としたもの(並び順はリトルエンディアン) + ドキュメントID + メタデータを暗号化しない場合は``[0xFF, 0xFF, 0xFF, 0xFF]``を追加 ### AESV2ユーザーパスワード暗号化キー ユーザーパスワード暗号化キーの決定は次の通りとなる。 * パディングのみの32バイトとドキュメントIDを結合しMD5ハッシュ値をとる * ハッシュ値を20回RC4暗号化を行う RC4暗号化キーには暗号化キーとループカウンタをXORしたものを使用する * RC4暗号化結果(元がMD5ハッシュ値のため必ず16バイト)に16バイトの0x00を連結する ```cs var ユーザーパスワード暗号化キー = MD5(パディング + ドキュメントID); var key = byte[16]; // 暗号化キーがMD5(128ビット)のため16バイト固定 for(var i = 0; i < 20; i++) { for (var j = 0; j < 16; j++) key[j] = 暗号化キー[j] ^ i; ユーザーパスワード暗号化キー = RC4(key, ユーザーパスワード暗号化キー); } ユーザーパスワード暗号化キー += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; ``` ## AESV3 鍵長256ビットの[AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)による暗号化を行う。 ユーザーパスワードとオーナーパスワードが必要になる。 Encrypt辞書には次の設定を行う。 | キー | 型 | 説明 | |--------|------------|-------------------------------------------------| | Filter | name | 必須、/Standardが組み込みのセキュリティハンドラ | | P | integer | 必須、パーミッション | | V | number | 必須、5固定 | | R | number | 必須、6固定 | | CF | dictionary | 必須、後述のCF辞書参照 | | O | string | 必須、オーナーパスワード暗号化キー、48バイト | | U | string | 必須、ユーザーパスワード暗号化キー、48バイト | | OE | string | 必須、オーナーパスワード生成文字列、32バイト | | UE | string | 必須、ユーザーパスワード生成文字列、32バイト | | Perms | string | 必須、パーミッション情報など、16バイト | | StmF | name | 推奨、streamの暗号化要否 | | StrF | name | 推奨、stringの暗号化要否 | | EFF | name | 推奨、埋め込みファイルの暗号化要否 | | Length | integer | 必須、256固定 | CF辞書はStmF、StrF、EFFから参照される暗号化方法である。 全てに下記の暗号化方法(256ビット長AESV3方式、開いた際にパスワード要求)が指定されているものとする。 ``` << /StdCF << /CFM /AESV3 /AuthEvent /DocOpen >> >> ``` ファイル暗号化キーはランダムな32バイトとなる。 ユーザーパスワード、オーナーパスワードはUTF8バイト表現で先頭127バイトのみを使用する。 ユーザーバリデーションサルトを8バイト、ユーザーキーサルトを8バイトランダムに決める。 オーナーバリデーションサルトを8バイト、オーナーキーサルトを8バイトランダムに決める。 ### AESV3ユーザーパスワード暗号化キー ユーザーパスワード暗号化キーの決定は次の通りとなる。 * ユーザーパスワード、ユーザーバリデーションサルトを元に[AESV3ハッシュ値](#AESV3ハッシュ値)を求める。 * ハッシュ値、ユーザーバリデーションサルト、ユーザーキーサルトを連結してユーザーパスワード暗号化キーとする。 ```cs ハッシュ値 = AESV3ハッシュ(ユーザーパスワード, ユーザーバリデーションサルト); ユーザーパスワード暗号化キー = ハッシュ値 + ユーザーバリデーションサルト + ユーザーキーサルト; ``` ### AESV3ユーザーパスワード生成文字列 ユーザーパスワード生成文字列の決定は次の通りとなる。 * ユーザーパスワード、ユーザーキーサルトを元に[AESV3ハッシュ値](#AESV3ハッシュ値)を求める。 * ハッシュ値を暗号化キー、ファイル暗号化キーを平文、16バイトの0x00をIVとして、 AES256パディングなしで暗号化したものをユーザーパスワード生成文字列とする。 ```cs ハッシュ値 = AESV3ハッシュ(ユーザーパスワード, ユーザーキーサルト); ユーザーパスワード生成文字列 = AES256_NoPading( key: ハッシュ値, plaintext: ファイル暗号化キー, iv: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] ); ``` ### AESV3オーナーパスワード暗号化キー オーナーパスワード暗号化キーの決定は次の通りとなる。 * オーナーパスワード、オーナーバリデーションサルト、[ユーザーパスワード暗号化キー](#AESV3ユーザーパスワード暗号化キー)を元に[AESV3ハッシュ値](#AESV3ハッシュ値)を求める。 * ハッシュ値、オーナーバリデーションサルト、オーナーキーサルトを連結してオーナーパスワード暗号化キーとする。 ```cs ハッシュ値 = AESV3ハッシュ(オーナーパスワード, オーナーバリデーションサルト, ユーザーパスワード暗号化キー); オーナーパスワード暗号化キー = ハッシュ値 + オーナーバリデーションサルト + オーナーキーサルト; ``` ### AESV3オーナーパスワード生成文字列 オーナーパスワード生成文字列の決定は次の通りとなる。 * オーナーパスワード、オーナーキーサルト、[ユーザーパスワード暗号化キー](#AESV3ユーザーパスワード暗号化キー)を元に[AESV3ハッシュ値](#AESV3ハッシュ値)を求める。 * ハッシュ値を暗号化キー、ファイル暗号化キーを平文、16バイトの0x00をIVとして、 AES256パディングなしで暗号化したものをオーナーパスワード生成文字列とする。 ```cs ハッシュ値 = AESV3ハッシュ(オーナーパスワード, オーナーキーサルト, ユーザーパスワード暗号化キー); オーナーパスワード生成文字列 = AES256_NoPading( key: ハッシュ値, plaintext: ファイル暗号化キー, iv: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] ); ``` ### AESV3パーミッション情報など パーミッション情報などの決定は次の通りとなる。 * 次のものをAES256ECBパディングなしで暗号化したものをパーミッション情報などとする。 パーミッション値を4バイト配列としたもの(並び順はリトルエンディアン) + ``[0xFF, 0xFF, 0xFF, 0xFF]`` + (メタデータを暗号化する場合は``T``、しない場合は``F`` [^AESV3-Perms]) + ``['a', 'd', 'b']`` + 4バイトのランダム値 [^AESV3-Perms]: PDF2.0の仕様ではtrueがT、falseがFであるとは明言されていない。
true、falseの頭文字をとったものであろうという慣習で多くのアプリケーションが実装している。
原文は以下の通り。
Set byte 8 to the ASCII character "T" or "F" according to the EncryptMetadata boolean. ### AESV3ハッシュ値 AESV3で利用するハッシュ値の求め方は次の通りとなる。 1. パスワード、サルト、ユーザーキー(オーナーパスワードの場合のみ利用)を連結したもののSHA256ハッシュ値をKとする。 2. パスワード、K、ユーザーキーを64回連結しK1とする。 3. Kの先頭16バイトを暗号化キー、K1を平文、Kの後半16バイトをIVとして、 AES128パディングなしで暗号化したものをEとする。 4. Eを符号なし128ビット整数とみなして3で割ったあまりを求める。 あまりが0ならSHA256、あまりが1ならSHA384、あまりが2ならSHA512のハッシュ関数を使用する。 ハッシュ関数にてEのハッシュ値をKに更新する。 5. 2.~4.を最低64回繰り返す。 64回目以降はEの末尾1バイトが``繰り返し回数 - 32``以下であれば6.に進み、それ以外は再度2.から繰り返す。 6. Kの先頭32バイトをハッシュ値とする。 ```cs var K = パスワード + サルト + ユーザーキー; for (var i = 1; ; i++) { var K1 = (パスワード + K + ユーザーキー) * 64; var E = AES256_NoPading(key: K[..16], plaintext: K1, iv: K[16..32]); switch (E % 3) { case 0: K = SHA256(E); break; case 1: K = SHA384(E); break; case 2: K = SHA512(E); break; } if (i > 63 && E[^1] <= i - 32) break; } ハッシュ値 = K[..32]; ``` # 暗号化PDFファイル 以下は暗号化PDFのサンプルファイルである。 ユーザーパスワードは``xyz987``、オーナーパスワードは``abc123``である。 PDFの中身は白紙1ページのみである。 * [暗号化なし](sample/encrypt-noencrypt-create.pdf){:target="_blank"} * [PDF1.7 AESV2](sample/encrypt-aesv2-create.pdf){:target="_blank"} * [PDF2.0 AESV3](sample/encrypt-aesv3-create.pdf){:target="_blank"}