S3のオブジェクトをSSE-Cで暗号化/復号化してみる

# はじめに
最近、S3の暗号化をSSE-Cで行うことがあったので、いろいろ調べてみた。
S3のオブジェクトをSSE-Cで暗号化/復号化しつつアップロード、ダウンロードする一連の流れをAWS SDK for Python(boto3)を用いて試してみた。

# S3の暗号化について
暗号化を使用することで転送時と保管時にデータを安全に保護できる
* 転送時：S3との間でデータを送受信するとき
* 保管時：S3データセンター内のディスクに格納されているとき

そして、S3の暗号化には**クライアント側**での暗号化と**サーバ側**の暗号化があるらしい。

## クライアント側 （CSE: Client Side Encryption）
S3バケットにアップロードする前にあらかじめクライアント側で暗号化しておいて、S3バケットにアップロードする方法

## サーバ側 （SSE: Server Side Encryption）
データを受信するアプリケーションまたはサービスによって、送信先でデータを暗号化する方法
暗号化するためのキーの種類によって以下の3つの方法に分類される

### 1. SSE-S3
* Amazon S3が暗号化キーの生成・管理・保管を行う
* SSE-S3の利用には料金は発生しない
    * 標準のS3バケットのリクエスト料金は発生する
* 事前の準備は不要でオプションで有効化するとすぐ利用できる
* 自分で暗号化に利用するキーを選択したり、変更したりはできない
* AES-256で暗号化される
* S3バケット及びオブジェクトにアクセスできる人であれば、復号化できるため、注意が必要

### 2. SSE-KMS
* AWS KMSが暗号化キーの生成・管理・保管を行う
    * AWS KMS参考（https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/overview.html）
* SSE-KMSの利用に料金が発生する
    * 標準のS3バケットへのリクエスト料金 + AWS KMSへの暗号化・復号化のリクエスト料金
* AWS KMSの設定が別途必要
* SSE-S3と異なる部分
  * 任意のキーを用いて暗号化できる
  * キー自体へのアクセス制御によって、暗号化・復号化できるユーザーを制御できる
  * 設定によってキーの自動ローテーションができる
  * キーの利用状況をAWS CloudTrailで追跡することができる

### 3. SSE-C （← 今回はこれ）
* ユーザー自身が暗号化キーの生成・管理・保管を行う
    * AWSではキーを保管しない
    * どのキーでどのオブジェクトが暗号化されたのかという情報は自分自身で管理しなければならない
* 暗号化・復号化に伴う料金は発生しない
* 任意のキーを用いて暗号化することができる
* AES-256暗号化タイプを使用
* HTTPSだけサポートしている
* コンソールからアクセスできない
    * CLIの`get`コマンドや`put`コマンドのオプションとしてキーを指定する
    * もしくはSDKを使う
* アクセスキーの流出や不正アクセスに強い
    * 暗号化キーが盗まれなければ、コンソールのパスワードが突破されても、S3バケットポリシーの設定が甘くて不正アクセスされても、容易にオブジェクトを復号されることはない
* デフォルト暗号化として設定できない

**※ デフォルト暗号化として設定できるのは「1. SSE-S3」と「2. SSE-KMS」のみ**
![スクリーンショット 2022-11-04 0.09.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/e7386168-d7ab-d23f-7381-32091db72ee2.png)



# SSE-Cで暗号化/復号化してみる
## シナリオ
### 1. アップロード
サンプル画像と暗号に必要なキー情報を用意して`put_object`でS3バケットへアップロードする
![スクリーンショット 2022-11-04 2.50.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/c0d868a9-3f74-2055-823e-d28a04974fac.png)

### 2. ダウンロード
復号に必要なキー情報を用意して`get_object`でローカルのtmpディレクトリ配下に対象画像をダウンロードする
![スクリーンショット 2022-11-04 2.51.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/db9919d2-14a6-f762-a820-504181c546f5.png)


### 必要な情報（パラメータ）
* `bucket`
    * バケット名
* `key`
    * オブジェクトキー名
* `body`
    * アップロードするファイル（`bytes` or `file-like object`）
* `sse-customer-algorithm`
    * オブジェクトを暗号化する際に使用するアルゴリズム
* `sse-customer-key`
    * 使用する暗号化キー
    * base64でエンコードしたもの
    * S3には保存されない
* `sse-customer-key-md5`
    * 暗号化キーの128ビットMD5ダイジェスト
    * 暗号化キーがエラーなく送信されたことを確認するためのメッセージ整合性チェックのために使用される
* `profile`
    * 名前付きプロファイル

※ 暗号化キーの作成やbase64でのエンコード等に関しては以下の記事が参考になりました。

https://dev.classmethod.jp/articles/amazon-s3-sse-c-aws-cli/#:~:text=%E3%81%A8%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%A7%E3%81%99%E3%80%82-,SSE%2DC%20%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88,-SSE%2DC%20%E3%81%AE


## 事前準備
###  `.env`ファイルに必要なパラメータを用意する
```plaintext:.env
BUCKET_NAME='XXXXXXXXXX'
KEY='sample.jpg'
SSE_CUSTOMER_KEY_BASE64='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
SSE_CUSTOMER_KEY_MD5='XXXXXXXXXXXXXXXXXXXXX=='
PROFILE='XXXX'
```
## 1. アップロード
### 1-1. boto3を使用してアップロードする
```python:upload_for_boto3.py
import os

import boto3
from dotenv import load_dotenv

# .envファイルの内容を読み込む
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(verbose=True, dotenv_path=dotenv_path)

# 環境変数を取得
BUCKET_NAME = os.environ.get("BUCKET_NAME")
KEY = os.environ.get("KEY")
SSE_CUSTOMER_KEY_BASE64 = os.environ.get("SSE_CUSTOMER_KEY_BASE64")
SSE_CUSTOMER_KEY_MD5 = os.environ.get("SSE_CUSTOMER_KEY_MD5")
PROFILE = os.environ.get("PROFILE")

# clientの作成
session = boto3.Session(profile_name=PROFILE)
client = session.client('s3')

# s3バケットへファイルをアップロードする
with open(KEY, 'rb') as image:
    response = client.put_object(
        Bucket=BUCKET_NAME,
        Key=KEY,
        Body=image,
        SSECustomerAlgorithm='AES256',
        SSECustomerKey=SSE_CUSTOMER_KEY_BASE64,
        SSECustomerKeyMD5=SSE_CUSTOMER_KEY_MD5
    )

print(response)
```

（参考　[boto3 put_object ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.put_object)）

### 1-2. 確認してみる
* `sample.jpg`がアップロードされていることが分かります。
![スクリーンショット 2022-11-04 6.01.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/fe8bd1ae-5f80-a3af-2ce1-da1ce088562e.png)

* オブジェクトの情報を確認すると「サーバー側の暗号化設定」についてはエラーが出ていますが、「追加のチェックサム」のメッセージから**SSE-C**で暗号化されていることが読み取れます。
![スクリーンショット 2022-11-04 6.02.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/8f5558c1-a08a-da24-f7db-741548ddfe0b.png)




* 「開く」ボタンを押しても閲覧できず、以下のメッセージが表示されることから、SSEで暗号化されていることが分かります。
![スクリーンショット 2022-11-04 6.07.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/70e7e2ba-d5fa-45aa-1635-60f8a435cfd2.png)
```
The object was stored using a form of Server Side Encryption.
The correct parameters must be provided to retrieve the object.
---
オブジェクトはSSEの形式で保存されています。
オブジェクトを取得するには、正しいパラメータを提供する必要があります。
```

## 2. ダウンロード
### 2-1. boto3を使用してダウンロードする
```python:download_for_boto3.py
import os
import uuid

import boto3
from dotenv import load_dotenv

# .envファイルの内容を読み込む
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(verbose=True, dotenv_path=dotenv_path)

# 環境変数を取得
BUCKET_NAME = os.environ.get("BUCKET_NAME")
KEY = os.environ.get("KEY")
SSE_CUSTOMER_KEY_BASE64 = os.environ.get("SSE_CUSTOMER_KEY_BASE64")
SSE_CUSTOMER_KEY_MD5 = os.environ.get("SSE_CUSTOMER_KEY_MD5")
PROFILE = os.environ.get("PROFILE")

# clientの作成
session = boto3.Session(profile_name=PROFILE)
client = session.client('s3')

# s3バケットからオブジェクトを取得する
response = client.get_object(
    Bucket=BUCKET_NAME,
    Key=KEY,
    SSECustomerAlgorithm='AES256',
    SSECustomerKey=SSE_CUSTOMER_KEY_BASE64,
    SSECustomerKeyMD5=SSE_CUSTOMER_KEY_MD5
)

# レスポンスの中身を読み込む
binary = response['Body'].read()

# 画像を保存
img_path = 'tmp/' + str(uuid.uuid4()) + '.jpg'
with open(img_path, "wb") as f:
    f.write(binary)

```

（参考　[boto3 get_object ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.get_object)）

### 2-2. 確認
正しくダウンロードされていることが確認できました。
![スクリーンショット 2022-11-04 6.34.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/f9640f71-c8f3-e5df-165e-5d53f166a77f.png)

## AWS CLIで行う場合
シェルスクリプトに以下を記述して実行します。

### 1. アップロード
```bash:upload_to_s3.sh
#!/bin/bash

# ./.envファイルを読み込んで変数として参照できるようにする
source ./.env

aws s3api put-object \
  --bucket $BUCKET_NAME \
  --key $KEY \
  --body $KEY \
  --sse-customer-algorithm AES256 \
  --sse-customer-key $SSE_CUSTOMER_KEY_BASE64 \
  --sse-customer-key-md5 $SSE_CUSTOMER_KEY_MD5 \
  --profile $PROFILE
```

### 2. ダウンロード
```shell:download_to_s3.sh
#!/bin/bash

# ./.envファイルを読み込んで変数として参照できるようにする
source ./.env

UUID=`uuidgen`
IMG_PATH="tmp/${UUID}.jpg"

aws s3api get-object $IMG_PATH \
  --bucket $BUCKET_NAME \
  --key $KEY \
  --sse-customer-algorithm AES256 \
  --sse-customer-key $SSE_CUSTOMER_KEY_BASE64 \
  --sse-customer-key-md5 $SSE_CUSTOMER_KEY_MD5 \
  --profile $PROFILE
```

# 参考

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/serv-side-encryption.html

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/UsingKMSEncryption.html

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html

https://dev.classmethod.jp/articles/amazon-s3-sse-c-aws-cli/#toc-16