Skip to content

Commit 7d6d7d6

Browse files
authored
Merge pull request #1 from smswz/unified-metadata-strategy
Consolidate metadata strategy
2 parents 77e065d + cdcf6a2 commit 7d6d7d6

File tree

7 files changed

+207
-450
lines changed

7 files changed

+207
-450
lines changed
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package software.amazon.encryption.s3.internal;
22

3-
import java.util.Map;
3+
import software.amazon.awssdk.services.s3.S3Client;
4+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
5+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
46

57
@FunctionalInterface
68
public interface ContentMetadataDecodingStrategy {
7-
ContentMetadata decodeMetadata(Map<String, String> response);
9+
ContentMetadata decodeMetadata(S3Client client, GetObjectRequest request, GetObjectResponse response);
810
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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.core.ResponseInputStream;
9+
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
10+
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
11+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
12+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
13+
import software.amazon.awssdk.services.s3.S3Client;
14+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
15+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
16+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
17+
import software.amazon.encryption.s3.S3EncryptionClientException;
18+
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
19+
import software.amazon.encryption.s3.materials.EncryptedDataKey;
20+
import software.amazon.encryption.s3.materials.EncryptionMaterials;
21+
import software.amazon.encryption.s3.materials.S3Keyring;
22+
23+
public abstract class ContentMetadataStrategy implements ContentMetadataEncodingStrategy, ContentMetadataDecodingStrategy {
24+
25+
private static final Base64.Encoder ENCODER = Base64.getEncoder();
26+
private static final Base64.Decoder DECODER = Base64.getDecoder();
27+
28+
public static final ContentMetadataDecodingStrategy INSTRUCTION_FILE = new ContentMetadataDecodingStrategy() {
29+
30+
private static final String FILE_SUFFIX = ".instruction";
31+
32+
@Override
33+
public ContentMetadata decodeMetadata(S3Client client, GetObjectRequest getObjectRequest, GetObjectResponse response) {
34+
GetObjectRequest instructionGetObjectRequest = GetObjectRequest.builder()
35+
.bucket(getObjectRequest.bucket())
36+
.key(getObjectRequest.key() + FILE_SUFFIX)
37+
.build();
38+
ResponseInputStream<GetObjectResponse> instruction = client.getObject(
39+
instructionGetObjectRequest);
40+
41+
Map<String, String> metadata = new HashMap<>();
42+
JsonNodeParser parser = JsonNodeParser.create();
43+
JsonNode objectNode = parser.parse(instruction);
44+
for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
45+
metadata.put(entry.getKey(), entry.getValue().asString());
46+
}
47+
48+
return ContentMetadataStrategy.readFromMap(metadata);
49+
}
50+
};
51+
52+
public static final ContentMetadataStrategy OBJECT_METADATA = new ContentMetadataStrategy() {
53+
54+
@Override
55+
public PutObjectRequest encodeMetadata(EncryptionMaterials materials,
56+
EncryptedContent encryptedContent, PutObjectRequest request) {
57+
Map<String,String> metadata = new HashMap<>(request.metadata());
58+
EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
59+
metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2, ENCODER.encodeToString(edk.ciphertext()));
60+
metadata.put(MetadataKeyConstants.CONTENT_NONCE, ENCODER.encodeToString(encryptedContent.nonce));
61+
metadata.put(MetadataKeyConstants.CONTENT_CIPHER, materials.algorithmSuite().cipherName());
62+
metadata.put(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits()));
63+
metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM, new String(edk.keyProviderInfo(), StandardCharsets.UTF_8));
64+
65+
try (JsonWriter jsonWriter = JsonWriter.create()) {
66+
jsonWriter.writeStartObject();
67+
for (Entry<String,String> entry : materials.encryptionContext().entrySet()) {
68+
jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
69+
}
70+
jsonWriter.writeEndObject();
71+
72+
String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
73+
metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, jsonEncryptionContext);
74+
} catch (JsonGenerationException e) {
75+
throw new S3EncryptionClientException("Cannot serialize encryption context to JSON.", e);
76+
}
77+
78+
return request.toBuilder().metadata(metadata).build();
79+
}
80+
81+
@Override
82+
public ContentMetadata decodeMetadata(S3Client client, GetObjectRequest request, GetObjectResponse response) {
83+
return ContentMetadataStrategy.readFromMap(response.metadata());
84+
}
85+
};
86+
87+
private static ContentMetadata readFromMap(Map<String, String> metadata) {
88+
// Get algorithm suite
89+
final String contentEncryptionAlgorithm = metadata.get(MetadataKeyConstants.CONTENT_CIPHER);
90+
AlgorithmSuite algorithmSuite;
91+
if (contentEncryptionAlgorithm == null
92+
|| contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF.cipherName())) {
93+
algorithmSuite = AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF;
94+
} else if (contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName())) {
95+
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
96+
} else {
97+
throw new S3EncryptionClientException(
98+
"Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
99+
}
100+
101+
// Do algorithm suite dependent decoding
102+
byte[] edkCiphertext;
103+
104+
// Currently, this is not stored within the metadata,
105+
// signal to keyring(s) intended for S3EC
106+
final String keyProviderId = S3Keyring.KEY_PROVIDER_ID;
107+
String keyProviderInfo;
108+
switch (algorithmSuite) {
109+
case ALG_AES_256_CBC_IV16_NO_KDF:
110+
// Extract encrypted data key ciphertext
111+
edkCiphertext = DECODER.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1));
112+
113+
// Hardcode the key provider id to match what V1 does
114+
keyProviderInfo = "AES";
115+
116+
break;
117+
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
118+
// Check tag length
119+
final int tagLength = Integer.parseInt(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH));
120+
if (tagLength != algorithmSuite.cipherTagLengthBits()) {
121+
throw new S3EncryptionClientException("Expected tag length (bits) of: "
122+
+ algorithmSuite.cipherTagLengthBits()
123+
+ ", got: " + tagLength);
124+
}
125+
126+
// Extract encrypted data key ciphertext and provider id
127+
edkCiphertext = DECODER.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2));
128+
keyProviderInfo = metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM);
129+
130+
break;
131+
default:
132+
throw new S3EncryptionClientException(
133+
"Unknown content encryption algorithm: " + algorithmSuite.id());
134+
}
135+
136+
// Build encrypted data key
137+
EncryptedDataKey edk = EncryptedDataKey.builder()
138+
.ciphertext(edkCiphertext)
139+
.keyProviderId(keyProviderId)
140+
.keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8))
141+
.build();
142+
143+
// Get encrypted data key encryption context
144+
final Map<String, String> encryptionContext = new HashMap<>();
145+
final String jsonEncryptionContext = metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT);
146+
try {
147+
JsonNodeParser parser = JsonNodeParser.create();
148+
JsonNode objectNode = parser.parse(jsonEncryptionContext);
149+
150+
for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
151+
encryptionContext.put(entry.getKey(), entry.getValue().asString());
152+
}
153+
} catch (Exception e) {
154+
throw new RuntimeException(e);
155+
}
156+
157+
// Get content nonce
158+
byte[] nonce = DECODER.decode(metadata.get(MetadataKeyConstants.CONTENT_NONCE));
159+
160+
return ContentMetadata.builder()
161+
.algorithmSuite(algorithmSuite)
162+
.encryptedDataKey(edk)
163+
.encryptedDataKeyContext(encryptionContext)
164+
.contentNonce(nonce)
165+
.build();
166+
}
167+
168+
public static ContentMetadata decode(S3Client client, GetObjectRequest request, GetObjectResponse response) {
169+
Map<String, String> metadata = response.metadata();
170+
ContentMetadataDecodingStrategy strategy;
171+
if (metadata != null
172+
&& metadata.containsKey(MetadataKeyConstants.CONTENT_NONCE)
173+
&& (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)
174+
|| metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2))) {
175+
strategy = OBJECT_METADATA;
176+
} else {
177+
strategy = INSTRUCTION_FILE;
178+
}
179+
180+
return strategy.decodeMetadata(client, request, response);
181+
}
182+
}

src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33
import java.io.ByteArrayInputStream;
44
import java.io.IOException;
55
import java.util.Collections;
6-
import java.util.HashMap;
76
import java.util.List;
8-
import java.util.Map;
97

108
import software.amazon.awssdk.core.ResponseInputStream;
119
import software.amazon.awssdk.core.sync.ResponseTransformer;
1210
import software.amazon.awssdk.http.AbortableInputStream;
13-
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
14-
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
1511
import software.amazon.awssdk.services.s3.S3Client;
1612
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
1713
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
@@ -47,37 +43,8 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
4743
throw new RuntimeException(e);
4844
}
4945

50-
GetObjectResponse response = objectStream.response();
51-
52-
// TODO: Need to differentiate metadata decoding strategy here
53-
ContentMetadataDecodingStrategy contentMetadataDecodingStrategy = S3ObjectMetadataStrategy
54-
.builder()
55-
.build();
56-
57-
Map metadata = response.metadata();
58-
59-
// If Metadata is not in S3 Object,
60-
// Pulls metadata from Instruction File which is stored parallel to S3 Object
61-
if ((metadata == null) || (metadata.get(MetadataKeyConstants.CONTENT_CIPHER) == null)) {
62-
contentMetadataDecodingStrategy = InstructionFileMetadataDecodingStrategy.builder().build();
63-
String instructionSuffix = ".instruction";
64-
65-
GetObjectRequest instructionGetObjectRequest = GetObjectRequest.builder()
66-
.bucket(getObjectRequest.bucket())
67-
.key(getObjectRequest.key() + instructionSuffix )
68-
.build();
69-
ResponseInputStream<GetObjectResponse> instruction = _s3Client.getObject(instructionGetObjectRequest);
70-
71-
Map<String, String> metadataContext = new HashMap<>();
72-
JsonNodeParser parser = JsonNodeParser.create();
73-
JsonNode objectNode = parser.parse(instruction);
74-
for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
75-
metadataContext.put(entry.getKey(), entry.getValue().asString());
76-
}
77-
metadata = metadataContext;
78-
}
79-
80-
ContentMetadata contentMetadata = contentMetadataDecodingStrategy.decodeMetadata(metadata);
46+
GetObjectResponse getObjectResponse = objectStream.response();
47+
ContentMetadata contentMetadata = ContentMetadataStrategy.decode(_s3Client, getObjectRequest, getObjectResponse);
8148

8249
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
8350
List<EncryptedDataKey> encryptedDataKeys = Collections.singletonList(contentMetadata.encryptedDataKey());
@@ -102,7 +69,7 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
10269
byte[] plaintext = contentDecryptionStrategy.decryptContent(contentMetadata, materials, ciphertext);
10370

10471
try {
105-
return responseTransformer.transform(response,
72+
return responseTransformer.transform(getObjectResponse,
10673
AbortableInputStream.create(new ByteArrayInputStream(plaintext)));
10774
} catch (Exception e) {
10875
throw new S3EncryptionClientException("Unable to transform response.", e);

src/main/java/software/amazon/encryption/s3/internal/InstructionFileMetadataDecodingStrategy.java

Lines changed: 0 additions & 128 deletions
This file was deleted.

0 commit comments

Comments
 (0)