# 暗号化
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"}