S3のユーザ提供キーによるサーバサイド暗号化 (SSE-C) を試してみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。

S3には以前から、Server-Side Encryption (SSE)という「コンテンツを暗号化してディスクに書き込む」オプションがありました。

参考: 【AWS発表】 Amazon S3でサーバーサイド暗号化が利用可能に

しかし、この仕組みはあくまでも、AWSにアップロードされたコンテンツをAWSが暗号化してディスクに書き込み、そしてダウンロード時にはディスクに書き込まれた暗号文をAWSが復号化してダウンロードする、という仕組みでした。すなわち、鍵の管理は完全にAWS任せであるため、悪意の第三者はオブジェクトへのアクセス権(要するにAPI credentials等)さえ手に入れてしまえば、SSEが掛かっていようといまいと関係なく、普通にファイルをダウンロードできてしまいます。

結局は「AWSのデータセンタにおいてディスクの盗難・紛失があっても大丈夫」といったレベルの保護なのかなぁ、という印象でした。

SSE-C

そして本日、Server-Side Encryption with Customer-Provided Encryption Keys (SSE-C)という機能がリリースとなりました。要するに、暗号化をAWSではなく、ユーザである我々が管理する鍵によって行えるようになりました。これにより、S3上に置かれたコンテンツを、より柔軟に保護できるようになります。

Javaによるサンプルコード

公開時コメント: この機能、ホントにさっきリリースされたばかりなんです。S3 APIとしては実装はされているようなのですが、最新のAWS Java SDK v1.7.12が未だ対応しておらず。恐らく次の 1.7.13 にて対応されるんだと思います。というわけでタイトルは今のところ「試してみた」ではなく「試してみた【い】」です…。実際ホントに試してみたら、【い】を外しますね…。

更新コメント: 1.7.13がリリースされたので試してみました! タイトルから【い】は外しますw

まずクライアントサイドにおける鍵の生成です。ここはAWSに限らない、一般的なJavaによるキー生成コードです。

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

private static SecretKey generateSecretKey() {
  try {
    KeyGenerator generator = KeyGenerator.getInstance("AES");
    generator.init(256, new SecureRandom());
    return generator.generateKey();
  } catch (Exception e) {
    e.printStackTrace();
  }
  return null;
}

このメソッドを使って生成したキーで、下記のようなSSECustomerKeyオブジェクトを作ります。これがAWSで扱うキーオブジェクトとなります。

SecretKey secretKey = generateSecretKey();
SSECustomerKey sseKey = new SSECustomerKey(secretKey);

この鍵を使って、データを暗号化しつつPutObjectしてみましょう。PutObjectRequest#withSSECustomerKeyメソッドで暗号化キーを指定できます。

AmazonS3 s3 = ...;

PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, keyName, file)
	.withSSECustomerKey(sseKey);
s3.putObject(putObjectRequest);

ダウンロードも同じ要領です。リクエストオブジェクトに、キーを食わせればOK。ひたすら簡単ですね…。

GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, keyName)
	.withSSECustomerKey(sseKey);
S3Object s3Object = s3.getObject(getObjectRequest);

では、SSE-Cオブジェクトに対して、キーを指定せずにGetObjectを行うとどうなるでしょうか。

GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, keyName);
S3Object s3Object = s3.getObject(getObjectRequest);

このように400エラー(InvalidRequest)としてレスポンスが返ってきました。

The object was stored using a form of Server Side Encryption.  The correct parameters must be provided to retrieve the object. (Service: Amazon S3; Status Code: 400; Error Code: InvalidRequest; Request ID: XXXXXXXXXXXXXXXX)

次に、SSE-Cオブジェクトに対して、間違ったキーを指定してリクエストしてみました。こちらは403(AccessDenied)ですね。

Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; Request ID: XXXXXXXXXXXXXXXX)

Management ConsoleからDLしようとした場合、現在はキーを指定する方法が無く、キーの無指定ということで、InvalidRequestです。

2014-06-13_1928

おまけ: キーの保存

さて、このSecretKeyですが、この鍵は我々ユーザが管理しなければなりません。現実問題として、この鍵は文字列等に変換してどこかに保存することになると思います。まぁ、下記のような感じでBase64エンコーディング等をしておけばいいのではないでしょうか。

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.amazonaws.util.Base64;

KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(256, new SecureRandom());

SecretKey originalKey = generator.generateKey();
String serializedKeyString = Base64.encodeAsString(originalKey.getEncoded());
System.out.println(serializedKeyString);
byte[] keyBytes = Base64.decode(serializedKeyString);
SecretKey deserializedKey = new SecretKeySpec(keyBytes, "AES");

assert deserializedKey.equals(originalKey);