Skip to content

Commit 4fd2fa8

Browse files
authored
feat: implement CBC decryption in async getObject (#59)
* feat: implement getObject async
1 parent b9834ce commit 4fd2fa8

File tree

2 files changed

+78
-25
lines changed

2 files changed

+78
-25
lines changed

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

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package software.amazon.encryption.s3.internal;
22

33
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4-
54
import software.amazon.awssdk.core.ResponseInputStream;
65
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
76
import software.amazon.awssdk.core.async.SdkPublisher;
@@ -23,6 +22,7 @@
2322
import javax.crypto.Cipher;
2423
import javax.crypto.SecretKey;
2524
import javax.crypto.spec.GCMParameterSpec;
25+
import javax.crypto.spec.IvParameterSpec;
2626
import javax.crypto.spec.SecretKeySpec;
2727
import java.io.InputStream;
2828
import java.nio.ByteBuffer;
@@ -64,29 +64,6 @@ public <T> CompletableFuture<T> getObject(GetObjectRequest getObjectRequest, Asy
6464
getObjectRequest));
6565
}
6666

67-
/**
68-
* This helps reduce code duplication between async and default getObject implementations.
69-
*/
70-
private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest getObjectRequest, final GetObjectResponse getObjectResponse,
71-
final ContentMetadata contentMetadata) {
72-
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
73-
if (!_enableLegacyUnauthenticatedModes && algorithmSuite.isLegacy()) {
74-
throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use legacy content decryption: " + algorithmSuite.cipherName());
75-
}
76-
77-
List<EncryptedDataKey> encryptedDataKeys = Collections.singletonList(contentMetadata.encryptedDataKey());
78-
79-
DecryptMaterialsRequest materialsRequest = DecryptMaterialsRequest.builder()
80-
.s3Request(getObjectRequest)
81-
.algorithmSuite(algorithmSuite)
82-
.encryptedDataKeys(encryptedDataKeys)
83-
.encryptionContext(contentMetadata.encryptedDataKeyContext())
84-
.ciphertextLength(getObjectResponse.contentLength())
85-
.build();
86-
87-
return _cryptoMaterialsManager.decryptMaterials(materialsRequest);
88-
}
89-
9067
public <T> T getObject(GetObjectRequest getObjectRequest,
9168
ResponseTransformer<GetObjectResponse, T> responseTransformer) {
9269
ResponseInputStream<GetObjectResponse> objectStream;
@@ -114,6 +91,29 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
11491
}
11592
}
11693

94+
/**
95+
* This helps reduce code duplication between async and default getObject implementations.
96+
*/
97+
private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest getObjectRequest, final GetObjectResponse getObjectResponse,
98+
final ContentMetadata contentMetadata) {
99+
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
100+
if (!_enableLegacyUnauthenticatedModes && algorithmSuite.isLegacy()) {
101+
throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use legacy content decryption: " + algorithmSuite.cipherName());
102+
}
103+
104+
List<EncryptedDataKey> encryptedDataKeys = Collections.singletonList(contentMetadata.encryptedDataKey());
105+
106+
DecryptMaterialsRequest materialsRequest = DecryptMaterialsRequest.builder()
107+
.s3Request(getObjectRequest)
108+
.algorithmSuite(algorithmSuite)
109+
.encryptedDataKeys(encryptedDataKeys)
110+
.encryptionContext(contentMetadata.encryptedDataKeyContext())
111+
.ciphertextLength(getObjectResponse.contentLength())
112+
.build();
113+
114+
return _cryptoMaterialsManager.decryptMaterials(materialsRequest);
115+
}
116+
117117
private ContentDecryptionStrategy selectContentDecryptionStrategy(final DecryptionMaterials materials) {
118118
switch (materials.algorithmSuite()) {
119119
case ALG_AES_256_CBC_IV16_NO_KDF:
@@ -161,6 +161,9 @@ public CompletableFuture<T> prepare() {
161161
@Override
162162
public void onResponse(GetObjectResponse response) {
163163
getObjectResponse = response;
164+
if (!_enableLegacyUnauthenticatedModes && getObjectRequest.range() != null) {
165+
throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use Ranged Get.");
166+
}
164167
// TODO: Implement instruction file handling - this is a bit less intuitive in async
165168
contentMetadata = ContentMetadataStrategy.decode(null, getObjectRequest, response);
166169
materials = prepareMaterialsFromRequest(getObjectRequest, response, contentMetadata);
@@ -180,7 +183,18 @@ public void onStream(SdkPublisher<ByteBuffer> ciphertextPublisher) {
180183
byte[] iv = contentMetadata.contentNonce();
181184
try {
182185
final Cipher cipher = CryptoFactory.createCipher(algorithmSuite.cipherName(), materials.cryptoProvider());
183-
cipher.init(Cipher.DECRYPT_MODE, contentKey, new GCMParameterSpec(tagLength, iv));
186+
switch (algorithmSuite) {
187+
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
188+
cipher.init(Cipher.DECRYPT_MODE, contentKey, new GCMParameterSpec(tagLength, iv));
189+
break;
190+
case ALG_AES_256_CBC_IV16_NO_KDF:
191+
cipher.init(Cipher.DECRYPT_MODE, contentKey, new IvParameterSpec(iv));
192+
break;
193+
case ALG_AES_256_CTR_IV16_TAG16_NO_KDF:
194+
// TODO: Ranged Get
195+
default:
196+
throw new S3EncryptionClientException("Unknown algorithm: " + algorithmSuite.cipherName());
197+
}
184198

185199
CipherPublisher plaintextPublisher = new CipherPublisher(cipher, ciphertextPublisher, getObjectResponse.contentLength());
186200
wrappedAsyncResponseTransformer.onStream(plaintextPublisher);

src/test/java/software/amazon/encryption/s3/S3AsyncEncryptionClientTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package software.amazon.encryption.s3;
22

3+
import com.amazonaws.services.s3.AmazonS3Encryption;
4+
import com.amazonaws.services.s3.AmazonS3EncryptionClient;
35
import com.amazonaws.services.s3.AmazonS3EncryptionClientV2;
46
import com.amazonaws.services.s3.AmazonS3EncryptionV2;
7+
import com.amazonaws.services.s3.model.CryptoConfiguration;
58
import com.amazonaws.services.s3.model.CryptoConfigurationV2;
69
import com.amazonaws.services.s3.model.CryptoMode;
710
import com.amazonaws.services.s3.model.CryptoStorageMode;
@@ -78,6 +81,42 @@ public void putDefaultGetAsync() {
7881
v3AsyncClient.close();
7982
}
8083

84+
@Test
85+
public void aesCbcV1toV3Async() {
86+
final String objectKey = "aes-cbc-v1-to-v3-async";
87+
88+
// V1 Client
89+
EncryptionMaterialsProvider materialsProvider =
90+
new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
91+
CryptoConfiguration v1CryptoConfig =
92+
new CryptoConfiguration();
93+
AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
94+
.withCryptoConfiguration(v1CryptoConfig)
95+
.withEncryptionMaterials(materialsProvider)
96+
.build();
97+
98+
final String input = "0bcdefghijklmnopqrst0BCDEFGHIJKLMNOPQRST";
99+
100+
v1Client.putObject(BUCKET, objectKey, input);
101+
102+
// V3 Client
103+
S3AsyncClient v3Client = S3AsyncEncryptionClient.builder()
104+
.aesKey(AES_KEY)
105+
.enableLegacyUnauthenticatedModes(true)
106+
.build();
107+
108+
CompletableFuture<ResponseBytes<GetObjectResponse>> futureResponse = v3Client.getObject(builder -> builder
109+
.bucket(BUCKET)
110+
.key(objectKey), AsyncResponseTransformer.toBytes());
111+
ResponseBytes<GetObjectResponse> response = futureResponse.join();
112+
String output = response.asUtf8String();
113+
assertEquals(input, output);
114+
115+
// Cleanup
116+
deleteObject(BUCKET, objectKey, v3Client);
117+
v3Client.close();
118+
}
119+
81120
@Test
82121
public void deleteObjectWithInstructionFileSuccessAsync() {
83122
final String objectKey = "async-delete-object-with-instruction-file";

0 commit comments

Comments
 (0)