AWS KMSを利用したOS内でのファイル暗号化・復号の検証

## はじめに
[前回記事](https://qiita.com/Mouflon_127000/items/b62385f88b59ed22d6e8)で行ったOpenSSLによるファイル暗号化・復号検証について、OS内で保管している鍵ではなく、AWS KMSを利用する方式で検証を行った。
CMKを直接利用して暗号化・復号を行うパターンと、エンベロープ暗号化方式による暗号化・復号の2パターンを検証した。

## 環境・事前準備
OS: AmazonLinux 2023
暗号化前のプレーンテキスト:

```text:plain-document.txt
Hello World !!
KMS encryption test.
```

### CMKの作成
以下の設定でCMKを作成する。
- キータイプ: 対称
- キーの使用法: 暗号化および復号化
- キーマテリアルオリジン: KMS
- リージョンごと: 単一リージョンキー
- キーエイリアス名: kmskey-test

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/3f7226f2-02e0-446d-8207-4575034f3742.png)

リソースポリシーはEC2インスタンスから暗号化・復号を行えるように、キーユーザーとしてEC2インスタンスにアタッチしているIAMロールを指定する。

```json:キーポリシー
{
  "Id": "key-consolepolicy-3",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Enable IAM User Permissions",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<アカウントID>:root"
      },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "Allow use of the key",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<アカウントID>:role/<EC2ロール名>"
      },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:ReEncrypt*",
        "kms:GenerateDataKey*",
        "kms:DescribeKey"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Allow attachment of persistent resources",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<アカウントID>:role/<EC2ロール名>"
      },
      "Action": [
        "kms:CreateGrant",
        "kms:ListGrants",
        "kms:RevokeGrant"
      ],
      "Resource": "*",
      "Condition": {
        "Bool": {
          "kms:GrantIsForAWSResource": "true"
        }
      }
    }
  ]
}
```

## CMKを直接利用して暗号化・復号
作成したCMKを直接指定して暗号化を行う。
暗号化の流れは以下の通り。

```bash
# ディレクトリ初期状態
$ ls
plain-document.txt

$ cat plain-document.txt
Hello World !!
KMS encryption test.

# CMKを指定して暗号化し"document.encrypted"として保存
# --plaintextはBase64でエンコードされたファイルを指定する必要があるため、"fileb://"で指定。
$ aws kms encrypt \
--key-id alias/kmskey-test \
--plaintext fileb://plain-document.txt \
--output text \
--query CiphertextBlob | base64 -d > document.encrypted

$ ls
document.encrypted  plain-document.txt

$ cat document.encrypted
<暗号化されたランダム文字列>
```

復号の流れは以下の通り。
```bash
# CMKを指定して復号し"document.decrypted"として保存
$ aws kms decrypt \
--ciphertext-blob fileb://document.encrypted \
--output text \
--query Plaintext | base64 -d > document.decrypted

# 復号後ファイル確認
$ cat document.decrypted
Hello World !!
KMS encryption test.

$ diff plain-document.txt document.decrypted
(差分なし)
```

### AWS CLIコマンド詳細
`aws kms encrypt`を実行すると以下のようなリターンがある。

```json
{
    "CiphertextBlob": "AQICAHg8vYqqonEW2/d3vac67xEkAbPr0yFxd5jZqfAApummIQHOtAR7AG8mzbpNdvgGkZYJAAAAgzCBgAYJKoZIhvcNAQcGoHMwcQIBADBsBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDP6GbAyX7Itr29PoVQIBEIA/0N04+xpzIEqxquXz7uNwpVc7mzHgDFRqcQ312tuEVBWye/7XqYe3M9Cwz4KzXgvmfTqgfFYnsFMrMb3bmzec",
    "KeyId": "<使用したキーID>",
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT"
}
```

`CiphertextBlob`の部分が`--plaintext fileb://plain-document.txt`で指定したファイルの暗号化後のデータである。
これをBase64でデコードして"document.encrypted"として保存している。
復号についてはこれの逆で`aws kms decrypt`を実行すると以下のようなリターンがある。

```json
{
    "KeyId": "<使用したキーID>",
    "Plaintext": "SGVsbG8gV29ybGQgISEKS01TIGVuY3J5cHRpb24gdGVzdC4K",
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT"
}
```

`Plaintext`の部分が復号されBase64でエンコードされたデータである。
これをBase64でデコードして"document.decrypted"として保存している。

https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/example_kms_Encrypt_section.html

https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/example_kms_Decrypt_section.html

## エンベロープ暗号化・復号
CMKを直接指定した暗号化・復号で利用可能なファイルサイズは最大で4KBという制限がある。
エンベロープ暗号化方式を採用することでこの制限は回避できる。

エンベロープ暗号化は、KMSに保管されたCMKから、ファイルを暗号化するために使用するカスタマーデータキー(CDK)を生成し、CDKでファイルの暗号化を行う(暗号化に使用するプレーンのCDKと暗号化されたCDKの両方が一緒に生成される)。
暗号化に使用したプレーンなCDKはファイル暗号化後に削除することで、手元には暗号化されたファイルと暗号化されたCDKのみが残る。

復号時はまずCDKを復号してから復号されたCDKを利用してファイルを復号する形になる。
暗号化の流れは以下の通り。

```bash
# ディレクトリ初期状態
$ ls
plain-document.txt

$ cat plain-document.txt
Hello World !!
KMS encryption test.

# CDKの生成
$ aws kms generate-data-key \
--key-id alias/kmskey-test \
--key-spec AES_256 \
--output json > datakey.json

# KMSのリターンから平文のCDKである"plain-datakey"と暗号化された"datakey.encrypted"をそれぞれ作成する
$ cat datakey.json | jq -r '.Plaintext' > plain-datakey
$ cat datakey.json | jq -r '.CiphertextBlob' | base64 -d > datakey.encrypted

# "plain-datakey"と"datakey.encrypted"が作成された
$ ls
datakey.encrypted  datakey.json  plain-datakey  plain-document.txt

# CDKでファイルを暗号化する
$ openssl aes-256-cbc -e -in plain-document.txt -out document.encrypted -pass file:plain-datakey

# "document.encrypted"が作成された
$ ls
datakey.encrypted  datakey.json  document.encrypted  plain-datakey  plain-document.txt

$ cat document.encrypted
<暗号化されたランダム文字列>

# 本来であれば暗号化されていないファイルとCDKの情報は削除するため、以下のコマンドで削除する。
# 検証のため、暗号化前のファイルはそのままとした。
$ rm plain-document.txt datakey.json plain-datakey
```

復号の流れは以下の通り。

```bash
# CDKの復号
$ aws kms decrypt \
--ciphertext-blob fileb://datakey.encrypted \
--output text \
--query Plaintext > datakey.decrypted

# "datakey.decrypted"が作成された
$ ls
datakey.decrypted  datakey.encrypted  datakey.json  document.encrypted  plain-datakey  plain-document.txt

# 復号されたCDKと暗号化に使用したCDKの比較。
# プレーンのCDKは通常ファイル暗号化後に削除するべきだが、比較のために残していた。
$ diff datakey.decrypted plain-datakey
(差分なし)

# 復号されたCDKでファイルを復号
$ openssl aes-256-cbc -d -in document.encrypted -out document.decrypted -pass file:datakey.decrypted

# "document.decrypted"が作成された
$ ls
datakey.decrypted  datakey.json        document.encrypted  plain-document.txt
datakey.encrypted  document.decrypted  plain-datakey

# 復号後ファイル確認
$ cat document.decrypted
Hello World !!
KMS encryption test.

$ diff document.decrypted plain-document.txt
(差分なし)
```

### AWS CLIコマンド詳細
`aws kms generate-data-key`を実行すると以下のようなリターンがある。

```json
{
    "CiphertextBlob": "AQIDAHg8vYqqonEW2/d3vac67xEkAbPr0yFxd5jZqfAApummIQFqNfNOXb/6pI6wxpuPG+zJAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQML+GdX/11/8VzDy3HAgEQgDuZyPjIawm4fLVnHa3fD2yK+tGKuzi6FeaFMb5EnW0shQgPr6/ZiFsrFz8jfRuVNmfycNnqyhthFi830Q==",
    "Plaintext": "TL7BJf6212vJBmzGK8KrMYXhe1tI5E/AciNbwvAtXH0=",
    "KeyId": "<使用したキーID>"
}
```

`Plaintext`の部分がファイル暗号化に使用するプレーンのCDKである。
`CiphertextBlob`の部分が暗号化されたCDKである。
今回はこれらの情報から一旦2つのファイルを作成した。

```
cat datakey.json | jq -r '.Plaintext' > plain-datakey
cat datakey.json | jq -r '.CiphertextBlob' | base64 -d > datakey.encrypted
```

`CiphertextBlob`の部分はBase64エンコードされているため、デコードしてからファイルとして保存している。

https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/example_kms_GenerateDataKey_section.html

## まとめ
KMSで作成したCMKを利用してOS内のファイル暗号化・復号を検証した。
KMSに関してはエンベロープ暗号化の際にCMKとCDKかそれぞれ何を暗号化しているのか、
ドキュメント上だけでは理解しづらかったが、実際にCDKを生成して使ってみると腑に落ちる。
BlackBeltのKMSの資料はかなり丁寧に書かれているため、実際にやってみてから資料を読むと解像度が上がる。

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2024_AWS_KeyManagementService_0331_v1.pdf

前回記事のようにopensslでキーを作成する場合はOS上に鍵が存在するため、
OS内でセキュリティを考慮しないといけなかったが、
KMSを利用する場合は暗号化・復号の元になっているCMKはAWS上に保管されるため、比較的安全性が高い。
しかし、一時的に使用するプレーンのCDK情報がOS上に残ったままであったり、
ログとしてプレーンテキストで出力されると悪用されてファイルを復号されてしまうリスクはあるため、
その点はプログラム上の実装で考慮する必要があると思った。