cloudpackエバンジェリストの吉田真吾@yoshidashingo)です。

01

1. AWS SDK for Javaの導入

単体でも利用できますが、今回はAWS Toolkit for Eclipse(プラグイ)ンとしてAWS SDK for Javaを導入します。

1-1. Eclipseをダウンロードする

  • 以下から「Eclipse IDE for Java EE Developers (Mac OS X 64 Bit)」をダウンロードしました。他のパッケージでも大丈夫だと思います。

1-2. AWS Toolkit for Eclipse をセットアップする

2. Amazon S3にクライアント暗号化したCSVデータをアップロードする

  • 元データはこんなCSVファイル
$ cat test.csv
1,100
2,200
3,300
4,400
5,500
6,600
7,700
8,800
9,900

2-1. 暗号キーの生成

  • ここを参考に暗号キーの生成を行います。

AWS SDK for Java を使用したクライアント側の暗号化の指定 – Amazon Simple Storage Service

ソース(Create256BitAESsecret.java)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class Create256BitAESsecret {

    private static String keyDir  = "<キーを保存するディレクトリのフルパス>";
    private static String keyName = "symmetric.key";

    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {

        //Generate symmetric 256 AES key.
        KeyGenerator symKeyGenerator = KeyGenerator.getInstance("AES");
        symKeyGenerator.init(256);
        SecretKey symKey = symKeyGenerator.generateKey();
        System.out.println("Symmetric key saved  (base 64): " + new String(Base64.encodeBase64(symKey.getEncoded())));

        //Save key.
        saveSymmetricKey(keyDir, symKey);

        //Load key.
        SecretKey symKeyLoaded = loadSymmetricAESKey(keyDir, "AES");

        //Compare with what we saved.
        System.out.println("Symmetric key loaded (base 64): " + new String(Base64.encodeBase64(symKeyLoaded.getEncoded())));
    }

    public static void saveSymmetricKey(String path, SecretKey secretKey)
        throws IOException {
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(
                secretKey.getEncoded());
        FileOutputStream keyfos = new FileOutputStream(path + "/" + keyName);
        keyfos.write(x509EncodedKeySpec.getEncoded());
        keyfos.close();
    }

    public static SecretKey loadSymmetricAESKey(String path, String algorithm)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException{
        //Read private key from file.
        FileInputStream keyfis = new FileInputStream(path + "/" + keyName);
        byte[] encodedPrivateKey = new byte[keyfis.available()];
        keyfis.read(encodedPrivateKey);
        keyfis.close();

        //Generate secret key.
        return new SecretKeySpec(encodedPrivateKey, "AES");
    }
}
  • 実行すると暗号キーファイルが出力されるとともに、キー値が出力されるので、次の項のプロパティに指定します。
    • ※ここはもうちょっとイケてる方法に替えておいたほうがいいと思います。

masterSymmetricKeyBase64 = xxxxxxxxxxxxxxxxxxxx

2-2. S3クライアント暗号化してファイルをアップロードする

プロパティファイル(UploadDirectoryUsingSymmetricKeyClientSideEncryption.properties)の作成
  • 以下のようにしてプロパティファイルを作成します。
# Base64 encoded AES 256 bit symmetric master key.
master_symmetric_key=xxxxxxxxxxxxxxxxxxxx

# Endpoint. Use s3-external-1.amazonaws.com for buckets in us-east
s3_endpoint=s3.amazonaws.com

# Bucket to upload data.
s3_bucket=<アップロード先のバケット名>

# S3 prefix to add to uploaded data files. All files loaded will have this prefix.
# Leave blank if no prefix is desired.
s3_prefix=

# Source files
src_dir=
S3クライアント暗号化してアップロードするソース(UploadDirectoryUsingSymmetricKeyClientSideEncryption.java)の作成
import java.io.File;
import java.util.Properties;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3EncryptionClient;
import com.amazonaws.services.s3.model.EncryptionMaterials;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;

public class UploadDirectoryUsingSymmetricKeyClientSideEncryption {

    private static final int MAX_RETRY_COUNT = 10;
    private static AmazonS3EncryptionClient encryptedS3Client;

    public static void main(String[] args) throws Exception {

      AWSCredentials awsCredentials = new ProfileCredentialsProvider().getCredentials();

        // Specify values in SymKeyEncryptAndUploadDirectoryToS3.properties file.
        Properties config = new Properties();
        config.load(UploadDirectoryUsingSymmetricKeyClientSideEncryption.class
                .getResourceAsStream("UploadDirectoryUsingSymmetricKeyClientSideEncryption.properties"));

        // Get property values.
        String masterSymmetricKeyBase64 = getProperty(config,
                "master_symmetric_key");
        //        String masterSymmetricKeyBase64 = "xxxxxxxxxxxxxxxxxxxx";
        String bucketName = getProperty(config, "s3_bucket");
        String s3Prefix = getProperty(config, "s3_prefix");
        String s3Endpoint = getProperty(config, "s3_endpoint");
        String sourceDir = getProperty(config, "src_dir");

        // Get secret key in correct format and create encryption materials.
        SecretKey mySymmetricKey = new SecretKeySpec(
                Base64.decodeBase64(masterSymmetricKeyBase64.getBytes()), "AES");
        EncryptionMaterials materials = new EncryptionMaterials(mySymmetricKey);

        encryptedS3Client = new AmazonS3EncryptionClient(awsCredentials, materials);
        encryptedS3Client.setEndpoint(s3Endpoint);

        // Upload all files.
        uploadAllFilesToS3(encryptedS3Client, bucketName, s3Prefix, new File(sourceDir));

    }

    private static void uploadAllFilesToS3(AmazonS3 s3, String bucketName,
            String s3Prefix, final File folder) {

        System.out.println("Reading files from directory " + folder);

        for (final File fileEntry : folder.listFiles()) {
            if (!fileEntry.isDirectory()) { // Skip sub directories.

                int retryCount = 0;
                boolean done = false;
                while (!done) {
                    try {
                        uploadToS3(s3, bucketName, s3Prefix, fileEntry);
                        done = true;
                    } catch (Exception e) {
                        retryCount++;
                        if (retryCount > MAX_RETRY_COUNT) {
                            System.out
                                    .println("Retry count exceeded max retry count "
                                            + MAX_RETRY_COUNT + ". Giving up");
                            throw new RuntimeException(e);
                        }

                        // Do retry after 10 seconds.
                        System.out.println("Failed to upload file " + fileEntry
                                + ". Retrying...");
                        try {
                            Thread.sleep(10 * 1000);
                        } catch (Exception te) {
                        }

                    }
                }// while

            }// for
        }

    }

    private static void uploadToS3(AmazonS3 s3, String bucketName,
            String s3Prefix, File file) {

        try {
            System.out.println("Uploading a new object to S3 object '"
                    + s3Prefix + "' from file " + file);
            String key = s3Prefix + "/" + file.getName();
            s3.putObject(new PutObjectRequest(bucketName, key, file));

        } catch (AmazonServiceException ase) {
            System.out
                    .println("Caught an AmazonServiceException, which means your request made it "
                            + "to Amazon S3, but was rejected with an error response for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());

            throw ase;
        } catch (AmazonClientException ace) {
            System.out
                    .println("Caught an AmazonClientException, which means the client encountered "
                            + "a serious internal problem while trying to communicate with S3, "
                            + "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
            throw ace;
        }

    }

    private static String getProperty(Properties config, String name) {

        if (config.containsKey(name)) {
            return config.getProperty(name);
        }

        throw new RuntimeException(name + " property not configured");
    }
}
  • 実行すると、S3クライアント暗号化されてCSVファイルが格納されます。

02

3. RedshiftにCOPY(インポート)する

3-1. クラスターへの接続

$ PGPASSWORD= psql -h  -p 5439 -d  -U 
psql (9.3.4, server 8.0.2)
SSL connection (cipher: ECDHE-RSA-AES256-SHA, bits: 256)
Type "help" for help.

mydb=# d
No relations found.

mydb=# l
                   List of databases
     name     |  owner  | encoding | access privileges
--------------+---------+----------+-------------------
 dev          | rdsdb   | UNICODE  |
 mydb         | awsuser | UNICODE  |
 padb_harvest | rdsdb   | UNICODE  |
 template0    | rdsdb   | UNICODE  | rdsdb=CT/rdsdb
 template1    | rdsdb   | UNICODE  | rdsdb=CT/rdsdb
(5 rows)
3-2. COPY先のテーブルの作成
mydb=# create table test (id int2, val int4);
CREATE TABLE
3-3. COPY(インポート)の実行
  • master_symmetric_keyに暗号化キーのキー値を渡して、パラメータに`encrypted`を指定して実行します。
mydb=# COPY test FROM 's3://<バケット名>//test.csv' CSV credentials 'aws_access_key_id=xxxxxxxxxx;aws_secret_access_key=xxxxxxxxxx;master_symmetric_key=<暗号キー値>' delimiter ',' encrypted;
INFO:  Load into table 'test' completed, 10 record(s) loaded successfully.
COPY
  • データの確認
  • mydb=# select * from test;
     id | val
    ----+------
      1 |  100
      3 |  300
      5 |  500
      7 |  700
      9 |  900
      2 |  200
      4 |  400
      6 |  600
      8 |  800
     10 | 1000
    (10 rows)
    
    問題なくS3クライアント暗号化してアップロードしたファイルのCOPYができました。

    アップロード元として今回は手元のMacを使いましたが、EC2などからアップロードを行う場合、EC2やRedshiftはVPC内に閉じて配置できますが、S3はパブリックネットワーク側にあるサービスなので、可能な限りセキュアにやりとりをしたいという需要は多いと思います。IAM RoleやS3 Bucket Policyなどを考慮するとともに、このS3クライアント暗号化も利用することでセキュリティレベルを高める(漏洩しても読み出せない)ことができますので、活用してみましょう。

    元記事は、こちら