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];

暗号化PDFファイル 🔗

以下は暗号化PDFのサンプルファイルである。
ユーザーパスワードはxyz987、オーナーパスワードはabc123である。
PDFの中身は白紙1ページのみである。

  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.