Skip to content

Commit 88c0120

Browse files
committed
Add Instruction File metadata decoding strategy(legacy)
1 parent 59bb8b1 commit 88c0120

File tree

5 files changed

+296
-19
lines changed

5 files changed

+296
-19
lines changed
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package software.amazon.encryption.s3.internal;
22

33
import java.util.Map;
4-
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
54

65
@FunctionalInterface
76
public interface ContentMetadataDecodingStrategy {
8-
ContentMetadata decodeMetadata(GetObjectResponse response);
7+
ContentMetadata decodeMetadata(Map<String, String> response);
98
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
import java.io.ByteArrayInputStream;
44
import java.io.IOException;
55
import java.util.Collections;
6+
import java.util.HashMap;
67
import java.util.List;
8+
import java.util.Map;
9+
710
import software.amazon.awssdk.core.ResponseInputStream;
811
import software.amazon.awssdk.core.sync.ResponseTransformer;
912
import software.amazon.awssdk.http.AbortableInputStream;
13+
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
14+
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
1015
import software.amazon.awssdk.services.s3.S3Client;
1116
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
1217
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
@@ -49,7 +54,30 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
4954
.builder()
5055
.build();
5156

52-
ContentMetadata contentMetadata = contentMetadataDecodingStrategy.decodeMetadata(response);
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);
5381

5482
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
5583
List<EncryptedDataKey> encryptedDataKeys = Collections.singletonList(contentMetadata.encryptedDataKey());
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
4+
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
5+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
6+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
7+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
8+
import software.amazon.encryption.s3.S3EncryptionClientException;
9+
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
10+
import software.amazon.encryption.s3.materials.EncryptedDataKey;
11+
import software.amazon.encryption.s3.materials.EncryptionMaterials;
12+
import software.amazon.encryption.s3.materials.S3Keyring;
13+
14+
import java.nio.charset.StandardCharsets;
15+
import java.util.Base64;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import java.util.Map.Entry;
19+
20+
/**
21+
* This Decrypts metadata from Instruction File which is stored parallel to S3 Object.
22+
* The name is not a typo
23+
*/
24+
public class InstructionFileMetadataDecodingStrategy implements ContentMetadataDecodingStrategy {
25+
private final Base64.Decoder _decoder;
26+
27+
private InstructionFileMetadataDecodingStrategy(Builder builder) {
28+
this._decoder = builder._decoder;
29+
}
30+
31+
public static Builder builder() { return new Builder(); }
32+
33+
@Override
34+
public ContentMetadata decodeMetadata(Map<String, String> metadata) {
35+
36+
// Get algorithm suite
37+
final String contentEncryptionAlgorithm = metadata.get(MetadataKeyConstants.CONTENT_CIPHER);
38+
AlgorithmSuite algorithmSuite;
39+
if (contentEncryptionAlgorithm == null
40+
|| contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF.cipherName())) {
41+
algorithmSuite = AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF;
42+
} else if (contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName())) {
43+
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
44+
} else {
45+
throw new S3EncryptionClientException(
46+
"Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
47+
}
48+
49+
// Do algorithm suite dependent decoding
50+
byte[] edkCiphertext;
51+
52+
// Currently, this is not stored within the metadata,
53+
// signal to keyring(s) intended for S3EC
54+
final String keyProviderId = S3Keyring.KEY_PROVIDER_ID;
55+
String keyProviderInfo;
56+
switch (algorithmSuite) {
57+
case ALG_AES_256_CBC_IV16_NO_KDF:
58+
// Extract encrypted data key ciphertext
59+
edkCiphertext = _decoder.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1));
60+
61+
// Hardcode the key provider id to match what V1 does
62+
keyProviderInfo = "AES";
63+
64+
break;
65+
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
66+
// Check tag length
67+
final int tagLength = Integer.parseInt(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH));
68+
if (tagLength != algorithmSuite.cipherTagLengthBits()) {
69+
throw new S3EncryptionClientException("Expected tag length (bits) of: "
70+
+ algorithmSuite.cipherTagLengthBits()
71+
+ ", got: " + tagLength);
72+
}
73+
74+
// Extract encrypted data key ciphertext and provider id
75+
edkCiphertext = _decoder.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2));
76+
keyProviderInfo = metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM);
77+
78+
break;
79+
default:
80+
throw new S3EncryptionClientException(
81+
"Unknown content encryption algorithm: " + algorithmSuite.id());
82+
}
83+
84+
// Build encrypted data key
85+
EncryptedDataKey edk = EncryptedDataKey.builder()
86+
.ciphertext(edkCiphertext)
87+
.keyProviderId(keyProviderId)
88+
.keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8))
89+
.build();
90+
91+
// Get encrypted data key encryption context
92+
final Map<String, String> encryptionContext = new HashMap<>();
93+
final String jsonEncryptionContext = metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT);
94+
try {
95+
JsonNodeParser parser = JsonNodeParser.create();
96+
JsonNode objectNode = parser.parse(jsonEncryptionContext);
97+
98+
for (Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
99+
encryptionContext.put(entry.getKey(), entry.getValue().asString());
100+
}
101+
} catch (Exception e) {
102+
throw new RuntimeException(e);
103+
}
104+
105+
// Get content nonce
106+
byte[] nonce = _decoder.decode(metadata.get(MetadataKeyConstants.CONTENT_NONCE));
107+
108+
return ContentMetadata.builder()
109+
.algorithmSuite(algorithmSuite)
110+
.encryptedDataKey(edk)
111+
.encryptedDataKeyContext(encryptionContext)
112+
.contentNonce(nonce)
113+
.build();
114+
}
115+
116+
public static class Builder {
117+
private Base64.Decoder _decoder = Base64.getDecoder();
118+
119+
public Builder base64Decoder(Base64.Decoder decoder) {
120+
this._decoder = decoder;
121+
return this;
122+
}
123+
124+
public InstructionFileMetadataDecodingStrategy build() {
125+
return new InstructionFileMetadataDecodingStrategy(this);
126+
}
127+
}
128+
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
1010
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
1111
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
12-
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
1312
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
1413
import software.amazon.encryption.s3.S3EncryptionClientException;
1514
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
@@ -63,8 +62,7 @@ public PutObjectRequest encodeMetadata(
6362
}
6463

6564
@Override
66-
public ContentMetadata decodeMetadata(GetObjectResponse response) {
67-
Map<String, String> metadata = response.metadata();
65+
public ContentMetadata decodeMetadata(Map<String, String> metadata) {
6866

6967
// Get algorithm suite
7068
final String contentEncryptionAlgorithm = metadata.get(MetadataKeyConstants.CONTENT_CIPHER);

src/test/java/S3EncryptionClientTest.java

Lines changed: 137 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,8 @@
66
import com.amazonaws.services.s3.AmazonS3EncryptionClient;
77
import com.amazonaws.services.s3.AmazonS3EncryptionClientV2;
88
import com.amazonaws.services.s3.AmazonS3EncryptionV2;
9-
import com.amazonaws.services.s3.model.CryptoConfiguration;
10-
import com.amazonaws.services.s3.model.CryptoConfigurationV2;
11-
import com.amazonaws.services.s3.model.CryptoMode;
12-
import com.amazonaws.services.s3.model.EncryptedPutObjectRequest;
13-
import com.amazonaws.services.s3.model.EncryptionMaterials;
14-
import com.amazonaws.services.s3.model.EncryptionMaterialsProvider;
15-
import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider;
16-
import com.amazonaws.services.s3.model.ObjectMetadata;
17-
import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider;
9+
import com.amazonaws.services.s3.model.*;
10+
1811
import java.io.ByteArrayInputStream;
1912
import java.nio.charset.StandardCharsets;
2013
import java.security.KeyPair;
@@ -35,11 +28,11 @@
3528
public class S3EncryptionClientTest {
3629

3730
// TODO: make these dynamic
38-
private static final String BUCKET = "845853869857-s3-research";
31+
private static final String BUCKET = "s3encryptionclient";
3932

40-
private static final String KMS_MASTER_KEY = "e45015eb-1643-448f-9145-8ed4679138e4";
33+
private static final String KMS_MASTER_KEY = "6c7db579-a16c-48c0-adea-604f6b449758";
4134

42-
private static final Region KMS_REGION = Region.getRegion(Regions.US_EAST_2);
35+
private static final Region KMS_REGION = Region.getRegion(Regions.US_WEST_2);
4336

4437
private static SecretKey AES_KEY;
4538
private static KeyPair RSA_KEY_PAIR;
@@ -117,6 +110,38 @@ public void AesWrapV1toV3() {
117110
assertEquals(input, output);
118111
}
119112

113+
@Test
114+
public void AesWrapV1toV3WithInstructionMode() {
115+
final String BUCKET_KEY = "aes-wrap-v1-to-v3-with-instruction-storage-mode";
116+
117+
// V1 Client
118+
EncryptionMaterialsProvider materialsProvider =
119+
new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
120+
CryptoConfiguration v1CryptoConfig =
121+
new CryptoConfiguration(CryptoMode.AuthenticatedEncryption)
122+
.withStorageMode(CryptoStorageMode.InstructionFile);
123+
AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
124+
.withCryptoConfiguration(v1CryptoConfig)
125+
.withEncryptionMaterials(materialsProvider)
126+
.build();
127+
128+
// V3 Client
129+
S3Client v3Client = S3EncryptionClient.builder()
130+
.aesKey(AES_KEY)
131+
.enableLegacyModes(true)
132+
.build();
133+
134+
// Asserts
135+
final String input = "AesGcmV1toV3";
136+
v1Client.putObject(BUCKET, BUCKET_KEY, input);
137+
138+
ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(GetObjectRequest.builder()
139+
.bucket(BUCKET)
140+
.key(BUCKET_KEY).build());
141+
String output = objectResponse.asUtf8String();
142+
assertEquals(input, output);
143+
}
144+
120145
@Test
121146
public void AesGcmV2toV3() {
122147
final String BUCKET_KEY = "aes-gcm-v3-to-v2";
@@ -257,6 +282,38 @@ public void RsaEcbV1toV3() {
257282
assertEquals(input, output);
258283
}
259284

285+
@Test
286+
public void RsaEcbV1toV3WithInstructionMode() {
287+
final String BUCKET_KEY = "rsa-ecb-v1-to-v3-with-instruction-mode";
288+
289+
// V1 Client
290+
EncryptionMaterialsProvider materialsProvider =
291+
new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
292+
CryptoConfiguration v1CryptoConfig =
293+
new CryptoConfiguration(CryptoMode.AuthenticatedEncryption)
294+
.withStorageMode(CryptoStorageMode.InstructionFile);;
295+
AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
296+
.withCryptoConfiguration(v1CryptoConfig)
297+
.withEncryptionMaterials(materialsProvider)
298+
.build();
299+
300+
// V3 Client
301+
S3Client v3Client = S3EncryptionClient.builder()
302+
.rsaKeyPair(RSA_KEY_PAIR)
303+
.enableLegacyModes(true)
304+
.build();
305+
306+
// Asserts
307+
final String input = "RsaEcbV1toV3";
308+
v1Client.putObject(BUCKET, BUCKET_KEY, input);
309+
310+
ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(GetObjectRequest.builder()
311+
.bucket(BUCKET)
312+
.key(BUCKET_KEY).build());
313+
String output = objectResponse.asUtf8String();
314+
assertEquals(input, output);
315+
}
316+
260317
@Test
261318
public void RsaOaepV2toV3() {
262319
final String BUCKET_KEY = "rsa-oaep-v2-to-v3";
@@ -265,7 +322,40 @@ public void RsaOaepV2toV3() {
265322
EncryptionMaterialsProvider materialsProvider =
266323
new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
267324
CryptoConfigurationV2 cryptoConfig =
268-
new CryptoConfigurationV2(CryptoMode.StrictAuthenticatedEncryption);
325+
new CryptoConfigurationV2(CryptoMode.StrictAuthenticatedEncryption)
326+
.withStorageMode(CryptoStorageMode.InstructionFile);
327+
AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
328+
.withCryptoConfiguration(cryptoConfig)
329+
.withEncryptionMaterialsProvider(materialsProvider)
330+
.build();
331+
332+
// V3 Client
333+
S3Client v3Client = S3EncryptionClient.builder()
334+
.rsaKeyPair(RSA_KEY_PAIR)
335+
.build();
336+
337+
// Asserts
338+
final String input = "RsaOaepV2toV3";
339+
v2Client.putObject(BUCKET, BUCKET_KEY, input);
340+
341+
ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(
342+
GetObjectRequest.builder()
343+
.bucket(BUCKET)
344+
.key(BUCKET_KEY).build());
345+
String output = objectResponse.asUtf8String();
346+
assertEquals(input, output);
347+
}
348+
349+
@Test
350+
public void RsaOaepV2toV3WithInstructionMode() {
351+
final String BUCKET_KEY = "rsa-oaep-v2-to-v3-with-instruction-mode";
352+
353+
// V2 Client
354+
EncryptionMaterialsProvider materialsProvider =
355+
new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
356+
CryptoConfigurationV2 cryptoConfig =
357+
new CryptoConfigurationV2(CryptoMode.StrictAuthenticatedEncryption)
358+
.withStorageMode(CryptoStorageMode.InstructionFile);
269359
AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
270360
.withCryptoConfiguration(cryptoConfig)
271361
.withEncryptionMaterialsProvider(materialsProvider)
@@ -402,6 +492,40 @@ public void KmsV1toV3() {
402492
assertEquals(input, output);
403493
}
404494

495+
@Test
496+
public void KmsV1toV3WithInstructionMode() {
497+
final String BUCKET_KEY = "kms-v1-to-v3-with-instruction-mode";
498+
499+
// V1 Client
500+
EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_MASTER_KEY);
501+
502+
CryptoConfiguration v1Config =
503+
new CryptoConfiguration(CryptoMode.AuthenticatedEncryption)
504+
.withStorageMode(CryptoStorageMode.InstructionFile)
505+
.withAwsKmsRegion(KMS_REGION);
506+
507+
AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
508+
.withCryptoConfiguration(v1Config)
509+
.withEncryptionMaterials(materialsProvider)
510+
.build();
511+
512+
// V3 Client
513+
S3Client v3Client = S3EncryptionClient.builder()
514+
.kmsKeyId(KMS_MASTER_KEY)
515+
.enableLegacyModes(true)
516+
.build();
517+
518+
// Asserts
519+
final String input = "KmsV1toV3";
520+
v1Client.putObject(BUCKET, BUCKET_KEY, input);
521+
522+
ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(GetObjectRequest.builder()
523+
.bucket(BUCKET)
524+
.key(BUCKET_KEY).build());
525+
String output = objectResponse.asUtf8String();
526+
assertEquals(input, output);
527+
}
528+
405529
@Test
406530
public void KmsContextV2toV3() {
407531
final String BUCKET_KEY = "kms-context-v2-to-v3";

0 commit comments

Comments
 (0)