From 2cdb8b379cd4dcc66588bda803ba5115e8bff57b Mon Sep 17 00:00:00 2001 From: Kess Plasmeier Date: Mon, 4 Aug 2025 14:21:38 -0700 Subject: [PATCH 1/2] chore: add client specification and Duvet annotations --- .gitmodules | 4 + Makefile | 14 +++ specification | 1 + .../encryption/s3/S3EncryptionClient.java | 86 ++++++++++++++++++- .../internal/GetEncryptedObjectPipeline.java | 8 ++ .../MultipartUploadObjectPipeline.java | 14 ++- .../encryption/s3/materials/S3Keyring.java | 6 +- 7 files changed, 127 insertions(+), 6 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..c5a78bcd0 --- /dev/null +++ b/specification @@ -0,0 +1 @@ +Subproject commit c5a78bcd0d0cb2ca4b7875f12103314986a1495d diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index 8b44bcda3..de4b8930d 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -101,6 +101,9 @@ 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 +//# The S3EC MUST adhere to the same interface for API operations as the conventional AWS SDK S3 client. /** * 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 +128,8 @@ public class S3EncryptionClient extends DelegatingS3Client { private final long _bufferSize; private final InstructionFileConfig _instructionFileConfig; + //= specification/s3-encryption/client.md#aws-sdk-compatibility + //# 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 +205,8 @@ public static Consumer withAdditionalCo .putExecutionAttribute(S3EncryptionClient.CONFIGURATION, multipartConfiguration); } + //= specification/s3-encryption/client.md#api-operations + //# 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 +244,8 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru final byte[] iv = contentMetadata.contentIv(); //Decrypt the data key using the current keyring + //= specification/s3-encryption/client.md#api-operations + //# 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 +264,8 @@ 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 + //# ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring. RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring(); EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials); @@ -296,6 +307,8 @@ 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 + //# PutObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}. *

@@ -322,6 +335,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 +346,8 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); try { + //= specification/s3-encryption/client.md#api-operations + //# PutObject MUST encrypt its input data before it is uploaded to S3. CompletableFuture futurePut = pipeline.putObject(putObjectRequest, AsyncRequestBody.fromInputStream( requestBody.contentStreamProvider().newStream(), @@ -356,6 +372,8 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod } + //= specification/s3-encryption/client.md#api-operations + //# GetObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#getObject(GetObjectRequest, ResponseTransformer)} *

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

@@ -501,9 +523,11 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest .build(); try { - // Delete the object + //= specification/s3-encryption/client.md#api-operations + //# 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 + //# 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 +542,8 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest } } + //= specification/s3-encryption/client.md#api-operations + //# DeleteObjects MUST be implemented by the S3EC. /** * See {@link S3Client#deleteObjects(DeleteObjectsRequest)}. *

@@ -534,9 +560,11 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); try { - // Delete the objects + //= specification/s3-encryption/client.md#api-operations + //# 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 + //# 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 +579,8 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq } } + //= specification/s3-encryption/client.md#api-operations + //# CreateMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#createMultipartUpload(CreateMultipartUploadRequest)} *

@@ -572,6 +602,8 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload } } + //= specification/s3-encryption/client.md#api-operations + //# UploadPart MAY be implemented by the S3EC. /** * See {@link S3Client#uploadPart(UploadPartRequest, RequestBody)} * @@ -595,6 +627,8 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } } + //= specification/s3-encryption/client.md#api-operations + //# CompleteMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#completeMultipartUpload(CompleteMultipartUploadRequest)} * @param request the request instance @@ -612,6 +646,8 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart } } + //= specification/s3-encryption/client.md#api-operations + //# AbortMultipartUpload MAY be implemented by the S3EC. /** * See {@link S3Client#abortMultipartUpload(AbortMultipartUploadRequest)} * @param request the request instance @@ -652,11 +688,17 @@ 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 + //# 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 + //# 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..3fa28d0e2 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,8 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); + //= specification/s3-encryption/client.md#api-operations + //# If implemented, CreateMultipartUpload MUST initiate a multipart upload. CreateMultipartUploadResponse response = _s3AsyncClient.createMultipartUpload(request).join(); MultipartUploadMaterials mpuMaterials = MultipartUploadMaterials.builder() @@ -133,13 +135,18 @@ 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 + //# Each part MUST be encrypted in sequence. materials.beginPartUpload(actualRequest.partNumber(), partContentLength); + //= specification/s3-encryption/client.md#api-operations + //# 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 + //# UploadPart MUST encrypt each part. final AsyncRequestBody cipherAsyncRequestBody = new CipherAsyncRequestBody( AsyncRequestBody.fromInputStream( requestBody.contentStreamProvider().newStream(), @@ -159,6 +166,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 response = _s3AsyncClient.uploadPart(actualRequest, noRetryBody).join(); } finally { materials.endPartUpload(); @@ -187,6 +195,8 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); + //= specification/s3-encryption/client.md#api-operations + //# CompleteMultipartUpload MUST complete the multipart upload. CompleteMultipartUploadResponse response = _s3AsyncClient.completeMultipartUpload(actualRequest).join(); _multipartUploadMaterials.remove(uploadId); @@ -198,6 +208,8 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq AbortMultipartUploadRequest actualRequest = request.toBuilder() .overrideConfiguration(API_NAME_INTERCEPTOR) .build(); + //= specification/s3-encryption/client.md#api-operations + //# 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..0d09173ba 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,11 @@ public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List Date: Tue, 5 Aug 2025 13:22:01 -0700 Subject: [PATCH 2/2] use implication/implementation/test annotation types instead of just comments for everything --- specification | 2 +- .../encryption/s3/S3EncryptionClient.java | 50 ++++++++++++++++++- .../internal/GetEncryptedObjectPipeline.java | 6 ++- .../MultipartUploadObjectPipeline.java | 7 +++ .../encryption/s3/materials/S3Keyring.java | 4 +- .../S3EncryptionClientCompatibilityTest.java | 6 +++ .../encryption/s3/S3EncryptionClientTest.java | 47 ++++++++++++++++- 7 files changed, 116 insertions(+), 6 deletions(-) diff --git a/specification b/specification index c5a78bcd0..616da1e36 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit c5a78bcd0d0cb2ca4b7875f12103314986a1495d +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 de4b8930d..c4d043cc9 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -103,7 +103,11 @@ //= 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). @@ -129,6 +133,7 @@ public class S3EncryptionClient extends DelegatingS3Client { 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); @@ -206,6 +211,7 @@ public static Consumer withAdditionalCo } //= 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. @@ -245,6 +251,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru //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() @@ -265,6 +272,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru //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); @@ -308,6 +316,7 @@ private void enforceRotation(EncryptionMaterials newEncryptionMaterials, GetObje } //= specification/s3-encryption/client.md#api-operations + //= type=implication //# PutObject MUST be implemented by the S3EC. /** * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}. @@ -347,6 +356,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod 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( @@ -373,6 +383,7 @@ 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)} @@ -396,6 +407,7 @@ public T getObject(GetObjectRequest getObjectRequest, 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) @@ -505,6 +517,7 @@ private T onAbort(UploadObjectObserver observer, T t) { } //= specification/s3-encryption/client.md#api-operations + //= type=implication //# DeleteObject MUST be implemented by the S3EC. /** * See {@link S3Client#deleteObject(DeleteObjectRequest)}. @@ -524,9 +537,11 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest try { //= specification/s3-encryption/client.md#api-operations + //= type=implementation //# DeleteObject MUST delete the given object key. DeleteObjectResponse deleteObjectResponse = _wrappedAsyncClient.deleteObject(actualRequest).join(); //= 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 @@ -543,6 +558,7 @@ 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)}. @@ -561,9 +577,11 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq .build(); try { //= specification/s3-encryption/client.md#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 + //= 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() @@ -580,6 +598,7 @@ 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)} @@ -603,6 +622,7 @@ 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)} @@ -628,6 +648,7 @@ 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)} @@ -647,6 +668,7 @@ 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)} @@ -689,15 +711,18 @@ public static class Builder implements S3BaseClientBuilder ciphertextPublisher) { || 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, @@ -158,6 +161,7 @@ public void onStream(SdkPublisher ciphertextPublisher) { 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, 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 3fa28d0e2..01c48a578 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java @@ -75,6 +75,7 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload .build(); //= specification/s3-encryption/client.md#api-operations + //= type=implication //# If implemented, CreateMultipartUpload MUST initiate a multipart upload. CreateMultipartUploadResponse response = _s3AsyncClient.createMultipartUpload(request).join(); @@ -136,9 +137,11 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ } final UploadPartResponse response; //= 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()); @@ -146,6 +149,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ try { //= specification/s3-encryption/client.md#api-operations + //= type=implication //# UploadPart MUST encrypt each part. final AsyncRequestBody cipherAsyncRequestBody = new CipherAsyncRequestBody( AsyncRequestBody.fromInputStream( @@ -167,6 +171,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 + //= type=implication response = _s3AsyncClient.uploadPart(actualRequest, noRetryBody).join(); } finally { materials.endPartUpload(); @@ -196,6 +201,7 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart .build(); //= specification/s3-encryption/client.md#api-operations + //= type=implication //# CompleteMultipartUpload MUST complete the multipart upload. CompleteMultipartUploadResponse response = _s3AsyncClient.completeMultipartUpload(actualRequest).join(); @@ -209,6 +215,7 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq .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 0d09173ba..822f09e8c 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/S3Keyring.java @@ -116,10 +116,12 @@ 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");