暗号化って結局どれを選べば良いの？OWASP Cryptographic Storage Cheat Sheetまとめ

# はじめに
WEBアプリケーションを開発するときに、「個人情報は暗号化して保存すること」といった要件を見かけます。一口に暗号化といっても、共通鍵暗号、公開鍵暗号、AES、DES、RSA、SHA、暗号モード、ブロック暗号と色々なワードが出てきて、どれを選んだら良いかわからなくなります（した）。本記事では、その時に調べてわかったことを備忘録的に書きます。

本記事では、以下に２つを参考にしています。

- [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)
- [CRYPTREC暗号リスト
(電子政府推奨暗号リスト)](https://www.cryptrec.go.jp/method.html)

なお、パスワードは、本記事で紹介する暗号化して保存することはしないでください。パスワードはハッシュ化して保存します。詳しくは、[OWASP ASVS 4.0 V2.4 からパスワードの保存を考える](https://qiita.com/koujimatsuda11/items/1f7909ea8b626554f556)という記事で紹介してます。
[OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)の方がもちろん詳しい。

# 暗号化の前に
OWASP Cryptographic Storage Cheat Sheetにも書かれてますが、もう一度「そのデータいる？」を検討しましょう。暗号化する以前に、保存しないのが一番です。
例えば、クレジットカード情報は多くのECサイトで決済時に入力しますが、ECサイトはクレジットカード番号を保存しません。保存しなくても良い仕組みができています。

可能な限り、暗号化しなければならないようなデータの保管は、避けてください。

# データをどのレベルで暗号化するか？
まず、考えるべきは、どのレベルで暗号化するか？

- アプリケーションレベル
- データベースレベル
- ファイルシステムレベル（BitLockerなど）
- ハードウェアレベル（HDDやSSDの暗号化）

例えば、ハードウェアレベルで暗号化した場合、HDDを持ち出すような物理的な攻撃には強いですが、リモートでアクセスした場合には、意味をなさないです。また、アプリケーションレベルで暗号化してデータベースに保存する場合、SQLレベルで検索することはできないでしょう。常にアプリケーションからそのデータを閲覧する以外に方法はありません。また、その場合も内部犯行による持ち出しの場合には、意味をなさないかもしれません。

# 暗号アルゴリズム
最適な暗号アルゴリズムは、常に変わります。[CRYPTREC暗号リスト](https://www.cryptrec.go.jp/list/cryptrec-ls-0001-2012r4.pdf)の変更履歴を見ると、年に1回見直されてます。今日安全な暗号アルゴリズムも、明日には攻撃方法が発見されているかもしれません。なので、**変更できる**ようにしておくことが重要です。


## 共通鍵暗号（Symmetric encryption）
暗号化と復号に同一の（共通の）鍵を用いる暗号方式です。暗号化と聞いて、最も思いつきやすい方式だと思います。

![FireShot Capture 001 - 無題のプレゼンテーション - Google スライド - docs.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/d5e6be16-6e91-04e0-94fe-6ea9aa83becc.png)

後述する公開鍵暗号に比べて、高速で暗号化と復号ができるいうメリットがあるものの、鍵が共通なので、安全な鍵の受け渡しが必要です。鍵さえわかってしまえば復号できるわけなので。また、比較的大きなデータでも暗号化を行うことができます。
例えば、ローカルに個人情報を暗号化して保存するときなどの鍵を受け渡しが必要ない時に使える暗号化方式です。

では、共通鍵暗号の場合、どの暗号アルゴリズムを使えば良いか。

OWASP Cryptographic Storage Cheat Sheetでは、

> AES with a key that's at least 128 bits (ideally 256 bits) and a secure mode

CRYPTREC暗号リスト(電子政府推奨暗号リスト)では、

> 128ビットブロック暗号：　AES、Camellia
> ストリーム暗号：　KCipher-2

なので、２つに記述のある`AES-128`もしくは`AES-256`が良いと思われます。AESが何かなどの細かい説明は端折ります。[wikipedia](https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard)とか見てもらえればと思います。

### secure mode(暗号利用モード)
AESは、ブロック暗号と呼ばれる暗号アルゴリズムですが、直接使用せず、暗号利用モードを使います。暗号利用モードとは、AESなどのブロック暗号を利用して、任意の長さの平文を暗号化できるようにするメカニズムです。

で、この暗号利用モードにもいくつか選択肢があります。

OWASP Cryptographic Storage Cheat Sheetでは、

> Where available, authenticated modes should always be used. The most commonly used authenticated modes are GCM and CCM.
> If GCM or CCM are not available, then CTR mode or CBC mode should be used. 

CRYPTREC暗号リスト(電子政府推奨暗号リスト)では、

> 秘匿モード ：　CBC,CFB,CTR,OFB
> 認証付き秘匿モード ：　CCM,GCM

可能ならば、CCMかGCMも使いましょう。

### 認証付き秘匿モード（CCM,GCM）
認証付き秘匿モードとは、復号する時に元のデータから改竄されていないかを同時にチェックするモードです。

```php:encrypt.php
$key = 'hogehoge';
$cipher = "aes-256-gcm";
$iv_len = openssl_cipher_iv_length($cipher);
$iv = random_bytes($iv_len); // 説明の都合で同じ初期ベクトルを使う

// a → aa
$a = base64_encode(random_bytes(100));
$aa = openssl_encrypt($a, $cipher, $key, 0, $iv, $tag_a);

// b → bb
$b = base64_encode(random_bytes(100));
$ivb = random_bytes($iv_len);
$bb = openssl_encrypt($b, $cipher, $key, 0, $iv, $tag_b);

// bbを戻す時にaの認証タグを使うとfalseになる
$result = openssl_decrypt($bb, $cipher, $key, 0, $iv, $tag_a); // false
```

暗号化した時に同時できる認証タグ（`$tag_a`、`$tag_b`）と暗号文のセットが異なっていれば、復号できなくなります。暗号文が、改竄された場合、復号できなくなるので、機密性と同時に完全性も担保できる優れものです。


### 暗号アルゴリズムと鍵の変更
暗号化を実装する時には、先にも書いたように暗号アルゴリズムが変わることを前提に設計する必要があります。また、鍵についても変わることを前提にした設計にする必要があります。変わった場合にどうするか？

- 全ての暗号文を復号して、新しい暗号アルゴリズムと鍵で暗号化し直す。
- 暗号化の際に、アルゴリズムと鍵のIDを保管しておき、古い暗号アルゴリズムと鍵でも復号できるようにしておき、保管の際に新しい暗号アルゴリズムと鍵を適用する

前者がベストです。ただ、全てを置き換えることは、手間がかかるかもしれません。
そのため、後者のように、暗号文を保存する時に、暗号アルゴリズムのIDと鍵のIDを一緒に保存しておくと良いでしょう。`{1,1}....`みたいな感じです。そうすることで、一気に変更する手間がなくなります。`iv`や`tag`と合わせて、json形式などで保存しておくと良いかもしれません。

```javascript:encrypt.json
{
    "key": 1,
    "cipher": 1,
    "txt": "暗号文",
    "iv": "$iv",
    "tag": "$tag"
}
```

### 暗号学的擬似乱数生成器（CSPRNG）
暗号化とは、ちょっと話がそれますが、暗号化鍵、初期化ベクトル（ストリーム暗号やブロック暗号の任意の暗号利用モードで暗号化する時に、毎回同じ平文から同じ暗号文を生成しないように指定する）、セッションID、CSRFトークン、パスワードリセットトークンなど、ランダムな文字列を生成する時に、乱数を生成します。これらの乱数は、ランダム性が重要となり、推測されてはなりません。そういった時に、暗号学的擬似乱数生成器（CSPRNG）を使います。

PHP7なら、`random_bytes()`, `random_int()`です。PHP5なら、`openssl_random_pseudo_bytes()`です。

なお、擬似乱数生成器（PRNG）は、高速でセキュリティ関連機能以外で利用できます。例えば、ランダムにページの表示順を変えるなどの機能です。

PHPなら`rand()`,`mt_rand()`, `array_rand()`, `uniqid()`です。

その他の言語は、[OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)を参考にしてください。

## 公開鍵暗号（Asymmetric encryption）
暗号化と復号に別の鍵（公開鍵と秘密鍵）を用い、一方の鍵（公開鍵）を公開できるようにした暗号方式です。共通鍵暗号は、共通鍵が漏洩してしまうと、簡単に復号されます。共通鍵の交換に非常にコストがかかりましたが、この問題を解決したのが公開鍵暗号です。

![FireShot Capture 003 - 無題のプレゼンテーション - Google スライド - docs.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/a8ea798a-b086-9a94-78ab-e3dfaef77864.png)

公開鍵で暗号化した暗号文は、対となる秘密鍵でしか復号できません。また、公開鍵から秘密鍵を得ることもできません。なので公開鍵をどれだけ公開しても、暗号が破られることはありません。

実際にやってみたいと思いますが、公開鍵暗号は、どういった暗号アルゴリズムを使ったら良いでしょうか？

OWASP Cryptographic Storage Cheat Sheetでは、

> elliptical curve cryptography (ECC) with a secure curve such as Curve25519. If ECC is not available and RSA must be used, then ensure that the key is at least 2048 bits.

CRYPTREC暗号リスト(電子政府推奨暗号リスト)では、

> 署名：DSA、ECDSA、RSA-PSS、RSASSA-PKCS1-v1_5
> 守秘：RSA-OAEP

ここでは、RSA-OAEPでやってみましょう。

```sh
// 鍵ペアを作成する
$ openssl genrsa 2048 > ./private_key.pem  
$ openssl rsa -pubout < private_key.pem > pub_key.pem
```

```php:rsa.php
// 公開鍵で暗号化
$data =random_bytes(100);
$public_key = file_get_contents(__DIR__ . "/pub_key.pem");
$result = openssl_public_encrypt($data, $crypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING);
```

`$crypted`に暗号文が入ります。`$result`に結果が`bool`で入ります。

なお、公開鍵暗号は、共通鍵暗号に比べて暗号化と復号に時間かかるというデメリットがあリます。さらに鍵長とpaddingの方式によって一度に暗号化できるサイズが決まってます。

鍵長：2048bitの場合は下記でした。

| padding | 平文の長さ |
|:------------------:|:------------------:|
| OPENSSL_PKCS1_PADDING | 245 byte |
| OPENSSL_PKCS1_OAEP_PADDING | 215 byte |

もちろん、長い平文を分割して、暗号化することも可能ですが、通常はめんどくさいんで、共通鍵暗号で暗号化します。
SSL/TLS通信でも、実際の暗号化は共通鍵暗号を使われますが、その共通鍵の交換に公開鍵暗号で鍵交換が行われています。

# 鍵の管理
共通鍵暗号、公開鍵暗号いずれの暗号化方式でも鍵を適切に管理する必要があります。

## 鍵の生成
鍵の生成に、暗号学的擬似乱数生成器（CSPRNG）のように推測されにくいランダムな文字で生成するようにします。共通のワードやフレーズを使うなどはやめましょう。パスワードとは違い、鍵を手入力する必要はないので、本当にランダムなもので良いはずです。

## 鍵のライフタイムとローテーション
鍵は様々なタイミングで変更する必要があります。

- 鍵が漏洩した時
- 鍵を知っている人が退職した時
- 予め定めた期間を経過した時
- 一定量暗号化した時（64ビット鍵の場合は34GB、128ビット鍵の場合は295エクサバイト)
- アルゴリズムに攻撃方法が見つかった時

頻繁に変更することはありませんが、1年以上運用するようなシステムであれば必ず1回ぐらいは変更する必要があるかもしれません。予め設計に組み込んでおく必要があります。変更方法は、「暗号アルゴリズムと鍵の変更」のセクションで記載しました。

## 鍵の保管
鍵の保管は、難しいですね。アプリケーションが、データを常に復号するためには、アプリケーションが鍵にアクセスできる必要がありますが、アプリケーションが侵害された場合は、鍵も盗まれる可能性があります。設定ファイルなどに記載していると盗まれるでしょう。下記に記載する方法は、完全に防御できるわけではないですが、盗まれるのを難しくします。

- 物理HSM
- 仮想HSM
- Amazon KMS や Azure Key Vault

上記のような鍵管理でできない場合でも、下記のことは必ず守るようにしましょう。

- 鍵をアプリケーションの中にハードコーディングしない（設定ファイルに書く）
- 設定ファイルには適切な閲覧権限を与える
- git（などのバージョン管理）に鍵をコミットしない
- 環境変数に鍵を保存しない。誤って公開されてします危険性があります。

特にやりがちなのが、git（などのバージョン管理）に鍵をコミットすることです。コミット履歴にも残るので結構厄介です。

また、鍵はデータとは別に保管する必要があります。例えば、データベースにデータを保管する場合は、鍵はファイルシステムに保管する必要があります。そうすることで、SQLインジェクションでデータを取られても直ちに解読される心配はないでしょう。

### 鍵を暗号化する鍵
データ暗号化するための鍵を暗号化して保管する方法もあります。この場合、少なくとも２つの個別の鍵を管理する必要があります。

- データを暗号化するための鍵（DEK）
- DEKを暗号化するための鍵（KEK）

DEKとKEKの鍵の管理方法の詳細は、Googlの[エンベロープ暗号化](https://cloud.google.com/kms/docs/envelope-encryption)というドキュメントに記載されています。
ちなみに、KEKとDEKを別々に保存できない場合は、このように複雑にすることはかえってデメリットになるかもしれませんが、攻撃者が復号するためのハードルをあげることにはなります。

暗号化手順
1. DEKを生成します。暗号化のたびに新しいDEKを作成します。
2. DEKでデータを暗号化します。
3. 一元管理されたKEKでDEKをラップ（暗号化）します
4. 暗号化データとラップされたDEKを一緒に保存します

復号手順
1. 暗号化データとラップされたDEKを取得します
2. KEKからDEKのラップを解除します
3. 平文のDEKでデータを復号します

ちなみに、Amazon KMSもこのエンベロープ暗号を使われています。

# Amazon KMSを使った実装
Amazon KMSで鍵を管理し、暗号化を実装してみます。暗号化には、共通鍵暗号 AESの256ビットGCMモードを使うことにします。

1. AWSマネジメントコンソール上でCMK（カスタマーマスターキー）を作成します。
2. AWS SDK for PHPを`composer`でインストールします
3. 暗号化処理を実装します
4. 復号処理を実装します

## 1. AWSマネジメントコンソール上でCMK（カスタマーマスターキー）を作成します。
CMKとは、上記でいうKEKです。

[キーの作成]をクリックします
![FireShot Capture 008 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/d43e5522-bf3a-dcb1-552e-de49cad8b878.png)

[エイリアス]に名前を入力して次へ
![FireShot Capture 010 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/16e61c7b-d8ae-573b-abc5-46105df953ab.png)

管理者を選択します。うっかり削除すると復号ができなくなるので、最小限にしましょう。
![FireShot Capture 012 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/4c614a36-168a-9d0e-2dcc-209c1f47e1ce.png)

利用できるアカウントやロールを選択します。AWS SDK for PHPを使うアカウントかロールを選択します。
![FireShot Capture 013 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/ba29dcc3-9f35-6145-e75f-d200a515d4c5.png)

完了をクリックして、完了です。
![FireShot Capture 014 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/49fbee56-0899-e1f3-ac61-609b02f1cdce.png)

できてますね。
![FireShot Capture 015 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/a0618557-49c2-8c6b-2621-6b7d8db21139.png)

クリックして開いた画面のARNをあとで使います。
![FireShot Capture 016 - KMS Console - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343282/9377736e-a4d9-6b57-8071-886c70601cb8.png)

## 2. AWS SDK for PHPをcomposerでインストールします

```sh
composer require aws/aws-sdk-php
```

詳細は、下記を参照してください
https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/getting-started_installation.html

## 3. 暗号化処理を実装します

```php
public function encrypt($message)
{       
    $KmsClient = new Aws\Kms\KmsClient([
        'profile' => 'default',
        'version' => '2014-11-01', // このバージョンを指定するようです
        'region'  => 'ap-northeast-1' // リージョンを指定します
    ]);

    // Generate a data key
    // CMK ID または ARN に指定します。
    $keyId = '';
    $keySpec = 'AES_256';

    $result = $KmsClient->generateDataKey([
        'KeyId' => $keyId,
        'KeySpec' => $keySpec,
    ]);

    $plaintextKey = $result['Plaintext']; // 平文キー
    $encryptedKey = $result['CiphertextBlob']; // 暗号化されたキー

    $cipher = $config["cipher"]["current"]; //aes-256-gcmが入っているものとしてください
    $iv_len = openssl_cipher_iv_length($cipher);
    $iv = random_bytes($iv_len);
    $crypted = openssl_encrypt($message, $cipher, $plaintextKey, 0, $iv, $tag);

    $result = [
        "iv" => bin2hex($iv),
        "cipher_id" => $config["cipher"]["current"]["id"],
        "text" => bin2hex($crypted),
        "key" => bin2hex($encryptedKey),
        "tag" => bin2hex($tag),
    ];
    return json_encode($result); // 暗号文と一緒にiv,key,tagをjson形式で保管
    
}
```

詳細は、[AWS KMS API のプログラミング](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/programming-top.html)を参照してください


## 4. 復号処理を実装します

```php
public function decrypt($crypted)
{
    [ 
        "iv" => $iv,
        "cipher_id" => $cipher_id,
        "text" => $text,
        "key" => $key,
        "tag" => $tag
    ] = json_decode($crypted, true);

    $cipher = $config["cipher"][$cipher_id]; //aes-256-gcmが入っているものとしてください
    $KmsClient = new Aws\Kms\KmsClient([
        'profile' => 'default',
        'version' => '2014-11-01',
        'region'  => 'ap-northeast-1'
    ]);

    $result = $KmsClient->decrypt([
        'CiphertextBlob' => hex2bin($key) // キーを復号
    ]);
    
    $plaintextKey = $result['Plaintext']; // 平文キー
    $message = openssl_decrypt(hex2bin($text), $cipher, $plaintextKey, 0, hex2bin($iv), hex2bin($tag));

    return $message; // 改竄されているとfalseになる
}

```

# まとめ
暗号化の要件があった場合には、自作するのではなくクラウドやミドルウェアが提供する暗号化機能を、まず検討しましょう。安全な暗号化アルゴリズムや鍵を使い続けるためには、労力が必要です。
それでもアプリケーションのレベルで暗号化が必要な場合は、本記事を参考にしてもらえたら幸いです。

# 参考
https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
https://www.cryptrec.go.jp/method.html
https://cloud.google.com/kms/docs/envelope-encryption
https://aws.amazon.com/jp/kms/
https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/programming-top.html