GCP KMS を使って秘密情報を対称暗号化・復号化する

# 概要

KMSを利用して、認証情報などの秘密を暗号化・復号化する方法を紹介します

# サンプルコードと環境

**サンプルコード**:
[「GCP で Google Calendar を CalDAV サーバに複製する」記事](https://qiita.com/coleyon/items/44e278f96ac1a4868f7a)で紹介した [バッチスクリプトのコード](https://github.com/coleyon/gcalapi-caldav-sample) を例にとります。コードを動かしたい人は、この記事よりも [readme.md](https://github.com/coleyon/gcalapi-caldav-sample/tree/kms-enc-dec) 見てもらうと早いです。

* （前記事）対策前のコード: https://github.com/coleyon/gcalapi-caldav-sample/tree/master
* （今記事）対策後のコード: https://github.com/coleyon/gcalapi-caldav-sample/tree/kms-enc-dec ([Diff](https://github.com/coleyon/gcalapi-caldav-sample/commit/1b247827a1d5feaa34a6de3e095a04c6ceee8dc8#diff-5bc02cefb3ea9e27f1a6776eabd1935d))

対策前のコードでは Calendar API を叩くためにGCPサービスアカウントの鍵 `credential.json` ファイルを認証情報として利用しますが、暗号化せず[コードに同梱](https://qiita.com/coleyon/items/44e278f96ac1a4868f7a#2-gcp%E3%81%AB%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%82%AD%E3%83%BC%E3%82%92%E5%BE%97%E3%82%8B)しています。対策後のコードでは、KMSを利用して鍵 `credential.json` の内容を暗号化し、かつ暗号化した鍵をコードに含めない（Cloud Functions実行時に環境変数で与える）ことでセキュリティを向上しています。

**環境**:

灰色アローは手作業、青アローはプログラム処理です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/0333be04-2fd2-f429-ebc7-aa4972892875.png)

1. 生JSON鍵を得てKMSで対称暗号化し、バイナリ暗号鍵を得る
2. バイナリ暗号鍵をASCII化し、Cloud Functions の環境変数にセットする
3. コードはgit、暗号鍵は環境変数をソースとして、Cloud Functions上にデプロイする
4. Cloud Functions上のスクリプトは、暗号鍵をKMS APIで復号化する

# GKE KeyRingを作る

暗号鍵メニューから、キーリングを適当な名前で作ります。今回は、今後も増えるだろうサービスアカウントのクレデンシャルをたくさんぶら下げるためのキーリング名にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/91278dfe-b4da-bf58-7218-5c686764ecb3.png)

# GKE Keyを作る

キーリングにぶら下げるキーを作ります。Calendar APIアクセス用サービスアカウントのJSON形式のクレデンシャルである事を端的に示すキー名で作りました。目的は`対称暗号化/復号化`です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/56ca4632-a0f8-9066-a054-e354e2e9c252.png)
サービスアカウントは強い権限（プロジェクトの管理者）を持たせる事が多いと思いますが、ローテーションを自動でかけて同じ暗号を使い続けないように設定する事もできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/a6f4a5c3-830b-8b1e-4af8-2eb2e79db6b9.png)


# 秘密を暗号化する

作業に先立って、生クレデンシャルJSONファイルは、非公開のStorage `private-bucket` を介してアップロードします。

暗号化はCloudShellで作業をします。サービスアカウントのJSON形式生クレデンシャル`credentials.json`を暗号化して、バイナリファイル`credentials.json.enc` を得ます。

```bash
$ gsutil cp gs://private-bucket/credentials.json ./
$ gcloud kms encrypt \
    --plaintext-file=./credentials.json \
    --ciphertext-file=credentials.json.enc \
    --location=global \
    --keyring=service-account-credentials \
    --key=gcal-api-json-cred \
    && rm ./credentials.json
```


# 暗号化文字列を得る

アプリケーションで取り扱いやすい様にPythonで`credentials.json.enc`を文字列に変換します。

```bash
$ python
>>> import base64, io
>>> with io.open('credentials.json.enc', 'rb') as f:
>>>     base64.b64encode(f.read()).decode('ascii')
QOsAErqw....bI=   # 暗号化されたクレデンシャル文字列
[1]+  Stopped                 python
$ exit
logout
There are stopped jobs.
$
```

# 秘密を復号化する

次のようなコードでもって、復号化して使います。

```python
import json
from base64 import b64decode
from google.oauth2 import service_account
from googleapiclient.discovery import build

def decrypt_credectial():
    kms_client = build('cloudkms', 'v1')
    key_path = 'projects/{pj}/locations/{loc}/keyRings/{key_ring}/cryptoKeys/{key}'.format(
        pj='GoogleのプロジェクトID',
        loc='KMSキーリングのロケーション。本例だとglobal',
        key_ring='KMSキーリング名',
        key='KMSキー名')
    crypto_keys = kms_client.projects().locations().keyRings().cryptoKeys()
    kms_request = crypto_keys.decrypt(name=key_path, body={'ciphertext': '暗号化クレデンシャル'})
    kms_response = kms_request.execute()
    return json.loads(b64decode(kms_response['plaintext'].encode('ascii')))

creds = service_account.Credentials.from_service_account_info(
    decrypt_credectial(),
    scopes=[
        'https://www.googleapis.com/auth/calendar.events.readonly',
        'https://www.googleapis.com/auth/calendar.readonly'
    ]
).with_subject('username@gsuite-domain')
```

前記事のコードと比べて[このような違い](https://github.com/coleyon/gcalapi-caldav-sample/commit/1b247827a1d5feaa34a6de3e095a04c6ceee8dc8#diff-5bc02cefb3ea9e27f1a6776eabd1935d)があります。生クレデンシャルを文字列やファイルとしてソースコードに含める必要が無くなって、安心感が出ました。