AWS Encryption SDKによるクライアントサイド暗号化

こんにちは、SREグループの水戸 (@y_310)です。重要なデータをストレージに保存する際は暗号化を一つのセキュリティ対策として考えることが多いかと思います。最近はEncryption At Restと呼ばれるストレージレベルの透過的な暗号化によってストレージへの直接アクセスによる漏洩を防ぐ仕組みが当たり前になってきていますが、それでも正当なアクセス権を持ったユーザの認証情報の漏洩により不正アクセスを受けファイルが漏洩してしまうなど想定される攻撃ルートは他にも色々とあります。 そういった場合の追加の防御策としてクライアントサイド暗号化を検討し実装方法を調査したところ、AWSが提供するAWS Encryption SDKを使うことでAWS KMSと組み合わせた暗号化が簡単に実装できることが分かりました。今回はAWS Encryption SDKの基本的な仕組みと使い方についてご紹介します。

クライアントサイド暗号化とは

そもそもクライアントサイド暗号化とは何かについて簡単に解説します。通常データをストレージに保存する際の登場人物として保存先のストレージと保存処理を行うアプリケーションが存在します。 クライアントサイド暗号化はデータをストレージに保存する前にあらかじめアプリケーション内でデータを暗号化し、暗号化済のデータをストレージに保管する方法です。不正アクセス等で暗号化済ファイルが漏洩しても鍵がなければ復号できないため、漏洩リスクを下げることができます。 ただアプリケーションロジックで暗号化する仕組みですので、基本的に保存処理を行うアプリケーションで個別に暗号化処理を実装する必要があります。

反対にサーバサイド暗号化という方式もあります。こちらはストレージ側に暗号化機能が備わっており、データを保存しストレージに書き込む際にストレージ内部で自動的に暗号化が行われた上で書き込まれ、参照する際にも自動的に復号され利用者に返されるという動作をします。冒頭でEncryption At Restと表記したのもこちらのサーバサイド暗号化の方式となります。こちらはアプリケーション側の変更なしに暗号化できるのが便利ですが、ストレージにアクセス権を持っているユーザの認証情報があればファイルの内容に即アクセスできてしまうため要件によっては不十分な対策となります。

サーバサイド暗号化はAmazon AuroraやS3など様々なマネージドサービスで提供されており、最近ではデフォルトで有効になっていることも多いです。

AWS Encryption SDK

AWS Encryption SDKはクライアントサイド暗号化を実装するための機能を提供しているライブラリです。現在C、.NET、JavaJavaScriptPythonSDKが提供されています。またCLI版もあります。弊社のメイン開発言語であるGoは残念ながら公式でサポートされていませんがコミュニティで開発されている https://github.com/chainifynet/aws-encryption-sdk-go があり、主要機能の大半はサポートされているようです。 エンベロープ暗号化という方法でクライアントサイド暗号化を実装しており、暗号鍵の管理にはAWS KMSが使用できますがAWSに依存しない形でも使用することができる設計になっているようです。

なお、同じようにクライアントサイド暗号化を実現するためのライブラリとしてAmazon S3 Encryption Clientという似たものが存在しており、S3に保存するオブジェクトの暗号化だけであればこちらも使えるようです。ただこちらはGoもサポートされていますがPythonがサポートされていないようです。そして2つのライブラリはどちらもKMSを使ったエンベロープ暗号化をするという意味では同様ですがファイル形式に互換性が無いため、どちらを利用するかはユースケースに応じてよく検討する必要があります。弊社ではGoで実装したアプリケーションサーバとLambda@Edge(PythonとNode.jsのみ使用可能)で使用する想定だったためAWS Encryption SDKを選定しました。

エンベロープ暗号化

ここでエンベロープ暗号化についても簡単に解説します。複数のファイルを暗号化したい時、ファイル単位で異なる暗号鍵を使用することが理想的です。ただ、その場合ファイルの数だけ異なる鍵を安全に管理する必要があり鍵の管理が非常に煩雑になる懸念があります。そのため、ファイル単位で生成した暗号鍵を1つのルートキーで暗号化し、管理しなければいけない鍵をルートキーに集約するというのがエンベロープ暗号化の手法になります。 具体的には、以下のステップで暗号化します。

  1. ファイルを暗号化する際にファイル単位の暗号鍵であるData Encryption Key (DEK)を生成
  2. DEKを使って対象ファイルを暗号化
  3. DEKをAWS KMSなどの鍵管理サービスで管理されているルートキー Key Encryption Key (KEK)を使って暗号化
  4. 平文のDEKを削除し、暗号化したDEKを暗号化したファイルと結合し、S3などのストレージに保存

復号する際は反対に、

  1. 暗号化ファイルから暗号化DEKを抽出
  2. 暗号化DEKをKMSに送信し復号
  3. 復号したDEKで暗号化ファイルを復号

となります。

以上の手順で安全を担保する上でいくつか重要なポイントがあります。 まずKMSはEncrypt APIでリクエストされたデータを暗号化しますが、その際に使用される鍵(KEK)はネットワーク外に漏洩することはなくネットワークを通過するのはあくまで平文のDEKと暗号化後のDEKのみとなります。これによってKEKが漏洩するリスクを大幅に下げています。

次に平文のDEKは暗号化後にすぐ廃棄するため、一般的な実装であれば平文のDEKはメモリ上にしか存在しなかったことになりDEKの漏洩リスクを下げています。

暗号化後のDEKについてはKEKが無い限り復号されるリスクはないため、暗号化後のファイル自体と同じセキュリティレベルの管理方法でよく、多くの場合暗号化後ファイルと結合して1ファイルとして管理する方式となっています。これによりDEKの管理の手間を大きく軽減することができます。

AWS Encryption SDKはこれらの手順を安全に実施し、公開された仕様に基づいて暗号化済DEKを含むファイルを生成する処理が実装されたライブラリとなります。 Amazon S3 Encryption Clientも同様のエンベロープ暗号化を行いますが、暗号化ファイルとDEKを結合したファイルフォーマットがAWS Encryption SDKと異なるため両者に互換性がないということになります。

エンベロープ暗号化の詳細については以下の公式ドキュメントの解説もご参照ください。

AWS Encryption SDKを使ったファイルの暗号化と復号

では実際にAWS Encryption SDKを使ってテキストファイルを暗号化し、その後復号したいと思います。Goで暗号化しPythonで復号することでGoのコミュニティ版ライブラリが仕様通りにファイルを生成できていることの確認もします。

Goでテキストファイルを暗号化

テキストファイルの内容

% cat example.txt
This is a plaintext message.
package main

import (
    "context"
    "io/ioutil"

    "github.com/chainifynet/aws-encryption-sdk-go/pkg/client"
    "github.com/chainifynet/aws-encryption-sdk-go/pkg/materials"
    "github.com/chainifynet/aws-encryption-sdk-go/pkg/providers/kmsprovider"
)

func main() {
    kmsARN := "KMS_KEY_ARN"

    sdkClient := client.NewClient()

    kmsKeyProvider, err := kmsprovider.New(kmsARN)
    if err != nil {
        panic(err)
    }

    cmm, err := materials.NewDefault(kmsKeyProvider)
    if err != nil {
        panic(err)
    }

    filename := "./example.txt"
    secretData, err := ioutil.ReadFile(filename)
    if err != nil {
        panic(err)
    }

    ciphertext, _, err := sdkClient.Encrypt(
        context.TODO(),
        secretData,
        nil,
        cmm,
    )
    if err != nil {
        panic(err)
    }

    ioutil.WriteFile("example.enc.txt", ciphertext, 0644)
}

実行結果

% go run main.go
% ls -l example.enc.txt
-rw-r--r--  1 user  staff   638B  5 30 11:56 example.enc.txt

Pythonで暗号化したテキストファイルを復号

import aws_encryption_sdk
from aws_encryption_sdk.identifiers import CommitmentPolicy

client = aws_encryption_sdk.EncryptionSDKClient()

kms_key_provider = aws_encryption_sdk.StrictAwsKmsMasterKeyProvider(key_ids=[
    "KMS_KEY_ARN"
])

with open('./example.enc.txt', 'rb') as f:
    ciphertext = f.read()

decrypted_plaintext, decryptor_header = client.decrypt(
    source=ciphertext,
    key_provider=kms_key_provider
)

with open('example.decrypted.txt', 'wb') as f:
    f.write(decrypted_plaintext)

実行結果

% python3 decrypt.py
% cat example.decrypted.txt
This is a plaintext message.

無事、Goで暗号化したファイルがPythonで復号できることを確認できました。

CLIでファイルを暗号化し、復号する

AWS Encryption SDKにはCLIも用意されています。 以下のように暗号化と復号ができ、出力されたファイルは当然ですがPythonやGoのSDKで作られたものとも互換性があります。

export KMS_KEY_ARN=arn:aws:kms:xxx

# 暗号化
aws-encryption-cli --encrypt --input ./example.txt \
  --wrapping-keys key=$KMS_KEY_ARN \
  --output example-cli.enc.txt \
  -S

# 復号
aws-encryption-cli --decrypt --input ./example-cli.enc.txt \
  --wrapping-keys key=$KMS_KEY_ARN \
  --output example-cli.decrypted.txt \
  -S

まとめ

AWS Encryption SDKを使うことで簡単にクライアントサイド暗号化を実装することができました。サポートされている言語が不足気味なところが少し難点ですが、詳細な仕様が公開されており必要があれば独自実装が可能な余地もあります。 より厳格なデータ管理が必要な場合の一つの手段として参考になれば幸いです。