初心者の苦闘ログとして残しておきます。
やりたかったこと
- AWS Lambda内で画像を暗号化して、S3に保存すること
- S3に保存されたデータは、復号鍵を持つまたは復号権限を持つ人が、復号して取得できること
- Pythonでやること
事前準備
- ① Lambdaを作成する
- ② KMSのマスターキーを作成する
- ③ KMSのデータキーを作成し、
CiphertextBlob
をLambdaの環境変数に設定する - ※アクセス権限はLambda、KMSともに設定してくだい
Lambdaの処理の流れ
- ②で作成したKMSを用いて③のデータキーを復号し、平文の暗号化キーを取得
- 画像データを取得
- 平文の暗号化キーをもとに、画像データを暗号化
- 暗号化した画像データをS3にPUT
ソースコード
import os
import json
import urllib.parse
import boto3
import base64
from Crypto.Cipher import AES # 補足①,②
print('Loading function')
s3_resource = boto3.resource('s3')
kms = boto3.client('kms')
def lambda_handler(event, context):
enc = base64.b64decode(os.environ['CiphertextBlob'])
decoded_key = kms.decrypt(CiphertextBlob = enc)
data_key = decoded_key['Plaintext']
# S3にPUTされた画像を暗号化したかったので、eventから取得しています。
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
try:
# 画像データの取得
obj = s3_resource.Object(bucket, key)
response = obj.get()
body = response['Body'].read()
# 補足③
cipher = AES.new(data_key, AES.MODE_EAX)
ciphertext, tag = cipher.encrypt_and_digest(body)
# 保存先
cipher_key = 'cipher/' + key
tag_key = 'tag/' + key
nonce_key = 'nonce/' + key
cipher_obj = s3_resource.Object(bucket,cipher_key)
tag_obj = s3_resource.Object(bucket,tag_key)
nonce_obj = s3_resource.Object(bucket,nonce_key)
#保存
cipher_obj.put( Body=ciphertext )
tag_obj.put( Body=tag )
nonce_obj.put( Body=cipher.nonce )
#蛇足:復号するときは
cipher_dec = AES.new(data_key, AES.MODE_EAX, cipher.nonce)
dec_data = cipher_dec.decrypt_and_verify(ciphertext, tag)
except Exception as e:
print(e)
print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
raise e
補足
補足①:暗号化ライブラリに何を用いるか
pycryptoと、pycryptodomeについての記事が多く見受けられました。
しかしpycryptoはバグが指摘されているまま更新がされていないらしく、現在(2021/3)は別のライブラリを用いたほうが良いとのことでした。
pycryptoからフォークしたpycryptodomeであれば、同じような使い方ができると思い、pycryptodomeを選択しました。
補足②:モジュールのimport
なにも考えずにfrom Crypto.Cipher import AES
を書いてテストをすると下記のエラーが。
No module named 'Crypto'
外部モジュールだから存在しないよ、とのことでエラーになってしまいました。
こちらの記事を参考にしながら、ローカルでインストールしてzipファイルをLambdaにアップロードしました。
しかしそれでも下記のエラーに。
[ERROR] OSError: Cannot load native module 'Crypto.Cipher._raw_ecb': Trying '_raw_ecb.cpython-37m-x86_64-linux-gnu.so':
エラーの原因がわからず、試しにpycyptoに変更しても同じくエラーに。
[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': cannot import name '_AES' from 'Crypto.Cipher' (/var/task/Crypto/Cipher/__init__.py)
原因を調べている中で、Lambdaレイヤーに設定する方法にたどり着きました。
こちらの記事をもとに、Lambdaレイヤーにpycryptodomeを設定したら、動かすことができました!!
補足②:暗号化の実施
KMSのマスターキーを使ってデータの暗号化を試みたところ、内容が多すぎるとのエラーが。
Encrypt operation: HTTP content length exceeded 200000 byte
これまで何も考えずにマスターキーで暗号化をしていましたが、KMSにはデータキーというデータ暗号化用のキーが存在しており、マスターキーは暗号化出来るサイズが制限されているとのことでした。
※マスターキーを使うのが駄目、というわけではないみたいです。
データキーを取得するにはgenerate_data_keyを実行すればよく、下記のような形で返却されます。
{
"CiphertextBlob": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"KeyId": "arn:aws:kms:ap-northeast-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222",
"Plaintext": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
CiphertextBlob
というのが暗号化されたデータキーで、Plaintext
が平文のデータキーです。
※CiphertextBlob
はbase64でencodeされているので、復号する際にはbase64でのデコードとKMSでのデコードが必要になります。
平文のデータキーは漏洩しないよう、保存などは行わずに破棄します。
今回はCiphertextBlob
をLambdaの環境変数に設定し、Lambda内で復号して利用することにしました。
わかっていないこと
AES暗号化には暗号化のタイプが色々と用意されているみたいなのですが、それぞれ何が違うのか、どんなメリデメがあるのか、などがさっぱりわかっておりません...
参考にした記事
- KMS > 暗号化
- KMS > マスターキーとデータキー
- pythonでAES暗号化/複合化
- AES対応のPython暗号化ライブラリを比較検証してみた
- AWS LambdaでPython外部ライブラリのLayerを作る前に
ありがとうございました。