PicoPDF

暗号化 🔗

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オーナーパスワード暗号化キー 🔗

オーナーパスワード暗号化キーの決定は次の通りとなる。

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暗号化キー 🔗

暗号化キーの決定は次の通りとなる。

AESV2ユーザーパスワード暗号化キー 🔗

ユーザーパスワード暗号化キーの決定は次の通りとなる。

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ハッシュ(ユーザーパスワード, ユーザーキーソルト);
ユーザーパスワード生成文字列 = 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ハッシュ(オーナーパスワード, オーナーキーソルト, ユーザーパスワード暗号化キー);
オーナーパスワード生成文字列 = AES256_NoPading(
        key: ハッシュ値,
        plaintext: ファイル暗号化キー,
        iv: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
    );

AESV3パーミッション情報など 🔗

パーミッション情報などの決定は次の通りとなる。

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バイトをハッシュ値とする。
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の内容は右下へ太線が一本表示されるのみである。

  1. PDF1.7の仕様ではパスワードの文字コードについて定めがない。
    PDF1.7ではパスワードの文字コードをUTF-8としておくのが無難であろう。
    PDF2.0の仕様よりUTF-8と記述が追加された。 

  2. PDF2.0の仕様ではtrueがT、falseがFであるとは明言されていない。
    true、falseの頭文字をとったものであろうという慣習で多くのアプリケーションが実装している。
    原文は以下の通り。
    Set byte 8 to the ASCII character “T” or “F” according to the EncryptMetadata boolean. 

  3. 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. 

  4. 暗号化しないのであればEncryptをつけなければよい。