暗号化 🔗
PDFのstringとstreamを暗号化することができる。
トレイラーに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による暗号化を行う。
ユーザーパスワードとオーナーパスワードとトレイラーに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バイト以下の場合1、パスワードの後ろに下記をつけて32バイトにする。(パディング)
パスワード長が32バイトを超えていても32バイトしか使用しない。
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したものを使用する
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を連結する
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による暗号化を行う。
ユーザーパスワードとオーナーパスワードが必要になる。
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ハッシュ(ユーザーパスワード, ユーザーバリデーションソルト);
ユーザーパスワード暗号化キー = ハッシュ値 + ユーザーバリデーションソルト + ユーザーキーソルト;
AESV3ユーザーパスワード生成文字列 🔗
ユーザーパスワード生成文字列の決定は次の通りとなる。
- ユーザーパスワード、ユーザーキーソルトを元にAESV3ハッシュ値を求める。
- ハッシュ値を暗号化キー、ファイル暗号化キーを平文、16バイトの0x00をIVとして、
AES256パディングなしで暗号化したものをユーザーパスワード生成文字列とする。
ハッシュ値 = 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オーナーパスワード生成文字列 🔗
オーナーパスワード生成文字列の決定は次の通りとなる。
- オーナーパスワード、オーナーキーソルト、ユーザーパスワード暗号化キーを元にAESV3ハッシュ値を求める。
- ハッシュ値を暗号化キー、ファイル暗号化キーを平文、16バイトの0x00をIVとして、
AES256パディングなしで暗号化したものをオーナーパスワード生成文字列とする。
ハッシュ値 = 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、しない場合はF2) +
['a', 'd', 'b']+
4バイトのランダム値
AESV3ハッシュ値 🔗
AESV3で利用するハッシュ値の求め方は次の通りとなる。
- パスワード、ソルト、ユーザーキー(オーナーパスワードの場合のみ利用)を連結したもののSHA256ハッシュ値をKとする。
- パスワード、K、ユーザーキーを64回連結しK1とする。
- Kの先頭16バイトを暗号化キー、K1を平文、Kの後半16バイトをIVとして、
AES128パディングなしで暗号化したものをEとする。 - Eを符号なし128ビット整数とみなして3で割ったあまりを求める。
あまりが0ならSHA256、あまりが1ならSHA384、あまりが2ならSHA512のハッシュ関数を使用する。
ハッシュ関数にてEのハッシュ値をKに更新する。 - 2.~4.を最低64回繰り返す。
64回目以降はEの末尾1バイトが繰り返し回数 - 32以下であれば6.に進み、それ以外は再度2.から繰り返す。 - Kの先頭32バイトをハッシュ値とする。
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];
暗号化フィルタ 🔗
AESV2やAESV3で暗号化キーを作成し、PDFの文字列(string)とstreamを暗号化する。
U、O、UE、UOやドキュメントIDはstringであるが暗号化対象外である。
AESV2はオブジェクト番号と世代番号から暗号化キーを求める。
AESの暗号化にはランダムにIVを決め、出力の先頭にIVを付ける。
ブロックサイズ16バイト、CBCモードで暗号化を行う。3
using var aes = Aes.Create();
aes.GenerateIV();
aes.SetKey(暗号化キー);
暗号文 = aes.IV + aes.EncryptCbc(平文, aes.IV);
AESV2の暗号化キー 🔗
AESV2は暗号化キーとオブジェクト番号と世代番号と固定値ソルトのMD5ハッシュ値を暗号化キーとする。
オブジェクト番号は3バイト、世代番号は2バイトとしてリトルエンディアンで暗号化キーを作成する。
暗号化キー = MD5(暗号化キー +
オブジェクト番号を3バイト配列としたもの(並び順はリトルエンディアン) +
世代番号を2バイト配列としたもの(並び順はリトルエンディアン) +
[0x73, 0x41, 0x6C, 0x54]
);
None 🔗
CFMにはNoneという種類がある。
UやOは設定するが暗号フィルタは使用しないというモードである。PDF2.0からはUEやOEも必須である。
U、O、UE、UOの設定方法は前述まで通りで変わりはない。
| V | R | CFM | 対応状況 |
|---|---|---|---|
| 4 | 4 | None | PDF1.7まで |
| 5 | 6 | None | PDF2.0より使用可 |
NoneについてPDFリーダーの挙動は仕様には定められていないが、おおむねPDFを開く際にパスワード入力を求められる。
パスワードがあっていないとPDFリーダーでは開くことができないが、PDFの中身は暗号化されていないため読むことができる。
PDFリーダーで読み込みたければトレイラーのEncryptを変更すればよい。
文字を足したり行を消すとクロスリファレンステーブルがずれてしまうため文字変更が無難である。
99 0 obj
<<
/Filter /Standard
/P -3904
/V 4
/CF << /StdCF << /CFM /None /AuthEvent /DocOpen /Length 128 >> >>
/R 4
/O <c5cf7d06120958aa5128672ca0d149cf8e1f0c7ac2ce8a312a7e7885e7e71cd2>
/U <c768d6653cc61ad94071624edaac96b900000000000000000000000000000000>
/StmF /StdCF
/StrF /StdCF
>>
endobj
(中略)
trailer
<<
/Size 100
/Root 1 0 R
/Xncrypt 99 0 R % EncryptをXncryptなどに変更する、実際はクロスリファレンステーブルの位置がずれるためこのようなコメントを付けてはいけない
>>
startxref
99999
%%EOF
多くのPDFリーダーがNoneに非対応である。4
| PDFリーダー | None(V=4) | None(V=5) |
|---|---|---|
| Adobe Acrobat 2026.001.21662 | 非対応(パスワード入力できず) | 非対応(パスワード入力できず) |
| Foxit PDF Reader 2026.1.1.36485 | パスワード入力しても正常に表示されない | パスワード入力しても正常に表示されない |
| Google Chrome 149.0.7827.114 | パスワード入力しても正常に表示されない | パスワード入力が常に失敗する |
| SumatraPDF v3.6.1 | パスワード入力が常に失敗する | 対応 |
暗号化PDFファイル 🔗
以下は暗号化PDFのサンプルファイルである。
ユーザーパスワードはxyz987、オーナーパスワードはabc123である。
PDFの中身は白紙1ページのみである。
Noneを設定したサンプルファイルである。
ユーザーパスワードはxyz987、オーナーパスワードはabc123である。
PDFの内容は右下へ太線が一本表示されるのみである。
-
PDF1.7の仕様ではパスワードの文字コードについて定めがない。
PDF1.7ではパスワードの文字コードをUTF-8としておくのが無難であろう。
PDF2.0の仕様よりUTF-8と記述が追加された。 ↩ -
PDF2.0の仕様ではtrueがT、falseがFであるとは明言されていない。
true、falseの頭文字をとったものであろうという慣習で多くのアプリケーションが実装している。
原文は以下の通り。
Set byte 8 to the ASCII character “T” or “F” according to the EncryptMetadata boolean. ↩ -
PDFの仕様にはパディング方法が定められていない。
原文は以下の通り。
AES-256 algorithm in Cipher Block Chaining (CBC) with padding mode with a 16-byte block size and an initialization vector that is randomly generated and placed as the first 16 bytes in the stream or string.
The key size (Length) shall be 256 bits. ↩ -
暗号化しないのであればEncryptをつけなければよい。 ↩