From 1bd8b7a61500080735d90bbff0ab19af35ff0a6a Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:39:13 -0700 Subject: [PATCH 1/6] chore: add client specification and Duvet annotations (#481) * use implication/implementation/test annotation types instead of just comments for everything --- .gitmodules | 4 + Makefile | 14 ++ specification | 1 + .../encryption/s3/S3EncryptionClient.java | 136 +++++++++++++++++- .../internal/GetEncryptedObjectPipeline.java | 12 ++ .../MultipartUploadObjectPipeline.java | 21 ++- .../encryption/s3/materials/S3Keyring.java | 8 +- .../S3EncryptionClientCompatibilityTest.java | 6 + .../encryption/s3/S3EncryptionClientTest.java | 47 +++++- 9 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 .gitmodules create mode 100644 Makefile create mode 160000 specification diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..74843aee7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "specification"] + path = specification + url = git@github.com:awslabs/aws-encryption-sdk-specification.git + branch = kessplas/s3-ec-v3 diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ec9f02e7f --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +# Used for misc supporting functions like Duvet and prettier. Builds, tests, etc. should use the usual Java/Maven tooling. + +duvet: | duvet_extract duvet_report + +duvet_extract: + rm -rf compliance + $(foreach file, $(shell find specification/s3-encryption -name '*.md'), duvet extract -o compliance -f MARKDOWN $(file);) + +duvet_report: + duvet \ + report \ + --spec-pattern "compliance/**/*.toml" \ + --source-pattern "src/**/*.java" \ + --html specification_compliance_report.html diff --git a/specification b/specification new file mode 160000 index 000000000..616da1e36 --- /dev/null +++ b/specification @@ -0,0 +1 @@ +Subproject commit 616da1e364a48c118cd19f42efc6cfc653c929ad diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index 8b44bcda3..c4d043cc9 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -101,6 +101,13 @@ import static software.amazon.encryption.s3.S3EncryptionClientUtilities.instructionFileKeysToDelete; import static software.amazon.encryption.s3.internal.ApiNameVersion.API_NAME_INTERCEPTOR; + +//= specification/s3-encryption/client.md#aws-sdk-compatibility +//= type=implication +//# The S3EC MUST adhere to the same interface for API operations as the conventional AWS SDK S3 client. +//= specification/s3-encryption/client.md#aws-sdk-compatibility +//= type=implementation +//# The S3EC SHOULD support invoking operations unrelated to client-side encryption e.g. CopyObject as the conventional AWS SDK S3 client would. /** * This client is a drop-in replacement for the S3 client. It will automatically encrypt objects * on putObject and decrypt objects on getObject using the provided encryption key(s). @@ -125,6 +132,9 @@ public class S3EncryptionClient extends DelegatingS3Client { private final long _bufferSize; private final InstructionFileConfig _instructionFileConfig; + //= specification/s3-encryption/client.md#aws-sdk-compatibility + //= type=implication + //# The S3EC MUST provide a different set of configuration options than the conventional S3 client. private S3EncryptionClient(Builder builder) { super(builder._wrappedClient); _wrappedClient = builder._wrappedClient; @@ -200,6 +210,9 @@ public static Consumer withAdditionalCo .putExecutionAttribute(S3EncryptionClient.CONFIGURATION, multipartConfiguration); } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# ReEncryptInstructionFile MAY be implemented by the S3EC. /** * Re-encrypts an instruction file with a new keyring while preserving the original encrypted object in S3. * This enables: @@ -237,6 +250,9 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru final byte[] iv = contentMetadata.contentIv(); //Decrypt the data key using the current keyring + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM. DecryptionMaterials decryptedMaterials = this._cryptoMaterialsManager.decryptMaterials( DecryptMaterialsRequest.builder() .algorithmSuite(algorithmSuite) @@ -255,6 +271,9 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru .build(); //Re-encrypt the data key with the new keyring while preserving other cryptographic parameters + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring(); EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials); @@ -296,6 +315,9 @@ private void enforceRotation(EncryptionMaterials newEncryptionMaterials, GetObje throw new S3EncryptionClientException("Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key"); } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# PutObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}. *

@@ -322,6 +344,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod throw new S3EncryptionClientException("Exception while performing Multipart Upload PutObject", e); } } + PutEncryptedObjectPipeline pipeline = PutEncryptedObjectPipeline.builder() .s3AsyncClient(_wrappedAsyncClient) .cryptoMaterialsManager(_cryptoMaterialsManager) @@ -332,6 +355,9 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); try { + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# PutObject MUST encrypt its input data before it is uploaded to S3. CompletableFuture futurePut = pipeline.putObject(putObjectRequest, AsyncRequestBody.fromInputStream( requestBody.contentStreamProvider().newStream(), @@ -356,6 +382,9 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# GetObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#getObject(GetObjectRequest, ResponseTransformer)} *

@@ -377,6 +406,9 @@ public T getObject(GetObjectRequest getObjectRequest, ResponseTransformer responseTransformer) throws AwsServiceException, SdkClientException { + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# GetObject MUST decrypt data received from the S3 server and return it as plaintext. GetEncryptedObjectPipeline pipeline = GetEncryptedObjectPipeline.builder() .s3AsyncClient(_wrappedAsyncClient) .cryptoMaterialsManager(_cryptoMaterialsManager) @@ -484,6 +516,9 @@ private T onAbort(UploadObjectObserver observer, T t) { throw new S3EncryptionClientException(t.getMessage(), t); } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# DeleteObject MUST be implemented by the S3EC. /** * See {@link S3Client#deleteObject(DeleteObjectRequest)}. *

@@ -501,9 +536,13 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest .build(); try { - // Delete the object + //= specification/s3-encryption/client.md#api-operations + //= type=implementation + //# DeleteObject MUST delete the given object key. DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient.deleteObject(actualRequest).join(); - // If Instruction file exists, delete the instruction file as well. + //= specification/s3-encryption/client.md#api-operations + //= type=implementation + //# DeleteObject MUST delete the associated instruction file using the default instruction file suffix. String instructionObjectKey = deleteObjectRequest.key() + DEFAULT_INSTRUCTION_FILE_SUFFIX; _wrappedAsyncClient.deleteObject(builder -> builder .overrideConfiguration(API_NAME_INTERCEPTOR) @@ -518,6 +557,9 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest } } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# DeleteObjects MUST be implemented by the S3EC. /** * See {@link S3Client#deleteObjects(DeleteObjectsRequest)}. *

@@ -534,9 +576,13 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); try { - // Delete the objects + //= specification/s3-encryption/client.md#api-operations + //= type=implementation + //# DeleteObjects MUST delete each of the given objects. DeleteObjectsResponse deleteObjectsResponse = _wrappedAsyncClient.deleteObjects(actualRequest).join(); - // If Instruction files exists, delete the instruction files as well. + //= specification/s3-encryption/client.md#api-operations + //= type=implementation + //# DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. List deleteObjects = instructionFileKeysToDelete(deleteObjectsRequest); _wrappedAsyncClient.deleteObjects(DeleteObjectsRequest.builder() .overrideConfiguration(API_NAME_INTERCEPTOR) @@ -551,6 +597,9 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq } } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# CreateMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#createMultipartUpload(CreateMultipartUploadRequest)} *

@@ -572,6 +621,9 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload } } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# UploadPart MAY be implemented by the S3EC. /** * See {@link S3Client#uploadPart(UploadPartRequest, RequestBody)} * @@ -595,6 +647,9 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# CompleteMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#completeMultipartUpload(CompleteMultipartUploadRequest)} * @param request the request instance @@ -612,6 +667,9 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart } } + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# AbortMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#abortMultipartUpload(AbortMultipartUploadRequest)} * @param request the request instance @@ -652,11 +710,20 @@ public static class Builder implements S3BaseClientBuilder ciphertextPublisher) { if (algorithmSuite.equals(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF) || algorithmSuite.equals(AlgorithmSuite.ALG_AES_256_CTR_IV16_TAG16_NO_KDF) || _enableDelayedAuthentication) { + //= specification/s3-encryption/client.md#enable-delayed-authentication + //= type=implication + //# When enabled, the S3EC MAY release plaintext from a stream which has not been authenticated. // CBC and GCM with delayed auth enabled use a standard publisher CipherPublisher plaintextPublisher = new CipherPublisher(ciphertextPublisher, getObjectResponse.contentLength(), desiredRange, contentMetadata.contentRange(), algorithmSuite.cipherTagLengthBits(), materials, iv); wrappedAsyncResponseTransformer.onStream(plaintextPublisher); } else { + //= specification/s3-encryption/client.md#enable-delayed-authentication + //= type=implication + //# When disabled the S3EC MUST NOT release plaintext from a stream which has not been authenticated. // Use buffered publisher for GCM when delayed auth is not enabled BufferedCipherPublisher plaintextPublisher = new BufferedCipherPublisher(ciphertextPublisher, getObjectResponse.contentLength(), materials, iv, _bufferSize); diff --git a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java index cb4dde03a..01c48a578 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java @@ -74,6 +74,9 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# If implemented, CreateMultipartUpload MUST initiate a multipart upload. CreateMultipartUploadResponse response = _s3AsyncClient.createMultipartUpload(request).join(); MultipartUploadMaterials mpuMaterials = MultipartUploadMaterials.builder() @@ -133,13 +136,21 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ throw new S3EncryptionClientException("No client-side information available on upload ID " + uploadId); } final UploadPartResponse response; - // Checks the parts are uploaded in series + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# Each part MUST be encrypted in sequence. materials.beginPartUpload(actualRequest.partNumber(), partContentLength); + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# Each part MUST be encrypted using the same cipher instance for each part. Cipher cipher = materials.getCipher(materials.getIv()); ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); try { + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# UploadPart MUST encrypt each part. final AsyncRequestBody cipherAsyncRequestBody = new CipherAsyncRequestBody( AsyncRequestBody.fromInputStream( requestBody.contentStreamProvider().newStream(), @@ -159,6 +170,8 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } // Ensures parts are not retried to avoid corrupting ciphertext AsyncRequestBody noRetryBody = new NoRetriesAsyncRequestBody(cipherAsyncRequestBody); + //= specification/s3-encryption/client.md#api-operations + //= type=implication response = _s3AsyncClient.uploadPart(actualRequest, noRetryBody).join(); } finally { materials.endPartUpload(); @@ -187,6 +200,9 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# CompleteMultipartUpload MUST complete the multipart upload. CompleteMultipartUploadResponse response = _s3AsyncClient.completeMultipartUpload(actualRequest).join(); _multipartUploadMaterials.remove(uploadId); @@ -198,6 +214,9 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq AbortMultipartUploadRequest actualRequest = request.toBuilder() .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); + //= specification/s3-encryption/client.md#api-operations + //= type=implication + //# AbortMultipartUpload MUST abort the multipart upload. return _s3AsyncClient.abortMultipartUpload(actualRequest).join(); } diff --git a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java index ebf3a6105..822f09e8c 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java @@ -5,13 +5,13 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import software.amazon.encryption.s3.S3EncryptionClientException; +import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Map; -import javax.crypto.SecretKey; /** * This serves as the base class for all the keyrings in the S3 encryption client. @@ -115,7 +115,13 @@ public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List v3Client.getObjectAsBytes(builder -> builder .bucket(BUCKET) .key(objectKey))); @@ -870,6 +873,9 @@ public void AesCbcV1toV3FailsWhenLegacyKeyringDisabled() { final String input = "AesCbcV1toV3"; v1Client.putObject(BUCKET, objectKey, input); + //= specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms + //= type=test + //# When disabled, the S3EC MUST NOT decrypt objects encrypted using legacy wrapping algorithms; it MUST throw an exception when attempting to decrypt an object encrypted with a legacy wrapping algorithm. assertThrows(S3EncryptionClientException.class, () -> v3Client.getObjectAsBytes(builder -> builder .bucket(BUCKET) .key(objectKey))); diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java index d2520c6b2..37c18a6fe 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java @@ -41,6 +41,8 @@ import software.amazon.encryption.s3.materials.CryptographicMaterialsManager; import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager; import software.amazon.encryption.s3.materials.KmsKeyring; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RsaKeyring; import software.amazon.encryption.s3.utils.BoundedInputStream; import software.amazon.encryption.s3.utils.S3EncryptionClientTestResources; @@ -98,6 +100,9 @@ public static void setUp() throws NoSuchAlgorithmException { RSA_KEY_PAIR = keyPairGen.generateKeyPair(); } + //= specification/s3-encryption/client.md#aws-sdk-compatibility + //= type=test + //# The S3EC SHOULD support invoking operations unrelated to client-side encryption e.g. CopyObject as the conventional AWS SDK S3 client would. @Test public void copyObjectTransparently() { final String objectKey = appendTestSuffix("copy-object-from-here"); @@ -161,10 +166,15 @@ public void deleteObjectWithInstructionFileSuccess() { v3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey)); S3Client s3Client = S3Client.builder().build(); - // Assert throw NoSuchKeyException when getObject for objectKey + //= specification/s3-encryption/client.md#api-operations + //= type=test + //# DeleteObject MUST delete the given object key. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder .bucket(BUCKET) .key(objectKey))); + //= specification/s3-encryption/client.md#api-operations + //= type=test + //# DeleteObject MUST delete the associated instruction file using the default instruction file suffix. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder .bucket(BUCKET) .key(objectKey + ".instruction"))); @@ -208,10 +218,15 @@ public void deleteObjectsWithInstructionFilesSuccess() { .delete(builder1 -> builder1.objects(objects))); S3Client s3Client = S3Client.builder().build(); - // Assert throw NoSuchKeyException when getObject for any of objectKeys + //= specification/s3-encryption/client.md#api-operations + //= type=test + //# DeleteObjects MUST delete each of the given objects. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder .bucket(BUCKET) .key(objectKeys[0]))); + //= specification/s3-encryption/client.md#api-operations + //= type=test + //# DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder .bucket(BUCKET) .key(objectKeys[0] + ".instruction"))); @@ -292,6 +307,9 @@ public void getNonExistentObject() { v3Client.close(); } + //= specification/s3-encryption/client.md#cryptographic-materials + //= type=test + //# The S3EC MUST accept either one CMM or one Keyring instance upon initialization. @Test public void s3EncryptionClientWithMultipleKeyringsFails() { assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builder() @@ -300,6 +318,22 @@ public void s3EncryptionClientWithMultipleKeyringsFails() { .build()); } + //= specification/s3-encryption/client.md#cryptographic-materials + //= type=test + //# If both a CMM and a Keyring are provided, the S3EC MUST throw an exception. + @Test + public void s3EncryptionClientWithCMMAndKeyringFails() { + CryptographicMaterialsManager defaultCMM = DefaultCryptoMaterialsManager.builder() + .keyring(RsaKeyring.builder() + .wrappingKeyPair(new PartialRsaKeyPair(RSA_KEY_PAIR)) + .build()) + .build(); + assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builder() + .aesKey(AES_KEY) + .cryptoMaterialsManager(defaultCMM) + .build()); + } + @Test public void s3EncryptionClientWithNoKeyringsFails() { assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builder() @@ -440,6 +474,9 @@ public void s3EncryptionClientWithCmmFromKmsKeyIdSucceeds() { v3Client.close(); } + //= specification/s3-encryption/client.md#wrapped-s3-client-s + //= type=test + //# The S3EC MUST support the option to provide an SDK S3 client instance during its initialization. @Test public void s3EncryptionClientWithWrappedS3ClientSucceeds() { final String objectKey = appendTestSuffix("wrapped-s3-client-with-kms-key-id"); @@ -462,6 +499,9 @@ public void s3EncryptionClientWithWrappedS3ClientSucceeds() { wrappingClient.close(); } + //= specification/s3-encryption/client.md#wrapped-s3-client-s + //= type=test + //# The S3EC MUST NOT support use of S3EC as the provided S3 client during its initialization; it MUST throw an exception in this case. /** * S3EncryptionClient implements S3Client, so it can be passed into the builder as a wrappedClient. * However, is not a supported use case, and the builder should throw an exception if this happens. @@ -854,6 +894,9 @@ public void s3EncryptionClientTopLevelCredentialsNullCreds() { } } + //= specification/s3-encryption/client.md#inherited-sdk-configuration + //= type=test + //# If the S3EC accepts SDK client configuration, the configuration MUST be applied to all wrapped SDK clients including the KMS client. @Test public void s3EncryptionClientTopLevelAlternateCredentials() { final String objectKey = appendTestSuffix("wrapped-s3-client-with-top-level-credentials"); From cc9eafc9649ded342ee2536d77d949469c9066ad Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:52:21 -0700 Subject: [PATCH 2/6] chore: move spec submodule to master, update annotations (#482) --- specification | 2 +- .../encryption/s3/S3EncryptionClient.java | 36 +++++++++---------- .../MultipartUploadObjectPipeline.java | 14 ++++---- .../encryption/s3/S3EncryptionClientTest.java | 8 ++--- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/specification b/specification index 616da1e36..85d4c1769 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit 616da1e364a48c118cd19f42efc6cfc653c929ad +Subproject commit 85d4c176901e29fb795ec551abd11fc7df076f94 diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index c4d043cc9..e1c275bd5 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -210,7 +210,7 @@ public static Consumer withAdditionalCo .putExecutionAttribute(S3EncryptionClient.CONFIGURATION, multipartConfiguration); } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# ReEncryptInstructionFile MAY be implemented by the S3EC. /** @@ -250,7 +250,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru final byte[] iv = contentMetadata.contentIv(); //Decrypt the data key using the current keyring - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM. DecryptionMaterials decryptedMaterials = this._cryptoMaterialsManager.decryptMaterials( @@ -271,7 +271,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru .build(); //Re-encrypt the data key with the new keyring while preserving other cryptographic parameters - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring(); @@ -315,7 +315,7 @@ private void enforceRotation(EncryptionMaterials newEncryptionMaterials, GetObje throw new S3EncryptionClientException("Re-encryption failed due to enforced rotation! Old keyring is still able to decrypt the newly encrypted data key"); } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implication //# PutObject MUST be implemented by the S3EC. /** @@ -355,7 +355,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); try { - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implication //# PutObject MUST encrypt its input data before it is uploaded to S3. CompletableFuture futurePut = pipeline.putObject(putObjectRequest, @@ -382,7 +382,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implication //# GetObject MUST be implemented by the S3EC. /** @@ -406,7 +406,7 @@ public T getObject(GetObjectRequest getObjectRequest, ResponseTransformer responseTransformer) throws AwsServiceException, SdkClientException { - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implication //# GetObject MUST decrypt data received from the S3 server and return it as plaintext. GetEncryptedObjectPipeline pipeline = GetEncryptedObjectPipeline.builder() @@ -516,7 +516,7 @@ private T onAbort(UploadObjectObserver observer, T t) { throw new S3EncryptionClientException(t.getMessage(), t); } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implication //# DeleteObject MUST be implemented by the S3EC. /** @@ -536,11 +536,11 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest .build(); try { - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implementation //# DeleteObject MUST delete the given object key. DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient.deleteObject(actualRequest).join(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implementation //# DeleteObject MUST delete the associated instruction file using the default instruction file suffix. String instructionObjectKey = deleteObjectRequest.key() + DEFAULT_INSTRUCTION_FILE_SUFFIX; @@ -557,7 +557,7 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest } } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implication //# DeleteObjects MUST be implemented by the S3EC. /** @@ -576,11 +576,11 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); try { - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implementation //# DeleteObjects MUST delete each of the given objects. DeleteObjectsResponse deleteObjectsResponse = _wrappedAsyncClient.deleteObjects(actualRequest).join(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=implementation //# DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. List deleteObjects = instructionFileKeysToDelete(deleteObjectsRequest); @@ -597,7 +597,7 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq } } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# CreateMultipartUpload MAY be implemented by the S3EC. /** @@ -621,7 +621,7 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload } } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# UploadPart MAY be implemented by the S3EC. /** @@ -647,7 +647,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# CompleteMultipartUpload MAY be implemented by the S3EC. /** @@ -667,7 +667,7 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart } } - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# AbortMultipartUpload MAY be implemented by the S3EC. /** @@ -965,7 +965,7 @@ public Builder enableMultipartPutObject(boolean _enableMultipartPutObject) { //= specification/s3-encryption/client.md#set-buffer-size //= type=implication - //# The S3EC SHOULD accept a configurable buffer size which refers to the maximum ciphertext length to store in memory when delayed authentication mode is disabled. + //# The S3EC SHOULD accept a configurable buffer size which refers to the maximum ciphertext length in bytes to store in memory when Delayed Authentication mode is disabled. /** * Sets the buffer size for safe authentication used when delayed authentication mode is disabled. * If buffer size is not given during client configuration, default buffer size is set to 64MiB. diff --git a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java index 01c48a578..ccec2e064 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java @@ -74,7 +74,7 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# If implemented, CreateMultipartUpload MUST initiate a multipart upload. CreateMultipartUploadResponse response = _s3AsyncClient.createMultipartUpload(request).join(); @@ -136,11 +136,11 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ throw new S3EncryptionClientException("No client-side information available on upload ID " + uploadId); } final UploadPartResponse response; - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# Each part MUST be encrypted in sequence. materials.beginPartUpload(actualRequest.partNumber(), partContentLength); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# Each part MUST be encrypted using the same cipher instance for each part. Cipher cipher = materials.getCipher(materials.getIv()); @@ -148,7 +148,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); try { - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# UploadPart MUST encrypt each part. final AsyncRequestBody cipherAsyncRequestBody = new CipherAsyncRequestBody( @@ -170,7 +170,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } // Ensures parts are not retried to avoid corrupting ciphertext AsyncRequestBody noRetryBody = new NoRetriesAsyncRequestBody(cipherAsyncRequestBody); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication response = _s3AsyncClient.uploadPart(actualRequest, noRetryBody).join(); } finally { @@ -200,7 +200,7 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# CompleteMultipartUpload MUST complete the multipart upload. CompleteMultipartUploadResponse response = _s3AsyncClient.completeMultipartUpload(actualRequest).join(); @@ -214,7 +214,7 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq AbortMultipartUploadRequest actualRequest = request.toBuilder() .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#optional-api-operations //= type=implication //# AbortMultipartUpload MUST abort the multipart upload. return _s3AsyncClient.abortMultipartUpload(actualRequest).join(); diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java index 37c18a6fe..04d5fc346 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java @@ -166,13 +166,13 @@ public void deleteObjectWithInstructionFileSuccess() { v3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey)); S3Client s3Client = S3Client.builder().build(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=test //# DeleteObject MUST delete the given object key. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder .bucket(BUCKET) .key(objectKey))); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=test //# DeleteObject MUST delete the associated instruction file using the default instruction file suffix. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder @@ -218,13 +218,13 @@ public void deleteObjectsWithInstructionFilesSuccess() { .delete(builder1 -> builder1.objects(objects))); S3Client s3Client = S3Client.builder().build(); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=test //# DeleteObjects MUST delete each of the given objects. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder .bucket(BUCKET) .key(objectKeys[0]))); - //= specification/s3-encryption/client.md#api-operations + //= specification/s3-encryption/client.md#required-api-operations //= type=test //# DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. assertThrows(S3Exception.class, () -> s3Client.getObject(builder -> builder From ab41a57882f674768c4f528a9069cf69aeb9a53f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:29:20 -0700 Subject: [PATCH 3/6] chore(spec): add spec and Duvet annotations for KmsKeyring (#483) --- Makefile | 1 + compliance_exceptions/s3-kms-keyring.txt | 15 +++ specification | 2 +- .../encryption/s3/S3EncryptionClient.java | 40 +++--- .../internal/GetEncryptedObjectPipeline.java | 2 +- .../MultipartUploadObjectPipeline.java | 14 +- .../encryption/s3/materials/Keyring.java | 27 ++++ .../encryption/s3/materials/KmsKeyring.java | 121 ++++++++++++++++++ .../encryption/s3/materials/S3Keyring.java | 42 +++++- 9 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 compliance_exceptions/s3-kms-keyring.txt diff --git a/Makefile b/Makefile index ec9f02e7f..1c60b7a64 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,5 @@ duvet_report: report \ --spec-pattern "compliance/**/*.toml" \ --source-pattern "src/**/*.java" \ + --source-pattern "compliance_exceptions/*.txt" \ --html specification_compliance_report.html diff --git a/compliance_exceptions/s3-kms-keyring.txt b/compliance_exceptions/s3-kms-keyring.txt new file mode 100644 index 000000000..29ea6f49b --- /dev/null +++ b/compliance_exceptions/s3-kms-keyring.txt @@ -0,0 +1,15 @@ +// The KMS Keyring in S3EC Java v3 does not validate KMS Key IDs. +// Passing an invalid key ID will eventually lead to KMS Request failures. + +//= specification/s3-encryption/materials/s3-kms-keyring.md#initialization +//= type=exception +//# The KmsKeyring MAY validate that the AWS KMS key identifier is not null or empty. +//= specification/s3-encryption/materials/s3-kms-keyring.md#initialization +//= type=exception +//# If the KmsKeyring validates that the AWS KMS key identifier is not null or empty, then it MUST throw an exception. +//= specification/s3-encryption/materials/s3-kms-keyring.md#initialization +//= type=exception +//# The KmsKeyring MAY validate that the AWS KMS key identifier is [a valid AWS KMS Key identifier](../../framework/aws-kms/aws-kms-key-arn.md#a-valid-aws-kms-identifier). +//= specification/s3-encryption/materials/s3-kms-keyring.md#initialization +//= type=exception +//# If the KmsKeyring validates that the AWS KMS key identifier is not a valid AWS KMS Key identifier, then it MUST throw an exception. diff --git a/specification b/specification index 85d4c1769..280a89401 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit 85d4c176901e29fb795ec551abd11fc7df076f94 +Subproject commit 280a894019cd1b4efc6b16cfb233bf1ec21bc508 diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index e1c275bd5..5377f8e96 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -212,7 +212,7 @@ public static Consumer withAdditionalCo //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# ReEncryptInstructionFile MAY be implemented by the S3EC. + //# - ReEncryptInstructionFile MAY be implemented by the S3EC. /** * Re-encrypts an instruction file with a new keyring while preserving the original encrypted object in S3. * This enables: @@ -252,7 +252,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru //Decrypt the data key using the current keyring //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM. + //# - ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM. DecryptionMaterials decryptedMaterials = this._cryptoMaterialsManager.decryptMaterials( DecryptMaterialsRequest.builder() .algorithmSuite(algorithmSuite) @@ -273,7 +273,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru //Re-encrypt the data key with the new keyring while preserving other cryptographic parameters //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. + //# - ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring(); EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials); @@ -317,7 +317,7 @@ private void enforceRotation(EncryptionMaterials newEncryptionMaterials, GetObje //= specification/s3-encryption/client.md#required-api-operations //= type=implication - //# PutObject MUST be implemented by the S3EC. + //# - PutObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}. *

@@ -357,7 +357,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod try { //= specification/s3-encryption/client.md#required-api-operations //= type=implication - //# PutObject MUST encrypt its input data before it is uploaded to S3. + //# - PutObject MUST encrypt its input data before it is uploaded to S3. CompletableFuture futurePut = pipeline.putObject(putObjectRequest, AsyncRequestBody.fromInputStream( requestBody.contentStreamProvider().newStream(), @@ -384,7 +384,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod //= specification/s3-encryption/client.md#required-api-operations //= type=implication - //# GetObject MUST be implemented by the S3EC. + //# - GetObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#getObject(GetObjectRequest, ResponseTransformer)} *

@@ -408,7 +408,7 @@ public T getObject(GetObjectRequest getObjectRequest, //= specification/s3-encryption/client.md#required-api-operations //= type=implication - //# GetObject MUST decrypt data received from the S3 server and return it as plaintext. + //# - GetObject MUST decrypt data received from the S3 server and return it as plaintext. GetEncryptedObjectPipeline pipeline = GetEncryptedObjectPipeline.builder() .s3AsyncClient(_wrappedAsyncClient) .cryptoMaterialsManager(_cryptoMaterialsManager) @@ -518,7 +518,7 @@ private T onAbort(UploadObjectObserver observer, T t) { //= specification/s3-encryption/client.md#required-api-operations //= type=implication - //# DeleteObject MUST be implemented by the S3EC. + //# - DeleteObject MUST be implemented by the S3EC. /** * See {@link S3Client#deleteObject(DeleteObjectRequest)}. *

@@ -538,11 +538,11 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest try { //= specification/s3-encryption/client.md#required-api-operations //= type=implementation - //# DeleteObject MUST delete the given object key. + //# - DeleteObject MUST delete the given object key. DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient.deleteObject(actualRequest).join(); //= specification/s3-encryption/client.md#required-api-operations //= type=implementation - //# DeleteObject MUST delete the associated instruction file using the default instruction file suffix. + //# - DeleteObject MUST delete the associated instruction file using the default instruction file suffix. String instructionObjectKey = deleteObjectRequest.key() + DEFAULT_INSTRUCTION_FILE_SUFFIX; _wrappedAsyncClient.deleteObject(builder -> builder .overrideConfiguration(API_NAME_INTERCEPTOR) @@ -559,7 +559,7 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest //= specification/s3-encryption/client.md#required-api-operations //= type=implication - //# DeleteObjects MUST be implemented by the S3EC. + //# - DeleteObjects MUST be implemented by the S3EC. /** * See {@link S3Client#deleteObjects(DeleteObjectsRequest)}. *

@@ -578,11 +578,11 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq try { //= specification/s3-encryption/client.md#required-api-operations //= type=implementation - //# DeleteObjects MUST delete each of the given objects. + //# - DeleteObjects MUST delete each of the given objects. DeleteObjectsResponse deleteObjectsResponse = _wrappedAsyncClient.deleteObjects(actualRequest).join(); //= specification/s3-encryption/client.md#required-api-operations //= type=implementation - //# DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. + //# - DeleteObjects MUST delete each of the corresponding instruction files using the default instruction file suffix. List deleteObjects = instructionFileKeysToDelete(deleteObjectsRequest); _wrappedAsyncClient.deleteObjects(DeleteObjectsRequest.builder() .overrideConfiguration(API_NAME_INTERCEPTOR) @@ -599,7 +599,7 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# CreateMultipartUpload MAY be implemented by the S3EC. + //# - CreateMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#createMultipartUpload(CreateMultipartUploadRequest)} *

@@ -623,7 +623,7 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# UploadPart MAY be implemented by the S3EC. + //# - UploadPart MAY be implemented by the S3EC. /** * See {@link S3Client#uploadPart(UploadPartRequest, RequestBody)} * @@ -649,7 +649,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# CompleteMultipartUpload MAY be implemented by the S3EC. + //# - CompleteMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#completeMultipartUpload(CompleteMultipartUploadRequest)} * @param request the request instance @@ -669,7 +669,7 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# AbortMultipartUpload MAY be implemented by the S3EC. + //# - AbortMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#abortMultipartUpload(AbortMultipartUploadRequest)} * @param request the request instance @@ -763,7 +763,7 @@ private Builder() { @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Pass mutability into wrapping client") public Builder wrappedClient(S3Client _wrappedClient) { //= specification/s3-encryption/client.md#wrapped-s3-client-s - //= type=exception + //= type=implementation //# The S3EC MUST NOT support use of S3EC as the provided S3 client during its initialization; it MUST throw an exception in this case. if (_wrappedClient instanceof S3EncryptionClient) { throw new S3EncryptionClientException("Cannot use S3EncryptionClient as wrapped client"); @@ -785,7 +785,7 @@ public Builder wrappedClient(S3Client _wrappedClient) { @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Pass mutability into wrapping client") public Builder wrappedAsyncClient(S3AsyncClient _wrappedAsyncClient) { //= specification/s3-encryption/client.md#wrapped-s3-client-s - //= type=exception + //= type=implementation //# The S3EC MUST NOT support use of S3EC as the provided S3 client during its initialization; it MUST throw an exception in this case. if (_wrappedAsyncClient instanceof S3AsyncEncryptionClient) { throw new S3EncryptionClientException("Cannot use S3AsyncEncryptionClient as wrapped client"); @@ -885,7 +885,7 @@ private void checkKeyOptions() { } //= specification/s3-encryption/client.md#cryptographic-materials - //= type=exception + //= type=implementation //# If both a CMM and a Keyring are provided, the S3EC MUST throw an exception. throw new S3EncryptionClientException("Only one may be set of: crypto materials manager, keyring, AES key, RSA key pair, KMS key id"); } diff --git a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java index b002b42b8..2e4898608 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java @@ -80,7 +80,7 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g //# When enabled, the S3EC MUST be able to decrypt objects encrypted with all content encryption algorithms (both legacy and fully supported). if (!_enableLegacyUnauthenticatedModes && algorithmSuite.isLegacy()) { //= specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes - //= type=exception + //= type=implementation //# When disabled, the S3EC MUST NOT decrypt objects encrypted using legacy content encryption algorithms; it MUST throw an exception when attempting to decrypt an object encrypted with a legacy content encryption algorithm. throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use legacy content decryption: " + algorithmSuite.cipherName()); } diff --git a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java index ccec2e064..be5f6e24d 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java @@ -76,7 +76,7 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# If implemented, CreateMultipartUpload MUST initiate a multipart upload. + //# - If implemented, CreateMultipartUpload MUST initiate a multipart upload. CreateMultipartUploadResponse response = _s3AsyncClient.createMultipartUpload(request).join(); MultipartUploadMaterials mpuMaterials = MultipartUploadMaterials.builder() @@ -138,11 +138,11 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ final UploadPartResponse response; //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# Each part MUST be encrypted in sequence. + //# - Each part MUST be encrypted in sequence. materials.beginPartUpload(actualRequest.partNumber(), partContentLength); //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# Each part MUST be encrypted using the same cipher instance for each part. + //# - Each part MUST be encrypted using the same cipher instance for each part. Cipher cipher = materials.getCipher(materials.getIv()); ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); @@ -150,7 +150,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ try { //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# UploadPart MUST encrypt each part. + //# - UploadPart MUST encrypt each part. final AsyncRequestBody cipherAsyncRequestBody = new CipherAsyncRequestBody( AsyncRequestBody.fromInputStream( requestBody.contentStreamProvider().newStream(), @@ -170,8 +170,6 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } // Ensures parts are not retried to avoid corrupting ciphertext AsyncRequestBody noRetryBody = new NoRetriesAsyncRequestBody(cipherAsyncRequestBody); - //= specification/s3-encryption/client.md#optional-api-operations - //= type=implication response = _s3AsyncClient.uploadPart(actualRequest, noRetryBody).join(); } finally { materials.endPartUpload(); @@ -202,7 +200,7 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# CompleteMultipartUpload MUST complete the multipart upload. + //# - CompleteMultipartUpload MUST complete the multipart upload. CompleteMultipartUploadResponse response = _s3AsyncClient.completeMultipartUpload(actualRequest).join(); _multipartUploadMaterials.remove(uploadId); @@ -216,7 +214,7 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq .build(); //= specification/s3-encryption/client.md#optional-api-operations //= type=implication - //# AbortMultipartUpload MUST abort the multipart upload. + //# - AbortMultipartUpload MUST abort the multipart upload. return _s3AsyncClient.abortMultipartUpload(actualRequest).join(); } diff --git a/src/main/java/software/amazon/encryption/s3/materials/Keyring.java b/src/main/java/software/amazon/encryption/s3/materials/Keyring.java index e6526108e..5859be184 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/Keyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/Keyring.java @@ -4,11 +4,38 @@ import java.util.List; +//= specification/s3-encryption/materials/keyrings.md#interface +//= type=implication +//# The Keyring interface and its operations SHOULD adhere to the naming conventions of the implementation language. /** * Keyring defines the interface for wrapping data keys. A {@link CryptographicMaterialsManager} will use * keyrings to encrypt and decrypt data keys. */ public interface Keyring { + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The Keyring interface and its operations SHOULD adhere to the naming conventions of the implementation language. + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The Keyring interface MUST include the OnEncrypt operation. + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The OnEncrypt operation MUST accept an instance of EncryptionMaterials as input. + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The OnEncrypt operation MUST return an instance of EncryptionMaterials as output. EncryptionMaterials onEncrypt(final EncryptionMaterials materials); + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The Keyring interface and its operations SHOULD adhere to the naming conventions of the implementation language. + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The Keyring interface MUST include the OnDecrypt operation. + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The OnDecrypt operation MUST accept an instance of DecryptionMaterials and a collection of EncryptedDataKey instances as input. + //= specification/s3-encryption/materials/keyrings.md#interface + //= type=implication + //# The OnDecrypt operation MUST return an instance of DecryptionMaterials as output. DecryptionMaterials onDecrypt(final DecryptionMaterials materials, final List encryptedDataKeys); } diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java index 6088811b3..bd3a90c0a 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java @@ -29,6 +29,9 @@ import java.util.Objects; import java.util.Optional; +//= specification/s3-encryption/materials/s3-kms-keyring.md#interface +//= type=implication +//# The KmsKeyring MUST implement the [Keyring interface](keyring-interface.md#interface) and include the behavior described in the [S3 Keyring](s3-keyring.md). /** * This keyring can wrap keys with the active keywrap algorithm and * unwrap with the active and legacy algorithms for KMS keys. @@ -41,10 +44,19 @@ public class KmsKeyring extends S3Keyring { private final KmsClient _kmsClient; private final String _wrappingKeyId; + //= specification/s3-encryption/materials/s3-kms-keyring.md#supported-wrapping-algorithm-modes + //= type=implication + //# The KmsKeyring MUST NOT support encryption using KmsV1 mode. private final DecryptDataKeyStrategy _kmsStrategy = new DecryptDataKeyStrategy() { + //= specification/s3-encryption/materials/s3-kms-keyring.md#decryptdatakey + //= type=implication + //# If the Key Provider Info of the Encrypted Data Key is "kms", the KmsKeyring MUST attempt to decrypt using KmsV1 mode. private static final String KEY_PROVIDER_INFO = "kms"; + //= specification/s3-encryption/materials/s3-kms-keyring.md#supported-wrapping-algorithm-modes + //= type=implication + //# The KmsV1 mode MUST be only enabled when legacy wrapping algorithms are enabled. @Override public boolean isLegacy() { return true; @@ -55,25 +67,58 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } + //= specification/s3-encryption/materials/s3-kms-keyring.md#supported-wrapping-algorithm-modes + //= type=implication + //# The KmsKeyring MUST support decryption using KmsV1 mode. @Override public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) { + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# To attempt to decrypt a particular [encrypted data key](../structures.md#encrypted-data-key), the + //# KmsKeyring MUST call [AWS KMS Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html) with the configured AWS KMS client. DecryptRequest request = DecryptRequest.builder() + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# - `KeyId` MUST be the configured AWS KMS key identifier. .keyId(_wrappingKeyId) + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# - `EncryptionContext` MUST be the [encryption context](../structures.md#encryption-context) + //# included in the input [decryption materials](../structures.md#decryption-materials). .encryptionContext(materials.encryptionContext()) + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# - `CiphertextBlob` MUST be the [encrypted data key ciphertext](../structures.md#ciphertext). .ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey)) + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# - A custom API Name or User Agent string SHOULD be provided in order to provide metrics on KMS + //# calls associated with the S3 Encryption Client. .overrideConfiguration(builder -> builder.addApiName(API_NAME)) .build(); + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# If the KmsKeyring fails to successfully decrypt the [encrypted data key](../structures.md#encrypted-data-key), then it MUST throw an exception. DecryptResponse response = _kmsClient.decrypt(request); + //= specification/s3-encryption/materials/s3-kms-keyring.md#kmsv1 + //= type=implication + //# The KmsKeyring MUST immediately return the plaintext as a collection of bytes. return response.plaintext().asByteArray(); } }; private final DataKeyStrategy _kmsContextStrategy = new DataKeyStrategy() { + //= specification/s3-encryption/materials/s3-kms-keyring.md#decryptdatakey + //= type=implication + //# If the Key Provider Info of the Encrypted Data Key is "kms+context", the KmsKeyring MUST attempt to decrypt using Kms+Context mode. private static final String KEY_PROVIDER_INFO = "kms+context"; private static final String ENCRYPTION_CONTEXT_ALGORITHM_KEY = "aws:x-amz-cek-alg"; + //= specification/s3-encryption/materials/s3-kms-keyring.md#supported-wrapping-algorithm-modes + //= type=implication + //# The Kms+Context mode MUST be enabled as a fully-supported (non-legacy) wrapping algorithm. @Override public boolean isLegacy() { return false; @@ -149,20 +194,51 @@ public EncryptionMaterials generateDataKey(EncryptionMaterials materials) { .build(); } + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# The KmsKeyring MUST implement the EncryptDataKey method. + //= specification/s3-encryption/materials/s3-kms-keyring.md#supported-wrapping-algorithm-modes + //= type=implication + //# The KmsKeyring MUST support encryption using Kms+Context mode. @Override public byte[] encryptDataKey(SecureRandom secureRandom, EncryptionMaterials materials) { HashMap encryptionContext = new HashMap<>(materials.encryptionContext()); EncryptRequest request = EncryptRequest.builder() + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# - `KeyId` MUST be the configured AWS KMS key identifier. .keyId(_wrappingKeyId) + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# - `EncryptionContext` MUST be the [encryption context](../structures.md#encryption-context) included + //# in the input [encryption materials](../structures.md#encryption-materials). .encryptionContext(encryptionContext) + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# - `PlaintextDataKey` MUST be the plaintext data key in the [encryption materials](../structures.md#encryption-materials). .plaintext(SdkBytes.fromByteArray(materials.plaintextDataKey())) + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# - A custom API Name or User Agent string SHOULD be provided in order to provide metrics on KMS calls associated with the S3 Encryption Client. .overrideConfiguration(builder -> builder.addApiName(API_NAME)) .build(); + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# The keyring MUST call [AWS KMS Encrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Encrypt.html) using the configured AWS KMS client. + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# If the call to [AWS KMS Encrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Encrypt.html) does not succeed, OnEncrypt MUST fail. EncryptResponse response = _kmsClient.encrypt(request); + //= specification/s3-encryption/materials/s3-kms-keyring.md#encryptdatakey + //= type=implication + //# If the call to AWS KMS Encrypt is successful, OnEncrypt MUST return the `CiphertextBlob` as a collection of bytes. return response.ciphertextBlob().asByteArray(); } + //= specification/s3-encryption/materials/s3-kms-keyring.md#supported-wrapping-algorithm-modes + //= type=implication + //# The KmsKeyring MUST support decryption using Kms+Context mode. @Override public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) { Map requestEncryptionContext = new HashMap<>(); @@ -177,23 +253,56 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData } } + + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# When decrypting using Kms+Context mode, the KmsKeyring MUST validate the provided (request) encryption context with the stored (materials) encryption context. // We are validating the encryption context to match S3EC V2 behavior // Refer to KMSMaterialsHandler in the V2 client for details Map materialsEncryptionContextCopy = new HashMap<>(materials.encryptionContext()); + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# The stored encryption context with the two reserved keys removed MUST match the provided encryption context. materialsEncryptionContextCopy.remove(KEY_ID_CONTEXT_KEY); materialsEncryptionContextCopy.remove(ENCRYPTION_CONTEXT_ALGORITHM_KEY); if (!materialsEncryptionContextCopy.equals(requestEncryptionContext)) { + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# If the stored encryption context with the two reserved keys removed does not match the provided encryption context, the KmsKeyring MUST throw an exception. throw new S3EncryptionClientException("Provided encryption context does not match information retrieved from S3"); } + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# To attempt to decrypt a particular [encrypted data key](../structures.md#encrypted-data-key), the KmsKeyring + //# MUST call [AWS KMS Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html) with the configured AWS KMS client. DecryptRequest request = DecryptRequest.builder() + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# - `KeyId` MUST be the configured AWS KMS key identifier. .keyId(_wrappingKeyId) + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# - `EncryptionContext` MUST be the [encryption context](../structures.md#encryption-context) + //# included in the input [decryption materials](../structures.md#decryption-materials). .encryptionContext(materials.encryptionContext()) + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# - `CiphertextBlob` MUST be the [encrypted data key ciphertext](../structures.md#ciphertext). .ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey)) + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# - A custom API Name or User Agent string SHOULD be provided in order to provide metrics on KMS calls associated with the S3 Encryption Client. .overrideConfiguration(builder -> builder.addApiName(API_NAME)) .build(); + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# If the KmsKeyring fails to successfully decrypt the [encrypted data key](../structures.md#encrypted-data-key), then it MUST throw an exception. DecryptResponse response = _kmsClient.decrypt(request); + //= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context + //= type=implication + //# The KmsKeyring MUST immediately return the plaintext as a collection of bytes. return response.plaintext().asByteArray(); } @@ -207,6 +316,9 @@ public KmsKeyring(Builder builder) { _kmsClient = builder._kmsClient; _wrappingKeyId = builder._wrappingKeyId; + //= specification/s3-encryption/materials/s3-kms-keyring.md#decryptdatakey + //= type=implication + //# The KmsKeyring MUST determine whether to decrypt using KmsV1 mode or Kms+Context mode. decryptDataKeyStrategies.put(_kmsStrategy.keyProviderInfo(), _kmsStrategy); decryptDataKeyStrategies.put(_kmsContextStrategy.keyProviderInfo(), _kmsContextStrategy); } @@ -243,6 +355,9 @@ protected Builder builder() { return this; } + //= specification/s3-encryption/materials/s3-kms-keyring.md#initialization + //= type=implication + //# On initialization, the caller MAY provide an AWS KMS SDK client instance. /** * Note that this does NOT create a defensive clone of KmsClient. Any modifications made to the wrapped * client will be reflected in this Builder. @@ -253,12 +368,18 @@ public Builder kmsClient(KmsClient kmsClient) { return this; } + //= specification/s3-encryption/materials/s3-kms-keyring.md#initialization + //= type=implication + //# On initialization, the caller MUST provide an AWS KMS key identifier. public Builder wrappingKeyId(String wrappingKeyId) { _wrappingKeyId = wrappingKeyId; return this; } public KmsKeyring build() { + //= specification/s3-encryption/materials/s3-kms-keyring.md#initialization + //= type=implication + //# If the caller does not provide an AWS KMS SDK client instance or provides a null value, the KmsKeyring MUST create a default KMS client instance. if (_kmsClient == null) { _kmsClient = KmsClient.create(); } diff --git a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java index 822f09e8c..993768cb6 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java @@ -13,6 +13,15 @@ import java.util.List; import java.util.Map; +//= specification/s3-encryption/materials/s3-keyring.md#overview +//= type=implication +//# The S3EC SHOULD implement an S3 Keyring to consolidate validation and other functionality common to all S3 Keyrings. +//= specification/s3-encryption/materials/s3-keyring.md#overview +//= type=implication +//# If implemented, the S3 Keyring MUST implement the Keyring interface. +//= specification/s3-encryption/materials/s3-keyring.md#overview +//= type=implication +//# If implemented, the S3 Keyring MUST NOT be able to be instantiated as a Keyring instance. /** * This serves as the base class for all the keyrings in the S3 encryption client. * Shared functionality is all performed here. @@ -60,16 +69,24 @@ public EncryptionMaterials onEncrypt(EncryptionMaterials materials) { // Allow encrypt strategy to modify the materials if necessary materials = encryptStrategy.modifyMaterials(materials); + //= specification/s3-encryption/materials/s3-keyring.md#onencrypt + //= type=implication + //# If the Plaintext Data Key in the input EncryptionMaterials is null, the S3 Keyring MUST call the GenerateDataKey method using the materials. if (materials.plaintextDataKey() == null) { materials = generateDataKeyStrategy().generateDataKey(materials); } - // Return materials if they already have an encrypted data key. + //= specification/s3-encryption/materials/s3-keyring.md#onencrypt + //= type=implication + //# If the materials returned from GenerateDataKey contain an EncryptedDataKey, the S3 Keyring MUST return the materials. if (!materials.encryptedDataKeys().isEmpty()) { return materials; } try { + //= specification/s3-encryption/materials/s3-keyring.md#onencrypt + //= type=implication + //# If the materials returned from GenerateDataKey do not contain an EncryptedDataKey, the S3 Keyring MUST call the EncryptDataKey method using the materials. byte[] encryptedDataKeyCiphertext = encryptStrategy.encryptDataKey(_secureRandom, materials); EncryptedDataKey encryptedDataKey = EncryptedDataKey.builder() .keyProviderId(S3Keyring.KEY_PROVIDER_ID) @@ -88,20 +105,35 @@ public EncryptionMaterials onEncrypt(EncryptionMaterials materials) { } } + //= specification/s3-encryption/materials/s3-keyring.md#abstract-methods + //= type=implication + //# - The S3 Keyring MUST define an abstract method GenerateDataKey. abstract protected GenerateDataKeyStrategy generateDataKeyStrategy(); + //= specification/s3-encryption/materials/s3-keyring.md#abstract-methods + //= type=implication + //# - The S3 Keyring MUST define an abstract method EncryptDataKey. abstract protected EncryptDataKeyStrategy encryptDataKeyStrategy(); @Override public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List encryptedDataKeys) { + //= specification/s3-encryption/materials/s3-keyring.md#ondecrypt + //= type=implementation + //# If the input DecryptionMaterials contains a Plaintext Data Key, the S3 Keyring MUST throw an exception. if (materials.plaintextDataKey() != null) { throw new S3EncryptionClientException("Decryption materials already contains a plaintext data key."); } + //= specification/s3-encryption/materials/s3-keyring.md#ondecrypt + //= type=implementation + //# If the input collection of EncryptedDataKey instances contains any number of EDKs other than 1, the S3 Keyring MUST throw an exception. if (encryptedDataKeys.size() != 1) { throw new S3EncryptionClientException("Only one encrypted data key is supported, found: " + encryptedDataKeys.size()); } + //= specification/s3-encryption/materials/s3-keyring.md#ondecrypt + //= type=implementation + //# If the input DecryptionMaterials contains a Plaintext Data Key, the S3 Keyring MUST throw an exception. EncryptedDataKey encryptedDataKey = encryptedDataKeys.get(0); final String keyProviderId = encryptedDataKey.keyProviderId(); if (!KEY_PROVIDER_ID.equals(keyProviderId)) { @@ -120,12 +152,15 @@ public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List decryptDataKeyStrategies(); abstract public static class Builder> { From a78cb522489af90c65dba0e83e2c3803aefacb3f Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Tue, 14 Oct 2025 06:08:16 -0700 Subject: [PATCH 4/6] feat: allow raw keyrings to decrypt with multiple wrapping keys (#485) * feat: allow raw keyrings to decrypt with multiple wrapping keys * remove exploratory tests in Compatibility tests class * add tests for ReEncrypt --- .../encryption/s3/S3EncryptionClient.java | 6 +- .../s3/internal/ContentMetadata.java | 41 +- .../ContentMetadataDecodingStrategy.java | 27 +- .../internal/GetEncryptedObjectPipeline.java | 3 +- .../s3/materials/AesKeyMaterial.java | 67 ++ .../encryption/s3/materials/AesKeyring.java | 65 +- .../s3/materials/DecryptMaterialsRequest.java | 18 + .../s3/materials/DecryptionMaterials.java | 21 + .../DefaultCryptoMaterialsManager.java | 1 + .../s3/materials/MaterialsDescription.java | 18 + .../s3/materials/RawKeyMaterial.java | 91 ++ .../encryption/s3/materials/RawKeyring.java | 54 +- .../s3/materials/RsaKeyMaterial.java | 65 ++ .../encryption/s3/materials/RsaKeyring.java | 55 +- .../AdditionalDecryptionKeyMaterialTest.java | 923 ++++++++++++++++++ .../S3EncryptionClientCompatibilityTest.java | 4 +- ...WithAdditionalDecryptionMaterialsTest.java | 599 ++++++++++++ .../internal/ContentMetadataStrategyTest.java | 2 +- .../s3/internal/ContentMetadataTest.java | 4 +- .../s3/materials/KeyMaterialTest.java | 178 ++++ .../materials/MaterialsDescriptionTest.java | 57 ++ 21 files changed, 2224 insertions(+), 75 deletions(-) create mode 100644 src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java create mode 100644 src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java create mode 100644 src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java create mode 100644 src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java create mode 100644 src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java create mode 100644 src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index 5377f8e96..8daa3da5c 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -68,6 +68,7 @@ import software.amazon.encryption.s3.materials.EncryptionMaterials; import software.amazon.encryption.s3.materials.Keyring; import software.amazon.encryption.s3.materials.KmsKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; import software.amazon.encryption.s3.materials.MultipartConfiguration; import software.amazon.encryption.s3.materials.PartialRsaKeyPair; import software.amazon.encryption.s3.materials.RawKeyring; @@ -246,7 +247,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru //Extract cryptographic parameters from the current instruction file that MUST be preserved during re-encryption final AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite(); final EncryptedDataKey originalEncryptedDataKey = contentMetadata.encryptedDataKey(); - final Map currentKeyringMaterialsDescription = contentMetadata.encryptedDataKeyMatDescOrContext(); + final MaterialsDescription currentKeyringMaterialsDescription = contentMetadata.materialsDescription(); final byte[] iv = contentMetadata.contentIv(); //Decrypt the data key using the current keyring @@ -257,6 +258,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru DecryptMaterialsRequest.builder() .algorithmSuite(algorithmSuite) .encryptedDataKeys(Collections.singletonList(originalEncryptedDataKey)) + .materialsDescription(contentMetadata.materialsDescription()) .s3Request(request) .build() ); @@ -277,7 +279,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring(); EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials); - final Map newMaterialsDescription = encryptedMaterials.materialsDescription().getMaterialsDescription(); + final MaterialsDescription newMaterialsDescription = encryptedMaterials.materialsDescription(); //Validate that the new keyring has different materials description than the old keyring if (newMaterialsDescription.equals(currentKeyringMaterialsDescription)) { throw new S3EncryptionClientException("New keyring must have new materials description!"); diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java index 4d61a2248..8a47a6f17 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadata.java @@ -5,6 +5,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.materials.EncryptedDataKey; +import software.amazon.encryption.s3.materials.MaterialsDescription; import java.util.Collections; import java.util.Map; @@ -17,11 +18,14 @@ public class ContentMetadata { private final String _encryptedDataKeyAlgorithm; /** - * This field stores either encryption context or material description. - * We use a single field to store both in order to maintain backwards - * compatibility with V2, which treated both as the same. + * This field stores the encryption context. */ - private final Map _encryptionContextOrMatDesc; + private final Map _encryptionContext; + + /** + * This field stores the materials description used for RSA and AES keyrings. + */ + private final MaterialsDescription _materialsDescription; private final byte[] _contentIv; private final String _contentCipher; @@ -33,7 +37,8 @@ private ContentMetadata(Builder builder) { _encryptedDataKey = builder._encryptedDataKey; _encryptedDataKeyAlgorithm = builder._encryptedDataKeyAlgorithm; - _encryptionContextOrMatDesc = builder._encryptionContextOrMatDesc; + _encryptionContext = builder._encryptionContext; + _materialsDescription = builder._materialsDescription; _contentIv = builder._contentIv; _contentCipher = builder._contentCipher; @@ -64,8 +69,16 @@ public String encryptedDataKeyAlgorithm() { */ @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying" + " implementation is immutable") - public Map encryptedDataKeyMatDescOrContext() { - return _encryptionContextOrMatDesc; + public Map encryptionContext() { + return _encryptionContext; + } + + /** + * Returns the materials description used for RSA and AES keyrings. + * @return the materials description + */ + public MaterialsDescription materialsDescription() { + return _materialsDescription; } public byte[] contentIv() { @@ -92,7 +105,8 @@ public static class Builder { private EncryptedDataKey _encryptedDataKey; private String _encryptedDataKeyAlgorithm; - private Map _encryptionContextOrMatDesc; + private Map _encryptionContext; + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private byte[] _contentIv; private String _contentCipher; @@ -118,8 +132,15 @@ public Builder encryptedDataKeyAlgorithm(String encryptedDataKeyAlgorithm) { return this; } - public Builder encryptionContextOrMatDesc(Map encryptionContextOrMatDesc) { - _encryptionContextOrMatDesc = Collections.unmodifiableMap(encryptionContextOrMatDesc); + public Builder encryptionContext(Map encryptionContext) { + _encryptionContext = Collections.unmodifiableMap(encryptionContext); + return this; + } + + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; return this; } diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index 3d1e4edd9..77e6eabc5 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -13,6 +13,7 @@ import software.amazon.encryption.s3.S3EncryptionClientException; import software.amazon.encryption.s3.algorithms.AlgorithmSuite; import software.amazon.encryption.s3.materials.EncryptedDataKey; +import software.amazon.encryption.s3.materials.MaterialsDescription; import software.amazon.encryption.s3.materials.S3Keyring; import java.io.ByteArrayOutputStream; @@ -137,8 +138,8 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo .keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8)) .build(); - // Get encrypted data key encryption context or materials description (depending on the keyring) - final Map encryptionContextOrMatDesc = new HashMap<>(); + // Parse the JSON materials description or encryption context + final Map matDescMap = new HashMap<>(); // The V2 client treats null value here as empty, do the same to avoid incompatibility String jsonEncryptionContext = metadata.getOrDefault(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, "{}"); // When the encryption context contains non-US-ASCII characters, @@ -150,19 +151,37 @@ private ContentMetadata readFromMap(Map metadata, GetObjectRespo JsonNode objectNode = parser.parse(decodedJsonEncryptionContext); for (Map.Entry entry : objectNode.asObject().entrySet()) { - encryptionContextOrMatDesc.put(entry.getKey(), entry.getValue().asString()); + matDescMap.put(entry.getKey(), entry.getValue().asString()); } } catch (Exception e) { throw new RuntimeException(e); } + // By default, assume the context is a materials description unless it's a KMS keyring + Map encryptionContext; + MaterialsDescription materialsDescription; + + if (keyProviderInfo.contains("kms")) { + // For KMS keyrings, use the map as encryption context + encryptionContext = matDescMap; + materialsDescription = MaterialsDescription.builder().build(); + } else { + // For all other keyrings (AES, RSA), use the map as materials description + materialsDescription = MaterialsDescription.builder() + .putAll(matDescMap) + .build(); + // Set an empty encryption context + encryptionContext = new HashMap<>(); + } + // Get content iv byte[] iv = DECODER.decode(metadata.get(MetadataKeyConstants.CONTENT_IV)); return ContentMetadata.builder() .algorithmSuite(algorithmSuite) .encryptedDataKey(edk) - .encryptionContextOrMatDesc(encryptionContextOrMatDesc) + .encryptionContext(encryptionContext) + .materialsDescription(materialsDescription) .contentIv(iv) .contentRange(contentRange) .build(); diff --git a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java index 2e4898608..314a50e8c 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java @@ -91,7 +91,8 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g .s3Request(getObjectRequest) .algorithmSuite(algorithmSuite) .encryptedDataKeys(encryptedDataKeys) - .encryptionContext(contentMetadata.encryptedDataKeyMatDescOrContext()) + .encryptionContext(contentMetadata.encryptionContext()) + .materialsDescription(contentMetadata.materialsDescription()) .ciphertextLength(getObjectResponse.contentLength()) .contentRange(getObjectRequest.range()) .build(); diff --git a/src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java b/src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java new file mode 100644 index 000000000..a5af68785 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/AesKeyMaterial.java @@ -0,0 +1,67 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +import javax.crypto.SecretKey; + +/** + * A concrete implementation of RawKeyMaterial for AES keys. + * This class provides a more convenient way to create key material for AES keyrings + * without having to specify the generic type parameter. + */ +public class AesKeyMaterial extends RawKeyMaterial { + + /** + * Creates a new AesKeyMaterial with the specified materials description and key material. + * + * @param materialsDescription the materials description + * @param keyMaterial the AES key material + */ + public AesKeyMaterial(MaterialsDescription materialsDescription, SecretKey keyMaterial) { + super(materialsDescription, keyMaterial); + } + + /** + * @return a new builder instance for AesKeyMaterial + */ + public static Builder aesBuilder() { + return new Builder(); + } + + /** + * Builder for AesKeyMaterial. + */ + public static class Builder { + private MaterialsDescription _materialsDescription; + private SecretKey _keyMaterial; + + /** + * Sets the materials description for this AES key material. + * + * @param materialsDescription the materials description + * @return a reference to this object so that method calls can be chained together. + */ + public Builder materialsDescription(MaterialsDescription materialsDescription) { + this._materialsDescription = materialsDescription; + return this; + } + + /** + * Sets the AES key material. + * + * @param keyMaterial the AES key material + * @return a reference to this object so that method calls can be chained together. + */ + public Builder keyMaterial(SecretKey keyMaterial) { + this._keyMaterial = keyMaterial; + return this; + } + + /** + * @return the built AesKeyMaterial + */ + public AesKeyMaterial build() { + return new AesKeyMaterial(_materialsDescription, _keyMaterial); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java index ec2e474df..f09acc5b4 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java @@ -20,7 +20,7 @@ * This keyring can wrap keys with the active keywrap algorithm and * unwrap with the active and legacy algorithms for AES keys. */ -public class AesKeyring extends RawKeyring { +public class AesKeyring extends RawKeyring { private static final String KEY_ALGORITHM = "AES"; @@ -41,13 +41,16 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.DECRYPT_MODE, _wrappingKey); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey); - return cipher.doFinal(encryptedDataKey); - } + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.DECRYPT_MODE, keyToUse); + + return cipher.doFinal(encryptedDataKey); + } }; private final DecryptDataKeyStrategy _aesWrapStrategy = new DecryptDataKeyStrategy() { @@ -65,14 +68,17 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.UNWRAP_MODE, _wrappingKey); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey); - Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); - return plaintextKey.getEncoded(); - } + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.UNWRAP_MODE, keyToUse); + + Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); + return plaintextKey.getEncoded(); + } }; private final DataKeyStrategy _aesGcmStrategy = new DataKeyStrategy() { @@ -126,22 +132,25 @@ public byte[] encryptDataKey(SecureRandom secureRandom, return encodedBytes; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - byte[] iv = new byte[IV_LENGTH_BYTES]; - byte[] ciphertext = new byte[encryptedDataKey.length - iv.length]; + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + byte[] iv = new byte[IV_LENGTH_BYTES]; + byte[] ciphertext = new byte[encryptedDataKey.length - iv.length]; - System.arraycopy(encryptedDataKey, 0, iv, 0, iv.length); - System.arraycopy(encryptedDataKey, iv.length, ciphertext, 0, ciphertext.length); + System.arraycopy(encryptedDataKey, 0, iv, 0, iv.length); + System.arraycopy(encryptedDataKey, iv.length, ciphertext, 0, ciphertext.length); - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv); - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.DECRYPT_MODE, _wrappingKey, gcmParameterSpec); + // Find the appropriate key material to use for decryption + SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey); - final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8); - cipher.updateAAD(aADBytes); - return cipher.doFinal(ciphertext); - } + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv); + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.DECRYPT_MODE, keyToUse, gcmParameterSpec); + + final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8); + cipher.updateAAD(aADBytes); + return cipher.doFinal(ciphertext); + } }; private final Map decryptDataKeyStrategies = new HashMap<>(); @@ -175,7 +184,7 @@ protected Map decryptDataKeyStrategies() { return decryptDataKeyStrategies; } - public static class Builder extends RawKeyring.Builder { + public static class Builder extends RawKeyring.Builder { private SecretKey _wrappingKey; private Builder() { diff --git a/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java b/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java index 0b7097120..33b02a186 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java +++ b/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java @@ -16,6 +16,7 @@ public class DecryptMaterialsRequest { private final AlgorithmSuite _algorithmSuite; private final List _encryptedDataKeys; private final Map _encryptionContext; + private final MaterialsDescription _materialsDescription; private final long _ciphertextLength; private final String _contentRange; @@ -24,6 +25,7 @@ private DecryptMaterialsRequest(Builder builder) { this._algorithmSuite = builder._algorithmSuite; this._encryptedDataKeys = builder._encryptedDataKeys; this._encryptionContext = builder._encryptionContext; + this._materialsDescription = builder._materialsDescription; this._ciphertextLength = builder._ciphertextLength; this._contentRange = builder._contentRange; } @@ -60,6 +62,14 @@ public Map encryptionContext() { return _encryptionContext; } + /** + * Returns the materials description used for RSA and AES keyrings. + * @return the materials description + */ + public MaterialsDescription materialsDescription() { + return _materialsDescription; + } + public long ciphertextLength() { return _ciphertextLength; } @@ -73,6 +83,7 @@ static public class Builder { public GetObjectRequest _s3Request = null; private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; private Map _encryptionContext = Collections.emptyMap(); + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private List _encryptedDataKeys = Collections.emptyList(); private long _ciphertextLength = -1; private String _contentRange = null; @@ -97,6 +108,13 @@ public Builder encryptionContext(Map encryptionContext) { return this; } + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; + return this; + } + public Builder encryptedDataKeys(List encryptedDataKeys) { _encryptedDataKeys = encryptedDataKeys == null ? Collections.emptyList() diff --git a/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java b/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java index b2df93a09..1977a2a0b 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java +++ b/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java @@ -27,6 +27,9 @@ final public class DecryptionMaterials implements CryptographicMaterials { // Should NOT contain sensitive information private final Map _encryptionContext; + // Materials description used for RSA and AES keyrings + private final MaterialsDescription _materialsDescription; + private final byte[] _plaintextDataKey; private long _ciphertextLength; @@ -37,6 +40,7 @@ private DecryptionMaterials(Builder builder) { this._s3Request = builder._s3Request; this._algorithmSuite = builder._algorithmSuite; this._encryptionContext = builder._encryptionContext; + this._materialsDescription = builder._materialsDescription; this._plaintextDataKey = builder._plaintextDataKey; this._ciphertextLength = builder._ciphertextLength; this._cryptoProvider = builder._cryptoProvider; @@ -65,6 +69,14 @@ public Map encryptionContext() { return _encryptionContext; } + /** + * Returns the materials description used for RSA and AES keyrings. + * @return the materials description + */ + public MaterialsDescription materialsDescription() { + return _materialsDescription; + } + public byte[] plaintextDataKey() { if (_plaintextDataKey == null) { return null; @@ -103,6 +115,7 @@ public Builder toBuilder() { .s3Request(_s3Request) .algorithmSuite(_algorithmSuite) .encryptionContext(_encryptionContext) + .materialsDescription(_materialsDescription) .plaintextDataKey(_plaintextDataKey) .ciphertextLength(_ciphertextLength) .cryptoProvider(_cryptoProvider) @@ -115,6 +128,7 @@ static public class Builder { private Provider _cryptoProvider = null; private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; private Map _encryptionContext = Collections.emptyMap(); + private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build(); private byte[] _plaintextDataKey = null; private long _ciphertextLength = -1; private String _contentRange = null; @@ -139,6 +153,13 @@ public Builder encryptionContext(Map encryptionContext) { return this; } + public Builder materialsDescription(MaterialsDescription materialsDescription) { + _materialsDescription = materialsDescription == null + ? MaterialsDescription.builder().build() + : materialsDescription; + return this; + } + public Builder plaintextDataKey(byte[] plaintextDataKey) { _plaintextDataKey = plaintextDataKey == null ? null : plaintextDataKey.clone(); return this; diff --git a/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java b/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java index 96d163a4a..3e5d3c4b3 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java +++ b/src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java @@ -36,6 +36,7 @@ public DecryptionMaterials decryptMaterials(DecryptMaterialsRequest request) { .s3Request(request.s3Request()) .algorithmSuite(request.algorithmSuite()) .encryptionContext(request.encryptionContext()) + .materialsDescription(request.materialsDescription()) .ciphertextLength(request.ciphertextLength()) .cryptoProvider(_cryptoProvider) .contentRange(request.contentRange()) diff --git a/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java b/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java index 148b20c49..0ac80282f 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java +++ b/src/main/java/software/amazon/encryption/s3/materials/MaterialsDescription.java @@ -96,6 +96,24 @@ public Set> entrySet() { return materialsDescription.entrySet(); } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + MaterialsDescription other = (MaterialsDescription) obj; + return getMaterialsDescription().equals(other.getMaterialsDescription()); + } + + @Override + public int hashCode() { + return materialsDescription.hashCode(); + } + /** * Builder for MaterialsDescription. */ diff --git a/src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java b/src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java new file mode 100644 index 000000000..161519a61 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/RawKeyMaterial.java @@ -0,0 +1,91 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +/** + * This class represents raw key material used by keyrings. + * It contains a materials description and the actual key material. + * + * @param the type of key material + */ +public class RawKeyMaterial { + + protected final MaterialsDescription _materialsDescription; + protected final T _keyMaterial; + + private RawKeyMaterial(Builder builder) { + this._materialsDescription = builder._materialsDescription; + this._keyMaterial = builder._keyMaterial; + } + + /** + * Protected constructor for subclasses. + * + * @param materialsDescription the materials description + * @param keyMaterial the key material + */ + protected RawKeyMaterial(MaterialsDescription materialsDescription, T keyMaterial) { + this._materialsDescription = materialsDescription; + this._keyMaterial = keyMaterial; + } + + /** + * @return a new builder instance + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * @return the materials description + */ + public MaterialsDescription getMaterialsDescription() { + return _materialsDescription; + } + + /** + * @return the key material + */ + public T getKeyMaterial() { + return _keyMaterial; + } + + /** + * Builder for RawKeyMaterial. + * + * @param the type of key material + */ + public static class Builder { + private MaterialsDescription _materialsDescription; + private T _keyMaterial; + + /** + * Sets the materials description for this raw key material. + * + * @param materialsDescription the materials description + * @return a reference to this object so that method calls can be chained together. + */ + public Builder materialsDescription(MaterialsDescription materialsDescription) { + this._materialsDescription = materialsDescription; + return this; + } + + /** + * Sets the key material. + * + * @param keyMaterial the key material + * @return a reference to this object so that method calls can be chained together. + */ + public Builder keyMaterial(T keyMaterial) { + this._keyMaterial = keyMaterial; + return this; + } + + /** + * @return the built RawKeyMaterial + */ + public RawKeyMaterial build() { + return new RawKeyMaterial<>(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java index 9ad240322..74f1d89cd 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/RawKeyring.java @@ -5,16 +5,46 @@ import org.apache.commons.logging.LogFactory; import software.amazon.encryption.s3.S3EncryptionClient; +import java.util.Map; + /** * This is an abstract base class for keyrings that use raw cryptographic keys (AES + RSA) + * + * @param the type of key material used by this keyring */ -public abstract class RawKeyring extends S3Keyring { +public abstract class RawKeyring extends S3Keyring { protected final MaterialsDescription _materialsDescription; + protected final Map> _additionalDecryptionKeyMaterial; - protected RawKeyring(Builder builder) { + protected RawKeyring(Builder builder) { super(builder); _materialsDescription = builder._materialsDescription; + _additionalDecryptionKeyMaterial = builder._additionalDecryptionKeyMaterial; + } + + /** + * Finds the appropriate key material to use for decryption based on the materials description. + * If a matching key material is found in the additionalDecryptionKeyMaterial map, it is returned. + * Otherwise, the default key material is returned. + * + * @param materials the decryption materials containing the materials description + * @param defaultKeyMaterial the default key material to use if no matching key material is found + * @return the key material to use for decryption + */ + protected T findKeyMaterialForDecryption(DecryptionMaterials materials, T defaultKeyMaterial) { + if (_additionalDecryptionKeyMaterial != null && !_additionalDecryptionKeyMaterial.isEmpty()) { + // Get the materials description from the decryption materials + MaterialsDescription materialsDescription = materials.materialsDescription(); + + // Check if there's a matching entry in the additionalDecryptionKeyMaterial map + RawKeyMaterial matchingKeyMaterial = _additionalDecryptionKeyMaterial.get(materialsDescription); + if (matchingKeyMaterial != null) { + return matchingKeyMaterial.getKeyMaterial(); + } + } + + return defaultKeyMaterial; } /** @@ -76,13 +106,17 @@ public void warnIfEncryptionContextIsPresent(EncryptionMaterials materials) { * * @param the type of keyring being built * @param the type of builder + * @param the type of key material used by this keyring */ public abstract static class Builder< - KeyringT extends RawKeyring, BuilderT extends Builder + KeyringT extends RawKeyring, + BuilderT extends Builder, + T > extends S3Keyring.Builder { protected MaterialsDescription _materialsDescription; + protected Map> _additionalDecryptionKeyMaterial; protected Builder() { super(); @@ -101,5 +135,19 @@ public BuilderT materialsDescription( _materialsDescription = materialsDescription; return builder(); } + + /** + * Sets the map of keys for which to use for decryption. + * + * @param additionalDecryptionKeyMaterial the map of additional key material for decryption, + * where the key is the materials description and the value is the key material + * @return a reference to this object so that method calls can be chained together. + */ + public BuilderT additionalDecryptionKeyMaterial( + Map> additionalDecryptionKeyMaterial + ) { + _additionalDecryptionKeyMaterial = additionalDecryptionKeyMaterial; + return builder(); + } } } diff --git a/src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java new file mode 100644 index 000000000..76ea89a2f --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyMaterial.java @@ -0,0 +1,65 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +/** + * A concrete implementation of RawKeyMaterial for RSA keys. + * This class provides a more convenient way to create key material for RSA keyrings + * without having to specify the generic type parameter. + */ +public class RsaKeyMaterial extends RawKeyMaterial { + + /** + * Creates a new RsaKeyMaterial with the specified materials description and key material. + * + * @param materialsDescription the materials description + * @param keyMaterial the RSA key material + */ + public RsaKeyMaterial(MaterialsDescription materialsDescription, PartialRsaKeyPair keyMaterial) { + super(materialsDescription, keyMaterial); + } + + /** + * @return a new builder instance for RsaKeyMaterial + */ + public static Builder rsaBuilder() { + return new Builder(); + } + + /** + * Builder for RsaKeyMaterial. + */ + public static class Builder { + private MaterialsDescription _materialsDescription; + private PartialRsaKeyPair _keyMaterial; + + /** + * Sets the materials description for this RSA key material. + * + * @param materialsDescription the materials description + * @return a reference to this object so that method calls can be chained together. + */ + public Builder materialsDescription(MaterialsDescription materialsDescription) { + this._materialsDescription = materialsDescription; + return this; + } + + /** + * Sets the RSA key material. + * + * @param keyMaterial the RSA key material + * @return a reference to this object so that method calls can be chained together. + */ + public Builder keyMaterial(PartialRsaKeyPair keyMaterial) { + this._keyMaterial = keyMaterial; + return this; + } + + /** + * @return the built RsaKeyMaterial + */ + public RsaKeyMaterial build() { + return new RsaKeyMaterial(_materialsDescription, _keyMaterial); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java index 91563b14c..ac9469327 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java @@ -23,7 +23,7 @@ * This keyring can wrap keys with the active keywrap algorithm and * unwrap with the active and legacy algorithms for RSA keys. */ -public class RsaKeyring extends RawKeyring { +public class RsaKeyring extends RawKeyring { private final PartialRsaKeyPair _partialRsaKeyPair; @@ -42,13 +42,16 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.DECRYPT_MODE, _partialRsaKeyPair.getPrivateKey()); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + PartialRsaKeyPair keyPairToUse = findKeyMaterialForDecryption(materials, _partialRsaKeyPair); - return cipher.doFinal(encryptedDataKey); - } + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.DECRYPT_MODE, keyPairToUse.getPrivateKey()); + + return cipher.doFinal(encryptedDataKey); + } }; private final DecryptDataKeyStrategy _rsaEcbStrategy = new DecryptDataKeyStrategy() { @@ -65,15 +68,18 @@ public String keyProviderInfo() { return KEY_PROVIDER_INFO; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.UNWRAP_MODE, _partialRsaKeyPair.getPrivateKey()); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + PartialRsaKeyPair keyPairToUse = findKeyMaterialForDecryption(materials, _partialRsaKeyPair); - Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.UNWRAP_MODE, keyPairToUse.getPrivateKey()); - return plaintextKey.getEncoded(); - } + Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY); + + return plaintextKey.getEncoded(); + } }; private final DataKeyStrategy _rsaOaepStrategy = new DataKeyStrategy() { @@ -127,16 +133,19 @@ public byte[] encryptDataKey(SecureRandom secureRandom, return ciphertext; } - @Override - public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { - final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); - cipher.init(Cipher.UNWRAP_MODE, _partialRsaKeyPair.getPrivateKey(), OAEP_PARAMETER_SPEC); + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException { + // Find the appropriate key material to use for decryption + PartialRsaKeyPair keyPairToUse = findKeyMaterialForDecryption(materials, _partialRsaKeyPair); - String dataKeyAlgorithm = materials.algorithmSuite().dataKeyAlgorithm(); - Key pseudoDataKey = cipher.unwrap(encryptedDataKey, dataKeyAlgorithm, Cipher.SECRET_KEY); + final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider()); + cipher.init(Cipher.UNWRAP_MODE, keyPairToUse.getPrivateKey(), OAEP_PARAMETER_SPEC); - return parsePseudoDataKey(materials, pseudoDataKey.getEncoded()); - } + String dataKeyAlgorithm = materials.algorithmSuite().dataKeyAlgorithm(); + Key pseudoDataKey = cipher.unwrap(encryptedDataKey, dataKeyAlgorithm, Cipher.SECRET_KEY); + + return parsePseudoDataKey(materials, pseudoDataKey.getEncoded()); + } private byte[] parsePseudoDataKey(DecryptionMaterials materials, byte[] pseudoDataKey) { int dataKeyLengthBytes = pseudoDataKey[0]; @@ -195,7 +204,7 @@ protected Map decryptDataKeyStrategies() { return decryptDataKeyStrategies; } - public static class Builder extends RawKeyring.Builder { + public static class Builder extends RawKeyring.Builder { private PartialRsaKeyPair _partialRsaKeyPair; private Builder() { diff --git a/src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java b/src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java new file mode 100644 index 000000000..5b51ec8d8 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java @@ -0,0 +1,923 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +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.PutObjectRequest; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; +import software.amazon.encryption.s3.materials.RawKeyMaterial; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; + +/** + * This class is an integration test for verifying the additionalDecryptionKeyMaterial feature + * in the S3EncryptionClient. + */ +public class AdditionalDecryptionKeyMaterialTest { + + private static SecretKey AES_KEY_1; + private static SecretKey AES_KEY_2; + private static SecretKey AES_KEY_3; + private static KeyPair RSA_KEY_PAIR_1; + private static KeyPair RSA_KEY_PAIR_2; + private static KeyPair RSA_KEY_PAIR_3; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + // Generate AES keys + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY_1 = keyGen.generateKey(); + AES_KEY_2 = keyGen.generateKey(); + AES_KEY_3 = keyGen.generateKey(); + + // Generate RSA key pairs + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_2 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_3 = keyPairGen.generateKeyPair(); + } + + /** + * Test AES keyring with null additionalDecryptionKeyMaterial map. + * This tests the default behavior when no additional key material is provided. + */ + @Test + public void testAesKeyringWithNullAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-null-additional-key-material"); + final String input = "AES with null additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an AES keyring with the same key but null additionalDecryptionKeyMaterial + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(null) // Explicitly set to null + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with empty additionalDecryptionKeyMaterial map. + * This tests the behavior when an empty map is provided. + */ + @Test + public void testAesKeyringWithEmptyAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-empty-additional-key-material"); + final String input = "AES with empty additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an AES keyring with the same key but empty additionalDecryptionKeyMaterial + Map> emptyMap = new HashMap<>(); + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(emptyMap) // Empty map + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with a singleton additionalDecryptionKeyMaterial map. + * This tests the behavior when a single additional key material is provided. + */ + @Test + public void testAesKeyringWithSingletonAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-singleton-additional-key-material"); + final String input = "AES with singleton additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a singleton map with the matching materials description and the same key used for encryption + Map> singletonMap = new HashMap<>(); + singletonMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(AES_KEY_1) // Use the same key that was used for encryption + .build()); + + // Create an AES keyring with a different key but with additionalDecryptionKeyMaterial containing the original key + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_3) // Different key than what was used for encryption + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(singletonMap) // Contains the key that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with multiple entries in the additionalDecryptionKeyMaterial map. + * This tests the behavior when multiple additional key materials are provided. + */ + @Test + public void testAesKeyringWithMultipleAdditionalKeyMaterials() { + final String objectKey = appendTestSuffix("aes-multiple-additional-key-materials"); + final String input = "AES with multiple additional key materials"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with multiple entries + Map> multipleMap = new HashMap<>(); + + // Add an entry that doesn't match + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + multipleMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(AES_KEY_2) + .build()); + + // Add the matching entry + multipleMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(AES_KEY_1) + .build()); + + // Create an AES keyring with a different key but with additionalDecryptionKeyMaterial containing the original key + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_3) // Different key than what was used for encryption + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(multipleMap) // Contains the key that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with additionalDecryptionKeyMaterial that doesn't match. + * This tests the behavior when no matching key material is found and it should fall back to the default key. + */ + @Test + public void testAesKeyringWithNonMatchingAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("aes-non-matching-additional-key-material"); + final String input = "AES with non-matching additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(AES_KEY_2) + .build()); + + // Create an AES keyring with the correct key as the default but with non-matching additionalDecryptionKeyMaterial + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) // Same key as used for encryption + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test AES keyring with additionalDecryptionKeyMaterial that doesn't match and a wrong default key. + * This tests the behavior when no matching key material is found and the default key is also wrong. + */ + @Test + public void testAesKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey() { + final String objectKey = appendTestSuffix("aes-non-matching-additional-key-material-wrong-default"); + final String input = "AES with non-matching additional key material and wrong default key"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an AES keyring with the first key and materials description + AesKeyring encryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(AES_KEY_2) + .build()); + + // Create an AES keyring with a wrong default key and non-matching additionalDecryptionKeyMaterial + AesKeyring decryptionKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_3) // Different key than what was used for encryption + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Attempt to decrypt the object, which should fail + assertThrows(S3EncryptionClientException.class, () -> decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build())); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with null additionalDecryptionKeyMaterial map. + * This tests the default behavior when no additional key material is provided. + */ + @Test + public void testRsaKeyringWithNullAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-null-additional-key-material"); + final String input = "RSA with null additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an RSA keyring with the same key pair but null additionalDecryptionKeyMaterial + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(null) // Explicitly set to null + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with empty additionalDecryptionKeyMaterial map. + * This tests the behavior when an empty map is provided. + */ + @Test + public void testRsaKeyringWithEmptyAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-empty-additional-key-material"); + final String input = "RSA with empty additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create an RSA keyring with the same key pair but empty additionalDecryptionKeyMaterial + Map> emptyMap = new HashMap<>(); + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(emptyMap) // Empty map + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with a singleton additionalDecryptionKeyMaterial map. + * This tests the behavior when a single additional key material is provided. + */ + @Test + public void testRsaKeyringWithSingletonAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-singleton-additional-key-material"); + final String input = "RSA with singleton additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a singleton map with the matching materials description and the same key pair used for encryption + Map> singletonMap = new HashMap<>(); + singletonMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with a different key pair but with additionalDecryptionKeyMaterial containing the original key pair + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build()) + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(singletonMap) // Contains the key pair that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with multiple entries in the additionalDecryptionKeyMaterial map. + * This tests the behavior when multiple additional key materials are provided. + */ + @Test + public void testRsaKeyringWithMultipleAdditionalKeyMaterials() { + final String objectKey = appendTestSuffix("rsa-multiple-additional-key-materials"); + final String input = "RSA with multiple additional key materials"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with multiple entries + Map> multipleMap = new HashMap<>(); + + // Add an entry that doesn't match + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + multipleMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build()) + .build()); + + // Add the matching entry + multipleMap.put(materialsDescription, RawKeyMaterial.builder() + .materialsDescription(materialsDescription) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with a different key pair but with additionalDecryptionKeyMaterial containing the original key pair + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build()) + .materialsDescription(MaterialsDescription.builder().put("different", "description").build()) + .additionalDecryptionKeyMaterial(multipleMap) // Contains the key pair that matches the materials description + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with additionalDecryptionKeyMaterial that doesn't match. + * This tests the behavior when no matching key material is found and it should fall back to the default key. + */ + @Test + public void testRsaKeyringWithNonMatchingAdditionalKeyMaterial() { + final String objectKey = appendTestSuffix("rsa-non-matching-additional-key-material"); + final String input = "RSA with non-matching additional key material"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with the correct key pair as the default but with non-matching additionalDecryptionKeyMaterial + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key pair that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Decrypt the object + ResponseBytes objectResponse = decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build()); + + // Verify the decrypted content + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } + + /** + * Test RSA keyring with additionalDecryptionKeyMaterial that doesn't match and a wrong default key. + * This tests the behavior when no matching key material is found and the default key is also wrong. + */ + @Test + public void testRsaKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey() { + final String objectKey = appendTestSuffix("rsa-non-matching-additional-key-material-wrong-default"); + final String input = "RSA with non-matching additional key material and wrong default key"; + + // Create a materials description for the encryption + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create an RSA keyring with the first key pair and materials description + RsaKeyring encryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .build(); + + // Create an S3 encryption client for encryption + S3Client encryptionClient = S3EncryptionClient.builder() + .keyring(encryptionKeyring) + .build(); + + // Encrypt and upload the object + encryptionClient.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build(), RequestBody.fromString(input)); + + // Create a map with a non-matching entry + Map> nonMatchingMap = new HashMap<>(); + MaterialsDescription nonMatchingDesc = MaterialsDescription.builder() + .put("purpose", "different") + .put("version", "2") + .build(); + nonMatchingMap.put(nonMatchingDesc, RawKeyMaterial.builder() + .materialsDescription(nonMatchingDesc) + .keyMaterial(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build()) + .build()); + + // Create an RSA keyring with a wrong default key pair and non-matching additionalDecryptionKeyMaterial + RsaKeyring decryptionKeyring = RsaKeyring.builder() + .wrappingKeyPair(PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build()) + .materialsDescription(materialsDescription) + .additionalDecryptionKeyMaterial(nonMatchingMap) // Contains a key pair that doesn't match + .build(); + + // Create an S3 encryption client for decryption + S3Client decryptionClient = S3EncryptionClient.builder() + .keyring(decryptionKeyring) + .build(); + + // Attempt to decrypt the object, which should fail + assertThrows(S3EncryptionClientException.class, () -> decryptionClient.getObjectAsBytes(GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .build())); + + // Cleanup + deleteObject(BUCKET, objectKey, decryptionClient); + encryptionClient.close(); + decryptionClient.close(); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java index 5f701abbd..8559d2fa8 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java @@ -17,6 +17,7 @@ import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.KMSEncryptionMaterials; import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.SimpleMaterialProvider; import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -28,6 +29,8 @@ import software.amazon.awssdk.services.s3.model.MetadataDirective; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.encryption.s3.internal.InstructionFileConfig; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -42,7 +45,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; - import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID; diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java new file mode 100644 index 000000000..77a3810b3 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java @@ -0,0 +1,599 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +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.encryption.s3.internal.InstructionFileConfig; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest; +import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse; +import software.amazon.encryption.s3.materials.AesKeyMaterial; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.MaterialsDescription; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RawKeyMaterial; +import software.amazon.encryption.s3.materials.RsaKeyMaterial; +import software.amazon.encryption.s3.materials.RsaKeyring; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static software.amazon.encryption.s3.S3EncryptionClient.withCustomInstructionFileSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; + +/** + * This class tests the ReEncryptInstructionFile operation with additionalDecryptionMaterials. + * It tests scenarios where the client is configured with additionalDecryptionMaterials and uses + * those materials to decrypt the instruction file during the re-encryption process. + */ +public class S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest { + + private static SecretKey AES_KEY_1; + private static SecretKey AES_KEY_2; + private static SecretKey AES_KEY_3; + private static KeyPair RSA_KEY_PAIR_1; + private static KeyPair RSA_KEY_PAIR_2; + private static KeyPair RSA_KEY_PAIR_3; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + // Generate AES keys + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY_1 = keyGen.generateKey(); + AES_KEY_2 = keyGen.generateKey(); + AES_KEY_3 = keyGen.generateKey(); + + // Generate RSA key pairs + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR_1 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_2 = keyPairGen.generateKeyPair(); + RSA_KEY_PAIR_3 = keyPairGen.generateKeyPair(); + } + + /** + * Test AES keyring with additionalDecryptionMaterials for ReEncryptInstructionFile. + * This test encrypts an object with AES_KEY_1, then uses a client with AES_KEY_2 as the primary key + * but with additionalDecryptionMaterials containing AES_KEY_1 to re-encrypt the instruction file. + */ + @Test + public void testAesKeyringReEncryptInstructionFileWithAdditionalDecryptionMaterials() { + // Create materials descriptions + MaterialsDescription originalMatDesc = MaterialsDescription.builder() + .put("purpose", "original") + .put("version", "1") + .build(); + + MaterialsDescription newMatDesc = MaterialsDescription.builder() + .put("purpose", "rotated") + .put("version", "2") + .build(); + + MaterialsDescription otherMatDesc = MaterialsDescription.builder() + .put("purpose", "testing") + .put("do not use", "just for testing multi-key") + .build(); + + // Create a map of additional decryption key materials containing all the keys + Map> additionalDecryptionKeyMaterial = new HashMap<>(); + additionalDecryptionKeyMaterial.put(originalMatDesc, RawKeyMaterial.builder() + .materialsDescription(originalMatDesc) + .keyMaterial(AES_KEY_1) + .build()); + additionalDecryptionKeyMaterial.put(newMatDesc, RawKeyMaterial.builder() + .materialsDescription(newMatDesc) + .keyMaterial(AES_KEY_2) + .build()); + additionalDecryptionKeyMaterial.put(otherMatDesc, AesKeyMaterial.aesBuilder() + .materialsDescription(otherMatDesc) + .keyMaterial(AES_KEY_3) + .build()); + + // Create an AES keyring with the first key and original materials description + AesKeyring originalKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_1) + .materialsDescription(originalMatDesc) + .build(); + + // Create an S3 client for the original encryption + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient originalClient = S3EncryptionClient.builder() + .keyring(originalKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Create a test object key and content + final String objectKey = appendTestSuffix("aes-re-encrypt-instruction-file-with-additional-decryption-materials"); + final String input = "Testing re-encryption of instruction file with AES Keyring and additional decryption materials"; + + // Encrypt and upload the object with the original keyring + originalClient.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + // Get the original instruction file to verify its contents + ResponseBytes originalInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the original instruction file + String originalInstructionFileContent = originalInstructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode originalInstructionFileNode = parser.parse(originalInstructionFileContent); + + String originalIv = originalInstructionFileNode.asObject().get("x-amz-iv").asString(); + String originalEncryptedDataKeyAlgorithm = originalInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String originalEncryptedDataKey = originalInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode originalMatDescNode = parser.parse(originalInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + assertEquals("original", originalMatDescNode.asObject().get("purpose").asString()); + assertEquals("1", originalMatDescNode.asObject().get("version").asString()); + + // Create a new AES keyring with a different key as primary but with additionalDecryptionKeyMaterial containing the original key + AesKeyring newKeyring = AesKeyring.builder() + .wrappingKey(AES_KEY_2) // Key used to ReEncrypt + .materialsDescription(newMatDesc) + .additionalDecryptionKeyMaterial(additionalDecryptionKeyMaterial) // contains the original key + .build(); + + // Create a client with the new keyring + S3EncryptionClient newClient = S3EncryptionClient.builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Re-encrypt the instruction file with the new keyring + ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + // The re-encryption should succeed because the new client contains the original key in additionalDecryptionMaterials + ReEncryptInstructionFileResponse response = newClient.reEncryptInstructionFile(reEncryptRequest); + + // Verify the response + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals("instruction", response.instructionFileSuffix()); + + // Get the re-encrypted instruction file to verify its contents + ResponseBytes reEncryptedInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the re-encrypted instruction file + String reEncryptedInstructionFileContent = reEncryptedInstructionFile.asUtf8String(); + JsonNode reEncryptedInstructionFileNode = parser.parse(reEncryptedInstructionFileContent); + + String reEncryptedIv = reEncryptedInstructionFileNode.asObject().get("x-amz-iv").asString(); + String reEncryptedDataKeyAlgorithm = reEncryptedInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String reEncryptedDataKey = reEncryptedInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode reEncryptedMatDescNode = parser.parse(reEncryptedInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + // Verify the re-encrypted instruction file has the new materials description + assertEquals("rotated", reEncryptedMatDescNode.asObject().get("purpose").asString()); + assertEquals("2", reEncryptedMatDescNode.asObject().get("version").asString()); + + // Verify the IV is preserved but the encrypted data key is different + assertEquals(originalIv, reEncryptedIv); + assertEquals(originalEncryptedDataKeyAlgorithm, reEncryptedDataKeyAlgorithm); + assertNotEquals(originalEncryptedDataKey, reEncryptedDataKey); + + // Verify decryption works with the new client (already created above) + + // Verify the object can be decrypted with the new key + ResponseBytes decryptedObject = newClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, decryptedObject.asUtf8String()); + + // Verify the original client can no longer decrypt the object with the original keyring + try { + originalClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertTrue(false, "Original client should not be able to decrypt the re-encrypted object"); + } catch (S3EncryptionClientException e) { + // Expected exception + assertTrue(e.getMessage().contains("Unable to AES/GCM unwrap")); + } + + // Cleanup + deleteObject(BUCKET, objectKey, newClient); + originalClient.close(); + newClient.close(); + } + + /** + * Test RSA keyring with additionalDecryptionMaterials for ReEncryptInstructionFile. + * This test encrypts an object with RSA_KEY_PAIR_1, then uses a client with RSA_KEY_PAIR_2 as the primary key + * but with additionalDecryptionMaterials containing RSA_KEY_PAIR_1 to re-encrypt the instruction file. + */ + @Test + public void testRsaKeyringReEncryptInstructionFileWithAdditionalDecryptionMaterials() { + // Create materials descriptions + MaterialsDescription originalMatDesc = MaterialsDescription.builder() + .put("purpose", "original") + .put("version", "1") + .build(); + + MaterialsDescription newMatDesc = MaterialsDescription.builder() + .put("purpose", "rotated") + .put("version", "2") + .build(); + + MaterialsDescription otherMatDesc = MaterialsDescription.builder() + .put("purpose", "testing") + .put("do not use", "just for testing multi-key") + .build(); + + // Create RSA key pairs for the test + PartialRsaKeyPair originalKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build(); + + PartialRsaKeyPair newKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build(); + + PartialRsaKeyPair otherKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build(); + + // Create a map of additional decryption key materials containing the original key pair + Map> additionalDecryptionKeyMaterial = new HashMap<>(); + additionalDecryptionKeyMaterial.put(originalMatDesc, RawKeyMaterial.builder() + .materialsDescription(originalMatDesc) + .keyMaterial(originalKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(newMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(newMatDesc) + .keyMaterial(newKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(otherMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(otherMatDesc) + .keyMaterial(otherKeyPair) + .build()); + + // Create an RSA keyring with the first key pair and original materials description + RsaKeyring originalKeyring = RsaKeyring.builder() + .wrappingKeyPair(originalKeyPair) + .materialsDescription(originalMatDesc) + .build(); + + // Create an S3 client for the original encryption + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient originalClient = S3EncryptionClient.builder() + .keyring(originalKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Create a test object key and content + final String objectKey = appendTestSuffix("rsa-re-encrypt-instruction-file-with-additional-decryption-materials"); + final String input = "Testing re-encryption of instruction file with RSA Keyring and additional decryption materials"; + + // Encrypt and upload the object with the original keyring + originalClient.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + // Get the original instruction file to verify its contents + ResponseBytes originalInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the original instruction file + String originalInstructionFileContent = originalInstructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode originalInstructionFileNode = parser.parse(originalInstructionFileContent); + + String originalIv = originalInstructionFileNode.asObject().get("x-amz-iv").asString(); + String originalEncryptedDataKeyAlgorithm = originalInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String originalEncryptedDataKey = originalInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode originalMatDescNode = parser.parse(originalInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + assertEquals("original", originalMatDescNode.asObject().get("purpose").asString()); + assertEquals("1", originalMatDescNode.asObject().get("version").asString()); + + // Create a new RSA keyring with a different key pair + RsaKeyring newKeyring = RsaKeyring.builder() + .wrappingKeyPair(newKeyPair) // Different key pair than what was used for original encryption + .materialsDescription(newMatDesc) + .additionalDecryptionKeyMaterial(additionalDecryptionKeyMaterial) + .build(); + + // Create a client with the new keyring + S3EncryptionClient newClient = S3EncryptionClient.builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Re-encrypt the instruction file with the new keyring + ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .build(); + + // The re-encryption should succeed because the new client contains the original key in additionalDecryptionMaterials + ReEncryptInstructionFileResponse response = newClient.reEncryptInstructionFile(reEncryptRequest); + + // Verify the response + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals("instruction", response.instructionFileSuffix()); + + // Get the re-encrypted instruction file to verify its contents + ResponseBytes reEncryptedInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the re-encrypted instruction file + String reEncryptedInstructionFileContent = reEncryptedInstructionFile.asUtf8String(); + JsonNode reEncryptedInstructionFileNode = parser.parse(reEncryptedInstructionFileContent); + + String reEncryptedIv = reEncryptedInstructionFileNode.asObject().get("x-amz-iv").asString(); + String reEncryptedDataKeyAlgorithm = reEncryptedInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String reEncryptedDataKey = reEncryptedInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode reEncryptedMatDescNode = parser.parse(reEncryptedInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + // Verify the re-encrypted instruction file has the new materials description + assertEquals("rotated", reEncryptedMatDescNode.asObject().get("purpose").asString()); + assertEquals("2", reEncryptedMatDescNode.asObject().get("version").asString()); + + // Verify the IV is preserved but the encrypted data key is different + assertEquals(originalIv, reEncryptedIv); + assertEquals(originalEncryptedDataKeyAlgorithm, reEncryptedDataKeyAlgorithm); + assertNotEquals(originalEncryptedDataKey, reEncryptedDataKey); + + // Verify decryption works with the new client (already created above) + + // Verify the object can be decrypted with the new key + ResponseBytes decryptedObject = newClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, decryptedObject.asUtf8String()); + + // Verify the original client can no longer decrypt the object with the original keyring + try { + originalClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertTrue(false, "Original client should not be able to decrypt the re-encrypted object"); + } catch (S3EncryptionClientException e) { + // Expected exception + assertTrue(e.getMessage().contains("Unable to RSA-OAEP-SHA1 unwrap")); + } + + // Cleanup + deleteObject(BUCKET, objectKey, newClient); + originalClient.close(); + newClient.close(); + } + + /** + * Test RSA keyring with custom suffix and additionalDecryptionMaterials for ReEncryptInstructionFile. + * This test encrypts an object with RSA_KEY_PAIR_1, then uses a client with RSA_KEY_PAIR_2 as the primary key + * but with additionalDecryptionMaterials containing RSA_KEY_PAIR_1 to re-encrypt the instruction file with a custom suffix. + */ + @Test + public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixAndAdditionalDecryptionMaterials() { + // Create materials descriptions + MaterialsDescription originalMatDesc = MaterialsDescription.builder() + .put("purpose", "original") + .put("access", "owner") + .build(); + + MaterialsDescription newMatDesc = MaterialsDescription.builder() + .put("purpose", "shared") + .put("access", "partner") + .build(); + + MaterialsDescription otherMatDesc = MaterialsDescription.builder() + .put("purpose", "testing") + .put("do not use", "just for testing multi-key") + .build(); + + // Create RSA key pairs for the test + PartialRsaKeyPair originalKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_1.getPublic()) + .privateKey(RSA_KEY_PAIR_1.getPrivate()) + .build(); + + PartialRsaKeyPair newKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_2.getPublic()) + .privateKey(RSA_KEY_PAIR_2.getPrivate()) + .build(); + + PartialRsaKeyPair otherKeyPair = PartialRsaKeyPair.builder() + .publicKey(RSA_KEY_PAIR_3.getPublic()) + .privateKey(RSA_KEY_PAIR_3.getPrivate()) + .build(); + + // Create a map of additional decryption key materials containing the original key pair + Map> additionalDecryptionKeyMaterial = new HashMap<>(); + additionalDecryptionKeyMaterial.put(originalMatDesc, RawKeyMaterial.builder() + .materialsDescription(originalMatDesc) + .keyMaterial(originalKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(newMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(newMatDesc) + .keyMaterial(newKeyPair) + .build()); + additionalDecryptionKeyMaterial.put(otherMatDesc, RsaKeyMaterial.rsaBuilder() + .materialsDescription(otherMatDesc) + .keyMaterial(otherKeyPair) + .build()); + + // Create an RSA keyring with the first key pair and original materials description + RsaKeyring originalKeyring = RsaKeyring.builder() + .wrappingKeyPair(originalKeyPair) + .materialsDescription(originalMatDesc) + .build(); + + // Create an S3 client for the original encryption + S3Client wrappedClient = S3Client.create(); + S3EncryptionClient originalClient = S3EncryptionClient.builder() + .keyring(originalKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Create a test object key and content + final String objectKey = appendTestSuffix("rsa-re-encrypt-instruction-file-with-custom-suffix-and-additional-decryption-materials"); + final String input = "Testing re-encryption of instruction file with RSA Keyring, custom suffix, and additional decryption materials"; + final String customSuffix = "partner-access"; + + // Encrypt and upload the object with the original keyring + originalClient.putObject( + builder -> builder.bucket(BUCKET).key(objectKey).build(), + RequestBody.fromString(input) + ); + + // Get the original instruction file to verify its contents + ResponseBytes originalInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + ".instruction").build() + ); + + // Parse the original instruction file + String originalInstructionFileContent = originalInstructionFile.asUtf8String(); + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode originalInstructionFileNode = parser.parse(originalInstructionFileContent); + + String originalIv = originalInstructionFileNode.asObject().get("x-amz-iv").asString(); + String originalEncryptedDataKeyAlgorithm = originalInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String originalEncryptedDataKey = originalInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode originalMatDescNode = parser.parse(originalInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + assertEquals("original", originalMatDescNode.asObject().get("purpose").asString()); + assertEquals("owner", originalMatDescNode.asObject().get("access").asString()); + + // Create a new RSA keyring with a different key pair + RsaKeyring newKeyring = RsaKeyring.builder() + .wrappingKeyPair(newKeyPair) // Different key pair than what was used for original encryption + .materialsDescription(newMatDesc) + .additionalDecryptionKeyMaterial(additionalDecryptionKeyMaterial) + .build(); + + // Create a client with the new keyring + S3EncryptionClient newClient = S3EncryptionClient.builder() + .keyring(newKeyring) + .instructionFileConfig( + InstructionFileConfig.builder() + .instructionFileClient(wrappedClient) + .enableInstructionFilePutObject(true) + .build() + ) + .build(); + + // Re-encrypt the instruction file with the new keyring and custom suffix + ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .newKeyring(newKeyring) + .instructionFileSuffix(customSuffix) + .build(); + + // The re-encryption should succeed because the new client contains the original key in additionalDecryptionMaterials + ReEncryptInstructionFileResponse response = newClient.reEncryptInstructionFile(reEncryptRequest); + + // Verify the response + assertEquals(BUCKET, response.bucket()); + assertEquals(objectKey, response.key()); + assertEquals(customSuffix, response.instructionFileSuffix()); + + // Get the re-encrypted instruction file with custom suffix to verify its contents + ResponseBytes reEncryptedInstructionFile = wrappedClient.getObjectAsBytes( + builder -> builder.bucket(BUCKET).key(objectKey + "." + customSuffix).build() + ); + + // Parse the re-encrypted instruction file + String reEncryptedInstructionFileContent = reEncryptedInstructionFile.asUtf8String(); + JsonNode reEncryptedInstructionFileNode = parser.parse(reEncryptedInstructionFileContent); + + String reEncryptedIv = reEncryptedInstructionFileNode.asObject().get("x-amz-iv").asString(); + String reEncryptedDataKeyAlgorithm = reEncryptedInstructionFileNode.asObject().get("x-amz-wrap-alg").asString(); + String reEncryptedDataKey = reEncryptedInstructionFileNode.asObject().get("x-amz-key-v2").asString(); + JsonNode reEncryptedMatDescNode = parser.parse(reEncryptedInstructionFileNode.asObject().get("x-amz-matdesc").asString()); + + // Verify the re-encrypted instruction file has the new materials description + assertEquals("shared", reEncryptedMatDescNode.asObject().get("purpose").asString()); + assertEquals("partner", reEncryptedMatDescNode.asObject().get("access").asString()); + + // Verify the IV is preserved but the encrypted data key is different + assertEquals(originalIv, reEncryptedIv); + assertEquals(originalEncryptedDataKeyAlgorithm, reEncryptedDataKeyAlgorithm); + assertNotEquals(originalEncryptedDataKey, reEncryptedDataKey); + + // Verify decryption works with the new client (already created above) + + // Verify the object can be decrypted with the new key and custom suffix + ResponseBytes decryptedObject = newClient.getObjectAsBytes( + GetObjectRequest.builder() + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withCustomInstructionFileSuffix("." + customSuffix)) + .build() + ); + assertEquals(input, decryptedObject.asUtf8String()); + + // Verify the original client can still decrypt using the default instruction file + ResponseBytes originalDecryptedObject = originalClient.getObjectAsBytes( + GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build() + ); + assertEquals(input, originalDecryptedObject.asUtf8String()); + + // Cleanup + deleteObject(BUCKET, objectKey, newClient); + originalClient.close(); + newClient.close(); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java index 43f6a979b..32a2a60cf 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java @@ -49,7 +49,7 @@ public void decodeWithObjectMetadata() { expectedContentMetadata = ContentMetadata.builder() .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) .encryptedDataKeyAlgorithm(null) - .encryptionContextOrMatDesc(new HashMap()) + .encryptionContext(new HashMap()) .contentIv(bytes) .build(); diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java index d40233ca0..f14e74227 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataTest.java @@ -35,7 +35,7 @@ public void setUp() { .encryptedDataKey(encryptedDataKey) .contentIv(contentIv) .encryptedDataKeyAlgorithm(encryptedDataKeyAlgorithm) - .encryptionContextOrMatDesc(encryptedDataKeyContext) + .encryptionContext(encryptedDataKeyContext) .build(); } @@ -57,7 +57,7 @@ public void testEncryptedDataKeyAlgorithm() { @Test public void testEncryptedDataKeyContext() { - assertEquals(encryptedDataKeyContext, actualContentMetadata.encryptedDataKeyMatDescOrContext()); + assertEquals(encryptedDataKeyContext, actualContentMetadata.encryptionContext()); } @Test diff --git a/src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java b/src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java new file mode 100644 index 000000000..d4de47d05 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/materials/KeyMaterialTest.java @@ -0,0 +1,178 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.encryption.s3.materials; + +import org.junit.jupiter.api.Test; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Tests for the AesKeyMaterial and RsaKeyMaterial classes. + */ +public class KeyMaterialTest { + + /** + * Test creating AesKeyMaterial using the builder. + */ + @Test + public void testAesKeyMaterial() throws NoSuchAlgorithmException { + // Generate an AES key + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesKey = keyGen.generateKey(); + + // Create a materials description + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create AesKeyMaterial using the builder + AesKeyMaterial aesKeyMaterial = AesKeyMaterial.aesBuilder() + .materialsDescription(materialsDescription) + .keyMaterial(aesKey) + .build(); + + // Verify the key material + assertEquals(materialsDescription, aesKeyMaterial.getMaterialsDescription()); + assertEquals(aesKey, aesKeyMaterial.getKeyMaterial()); + } + + /** + * Test creating RsaKeyMaterial using the builder. + */ + @Test + public void testRsaKeyMaterial() throws NoSuchAlgorithmException { + // Generate an RSA key pair + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair rsaKeyPair = keyPairGen.generateKeyPair(); + + // Create a PartialRsaKeyPair + PartialRsaKeyPair partialRsaKeyPair = PartialRsaKeyPair.builder() + .publicKey(rsaKeyPair.getPublic()) + .privateKey(rsaKeyPair.getPrivate()) + .build(); + + // Create a materials description + MaterialsDescription materialsDescription = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + + // Create RsaKeyMaterial using the builder + RsaKeyMaterial rsaKeyMaterial = RsaKeyMaterial.rsaBuilder() + .materialsDescription(materialsDescription) + .keyMaterial(partialRsaKeyPair) + .build(); + + // Verify the key material + assertEquals(materialsDescription, rsaKeyMaterial.getMaterialsDescription()); + assertEquals(partialRsaKeyPair, rsaKeyMaterial.getKeyMaterial()); + } + + /** + * Test using AesKeyMaterial with additionalDecryptionKeyMaterial. + */ + @Test + public void testAesKeyMaterialWithAdditionalDecryptionKeyMaterial() throws NoSuchAlgorithmException { + // Generate AES keys + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey aesKey1 = keyGen.generateKey(); + SecretKey aesKey2 = keyGen.generateKey(); + + // Create materials descriptions + MaterialsDescription materialsDescription1 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + MaterialsDescription materialsDescription2 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "2") + .build(); + + // Create a map with AesKeyMaterial + Map> additionalKeyMaterial = new HashMap<>(); + + // Old way (with explicit type parameters) + additionalKeyMaterial.put(materialsDescription1, RawKeyMaterial.builder() + .materialsDescription(materialsDescription1) + .keyMaterial(aesKey1) + .build()); + + // New way (with concrete type) + additionalKeyMaterial.put(materialsDescription2, AesKeyMaterial.aesBuilder() + .materialsDescription(materialsDescription2) + .keyMaterial(aesKey2) + .build()); + + // Verify the map entries + assertNotNull(additionalKeyMaterial.get(materialsDescription1)); + assertNotNull(additionalKeyMaterial.get(materialsDescription2)); + assertEquals(aesKey1, additionalKeyMaterial.get(materialsDescription1).getKeyMaterial()); + assertEquals(aesKey2, additionalKeyMaterial.get(materialsDescription2).getKeyMaterial()); + } + + /** + * Test using RsaKeyMaterial with additionalDecryptionKeyMaterial. + */ + @Test + public void testRsaKeyMaterialWithAdditionalDecryptionKeyMaterial() throws NoSuchAlgorithmException { + // Generate RSA key pairs + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair rsaKeyPair1 = keyPairGen.generateKeyPair(); + KeyPair rsaKeyPair2 = keyPairGen.generateKeyPair(); + + // Create PartialRsaKeyPairs + PartialRsaKeyPair partialRsaKeyPair1 = PartialRsaKeyPair.builder() + .publicKey(rsaKeyPair1.getPublic()) + .privateKey(rsaKeyPair1.getPrivate()) + .build(); + PartialRsaKeyPair partialRsaKeyPair2 = PartialRsaKeyPair.builder() + .publicKey(rsaKeyPair2.getPublic()) + .privateKey(rsaKeyPair2.getPrivate()) + .build(); + + // Create materials descriptions + MaterialsDescription materialsDescription1 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "1") + .build(); + MaterialsDescription materialsDescription2 = MaterialsDescription.builder() + .put("purpose", "test") + .put("version", "2") + .build(); + + // Create a map with RsaKeyMaterial + Map> additionalKeyMaterial = new HashMap<>(); + + // Old way (with explicit type parameters) + additionalKeyMaterial.put(materialsDescription1, RawKeyMaterial.builder() + .materialsDescription(materialsDescription1) + .keyMaterial(partialRsaKeyPair1) + .build()); + + // New way (with concrete type) + additionalKeyMaterial.put(materialsDescription2, RsaKeyMaterial.rsaBuilder() + .materialsDescription(materialsDescription2) + .keyMaterial(partialRsaKeyPair2) + .build()); + + // Verify the map entries + assertNotNull(additionalKeyMaterial.get(materialsDescription1)); + assertNotNull(additionalKeyMaterial.get(materialsDescription2)); + assertEquals(partialRsaKeyPair1, additionalKeyMaterial.get(materialsDescription1).getKeyMaterial()); + assertEquals(partialRsaKeyPair2, additionalKeyMaterial.get(materialsDescription2).getKeyMaterial()); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java b/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java index 6a5230718..06073538e 100644 --- a/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java +++ b/src/test/java/software/amazon/encryption/s3/materials/MaterialsDescriptionTest.java @@ -1,6 +1,7 @@ package software.amazon.encryption.s3.materials; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -118,4 +119,60 @@ public void testMaterialsDescriptionRsaKeyring() { .build(); assertNotNull(rsaKeyring); } + + @Test + public void testEquals() { + // Create two identical MaterialsDescription objects + MaterialsDescription desc1 = MaterialsDescription.builder() + .put("key1", "value1") + .put("key2", "value2") + .build(); + + MaterialsDescription desc2 = MaterialsDescription.builder() + .put("key1", "value1") + .put("key2", "value2") + .build(); + + // Create a MaterialsDescription with different values + MaterialsDescription desc3 = MaterialsDescription.builder() + .put("key1", "value1") + .put("key2", "different") + .build(); + + // Create a MaterialsDescription with different keys + MaterialsDescription desc4 = MaterialsDescription.builder() + .put("key1", "value1") + .put("different", "value2") + .build(); + + // Create a MaterialsDescription with different number of entries + MaterialsDescription desc5 = MaterialsDescription.builder() + .put("key1", "value1") + .build(); + + // Test reflexivity + assertEquals(desc1, desc1); + + // Test symmetry + assertEquals(desc1, desc2); + assertEquals(desc2, desc1); + + // Test with different values + assertNotEquals(desc1, desc3); + + // Test with different keys + assertNotEquals(desc1, desc4); + + // Test with different number of entries + assertNotEquals(desc1, desc5); + + // Test with null + assertNotEquals(desc1, null); + + // Test with different type + assertNotEquals(desc1, "not a MaterialsDescription"); + + // Test hashCode + assertEquals(desc1.hashCode(), desc2.hashCode()); + } } From a210653476c2628e0cd2f04e7370187c08c8b1ec Mon Sep 17 00:00:00 2001 From: Kess Plasmeier <76071473+kessplas@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:25:03 -0700 Subject: [PATCH 5/6] chore(release): skip openjdk11 during release validation (#487) --- codebuild/release/release.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/codebuild/release/release.yml b/codebuild/release/release.yml index 28e88665a..e4042b4ed 100644 --- a/codebuild/release/release.yml +++ b/codebuild/release/release.yml @@ -22,19 +22,9 @@ batch: JAVA_NUMERIC_VERSION: 8 image: aws/codebuild/standard:3.0 - - identifier: validate_staging_release_openjdk11 - depend-on: - - validate_staging_release_openjdk8 - buildspec: codebuild/release/validate-staging.yml - env: - variables: - JAVA_ENV_VERSION: openjdk11 - JAVA_NUMERIC_VERSION: 11 - image: aws/codebuild/standard:3.0 - - identifier: validate_staging_release_corretto8 depend-on: - - validate_staging_release_openjdk11 + - validate_staging_release_openjdk8 buildspec: codebuild/release/validate-staging.yml env: variables: From 176200cbd8b4a7d86fe826ee1c3ce09e6b3c2394 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 27 Oct 2025 20:44:10 +0000 Subject: [PATCH 6/6] Amazon S3 Encryption Client 3.5.0 Release -- 2025-10-27 ## [3.5.0](https://github.com/aws/aws-s3-encryption-client-java/compare/v3.4.0...v3.5.0) (2025-10-27) ### Features * allow raw keyrings to decrypt with multiple wrapping keys ([#485](https://github.com/aws/aws-s3-encryption-client-java/issues/485)) ([a78cb52](https://github.com/aws/aws-s3-encryption-client-java/commit/a78cb522489af90c65dba0e83e2c3803aefacb3f)) ### Maintenance * add client specification and Duvet annotations ([#481](https://github.com/aws/aws-s3-encryption-client-java/issues/481)) ([1bd8b7a](https://github.com/aws/aws-s3-encryption-client-java/commit/1bd8b7a61500080735d90bbff0ab19af35ff0a6a)) * move spec submodule to master, update annotations ([#482](https://github.com/aws/aws-s3-encryption-client-java/issues/482)) ([cc9eafc](https://github.com/aws/aws-s3-encryption-client-java/commit/cc9eafc9649ded342ee2536d77d949469c9066ad)) * **release:** skip openjdk11 during release validation ([#487](https://github.com/aws/aws-s3-encryption-client-java/issues/487)) ([a210653](https://github.com/aws/aws-s3-encryption-client-java/commit/a210653476c2628e0cd2f04e7370187c08c8b1ec)) * **spec:** add spec and Duvet annotations for KmsKeyring ([#483](https://github.com/aws/aws-s3-encryption-client-java/issues/483)) ([ab41a57](https://github.com/aws/aws-s3-encryption-client-java/commit/ab41a57882f674768c4f528a9069cf69aeb9a53f)) --- CHANGELOG.md | 13 +++++++++++++ pom.xml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 819270531..a42f6c8e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [3.5.0](https://github.com/aws/aws-s3-encryption-client-java/compare/v3.4.0...v3.5.0) (2025-10-27) + +### Features + +* allow raw keyrings to decrypt with multiple wrapping keys ([#485](https://github.com/aws/aws-s3-encryption-client-java/issues/485)) ([a78cb52](https://github.com/aws/aws-s3-encryption-client-java/commit/a78cb522489af90c65dba0e83e2c3803aefacb3f)) + +### Maintenance + +* add client specification and Duvet annotations ([#481](https://github.com/aws/aws-s3-encryption-client-java/issues/481)) ([1bd8b7a](https://github.com/aws/aws-s3-encryption-client-java/commit/1bd8b7a61500080735d90bbff0ab19af35ff0a6a)) +* move spec submodule to master, update annotations ([#482](https://github.com/aws/aws-s3-encryption-client-java/issues/482)) ([cc9eafc](https://github.com/aws/aws-s3-encryption-client-java/commit/cc9eafc9649ded342ee2536d77d949469c9066ad)) +* **release:** skip openjdk11 during release validation ([#487](https://github.com/aws/aws-s3-encryption-client-java/issues/487)) ([a210653](https://github.com/aws/aws-s3-encryption-client-java/commit/a210653476c2628e0cd2f04e7370187c08c8b1ec)) +* **spec:** add spec and Duvet annotations for KmsKeyring ([#483](https://github.com/aws/aws-s3-encryption-client-java/issues/483)) ([ab41a57](https://github.com/aws/aws-s3-encryption-client-java/commit/ab41a57882f674768c4f528a9069cf69aeb9a53f)) + ## [3.4.0](https://github.com/aws/aws-s3-encryption-client-java/compare/v3.3.5...v3.4.0) (2025-07-30) ### Features diff --git a/pom.xml b/pom.xml index bb1fa3ebf..77fd5d49a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ software.amazon.encryption.s3 amazon-s3-encryption-client-java - 3.4.0 + 3.5.0 jar Amazon S3 Encryption Client