Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 0 additions & 103 deletions aws-java-sdk-s3/pom.xml

This file was deleted.

10 changes: 5 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
<modelVersion>4.0.0</modelVersion>

<groupId>software.amazon.encryption</groupId>
<artifactId>s3-client</artifactId>
<artifactId>s3</artifactId>
<version>3.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>AWS S3 Encryption Client</name>
<description>The AWS S3 Encryption Client provides client-side encryption for S3</description>
<url>https://github.com/aws/aws-s33c-java</url>
<url>https://github.com/aws/aws-s3-encryption-client-java</url>

<licenses>
<license>
Expand Down Expand Up @@ -51,7 +51,7 @@
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.17.154</version>
<version>2.17.204</version>
<optional>true</optional>
<type>pom</type>
<scope>import</scope>
Expand Down Expand Up @@ -80,14 +80,14 @@
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.17.154</version>
<version>2.17.204</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>kms</artifactId>
<version>2.17.154</version>
<version>2.17.204</version>
<optional>true</optional>
</dependency>
</dependencies>
Expand Down
224 changes: 224 additions & 0 deletions src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package software.amazon.encryption.s3;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.http.AbortableInputStream;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.InvalidObjectStateException;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
import software.amazon.encryption.s3.materials.DecryptionMaterials;
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
import software.amazon.encryption.s3.materials.EncryptionMaterialsRequest;
import software.amazon.encryption.s3.materials.EncryptedDataKey;
import software.amazon.encryption.s3.materials.EncryptionMaterials;
import software.amazon.encryption.s3.materials.MaterialsManager;

public class S3EncryptionClient implements S3Client {

private final S3Client _wrappedClient;
private final MaterialsManager _materialsManager;

public S3EncryptionClient(S3Client client, MaterialsManager materialsManager) {
_wrappedClient = client;
_materialsManager = materialsManager;
}

@Override
public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBody requestBody)
throws AwsServiceException, SdkClientException, S3Exception {

// TODO: This is proof-of-concept code and needs to be refactored

// Get content encryption key
EncryptionMaterials materials = _materialsManager.getEncryptionMaterials(EncryptionMaterialsRequest.builder()
.build());
SecretKey contentKey = materials.dataKey();
// Encrypt content
byte[] iv = new byte[12]; // default GCM IV length
new SecureRandom().nextBytes(iv);

final String contentEncryptionAlgorithm = "AES/GCM/NoPadding";
final Cipher cipher;
try {
cipher = Cipher.getInstance(contentEncryptionAlgorithm);
cipher.init(Cipher.ENCRYPT_MODE, contentKey, new GCMParameterSpec(128, iv));
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException
| InvalidKeyException e) {
throw new RuntimeException(e);
}

byte[] ciphertext;
try {
byte[] input = IoUtils.toByteArray(requestBody.contentStreamProvider().newStream());
ciphertext = cipher.doFinal(input);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (IllegalBlockSizeException e) {
throw new RuntimeException(e);
} catch (BadPaddingException e) {
throw new RuntimeException(e);
}

// Save content metadata into request
Base64.Encoder encoder = Base64.getEncoder();
Map<String,String> metadata = new HashMap<>(putObjectRequest.metadata());
EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
metadata.put("x-amz-key-v2", encoder.encodeToString(edk.ciphertext()));
metadata.put("x-amz-iv", encoder.encodeToString(iv));
metadata.put("x-amz-matdesc", /* TODO: JSON encoded */ "{}");
metadata.put("x-amz-cek-alg", contentEncryptionAlgorithm);
metadata.put("x-amz-tag-len", /* TODO: take from algo suite */ "128");
metadata.put("x-amz-wrap-alg", edk.keyProviderId());

try (JsonWriter jsonWriter = JsonWriter.create()) {
jsonWriter.writeStartObject();
for (Entry<String,String> entry : materials.encryptionContext().entrySet()) {
jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
}
jsonWriter.writeEndObject();

String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
metadata.put("x-amz-matdesc", jsonEncryptionContext);
} catch (JsonGenerationException e) {
throw new RuntimeException(e);
}

putObjectRequest = putObjectRequest.toBuilder().metadata(metadata).build();

return _wrappedClient.putObject(putObjectRequest, RequestBody.fromBytes(ciphertext));
}

@Override
public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<GetObjectResponse, T> responseTransformer)
throws NoSuchKeyException, InvalidObjectStateException, AwsServiceException, SdkClientException, S3Exception {

// TODO: This is proof-of-concept code and needs to be refactored

ResponseInputStream<GetObjectResponse> objectStream = _wrappedClient.getObject(getObjectRequest);
byte[] output;
try {
output = IoUtils.toByteArray(objectStream);
} catch (IOException e) {
throw new RuntimeException(e);
}

GetObjectResponse response = objectStream.response();
Map<String, String> metadata = response.metadata();

// Build encrypted data key
Base64.Decoder decoder = Base64.getDecoder();
byte[] edkCiphertext = decoder.decode(metadata.get("x-amz-key-v2"));
String keyProviderId = metadata.get("x-amz-wrap-alg");
EncryptedDataKey edk = EncryptedDataKey.builder()
.ciphertext(edkCiphertext)
.keyProviderId(keyProviderId)
.build();
List<EncryptedDataKey> encryptedDataKeys = Collections.singletonList(edk);

// Get encryption context
final Map<String, String> encryptionContext = new HashMap<>();
final String jsonEncryptionContext = metadata.get("x-amz-matdesc");
try {
JsonNodeParser parser = JsonNodeParser.create();
JsonNode objectNode = parser.parse(jsonEncryptionContext);

for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
encryptionContext.put(entry.getKey(), entry.getValue().asString());
}
} catch (Exception e) {
throw new RuntimeException(e);
}

// Get decryption materials
final String contentEncryptionAlgorithm = metadata.get("x-amz-cek-alg");
AlgorithmSuite algorithmSuite = null;
if (contentEncryptionAlgorithm.equals("AES/GCM/NoPadding")) {
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;;
}

if (algorithmSuite == null) {
throw new RuntimeException("Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
}

DecryptMaterialsRequest request = DecryptMaterialsRequest.builder()
.algorithmSuite(algorithmSuite)
.encryptedDataKeys(encryptedDataKeys)
.encryptionContext(encryptionContext)
.build();
DecryptionMaterials materials = _materialsManager.decryptMaterials(request);

// Get content encryption information
SecretKey contentKey = new SecretKeySpec(materials.plaintextDataKey(), "AES");
final int tagLength = Integer.parseInt(metadata.get("x-amz-tag-len"));
byte[] iv = decoder.decode(metadata.get("x-amz-iv"));
final Cipher cipher;
byte[] plaintext;
try {
cipher = Cipher.getInstance(contentEncryptionAlgorithm);
cipher.init(Cipher.DECRYPT_MODE, contentKey, new GCMParameterSpec(tagLength, iv));
plaintext = cipher.doFinal(output);
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException
| InvalidKeyException e) {
throw new RuntimeException(e);
} catch (IllegalBlockSizeException e) {
throw new RuntimeException(e);
} catch (BadPaddingException e) {
throw new RuntimeException(e);
}

try {
return responseTransformer.transform(response,
AbortableInputStream.create(new ByteArrayInputStream(plaintext)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public String serviceName() {
return _wrappedClient.serviceName();
}

@Override
public void close() {
_wrappedClient.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package software.amazon.encryption.s3;

import software.amazon.awssdk.core.exception.SdkClientException;

public class S3EncryptionClientException extends SdkClientException {

public S3EncryptionClientException(String message) {
super(SdkClientException.builder()
.message(message));
}

public S3EncryptionClientException(String message, Throwable cause) {
super(SdkClientException.builder()
.message(message)
.cause(cause));
}
}
Loading