PDF 1.7 での暗号化について

# はじめに

ドワンゴ内で使われている基幹システムで、ログインパスワードで暗号化されたPDFがダウンロードできるという機能をもったものがあります。
ある日その機能について、パスワードで暗号化されたPDFは記録に不便だよね、というSlack上の雑談がなされていた中、ある社員が

「サーバーにログインパスワードを平文保存してないと 同じパスワードで暗号化できないでしょ」

といい、そうだねー、パスワードの平文保存はちょっと怖いよねー、的なリアクションがいくつかつきました。

たしかに生のパスワードをサーバーで保存していないと、ログインパスワードで暗号化されたPDFの生成はできないようにも思えます。しかし本当にできないのでしょうか？
そんなことから興味を持ってPDF の暗号化の仕組みについて調べてみました。

# PDF の仕様

2008 年 7 月に PDF 1.7 というバージョンが ISO 32000-1 として標準化されています。
ISO 仕様は有償で手に入れることもできますが、この 1.7 については[Adobe Developer Connection](https://www.adobe.com/devnet/pdf/pdf_reference.html)で無償にて見ることが可能です。
今回の記事はこの ISO 32000-1 バージョン 1.7 を参照してPDFの暗号化の方法を説明します。

前述の通り、PDFの仕様はISO標準化されています。ただしAdobeによる拡張がされており、例えば暗号化ではISO 32000-1では128bitの鍵までしか認められていませんが、Adobe拡張では256bitまで利用できるようになっています。Adobe拡張の部分については仕様書が公開されていないため、今記事ではその部分については触れません。

なお、2017 年 7 月には [ISO 32000-2](https://www.iso.org/standard/63534.html) としてバージョン 2.0 が公開されています。
この2.0では上記のISO 32000-1が固まったあとにAdobe拡張として入った機能が多く採用されています。
私はこの仕様書は入手していないため、この記事では バージョン 1.7 での暗号化のみを説明します。
申し訳ありませんがこの記事を書くためだけに2万円超えは払えなかったのです。 ~~誰かお金くれたら続き書きます~~

# この記事で登場するPDFについての知識

## PDFで定義されているデータ構造

暗号化について説明する前に、PDFを読むための簡単な説明をします。

まずデータ型、PDFでは `object` と呼ばれているのですが次の8種類のみです。

- boolean
  - 真偽値。`true`, `false` の二種類
- integer
  - 整数または小数
- string
  - 文字列
  - `(Hello)` のようにカッコで囲む形式と `<48656c6c6f>` のように `<>` で囲んで16進数で書く方法があります。
- name
  - Rubyのリテラルのようなもの。 `/XXX` で書き表す。この記事でも `/Name` のように表します。
- array
  - 配列。値はarrayを含むすべてのデータ型が利用可能です。
- dictionary
  - 辞書。キーは必ずnameでなければならない。値はすべてのデータ型が利用可能。
  - `<< ... >>` で囲まれる。
- stream
  - バイナリ長とバイナリ列を持つ辞書。バイナリ列はstringと違ってそのまま記述できます。
- null
  - `null`

ちなみにこの記事では `boolean`, `integer`, `string`, `name`, `array`, `dictinary` の6種類だけ登場します。
ストリームだけちょっと特殊ですが、それを除けばJSONみたいなものと思えばわかりやすいかもしれません。

PDFの暗号化ではこのうち `string`, `stream` が暗号化されます。

## オブジェクトのラベリング（間接オブジェクト）

PDF内のオブジェクトは別のオブジェクトから参照できるようにラベリングすることができます。
ラベリングは2つの整数と `obj`, `endobj` の2つでオブジェクトを囲むことで表現します。

このとき最初の数字 (例: 12) を オブジェクト番号 (`object number`), 二番目の数字 (例: 0) を 世代番号 (`generation number`) といいます。

```
12 0 obj
なんらかのオブジェクト
endobj
```

このようにラベリングされたオブジェクトを間接オブジェクト (`indirect object`) と呼びます。

間接オブジェクトは別のオブジェクトから参照することができます。

```
12 0 R
```

`obj` 〜 `endobj` がなく、代わりに `R` になっている場合、
`object number = 12 && generation number = 0` のオブジェクトと同じものを表します。

このように別のオブジェクトを参照することを間接参照 (`indirect reference`) と呼びます。

## PDFのファイル構造

PDFは以下のような構造を取ります。

- ヘッダー (Header)
- 本文 (Body)
- 相互関連テーブル (Cross-reference table)
- トレーラー (Trailer)

暗号化PDFの暗号/復号についての情報はファイル末尾のトレーラーという `dictionary` に入っています。

### トレーラーの情報

トレーラーはPDF末尾の `trailer` から `%%EOF` に囲まれた部分です。
PDFビューアーが表示するために重要な情報がここに入っているのですが、この中にある `dictionary` の中にPDFの暗号・復号に必要な情報があります。

- **ID**: array of string
  - ファイル識別子となる文字列が２つ入っています。1つ目がファイル自体の識別子、もう一つがファイルが更新する度に変更される識別子です。
  - **生成ルールはありません。** 何らかのハッシュでもいいですしSecureRandomのようなものでも構いません。
    - 仕様書では「現在時間」「ファイル名」「メタ情報」などでユニークになるような値をつけましょう、とされてます。
    - 暗号化では1つ目の値のみ使用します。
- **Encrypt**: dictionary
  - 暗号化形式やパスワードの正当性チェック用の値など。後述。

Encryptは次のようなものが含まれています（※この記事で登場しないものは省略してます）。

- Filter: name (必須)
  - PDFの暗号化形式を表します。 `/Standard` (パスワード) または `/Entrust.PPKEF`, `/Adobe.PPKLite`, `/Adobe.PubSec`, 他. (公開鍵) のいずれかです。
- V: number (省略可)
  - 暗号化のバージョンです。V=1 (PDF 1.3) では鍵長が40bitまでしか使えませんが、V=3からは128bitが, V=5からは256bitが使えるようになります。
  - オプションの値で省略時は0なんですが0はもはや使われてないアルゴリズムらしく実際に存在する暗号化PDFはすべてV >= 1です。
- Length: integer (省略可)
  - 鍵のビット長です。40, 128, 256が選択できます。省略可能でデフォルトは40bit。ローカルで攻撃できることを考えると暗号強度としては弱いですね。
- CF: dictionary (省略可)
  - 暗号形式などが記述されています。例えばアルゴリズム（RC4, AESv2, AESv3）など。

これ以外にパスワード暗号で使用されるものとして、

- R: number (必須)
  - リビジョン番号。パスワード暗号方式のバージョンのようなものです。
    - 2: V == 1 かつパーミッションが全部無効のとき
    - 3: `2 <= V <= 3 ` かつセキュリティリビジョンが3以上でのみ使えるパーミッションがすべて無効のとき
    - 4: V == 4 のとき
- **P**: integer (必須)
  - PDF文書のパーミッションを表す整数です。ドキュメントを印刷できるとか編集できるとかのフラグがあります。
  - 何ビット目が何のパーミッションかが決まっています。
- **O**: string (必須)
  - オーナーパスワード (未設定のときはユーザーパスワード)、 **ID**, **P** から一方向生成される32バイトの文字列です。
  - 生成式は後述。
- **U**: string (必須)
  - ユーザーパスワード、**ID**, **P**, **O** から一方向生成される32バイトの文字列です。
  - 生成式は後述。
- **EncryptMetadata**: boolean (省略可)
  - メタデータのstreamが暗号化されているかのフラグです。V >= 4のときにだけ暗号鍵生成式に含まれます。
  - 省略時はtrue (V >= 4)

があります。

# パスワードによる暗号化

バージョンによって若干異なりますが、PDF の暗号化に使えるのは RC4 または AES です。

## PDF の鍵生成ロジック

暗号化の鍵にはトレーラーの `/ID` (array of string) の第一要素、およびトレーラーの `/Encrypt` (dictionary) 内の `/Filter` 辞書に含まれる **O** (string), **P** (integer), **EncryptMetadata** が使用されます。
**O** は オーナーパスワード （未設定時はユーザーパスワード) から生成したデータ, **P** は PDFの `permission` です。

### 暗号化のベースとなる鍵の生成

R <= 4の場合、暗号化のベースとなる鍵の生成は次のように行います。

1. パスワードを 32 バイトにします。長い場合は先頭 32 バイト、短い場合は 32 バイトになるように以下の固定の文字列で埋めます。
   1. `<28 BF 4E 5E 4E 75 8A 41 64 00 4E 56 FF FA 01 08 2E 2E 00 B6 D0 68 3E 80 2F 0C A9 FE 64 53 69 7A>`
2. 32 バイトの文字列 **O** を①に結合します。
3. パーミッション **P** をリトルエンディアン形式での32bit符号なし整数 (4バイト) にして結合します。
4. **ID** (array of string) の最初の要素を結合します。
5. (R == 4の場合) **EncryptMetadata** が `false` (メタデータを暗号化しない) 場合、 `0xFFFFFFFF` (4バイト) を結合します。
6. 結合した文字列のMD5ハッシュ値を取ります。
7. (R >= 3の場合) ⑥のハッシュ値 (128bit) または先頭の40bit を新たなMD5の入力としてMD5ハッシュ値 またはハッシュ値先頭の40bit を取る処理を50回繰り返します。
8. こうして得られたMD5ハッシュ値 (128bit) または先頭の40bit が暗号化のための鍵となります。

以下はRubyでR <= 4での暗号化の鍵を計算するサンプルコードです。

```ruby:generate_old_encrypt_key.rb
require 'openssl'

$PADDING = ['28BF4E5E4E758A4164004E56FFFA01082E2E00B6D0683E802F0CA9FE6453697A'].pack('H*')

def pad_password password
    data = password[0, 32]
    data << $PADDING[0, 32-password.length]
end

def generate_old_encrypt_key id, o, p, r, encrypt_metadata, password, length
    data = pad_password(password) # 32バイトに丸める
    data << o
    data << [p].pack('l') # Pのリトルエンディアン形式での32bit符号なし整数
    data << id
    data << [0xFFFFFFFF].pack('l') if !encrypt_metadata && r >= 4
    key = OpenSSL::Digest::MD5.digest(data)
    if r >= 3
      50.times do
        key = OpenSSL::Digest::MD5.digest(key)
        key = key[0, length/8]
      end
    end
    key[0, length/8]
end
```

```ruby
require_relative 'generate_old_encrypt_key'

# 適当なPDFから取った値から計算してみる
ID = ['921da799d71f3aa98ca93d50ac3e4baf'].pack('H*')
R = 4 # Security Handler Revision
Length = 128 # Key Length (bit)
O = ['bac1e487bed9fdc0e586c32c124bd7a6bc0121df9639a3052c75b239893fa00c'].pack('H*')
P = -4
encrypt_metadata = true
password = 'testtest'

key = generate_old_encrypt_key ID, O, P, R, encrypt_metadata, password, Length
key = key[0, Length/8]
puts key.unpack('H*') # 1a2a3335a13f6a5beae15fabb6e24883
```

R >= 5からはMD5, RC4に変わってSHA2, AESを用いて鍵を生成するように強化されています。

上記の方法で生成された **Length** ビットの鍵をベースに使ってPDF内のinteger, boolean以外の値を RC4 または AES で暗号化します。[^all_encrypt]
[^all_encrypt]: 暗号化対象の例外は鍵生成に使われる **ID** (array of string) と **Encrypt** (dictionary) 内のstring それと stream内のstring (stream自体がencryptされてるので二重に暗号化する必要はない)。

ただし、鍵はオブジェクトごとに変更されます。

#### オブジェクトの暗号鍵の生成方法と暗号化

R <= 4でのオブジェクトの暗号鍵の生成は次のように行います。

1. ベース鍵を用意します。
2. オブジェクトのobject numberの下位24bit (3バイト) と generation numberの下位16bit (2バイト) をリトルエンディアン形式で鍵の後ろに結合します。
3. AESの場合、"sAlT" (4バイト) を鍵の後ろに結合します。
4. MD5ハッシュ値を取ります。
5. MD5ハッシュ値の先頭 (ベース鍵長 + 5) バイト がオブジェクトの暗号鍵となります。16バイトを超える場合は先頭16バイトを暗号鍵とします。

AESの場合はCBCモードを選び、ivにはランダムな文字列の先頭16バイトを使用します。
得られた暗号鍵で各オブジェクト内のstring, streamを暗号化してPDFとして出力します。
（復号の場合はその逆）

## パスワードの認証に使う **O** と **U** の生成

ユーザーパスワード、オーナーパスワードが正しいかの確認を行う **U**, **O** という２つの値が Encrypt 辞書に入っています。

この値のうち、先述の通り **O** はデータの復号に使われますが、 **U** は復号時には使われません。

せっかく調べたので一応 **O**, **U** の2つの生成方法を書いておきます。

### **O** (owner password value) の生成

**O** (owner password value) の生成は次のように行います。

1. オーナーパスワードを暗号化の鍵生成の1と同様の方式で32バイトにします。もしオーナーパスワードが空なら代わりにユーザーパスワードを使います。
2. MD5ハッシュ値を取ります。Security handlerのリビジョン3以降の場合、ハッシュ値を新たなMD5の入力してハッシュを取り直す処理を50回繰り返します。
3. user passwordを鍵生成の1と同様の方式で32バイトにします。
4. ②のハッシュ値の **Length** ビット (40 or 128) を鍵としたRC4で③を暗号化します。
5. Security handlerのリビジョン3以降の場合、④を19回繰り返します。つまりRC4で暗号化した結果を新たな鍵でRC4暗号化する処理を繰り返します。鍵は④の鍵をそのまま使うのではなく、④の鍵を繰り返しごとに1〜19の1バイトでXORしたものを使います。

以下はRubyで **O** の値を計算するサンプルコードです。

```ruby:calculate_old_user_password_value.rb
require 'openssl'
require_relative 'generate_old_encrypt_key'

def encrypt data, password
    cipher = OpenSSL::Cipher::RC4.new.encrypt
    cipher.key = password
    data = cipher.update(data)
    data << cipher.final
    data
end

def calculate_old_owner_password_value id, r, owner_password, user_password, length
    password = OpenSSL::Digest::MD5.digest(pad_password(owner_password || user_password))
    50.times { password = OpenSSL::Digest::MD5.digest(password) } if r >= 3
    password = password[0, length / 8]
    
    data = encrypt(pad_password(user_password), password)
    if r >= 3
        19.times do |i|
            new_password = password.unpack('C*').map{|c| c ^ (i+1) }.pack('C*')
            data = encrypt(data, new_password)
        end
    end
    data
end
```

```ruby
# 適当なPDFから取った値から計算してみる

ID = ['921da799d71f3aa98ca93d50ac3e4baf'].pack('H*')
R = 4 # Security Handler Revision
Length = 128 # Key Length
owner_password = nil
user_password = 'testtest'
O = ['bac1e487bed9fdc0e586c32c124bd7a6bc0121df9639a3052c75b239893fa00c'].pack('H*') # 今回の計算の正解値

owner_password_value = calculate_old_owner_password_value(ID, R, owner_password, user_password, Length)
puts owner_password_value.unpack('H*') # bac1e487bed9fdc0e586c32c124bd7a6bc0121df9639a3052c75b239893fa00c
```

このため、 `O` はオーナーパスワードとユーザーパスワードが決まった時点で必ず一つの値になります (その他R, Lengthに依存)。

オーナーパスワードは **P** が改ざんされてないことの確認はできますが、復号には一切利用しません。復号時に使用するのはあくまでオーナーパスワードから事前に生成された **O** の値だけです。ジサし
[Apache PDFBoxのCLIのDecryptツール](https://pdfbox.apache.org/2.0/commandline.html#decrypt)では復号にユーザーパスワードのみを渡すことで復号できますが、「NOTE: You must have the owner password to decrypt the document!」（注意：オーナーパスワードを保持していないPDFを復号してはいけません！）と書いてあります。技術的に可能ではあってもオーナーパスワードをもっていないPDFを復号することのないようにしてください。

### **U** (user password value) の生成

**U** (user password value) はリビジョン `R` によって異なります。

#### `R` < 3 の場合の **U** の生成

1. ユーザーパスワード, **O**, **ID**, **P** から暗号化の鍵の生成式で **Length** ビット (40 or 128) の値を生成します。
2. ①の **Length** ビット (40 or 128) を鍵としたRC4で「暗号化の鍵の生成」の32バイトの固定文字列を暗号化します。
3. 暗号化された32バイトの文字列が **R** < 3 での **U** の値です。

#### `R` == 4 の場合の **U** の生成

1. ユーザーパスワード, **O**, **ID**, **P** から暗号化の鍵の生成式で **Length** ビット (40 or 128) の値を生成します。
2. ①の **Length** ビット (40 or 128) を鍵としたRC4で「暗号化の鍵の生成」の32バイトの固定文字列と **O**, **P** のリトルエンディアン32bit、**ID** 文字列を結合します。
3. **encrypt_metadata** が false であれば `0xFFFFFFFF` を32bitで②に結合します。
4. ③の結合文字列のMD5ハッシュ値を取ります。
5. MD5ハッシュ値をさらにMD5ハッシュする、という作業をもう50回繰り返します。最終的な出力である128bitのMD5ハッシュ値が **R** == 4 での **U** の値です。

以下はRubyでR = 4での **U** の値を計算するサンプルコードです。

```ruby:generate_old_encrypt_key.rb
require 'openssl'

$PADDING = ['28BF4E5E4E758A4164004E56FFFA01082E2E00B6D0683E802F0CA9FE6453697A'].pack('H*')

def pad_password password
    data = password[0, 32]
    data << $PADDING[0, 32-password.length]
end

def generate_old_encrypt_key id, o, p, r, encrypt_metadata, password
    data = pad_password(password) # 32バイトに丸める
    data << o
    data << [p].pack('l') # Pのリトルエンディアン形式での32bit符号なし整数
    data << id
    data << [0xFFFFFFFF].pack('l') unless encrypt_metadata
    key = OpenSSL::Digest::MD5.digest(data)
    50.times { key = OpenSSL::Digest::MD5.digest(key) } if r >= 3
    key
end
```

```ruby:calculate_old_user_password_value.rb
require 'openssl'
require_relative 'generate_old_encrypt_key'

def encrypt data, password
    cipher = OpenSSL::Cipher::RC4.new.encrypt
    cipher.key = password
    data = cipher.update(data)
    data << cipher.final
    data
end

def calculate_old_user_password_value id, o, p, r, encrypt_metadata, password, length
    key = generate_old_encrypt_key id, o, p, r, encrypt_metadata, password
    key = key[0, length/8] # 先頭 length bit
    
    hash = OpenSSL::Digest::MD5.digest($PADDING + id)
    data = encrypt(hash, key)
    19.times do |i|
        new_key = key.unpack('C*').map{|c| c ^ (i+1) }.pack('C*')
        data = encrypt(data, new_key)
    end
    data << 0.chr * 16
end
```

```ruby
# 適当なPDFから取った値から計算してみる

ID = ['921da799d71f3aa98ca93d50ac3e4baf'].pack('H*')
R = 4 # Security Handler Revision
Length = 128 # Key Length (bit)
O = ['bac1e487bed9fdc0e586c32c124bd7a6bc0121df9639a3052c75b239893fa00c'].pack('H*')
U = ['b9ef1c7024795c3a6c0ec34c37fe305800000000000000000000000000000000'].pack('H*') # 今回の計算の正解値
P = -4
encrypt_metadata = true
password = 'testtest'

user_password_value = calculate_old_user_password_value ID, O, P, R, encrypt_metadata, password, Length
puts user_password_value.unpack('H*') # b9ef1c7024795c3a6c0ec34c37fe305800000000000000000000000000000000
```

## 公開鍵による暗号化と秘密鍵による復号

PDFではパスワードによる暗号化以外にも公開鍵・秘密鍵のペアによる電子署名および暗号化も行うことができます。

暗号化は公開鍵で行い、秘密鍵で復号するという手順を取ります。

### Adobe Acrobat以外での公開鍵による PDF の暗号化

通常はAdobe Acrobatで公開鍵による暗号化すると思うのですが、Adobe Acrobat をもっていない環境でも公開鍵による暗号化を行うことは可能です。
用意するものは鍵を作成するツールと暗号化するツールの２つ。今回は「Acrobat Reader DC」と「[Apache PDFBox](https://pdfbox.apache.org/)」を用いて公開鍵による暗号化 PDF を作成します。

#### Acrobat Reader DCで公開鍵・秘密鍵の作成

1. Acrobat Reader DC の環境設定を開き、「署名」→「ID と信頼済み証明書」の「詳細」から鍵を作成します。
   1. 6 文字以上でパスワードをつけろ、と聞かれるので入力します。
1. 作成した鍵の「書き出し」を選び、公開鍵 ( `***.cer` ) を保存します。

#### Apache PDFBoxで暗号化

保存した公開鍵を使って PDF を暗号化します。暗号化の際は公開鍵ファイル ( `***.cer` ) のみを指定して、パスワードは設定しません。

```console
$ java -jar pdfbox-app-2.0.12.jar Encrypt -keyLength 128 -certFile <保存した公開鍵.cer> input.pdf output.pdf
```

先程鍵を作成した Acrobat Reader DC で暗号化した PDF を開くと署名情報が表示されるとともにパスワード入力を求められます。
秘密鍵作成時に設定したパスワードを入力することで PDF を見ることができます。

なお、公開鍵による PDF はあまり普及していないせいなのかよくわからないのですが、Mac の Preview アプリ では白紙で表示され、Google Chrome / Safari では読み込みに失敗します。
(いずれもパスワードによる暗号化PDFは開けます）

GIMP ではなぜかパスワードを聞かれますが秘密鍵なのでパスワードの入力しようがありません。

# 本題：生パスワードを保存せずにサーバーはパスワードで復号できるPDFを生成できるのか？

さて、長々とPDFの暗号化の仕組みについて紹介してきましたが、ここで冒頭の社内での話題にありました「生のパスワードをサーバーで保存していなくてもログインパスワードで暗号化されたPDFの生成はできるのか？」という話題に戻ります。

PDFオブジェクトの暗号鍵の生成に必要なのはこの2つです。

1. ベース鍵
2. 間接オブジェクトのオブジェクト番号と世代番号

オブジェクト番号と世代番号はオブジェクトの暗号化の鍵には使われますがそれ自体は暗号化されません。つまり、ベース鍵さえあればすべてのオブジェクトを暗号化できます。

そしてそのベース鍵の生成に必要なのは、次の5つです。

1. ユーザーパスワード
2. ファイル識別子 **ID**
3. オーナーパスワード（未設定時はユーザーパスワード）から生成した **O**
4. パーミッション **P**
5. メタデータが暗号化しているかの **EncryptMetadata**

この5要素から生成したデータのMD5ハッシュ値がオブジェクトを暗号化するための鍵となるわけです（PDF 1.7の場合）。
この鍵であるMD5ハッシュ値をPDF生成の前に作成しておければパスワード自体がなくてもPDFを暗号化することができます。

幸い、このうちファイルごとに変わる **ID** は生成ルールがないため、今回は都合のよいように生成することにしましょう。
例えば最初にアカウント作成時に100回なり1000回なりの **ID** および ベース鍵を生成し保存しておくことで
一応「生パスワードは保存してない」といえるんじゃないでしょうか？
ちょっと工夫して、**ID** 自体はログインユーザーの情報から生成可能な数列にしてしまい、保存するのはベース鍵だけでいいようにしてもいいかもしれません。

・・・長かった割にオチがちょっと強引でしたね😊

ほんとうは公開鍵・秘密鍵による暗号化PDFであれば公開鍵のみで暗号化が可能なため、公開鍵PDFが普及するといいんでしょうけど。

# 終わりに

PDFは長い歴史を経ているせいなのかはたまたAdobeの中の人の趣味なのかわからないんですが、
暗号鍵を作るときにMD5ハッシュ値再生成を50回決め打ちでやってたり、
19回決め打ちでやってたりと謎ルールが多くて調べてておもしろかったです。

またデータを守るユーザーパスワードと、パーミッションを守るオーナーパスワードの２つを用意しているのもデジタル文書としての役割を果たす上で生まれてきたのだと思います。その割にはユーザーパスワードさえわかればユーザーパスワードがなくとも技術的には復号できてしまう仕組みはどうかと思いますが...。

あと、しれっと暗号鍵生成式のとこで書いてるんですがPDF 1.7でのパスワードには実は32バイト制限があるため、
例えばmacOSのPreviewアプリで暗号化PDFを作るときに32バイトより長いパスワードを使っても
何も警告されないですし、当然復号時も先頭32バイトが合っていれば復号できてしまうのは知らないとちょっと怖いなと思ったりしました。
2.0の仕様書を見てないのでちゃんとは確認できてないんですが、PDFBoxのコードを見ると **R** >= 5ではパスワードをちょん切らずSHA2に全文字分突っ込んでいました。
もし32バイトより長いパスワードを使いたい場合には必ず **R** >= 5 (あるいは256bitの暗号鍵) に対応した暗号化ツールを使いましょう。公開鍵暗号化のところで紹介したApache PDFBoxはもちろん対応しています。

https://svn.apache.org/viewvc/pdfbox/tags/2.0.12/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java?view=markup

また暗号化ソフトによっては弱い暗号がしれっと使われていることもあるので注意が必要です。
たとえばPreviewアプリでは128bit AESが使われますが、LibreOffice 6.1.3では128bit RC4が使われていました。
暗号化されたPDFファイルにどのような暗号が使われているかはPDFBoxなどのライブラリを使ったりAcrobat Reader DCでファイルのプロパティから確認することが可能です。

なお、当たり前ですがPDF用の暗号化用ハッシュ値はそのまま保存するのはパスワードの平文保存と同じリスクとなります。
もしこの記事のように「ログインパスワードで開ける暗号化PDFを作成したい」ために暗号鍵を事前に生成しておく場合は、当然暗号鍵を暗号化するなり守らないとしないといけませんのでご注意ください。

# 動作確認環境

- macOS Preview.app 10.1
- LibreOffice 6.1.3
- Adobe Acrobat Reader DC 19.008.20080
- Apache PDFBox 2.0.12

# 参考にした記事のリンク

- [PDF Reference and Adobe Extensions to the PDF Specification | Adobe Developer Connection](https://www.adobe.com/devnet/pdf/pdf_reference.html)
- [詳細 PDF 入門 ー 実装して学ぼう！PDF ファイルの構造とその書き方読み方](https://itchyny.hatenablog.com/entry/2015/09/16/100000)
- [Portable Document Format - Wikipedia](https://ja.wikipedia.org/wiki/Portable_Document_Format)
- [デジタル ID を使用して暗号化された PDF のワークフロー](https://helpx.adobe.com/jp/acrobat/kb/cq03242102.html)