Skip to content

Commit 90fbdbd

Browse files
committed
Create put object pipeline.
Factored out the content encryption and metadata storage strategies.
1 parent 6a6d63d commit 90fbdbd

File tree

7 files changed

+286
-79
lines changed

7 files changed

+286
-79
lines changed

src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java

Lines changed: 16 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22

33
import java.io.ByteArrayInputStream;
44
import java.io.IOException;
5-
import java.nio.charset.StandardCharsets;
65
import java.security.InvalidAlgorithmParameterException;
76
import java.security.InvalidKeyException;
87
import java.security.NoSuchAlgorithmException;
9-
import java.security.SecureRandom;
108
import java.util.Base64;
119
import java.util.Collections;
1210
import java.util.HashMap;
1311
import java.util.List;
1412
import java.util.Map;
15-
import java.util.Map.Entry;
1613
import javax.crypto.BadPaddingException;
1714
import javax.crypto.Cipher;
1815
import javax.crypto.IllegalBlockSizeException;
@@ -28,23 +25,17 @@
2825
import software.amazon.awssdk.http.AbortableInputStream;
2926
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
3027
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
31-
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
32-
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
3328
import software.amazon.awssdk.services.s3.S3Client;
3429
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
3530
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
36-
import software.amazon.awssdk.services.s3.model.InvalidObjectStateException;
37-
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
3831
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
3932
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
40-
import software.amazon.awssdk.services.s3.model.S3Exception;
4133
import software.amazon.awssdk.utils.IoUtils;
4234
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
43-
import software.amazon.encryption.s3.materials.DecryptionMaterials;
35+
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
4436
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
45-
import software.amazon.encryption.s3.materials.EncryptionMaterialsRequest;
37+
import software.amazon.encryption.s3.materials.DecryptionMaterials;
4638
import software.amazon.encryption.s3.materials.EncryptedDataKey;
47-
import software.amazon.encryption.s3.materials.EncryptionMaterials;
4839
import software.amazon.encryption.s3.materials.MaterialsManager;
4940

5041
public class S3EncryptionClient implements S3Client {
@@ -59,78 +50,25 @@ public S3EncryptionClient(S3Client client, MaterialsManager materialsManager) {
5950

6051
@Override
6152
public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBody requestBody)
62-
throws AwsServiceException, SdkClientException, S3Exception {
63-
64-
// TODO: This is proof-of-concept code and needs to be refactored
65-
66-
// Get content encryption key
67-
EncryptionMaterials materials = _materialsManager.getEncryptionMaterials(EncryptionMaterialsRequest.builder()
68-
.build());
69-
SecretKey contentKey = materials.dataKey();
70-
// Encrypt content
71-
byte[] iv = new byte[12]; // default GCM IV length
72-
new SecureRandom().nextBytes(iv);
73-
74-
final String contentEncryptionAlgorithm = "AES/GCM/NoPadding";
75-
final Cipher cipher;
76-
try {
77-
cipher = Cipher.getInstance(contentEncryptionAlgorithm);
78-
cipher.init(Cipher.ENCRYPT_MODE, contentKey, new GCMParameterSpec(128, iv));
79-
} catch (NoSuchAlgorithmException
80-
| NoSuchPaddingException
81-
| InvalidAlgorithmParameterException
82-
| InvalidKeyException e) {
83-
throw new RuntimeException(e);
84-
}
53+
throws AwsServiceException, SdkClientException {
8554

86-
byte[] ciphertext;
87-
try {
88-
byte[] input = IoUtils.toByteArray(requestBody.contentStreamProvider().newStream());
89-
ciphertext = cipher.doFinal(input);
90-
} catch (IOException e) {
91-
throw new RuntimeException(e);
92-
} catch (IllegalBlockSizeException e) {
93-
throw new RuntimeException(e);
94-
} catch (BadPaddingException e) {
95-
throw new RuntimeException(e);
96-
}
97-
98-
// Save content metadata into request
99-
Base64.Encoder encoder = Base64.getEncoder();
100-
Map<String,String> metadata = new HashMap<>(putObjectRequest.metadata());
101-
EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
102-
metadata.put("x-amz-key-v2", encoder.encodeToString(edk.ciphertext()));
103-
metadata.put("x-amz-iv", encoder.encodeToString(iv));
104-
metadata.put("x-amz-matdesc", /* TODO: JSON encoded */ "{}");
105-
metadata.put("x-amz-cek-alg", contentEncryptionAlgorithm);
106-
metadata.put("x-amz-tag-len", /* TODO: take from algo suite */ "128");
107-
metadata.put("x-amz-wrap-alg", edk.keyProviderId());
108-
109-
try (JsonWriter jsonWriter = JsonWriter.create()) {
110-
jsonWriter.writeStartObject();
111-
for (Entry<String,String> entry : materials.encryptionContext().entrySet()) {
112-
jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
113-
}
114-
jsonWriter.writeEndObject();
115-
116-
String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
117-
metadata.put("x-amz-matdesc", jsonEncryptionContext);
118-
} catch (JsonGenerationException e) {
119-
throw new RuntimeException(e);
120-
}
121-
122-
putObjectRequest = putObjectRequest.toBuilder().metadata(metadata).build();
55+
PutEncryptedObjectPipeline pipeline = PutEncryptedObjectPipeline.builder()
56+
.s3Client(_wrappedClient)
57+
.materialsManager(_materialsManager)
58+
.build();
12359

124-
return _wrappedClient.putObject(putObjectRequest, RequestBody.fromBytes(ciphertext));
60+
return pipeline.putObject(putObjectRequest, requestBody);
12561
}
12662

12763
@Override
128-
public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<GetObjectResponse, T> responseTransformer)
129-
throws NoSuchKeyException, InvalidObjectStateException, AwsServiceException, SdkClientException, S3Exception {
64+
public <T> T getObject(GetObjectRequest getObjectRequest,
65+
ResponseTransformer<GetObjectResponse, T> responseTransformer)
66+
throws AwsServiceException, SdkClientException {
13067

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

133-
ResponseInputStream<GetObjectResponse> objectStream = _wrappedClient.getObject(getObjectRequest);
70+
ResponseInputStream<GetObjectResponse> objectStream = _wrappedClient.getObject(
71+
getObjectRequest);
13472
byte[] output;
13573
try {
13674
output = IoUtils.toByteArray(objectStream);
@@ -169,11 +107,12 @@ public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<Ge
169107
final String contentEncryptionAlgorithm = metadata.get("x-amz-cek-alg");
170108
AlgorithmSuite algorithmSuite = null;
171109
if (contentEncryptionAlgorithm.equals("AES/GCM/NoPadding")) {
172-
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;;
110+
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
173111
}
174112

175113
if (algorithmSuite == null) {
176-
throw new RuntimeException("Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
114+
throw new RuntimeException(
115+
"Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
177116
}
178117

179118
DecryptMaterialsRequest request = DecryptMaterialsRequest.builder()

src/main/java/software/amazon/encryption/s3/algorithms/Constants.java renamed to src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmConstants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package software.amazon.encryption.s3.algorithms;
22

3-
class Constants {
3+
class AlgorithmConstants {
44
// Maximum length of the content that can be encrypted in GCM mode.
55
static final long GCM_MAX_CONTENT_LENGTH_BITS = (1L<<39) - 256;
66
}

src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmSuite.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public enum AlgorithmSuite {
99
128,
1010
96,
1111
128,
12-
Constants.GCM_MAX_CONTENT_LENGTH_BITS);
12+
AlgorithmConstants.GCM_MAX_CONTENT_LENGTH_BITS);
1313

1414
private int _id;
1515
private String _dataKeyAlgorithm;
@@ -51,6 +51,10 @@ public String cipherName() {
5151
return _cipherName;
5252
}
5353

54+
public int cipherTagLengthBits() {
55+
return _cipherTagLengthBits;
56+
}
57+
5458
public int nonceLengthBytes() {
5559
return _cipherNonceLengthBits / 8;
5660
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
import java.io.IOException;
4+
import java.security.InvalidAlgorithmParameterException;
5+
import java.security.InvalidKeyException;
6+
import java.security.NoSuchAlgorithmException;
7+
import java.security.SecureRandom;
8+
import javax.crypto.BadPaddingException;
9+
import javax.crypto.Cipher;
10+
import javax.crypto.IllegalBlockSizeException;
11+
import javax.crypto.NoSuchPaddingException;
12+
import javax.crypto.SecretKey;
13+
import javax.crypto.spec.GCMParameterSpec;
14+
import software.amazon.awssdk.utils.IoUtils;
15+
import software.amazon.encryption.s3.S3EncryptionClientException;
16+
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
17+
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline.ContentEncryptionStrategy;
18+
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline.EncryptedContent;
19+
import software.amazon.encryption.s3.materials.EncryptionMaterials;
20+
21+
/**
22+
* This class will encrypt data according to the algorithm suite constants
23+
*/
24+
public class AesGcmContentEncryptionStrategy implements ContentEncryptionStrategy {
25+
26+
final private SecureRandom _secureRandom;
27+
28+
private AesGcmContentEncryptionStrategy(Builder builder) {
29+
this._secureRandom = builder._secureRandom;
30+
}
31+
32+
public static Builder builder() { return new Builder(); }
33+
34+
@Override
35+
public EncryptedContent encryptContent(EncryptionMaterials materials, byte[] content) {
36+
final AlgorithmSuite algorithmSuite = materials.algorithmSuite();
37+
38+
final byte[] nonce = new byte[algorithmSuite.nonceLengthBytes()];
39+
_secureRandom.nextBytes(nonce);
40+
41+
final String cipherName = algorithmSuite.cipherName();
42+
try {
43+
final Cipher cipher = Cipher.getInstance(cipherName);
44+
45+
cipher.init(Cipher.ENCRYPT_MODE,
46+
materials.dataKey(),
47+
new GCMParameterSpec(algorithmSuite.cipherTagLengthBits(), nonce));
48+
49+
EncryptedContent result = new EncryptedContent();
50+
result.nonce = nonce;
51+
result.ciphertext = cipher.doFinal(content);
52+
53+
return result;
54+
} catch (NoSuchAlgorithmException
55+
| NoSuchPaddingException
56+
| InvalidAlgorithmParameterException
57+
| InvalidKeyException
58+
| IllegalBlockSizeException
59+
| BadPaddingException e) {
60+
throw new S3EncryptionClientException("Unable to " + cipherName + " content encrypt.", e);
61+
}
62+
}
63+
64+
public static class Builder {
65+
private SecureRandom _secureRandom = new SecureRandom();
66+
67+
private Builder() {}
68+
69+
public Builder secureRandom(SecureRandom secureRandom) {
70+
_secureRandom = secureRandom;
71+
return this;
72+
}
73+
74+
public AesGcmContentEncryptionStrategy build() {
75+
return new AesGcmContentEncryptionStrategy(this);
76+
}
77+
}
78+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
public class MetadataKey {
4+
public static final String ENCRYPTED_DATA_KEY = "x-amz-key-v2";
5+
// This is the name of the keyring/algorithm e.g. AES/GCM or kms+context
6+
public static final String ENCRYPTED_DATA_KEY_ALGORITHM = "x-amz-wrap-alg";
7+
public static final String ENCRYPTED_DATA_KEY_CONTEXT = "x-amz-matdesc";
8+
9+
public static final String CONTENT_NONCE = "x-amz-iv";
10+
// This is usually an actual Java cipher e.g. AES/GCM/NoPadding
11+
public static final String CONTENT_CIPHER = "x-amz-cek-alg";
12+
public static final String CONTENT_CIPHER_TAG_LENGTH = "x-amz-tag-len";
13+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.Base64;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.Map.Entry;
8+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
9+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
10+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
11+
import software.amazon.encryption.s3.S3EncryptionClientException;
12+
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline.EncryptedContent;
13+
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline.MetadataEncodingStrategy;
14+
import software.amazon.encryption.s3.materials.EncryptedDataKey;
15+
import software.amazon.encryption.s3.materials.EncryptionMaterials;
16+
17+
/**
18+
* This stores encryption metadata in the S3 object metadata.
19+
* The name is not a typo
20+
*/
21+
public class ObjectMetadataMetadataEncodingStrategy implements MetadataEncodingStrategy {
22+
private final Base64.Encoder _encoder;
23+
24+
private ObjectMetadataMetadataEncodingStrategy(Builder builder) {
25+
this._encoder = builder._encoder;
26+
}
27+
28+
public static Builder builder() { return new Builder(); }
29+
30+
@Override
31+
public PutObjectRequest encodeMetadata(
32+
EncryptionMaterials materials,
33+
EncryptedContent encryptedContent,
34+
PutObjectRequest request) {
35+
Map<String,String> metadata = new HashMap<>(request.metadata());
36+
EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
37+
metadata.put(MetadataKey.ENCRYPTED_DATA_KEY, _encoder.encodeToString(edk.ciphertext()));
38+
metadata.put(MetadataKey.CONTENT_NONCE, _encoder.encodeToString(encryptedContent.nonce));
39+
metadata.put(MetadataKey.CONTENT_CIPHER, materials.algorithmSuite().cipherName());
40+
metadata.put(MetadataKey.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits()));
41+
metadata.put(MetadataKey.ENCRYPTED_DATA_KEY_ALGORITHM, edk.keyProviderId());
42+
43+
try (JsonWriter jsonWriter = JsonWriter.create()) {
44+
jsonWriter.writeStartObject();
45+
for (Entry<String,String> entry : materials.encryptionContext().entrySet()) {
46+
jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
47+
}
48+
jsonWriter.writeEndObject();
49+
50+
String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
51+
metadata.put(MetadataKey.ENCRYPTED_DATA_KEY_CONTEXT, jsonEncryptionContext);
52+
} catch (JsonGenerationException e) {
53+
throw new S3EncryptionClientException("Cannot serialize encryption context to JSON.", e);
54+
}
55+
56+
return request.toBuilder().metadata(metadata).build();
57+
}
58+
59+
public static class Builder {
60+
private Base64.Encoder _encoder = Base64.getEncoder();
61+
62+
public Builder base64Encoder(Base64.Encoder encoder) {
63+
this._encoder = encoder;
64+
return this;
65+
}
66+
67+
public ObjectMetadataMetadataEncodingStrategy build() {
68+
return new ObjectMetadataMetadataEncodingStrategy(this);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)