S3のユーザ提供キーによるクライアントサイド暗号化 (CSE) を使い倒す

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

AWS SDK が提供するS3ファイルのクライアントサイドの暗号化(CSE)をご紹介します。CSEは、ユーザー自身が作成した秘密鍵によって、暗号化・復号化する暗号化方式ですので、ユーザー以外が復号化することは不可能な高いセキュリティを提供します。今回はCSEと、この機能を簡単に利用できるユーティリティクラスを作成しましたのでご紹介します。

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

クライアントサイドの暗号化(Client Side Encryption(以降、CSEと略す))はお客様が用意したキーによって、クライアント内で暗号化したオブジェクトをS3に登録しますので、暗号化されたオブジェクトはユーザー以外に復号化が不可能です。

  • クライアント内で暗合したオブジェクトをS3に登録
  • 暗号化されたオブジェクトはユーザー以外に複合化が不可能
  • AWS SDK(Java、Ruby、PHP、.NET)のクライアントサイドの機能として暗号化機能を提供

cse-flow

以降では、AWS SDK for Java を使ったクライアントサイド暗号化 (CSE)の方法について、解説します。

  1. Java のポリシーファイルを書き換え
  2. 暗号化ファイルの作成
  3. CSE で暗号化したファイルをS3にアップロード
  4. 暗号化したS3ファイルを CSE でダウンロード

Java のポリシーファイルを書き換え

Java の Cipher という暗号化/復号化を行うクラスは、デフォルトの状態ではAESの鍵の長さは128bitしか使えません。その理由は、特定の国々に対して高度な暗号化技術を輸出することが米国輸出規制に抵触するためです。日本ではこの制限を受けないので、Javaのポリシーファイルをダウンロードして、書き換えれば256bitも使えるようになります。

なお、ポリシーファイルの適用を忘れると、ファイルのアップロードの暗号化で、”Unable to encrypt symmetric key” のエラーに遭遇します。

Javaのポリシーファイルをダウンロード

Javaのポリシーファイルの適用

以下のディレクトリに「US_export_policy.jar」と「local_policy.jar」をコピーして、既存のファイルを上書きしてください。

Linux / Mac

$JAVA_HOME/jre/lib/security

Windows

%JAVA_HOME%¥jre¥lib¥security

暗号化キー/秘密鍵の作成

以下のコードで、AES(256bit)の秘密鍵を作成します。CSEはこのキーを利用してファイルを暗号化します。 例 1: クライアント側の対称マスターキーを使用したファイルの暗号化とアップロードを参考にしています。

public class GenerateSymmetricMasterKey {

    private static final String keyDir  = "<キーファイルのディレクトリを指定>"; 
    private static final String keyName = "secret.key";
    
    public static void main(String[] args) throws Exception {
        //Generate symmetric 256 bit AES key.
        KeyGenerator symKeyGenerator = KeyGenerator.getInstance("AES");
        symKeyGenerator.init(256); 
        SecretKey symKey = symKeyGenerator.generateKey();
 
        //Save key.
        saveSymmetricKey(keyDir, symKey);
        
        //Base64 Encoded String.
        String encoded = Base64.getEncoder().encodeToString(symKey.getEncoded());
        System.out.println(encoded);
    }

    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();
    }
}

CSE のユーティリティクラスのサンプル

以降、ファイルのアップロード・ダウンロードで利用する CSE のユーティリティクラスです。これといった簡単なサンプルが無いので書いてみました。

public class S3ClientSideEncryption {

	/** 秘密鍵 */
	protected SecretKey symmetricKey = null;
	/** 秘密鍵 (Base64) */
	protected String symmetricKeyBase64 = null;

	/** SSE-S3 オプション */
	protected boolean SSES3Enabled = false;

	/** コンストラクタ */
	public S3ClientSideEncryption(String masterKeyDir)
			throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, IOException {
		super();

		//Load symmetric 256 bit AES key.
		symmetricKey = S3ClientSideEncryption.loadSymmetricAESKey(masterKeyDir);

		//Base64 Encoded String.
		symmetricKeyBase64 = Base64.getEncoder().encodeToString(symmetricKey.getEncoded());
	}

	/** ファイルのS3アップロード */
	public PutObjectResult putFile(String bucketName, String key, File uploadFile) {
		PutObjectResult result = null;
		try {
			EncryptionMaterials encryptionMaterials = new EncryptionMaterials(symmetricKey);
			AmazonS3EncryptionClient encryptionClient = new AmazonS3EncryptionClient(
					new ProfileCredentialsProvider(),
					new StaticEncryptionMaterialsProvider(encryptionMaterials));
			PutObjectRequest putRequest = new PutObjectRequest(bucketName, key, uploadFile);
			// SSE-S3の指定
			if (SSES3Enabled) {
				// メタデータで指定
				ObjectMetadata objectMetadata = new ObjectMetadata();
				objectMetadata.setSSEAlgorithm(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION);
				putRequest.setMetadata(objectMetadata);
			}
			result = encryptionClient.putObject(putRequest);

		} catch (AmazonClientException e) {
			e.printStackTrace();
		}
		return result;
	}

	/** S3ファイルのダウンロード */
	public long getFile(String bucketName, String key, File downloadFile) {
		long fileSize = -1;
		try {
			EncryptionMaterials encryptionMaterials = new EncryptionMaterials(symmetricKey);
			AmazonS3EncryptionClient encryptionClient = new AmazonS3EncryptionClient(
					new ProfileCredentialsProvider(),
					new StaticEncryptionMaterialsProvider(encryptionMaterials));
			S3Object downloadedObject = encryptionClient.getObject(bucketName, key);
			fileSize = streamToFile(downloadedObject.getObjectContent(), downloadFile);
		} catch (AmazonClientException e) {
			e.printStackTrace();
		}
		return fileSize;
	}

	/** InputStreamからファイル出力 */
	private long streamToFile(InputStream inputStream, File downloadFile) {
		long fileSize = 0;
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(downloadFile);
			byte[] buffer = new byte[1024*1024];
			int readSize = -1;
			while((readSize = inputStream.read(buffer, 0, buffer.length)) != -1) {
				fos.write(buffer, 0, readSize);
				fileSize+=readSize;
			}
			fos.flush();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {}
			}
		}
		return fileSize;
	}

	/** 秘密鍵のBase64エンコード文字列の取得 */
	public String getSymmetricKeyBase64() {return symmetricKeyBase64;}

	/** SSE-S3 の有効化設定 */
	public void setSSES3Enabled(boolean sSES3Enabled) {SSES3Enabled = sSES3Enabled;}

	// 秘密鍵のロード
    public static SecretKey loadSymmetricAESKey(String symmetricKeyPath)
            throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException{
            //Read private key from file.
            File keyFile = new File(symmetricKeyPath);
            FileInputStream keyfis = new FileInputStream(keyFile);
            byte[] encodedPrivateKey = new byte[(int)keyFile.length()];
            keyfis.read(encodedPrivateKey);
            keyfis.close(); 

            //Generate secret key.
            return new SecretKeySpec(encodedPrivateKey, "AES");
    }

}

CSE で暗号化したファイルをS3にアップロード

データファイルを暗号化して、S3バケットにファイルを保存します。暗号化は AWS SDK for Java が暗号化します。秘密鍵を指定して AmazonS3EncryptionClient のインスタンスでファイルを S3 にアップロードします。

以下のサンプルコードの「秘密鍵ファイルのパス」「バケット名」「キー名」「アップロードするファイルパス」を環境に合わせて書き換えてください。

    /** 秘密鍵ファイルのパス */
    private static final String symmetricKeyPath = "<秘密鍵のファイルパス>";
    
    /** バケット名(ex. cm-bucket) */
    private static final String bucketName = "<バケット名>";
    /** キー名(ex. users.csv) */
    private static final String key = "<ファイル名もしくはファイルパス>";
    
    /** アップロードするファイルパス */
    private static final String uploadPath = "<アップロードするファイルパス>";

    public static void main(String[] args) {
        try {
            // オブジェクトの生成
            S3ClientSideEncryption s3cse = new S3ClientSideEncryption(symmetricKeyPath);
            System.out.println("master_symmetric_key: " + s3cse.getSymmetricKeyBase64());
            
            // ファイルのS3アップロード
            File uploadFile = new File(uploadPath);
            if (uploadFile.exists()) {
                PutObjectResult result = s3cse.putFile(bucketName, key, uploadFile);
                if (result == null) {
                    System.err.println("Upload failed.");
                } else {
                    System.out.println("Upload success.");
                }
            } else {
                System.err.println("File not found.:" + uploadPath);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

暗号化したS3ファイルを CSE でダウンロード

暗号化したデータファイルを、復号化してダウンロードします。復号化は AWS SDK for Java が復号化します。密鍵を指定して AmazonS3EncryptionClient のインスタンスでファイルを S3 にダウンロードします。

以下のサンプルコードの「秘密鍵ファイルのパス」「バケット名」「キー名」「ダウンロードするファイルパス」を環境に合わせて書き換えてください。

    /** 秘密鍵ファイルのパス */
    private static final String symmetricKeyPath = "<秘密鍵のファイルパス>";
    
    /** バケット名(ex. cm-bucket) */
    private static final String bucketName = "<バケット名>";
    /** キー名(ex. users.csv) */
    private static final String key = "<ファイル名もしくはファイルパス>";
    
    /** ダウンロードするファイルパス */
    private static final String downloadPath = "<ダウンロードするファイルパス>";

    public static void main(String[] args) {
        try {
            // オブジェクトの生成
            S3ClientSideEncryption s3cse = new S3ClientSideEncryption(symmetricKeyPath);
            
            // S3ファイルのダウンロード
            File downloadFile = new File(downloadPath);
            long fileSize = s3cse.getFile(bucketName, key, downloadFile);
            if (fileSize >= 0) {
                System.out.println("Download Success.:" + downloadFile.getPath());
            } else {
                System.err.println("Download Failed.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

CSE で暗号化したファイルをS3にアップロードして更にSSE-S3暗号化

データファイルをCSE暗号化に加え、更にSSE-S3(Server Side Encryption with Amazon S3-Managed Keys:サーバーサイド暗号化)の2重の暗号をかけて、S3バケットにファイルを保存することもできます。CSE暗号化は AWS SDK for Java が暗号化します。秘密鍵を指定して AmazonS3EncryptionClient のインスタンスでファイルを S3 にアップロードします。また、SSE-S3暗号化はアップロードするときのオプションを指定することで、S3に格納されるときに更にSSE-S3暗号化されます。

なお、この2重の暗号化をかけた場合でも、「暗号化したS3ファイルを CSE でダウンロード」の方法で復号化されたファイルのダウンロードが可能です。

以下のサンプルコードの「秘密鍵ファイルのパス」「バケット名」「キー名」「アップロードするファイルパス」を環境に合わせて書き換えてください。

    /** 秘密鍵ファイルのパス */
    private static final String symmetricKeyPath = "<秘密鍵のファイルパス>";
    
    /** バケット名(ex. cm-bucket) */
    private static final String bucketName = "<バケット名>";
    /** キー名(ex. users.csv) */
    private static final String key = "<ファイル名もしくはファイルパス>";
    
    /** アップロードするファイルパス */
    private static final String uploadPath = "<アップロードするファイルパス>";

    public static void main(String[] args) {
        try {
            // オブジェクトの生成
            S3ClientSideEncryption s3cse = new S3ClientSideEncryption(symmetricKeyPath);
            System.out.println("master_symmetric_key: " + s3cse.getSymmetricKeyBase64());
            
            // ファイルのS3アップロード
            File uploadFile = new File(uploadPath);
            if (uploadFile.exists()) {
                s3cse.setSSES3Enabled(true);
                PutObjectResult result = s3cse.putFile(bucketName, key, uploadFile);
                if (result == null) {
                    System.err.println("Upload failed.");
                } else {
                    System.out.println("Upload success.");
                }
            } else {
                System.err.println("File not found.:" + uploadPath);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Amazon S3 の暗号化に関する情報

最後に

正直、いまさら感のあるエントリーですが、 Amazon Redshift 暗号化されたデータファイルを Amazon S3 からロードする のブログの半分以上が CSE の解説になったので分けてブログ化しました。といっても、ここまで簡潔かつ具体的に解説しているサイトはないので、参考になるとは思います。

AWS SDK が提供する機能(API)は、raw(生)な機能が多く、クライアントサイド暗号化(CSE)における、S3 のユースケースで最も一般的なファイルのアップロード、ダウンロードが簡潔に扱えるAPIがありませんので、重宝していただけたら幸いです。