暗号化 🔗
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];
暗号化PDFファイル 🔗
以下は暗号化PDFのサンプルファイルである。
ユーザーパスワードはxyz987、オーナーパスワードはabc123である。
PDFの中身は白紙1ページのみである。