CloudKMSとHSMで重要なデータを暗号化する

## 概要
GCPのCloudKMSとHSMを利用して、DBなどに保存する情報を暗号化する方法を記述しています。

## 想定されるユースケース
1. 画像データ、バイナリデータ(鍵や生体情報など)を暗号化して安全に保存しておきたい。

## 想定される課題
あるデータを安全に保存したい場合、まず思いつくのは対称暗号による暗号化だと思います。
例えば、あるデータをAESを利用して暗号化し、DBに保存しておけば、DBがハックされてデータが流出したとしても安全だと考えられるかもしれません。
いいえ、本当にそうでしょうか。
このような場合、私は2つの課題があると思います。

- 課題１: 攻撃者がDBへアクセスできる間に、私のサービスへ好きなデータを登録でき、そのデータが暗号化されてDBに保存された際にそれを特定できる場合、暗号化前と暗号化後のデータを利用して暗号鍵を推測できるかもしれません(選択平文攻撃)。
- 課題２: 攻撃者がDBへアクセスできた場合、私はどのようにして暗号鍵を保存するべきでしょうか。

## CloudKMSとは
この2つの課題を解決するのがCloudKMSです。

[Cloud Key Management Service  \|  Cloud KMS  |  Google Cloud](https://cloud.google.com/kms)

> **スケーラブル、自動、高速**
数百万もの暗号鍵の保持が可能で、データ暗号化の粒度レベルも選択できます。鍵の定期的な自動ローテーションを設定すると、新しいプライマリ バージョンを使用してデータを暗号化し、1 つの鍵バージョンでアクセスできるデータ範囲を制限できます。アクティブな鍵バージョンはいくつでも保持できます。Google Cloud Platform は低レイテンシなので、瞬時に鍵にアクセスできます。

CloudKMSには暗号鍵の自動ローテーション機能があります。
暗号鍵のローテーション期間(例えば90日など)を設定し、その期間が過ぎたら新しい暗号鍵を生成して使用します。
過去の暗号鍵は保持されており、過去の暗号化データは対応する暗号鍵を自動で見つけて復号してくれます。

この機能により「課題１」が緩和されます。
攻撃者が暗号鍵を推測することに成功したとしても、ローテーションによって一部のデータしか復号することができないでしょう。
また、過去の暗号鍵で暗号化されたデータに対して、暗号化前と暗号化後の対応するデータを入手することが不可能になります。
よって、ローテーション期間に対して十分に古いデータは暗号鍵を推測することが困難になります。

> **暗号鍵の管理**
Cloud KMS はクラウドでホスティングされる鍵管理サービスです。このサービスを利用することで、オンプレミス型と同じ方法でクラウド サービスの暗号鍵を管理できます。AES256、RSA 2048、RSA 3072、RSA 4096、EC P256、EC P384 の各規格に対応し、暗号鍵の生成、使用、ローテーション、破棄をサポートしています。Cloud KMS は Cloud IAM と Cloud Audit Logging に統合されているため、個々の鍵の権限を管理し、それらの用途をモニタリングできます。Cloud KMS を使用すれば、Google Cloud Platform に保存するシークレットや機密データを保護できます。

CloudKMSは暗号鍵を安全に保管してくれます。
CloudKMSから暗号鍵を取り出すことはできず、KMSのAPIを利用してデータを渡すと、暗号化されたデータが手に入ります。

この機能により「課題２」が解決されます。
鍵の保存先としてCloudHSMを利用する事もできます。その場合は、さらに安全に暗号鍵を保存できるでしょう。
(HSMについてはこれだけです。利用する際にもオプションとして選択するだけで利用できます。機能については以下の記事を参照してください。)

[Cloud HSM - クラウドホスト型ハードウェア セキュリティ モジュール  \|  Hardware Security Module](https://cloud.google.com/hsm)

## 実装方法
詳細な実装方法については Google 公式の素晴らしいドキュメントがあるので割愛します。

[対称鍵によるデータの暗号化と復号  \|  Cloud KMS ドキュメント  |  Google Cloud](https://cloud.google.com/kms/docs/encrypt-decrypt?hl=ja)

### 暗号化できるデータのサイズは 64KiB まで

[Method: cryptoKeys.encrypt  \|  Cloud KMS ドキュメント  |  Google Cloud](https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/encrypt?hl=ja)

> Required. The data to encrypt. Must be no larger than 64KiB.
> 
> The maximum size depends on the key version's protectionLevel. For SOFTWARE keys, the plaintext must be no larger than 64KiB. For HSM keys, the combined length of the plaintext and additionalAuthenticatedData fields must be no larger than 8KiB.

こちらのAPIリファレンスにあるように、 `Encrypt()` に入力可能なデータのサイズは 64KiB までです。
(保管先をHSMにした場合は 8KiB までです)
なんということでしょう。
これでは、僕の自慢のアイコン(3.4MB)は暗号化できません。

> Cloud KMS を使用すれば、Google Cloud Platform に保存するシークレットや機密データを保護できます。
> 

とあるように、 CloudKMS のコンセプトはシークレット(秘密鍵)の安全な保存なのです。
となれば、秘密鍵を預けるしかありません。

対称暗号の秘密鍵を作成し、それを用いてデータを暗号化します。
そして秘密鍵を CloudKMS を利用して暗号化し、暗号化した秘密鍵を保管しておきます。
こうすることで、どんな大きさのデータであっても暗号化できます。

### 秘密鍵はデータごとに生成する
CloudKMS で暗号化する秘密鍵は使い回すことができますが、それでは CloudKMS を利用する意味がなくなってしまいます。
その秘密鍵は、攻撃者が暗号化前と暗号化後の対応するデータを入手することによって推測されてしまう可能性があるからです。

データごとに秘密鍵を生成する場合にも同じように、攻撃者はその秘密鍵を推測できるかもしれません。
しかし、その鍵によって復号できるのは自身のデータだけになります。
また、「推測された秘密鍵」を利用して「CloudKMS に保存された秘密鍵」を推測できるかもしれません。
しかし、「CloudKMS に保存された秘密鍵」は自動ローテーションされているため、それによって復号できる秘密鍵は限られます。
攻撃者が解読したいデータが十分に古いものであれば、その鍵を推測したり、解読するのは非常に困難になります。

以上を踏まえると、以下のような実装になるでしょう。

```js
// make secret and encryption image
const plainSymmetricKey = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv(algorithm, plainSymmetricKey, iv);
let cipherImage = cipher.update(image);
cipherImage = Buffer.concat([cipherImage, cipher.final()]);

// encrypt secret by kms
const client = new kms.KeyManagementServiceClient();
const cryptoKeyPath = client.cryptoKeyPath(
    kmsConfig.projectId,
    kmsConfig.locationId,
    kmsConfig.keyRingId,
    kmsConfig.cryptoKeyId
);
const responses = await client.encrypt({
    name: cryptoKeyPath,
    plaintext: plainSymmetricKey,
});
const cipherSymmetricKey = responses[0].ciphertext

// save image and secret
await Image().create({
    image: cipherImage,
    key: cipherSymmetricKey,
    iv: iv,
});
```

## まとめ
- CloudKMS を利用することでデータの流出に備えることができる
- CloudKMS に保管された鍵で暗号化できるデータは 64KiB まで
- データを暗号化するためのシークレットはデータごとに生成する
- 自動ローテーション機能でバージョンを意識せずにシークレットを暗号化して保存できる