diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c34e0db2f..e1ea6dcf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,8 @@ jobs: - name: Test run: | + export AWS_S3EC_TEST_ALT_KMS_KEY_ARN=arn:aws:kms:${{ vars.CI_AWS_REGION }}:${{ secrets.CI_AWS_ACCOUNT_ID }}:key/${{ vars.CI_ALT_KMS_KEY_ID }} + export AWS_S3EC_TEST_ALT_ROLE_ARN=arn:aws:iam::${{ secrets.CI_AWS_ACCOUNT_ID }}:role/service-role/${{ vars.CI_ALT_ROLE }} export AWS_S3EC_TEST_BUCKET=${{ vars.CI_S3_BUCKET }} export AWS_S3EC_TEST_KMS_KEY_ID=arn:aws:kms:${{ vars.CI_AWS_REGION }}:${{ secrets.CI_AWS_ACCOUNT_ID }}:key/${{ vars.CI_KMS_KEY_ID }} export AWS_S3EC_TEST_KMS_KEY_ALIAS=arn:aws:kms:${{ vars.CI_AWS_REGION }}:${{ secrets.CI_AWS_ACCOUNT_ID }}:alias/${{ vars.CI_KMS_KEY_ALIAS }} diff --git a/README.md b/README.md index c52ba3fd4..4833395b4 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ The other values are added as variables (by clicking the "New repository variabl * `CI_S3_BUCKET` - the S3 bucket to use, e.g. s3ec-github-test-bucket. * `CI_KMS_KEY_ID` - the short KMS key ID to use, e.g. c3eafb5f-e87d-4584-9400-cf419ce5d782. * `CI_KMS_KEY_ALIAS` - the KMS key alias to use, e.g. S3EC-Github-KMS-Key. Note that the alias must reference the key ID above. +* `CI_ALT_ROLE` - an alternate role to use that is different from the role defined above. It must have permission to use the KMS key below and the S3 bucket above. +* `CI_ALT_KMS_KEY_ID`- the KMS key of an alternate KMS key to use. The alternate role must have access to use the key and the role for `CI_AWS_ROLE` must not have access to the key. ## Migration @@ -44,6 +46,12 @@ However, this version does not support V2's Unencrypted Object Passthrough. This library can only read encrypted objects from S3, unencrypted objects MUST be read with the base S3 Client. +## Client Configuration + +The S3 Encryption Client uses "wrapped" clients to make its requests to S3 and/or KMS. +You can configure each client independently, or apply a "top-level" configuration which is applied to all wrapped clients. +Refer to the Client Configuration Example in the [Examples directory](https://github.com/aws/amazon-s3-encryption-client-java/tree/main/src/examples/java/software/amazon/encryption/s3/examples) for examples of each configuration method. + ### Examples #### V2 KMS Materials Provider to V3 ```java diff --git a/cfn/S3EC-GitHub-CF-Template.yml b/cfn/S3EC-GitHub-CF-Template.yml index 9b498ca47..4a9effae2 100644 --- a/cfn/S3EC-GitHub-CF-Template.yml +++ b/cfn/S3EC-GitHub-CF-Template.yml @@ -14,6 +14,20 @@ Resources: Action: 'kms:*' Resource: '*' + S3ECGitHubKMSKeyIDAlternate: + Type: 'AWS::KMS::Key' + Properties: + Description: Alternate KMS Key for GitHub Action Workflow + Enabled: true + KeyPolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' + Action: 'kms:*' + Resource: '*' + S3ECGitHubKMSKeyAlias: Type: 'AWS::KMS::Alias' Properties: @@ -73,6 +87,89 @@ Resources: } ManagedPolicyName: S3EC-GitHub-KMS-Key-Policy + S3ECGitHubKMSKeyPolicyAlternate: + Type: 'AWS::IAM::ManagedPolicy' + Properties: + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Resource": [ + "arn:aws:kms:*:${AWS::AccountId}:key/${S3ECGitHubKMSKeyIDAlternate}" + ], + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair" + ] + } + ] + } + ManagedPolicyName: S3EC-GitHub-KMS-Key-Policy-Alternate + + S3ECGithubTestRoleAlternate: + Type: 'AWS::IAM::Role' + Properties: + Path: /service-role/ + RoleName: S3EC-GitHub-test-role-alternate + AssumeRolePolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { "Federated": "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:aws/amazon-s3-encryption-client-java:*" + } + } + }, + { + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::${AWS::AccountId}:role/ToolsDevelopment" }, + "Action": "sts:AssumeRole" + }, + { + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::${AWS::AccountId}:role/service-role/S3EC-GitHub-test-role" }, + "Action": "sts:AssumeRole" + } + ] + } + Description: >- + Grant GitHub S3 put and get and KMS (alt key) encrypt, decrypt, and generate access + for testing + ManagedPolicyArns: + - !Ref S3ECGitHubKMSKeyPolicyAlternate + - !Ref S3ECGitHubS3BucketPolicy + + S3ECGitHubAssumeAlternatePolicy: + Type: 'AWS::IAM::ManagedPolicy' + Properties: + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Resource": [ + "arn:aws:iam::${AWS::AccountId}:role/service-role/${S3ECGithubTestRoleAlternate}" + ], + "Action": [ + "sts:AssumeRole" + ] + } + ] + } + ManagedPolicyName: S3EC-GitHub-Assume-Alternate-Policy + S3ECGithubTestRole: Type: 'AWS::IAM::Role' Properties: @@ -108,3 +205,6 @@ Resources: ManagedPolicyArns: - !Ref S3ECGitHubKMSKeyPolicy - !Ref S3ECGitHubS3BucketPolicy + - !Ref S3ECGitHubAssumeAlternatePolicy + + diff --git a/pom.xml b/pom.xml index ad9c29664..44d0f1baf 100644 --- a/pom.xml +++ b/pom.xml @@ -155,6 +155,14 @@ test + + software.amazon.awssdk + sts + 2.20.38 + true + test + + diff --git a/src/examples/java/software/amazon/encryption/s3/examples/AsyncClientExample.java b/src/examples/java/software/amazon/encryption/s3/examples/AsyncClientExample.java index ac30ec93f..6cc1e9e89 100644 --- a/src/examples/java/software/amazon/encryption/s3/examples/AsyncClientExample.java +++ b/src/examples/java/software/amazon/encryption/s3/examples/AsyncClientExample.java @@ -1,9 +1,5 @@ package software.amazon.encryption.s3.examples; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID; -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; - import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; @@ -14,6 +10,10 @@ import java.util.concurrent.CompletableFuture; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; + public class AsyncClientExample { public static final String OBJECT_KEY = appendTestSuffix("async-client-example"); @@ -33,7 +33,7 @@ public static void AsyncClient(String bucket) { final String input = "PutAsyncGetAsync"; // Instantiate the S3 Async Encryption Client to encrypt and decrypt - // by specifying an AES Key with the aesKey builder parameter. + // by specifying a KMS key with the kmsKeyId parameter. // // This means that the S3 Async Encryption Client can perform both encrypt and decrypt operations // as part of the S3 putObject and getObject operations. diff --git a/src/examples/java/software/amazon/encryption/s3/examples/ClientConfigurationExample.java b/src/examples/java/software/amazon/encryption/s3/examples/ClientConfigurationExample.java new file mode 100644 index 000000000..225eab078 --- /dev/null +++ b/src/examples/java/software/amazon/encryption/s3/examples/ClientConfigurationExample.java @@ -0,0 +1,252 @@ +package software.amazon.encryption.s3.examples; + +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.encryption.s3.S3AsyncEncryptionClient; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.materials.KmsKeyring; +import software.amazon.encryption.s3.utils.S3EncryptionClientTestResources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.*; + +public class ClientConfigurationExample { + + public static void main(String[] args) { + CustomClientConfiguration(); + TopLevelClientConfiguration(); + CustomClientConfigurationAsync(); + TopLevelClientConfigurationAsync(); + } + + /** + * This example demonstrates how to use specific client configuration for + * the S3 and KMS clients used within the S3 Encryption Client. + */ + public static void CustomClientConfiguration() { + final String objectKey = appendTestSuffix("custom-client-configuration-example"); + final String input = "CustomClientConfigurationExample"; + // Load your AWS credentials from an external source. + final AwsCredentialsProvider defaultCreds = DefaultCredentialsProvider.create(); + // This example uses two different sets of credentials. + final AwsCredentialsProvider altCreds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + + // Instantiate the wrapped S3 client with the default credentials + // and the region to use with S3. + final S3Client wrappedClient = S3Client.builder() + .credentialsProvider(defaultCreds) + .region(Region.of(S3_REGION.toString())) + .build(); + + /* + * Instantiate the wrapped Async S3 client with the default credentials + * and the region to use with S3. + * The default S3 Encryption Client uses the async client for + * operations requiring encryption or decryption. + * All other operations such as bucket-related operations use the default client, + * which is configured below. + */ + final S3AsyncClient wrappedAsyncClient = S3AsyncClient.builder() + .credentialsProvider(defaultCreds) + .region(Region.of(S3_REGION.toString())) + .build(); + + // Instantiate the KMS client with alternate credentials. + // This client will be used for all KMS requests. + final KmsClient kmsClient = KmsClient.builder() + .credentialsProvider(altCreds) + .region(Region.of(KMS_REGION.toString())) + .build(); + + // Instantiate a KMS Keyring to use with the S3 Encryption Client. + final KmsKeyring kmsKeyring = KmsKeyring.builder() + .wrappingKeyId(ALTERNATE_KMS_KEY) + .kmsClient(kmsClient) + .build(); + + // Instantiate the S3 Encryption Client using the configured clients and keyring. + final S3Client s3Client = S3EncryptionClient.builder() + .wrappedClient(wrappedClient) + .wrappedAsyncClient(wrappedAsyncClient) + .keyring(kmsKeyring) + .build(); + + // Use the client to call putObject. + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + RequestBody.fromString(input)); + + // Use the client to call getObject. + ResponseBytes objectResponse = s3Client.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build()); + String output = objectResponse.asUtf8String(); + // Check that the output matches the input. + assertEquals(input, output); + + // Delete the object. + deleteObject(BUCKET, objectKey, s3Client); + // Close the S3 Client. + s3Client.close(); + } + + /** + * This example demonstrates how to use a single client configuration for + * the S3 and KMS clients used within the S3 Encryption Client. + */ + public static void TopLevelClientConfiguration() { + final String objectKey = appendTestSuffix("top-level-client-configuration-example"); + final String input = "TopLevelClientConfigurationExample"; + // Load your AWS credentials from an external source. + final AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + + // Instantiate the S3 Encryption Client via its builder. + // By passing the creds into the credentialsProvider parameter, + // the S3EC will use these creds for both S3 and KMS requests. + // NOTE: If you use both the "top-level" configuration AND + // custom configuration such as the above example, the custom client + // configuration will take precedence. + final S3Client s3Client = S3EncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(ALTERNATE_KMS_KEY) + .build(); + + // Use the client to call putObject. + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + RequestBody.fromString(input)); + + // Use the client to call getObject. + ResponseBytes objectResponse = s3Client.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build()); + String output = objectResponse.asUtf8String(); + // Check that the output matches the input. + assertEquals(input, output); + + // Delete the object. + deleteObject(BUCKET, objectKey, s3Client); + // Close the S3 Client. + s3Client.close(); + } + + /** + * This example demonstrates how to use specific client configuration for + * the S3 and KMS clients used within the S3 Async Encryption Client. + */ + public static void CustomClientConfigurationAsync() { + final String objectKey = appendTestSuffix("custom-client-configuration-example-async"); + final String input = "CustomClientConfigurationExample"; + // Load your AWS credentials from an external source. + final AwsCredentialsProvider defaultCreds = DefaultCredentialsProvider.create(); + // This example uses two different sets of credentials. + final AwsCredentialsProvider altCreds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + + // Instantiate the wrapped (async) S3 client with the default credentials + // and the region to use with S3. + final S3AsyncClient wrappedAsyncClient = S3AsyncClient.builder() + .credentialsProvider(defaultCreds) + .region(Region.of(S3_REGION.toString())) + .build(); + + // Instantiate the KMS client with alternate credentials. + // This client will be used for all KMS requests. + final KmsClient kmsClient = KmsClient.builder() + .credentialsProvider(altCreds) + .region(Region.of(KMS_REGION.toString())) + .build(); + + // Instantiate a KMS Keyring to use with the S3 Encryption Client. + final KmsKeyring kmsKeyring = KmsKeyring.builder() + .wrappingKeyId(ALTERNATE_KMS_KEY) + .kmsClient(kmsClient) + .build(); + + // Instantiate the S3 Async Encryption Client using the configured clients and keyring. + final S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .wrappedClient(wrappedAsyncClient) + .keyring(kmsKeyring) + .build(); + + // Use the async client to call putObject and block on its response + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + + // Use the async client to call getObject and block on its response + ResponseBytes objectResponse = s3Client.getObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), AsyncResponseTransformer.toBytes()).join(); + String output = objectResponse.asUtf8String(); + // Check that the output matches the input. + assertEquals(input, output); + + // Delete the object. + deleteObject(BUCKET, objectKey, s3Client); + // Close the S3 Client. + s3Client.close(); + } + + /** + * This example demonstrates how to use a single client configuration for + * the S3 and KMS clients used within the S3 Encryption Client. + */ + public static void TopLevelClientConfigurationAsync() { + final String objectKey = appendTestSuffix("top-level-client-configuration-async-example"); + final String input = "TopLevelClientConfigurationExample"; + // Load your AWS credentials from an external source. + final AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + + // Instantiate the S3 Async Encryption Client via its builder. + // By passing the creds into the credentialsProvider parameter, + // the S3EC will use these creds for both S3 and KMS requests. + // NOTE: If you use both the "top-level" configuration AND + // custom configuration such as the above example, the custom client + // configuration will take precedence. + final S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(ALTERNATE_KMS_KEY) + .build(); + + // Use the async client to call putObject and block on its response. + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + + // Use the async client to call getObject and block on its response. + ResponseBytes objectResponse = s3Client.getObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), AsyncResponseTransformer.toBytes()).join(); + String output = objectResponse.asUtf8String(); + // Check that the output matches the input. + assertEquals(input, output); + + // Delete the object. + deleteObject(BUCKET, objectKey, s3Client); + // Close the S3 Client. + s3Client.close(); + } +} diff --git a/src/examples/java/software/amazon/encryption/s3/examples/PartialKeyPairExample.java b/src/examples/java/software/amazon/encryption/s3/examples/PartialKeyPairExample.java index 74d3c3f91..7efb629ba 100644 --- a/src/examples/java/software/amazon/encryption/s3/examples/PartialKeyPairExample.java +++ b/src/examples/java/software/amazon/encryption/s3/examples/PartialKeyPairExample.java @@ -159,7 +159,7 @@ static void useOnlyPrivateKey(final String bucket) { s3ClientPrivateKeyOnly.close(); } - public static void cleanup(final String bucket) { + private static void cleanup(final String bucket) { // The S3 Encryption client is not required when deleting encrypted // objects, use the S3 Client. final S3Client s3Client = S3Client.builder().build(); diff --git a/src/main/java/software/amazon/encryption/s3/S3AsyncEncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3AsyncEncryptionClient.java index 5fe9188f0..a06185239 100644 --- a/src/main/java/software/amazon/encryption/s3/S3AsyncEncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3AsyncEncryptionClient.java @@ -3,11 +3,19 @@ package software.amazon.encryption.s3; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.awscore.client.builder.AwsAsyncClientBuilder; import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; import software.amazon.awssdk.services.s3.DelegatingS3AsyncClient; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.internal.crt.S3CrtAsyncClient; @@ -33,6 +41,7 @@ import software.amazon.encryption.s3.materials.RsaKeyring; import javax.crypto.SecretKey; +import java.net.URI; import java.security.KeyPair; import java.security.Provider; import java.security.SecureRandom; @@ -180,7 +189,7 @@ private CompletableFuture multipartPutObject(PutObjectRequest */ @Override public CompletableFuture getObject(GetObjectRequest getObjectRequest, - AsyncResponseTransformer asyncResponseTransformer) { + AsyncResponseTransformer asyncResponseTransformer) { GetEncryptedObjectPipeline pipeline = GetEncryptedObjectPipeline.builder() .s3AsyncClient(_wrappedClient) .cryptoMaterialsManager(_cryptoMaterialsManager) @@ -250,7 +259,7 @@ public void close() { // This is very similar to the S3EncryptionClient builder // Make sure to keep both clients in mind when adding new builder options - public static class Builder { + public static class Builder implements AwsAsyncClientBuilder { private S3AsyncClient _wrappedClient; private CryptographicMaterialsManager _cryptoMaterialsManager; private Keyring _keyring; @@ -265,6 +274,19 @@ public static class Builder { private SecureRandom _secureRandom = new SecureRandom(); private long _bufferSize = -1L; + // generic AwsClient configuration to be shared by default clients + private AwsCredentialsProvider _awsCredentialsProvider = null; + private Region _region = null; + private boolean _dualStackEnabled = false; + private boolean _fipsEnabled = false; + private ClientOverrideConfiguration _overrideConfiguration = null; + // this should only be applied to S3 clients + private URI _endpointOverride = null; + // async specific configuration + private ClientAsyncConfiguration _clientAsyncConfiguration = null; + private SdkAsyncHttpClient _sdkAsyncHttpClient = null; + private SdkAsyncHttpClient.Builder _sdkAsyncHttpClientBuilder = null; + private Builder() { } @@ -489,6 +511,156 @@ public Builder secureRandom(SecureRandom secureRandom) { return this; } + /** + * The credentials provider to use for all inner clients, including KMS, if a KMS key ID is provided. + * Note that if a wrapped client is configured, the wrapped client will take precedence over this option. + * @param awsCredentialsProvider + * @return + */ + public Builder credentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { + _awsCredentialsProvider = awsCredentialsProvider; + return this; + } + + /** + * The AWS region to use for all inner clients, including KMS, if a KMS key ID is provided. + * Note that if a wrapped client is configured, the wrapped client will take precedence over this option. + * @param region + * @return + */ + public Builder region(Region region) { + _region = region; + return this; + } + + /** + * Configure whether the SDK should use the AWS dualstack endpoint. + * + *

If this is not specified, the SDK will attempt to determine whether the dualstack endpoint should be used + * automatically using the following logic: + *

    + *
  1. Check the 'aws.useDualstackEndpoint' system property for 'true' or 'false'.
  2. + *
  3. Check the 'AWS_USE_DUALSTACK_ENDPOINT' environment variable for 'true' or 'false'.
  4. + *
  5. Check the {user.home}/.aws/credentials and {user.home}/.aws/config files for the 'use_dualstack_endpoint' + * property set to 'true' or 'false'.
  6. + *
+ * + *

If the setting is not found in any of the locations above, 'false' will be used. + */ + public Builder dualstackEnabled(Boolean isDualStackEnabled) { + _dualStackEnabled = isDualStackEnabled; + return this; + } + + /** + * Configure whether the wrapped SDK clients should use the AWS FIPS endpoints. + * Note that this option only enables FIPS for the service endpoints which the SDK clients use, + * it does not enable FIPS for the S3EC itself. Use a FIPS-enabled CryptoProvider for full FIPS support. + * + *

If this is not specified, the SDK will attempt to determine whether the FIPS endpoint should be used + * automatically using the following logic: + *

    + *
  1. Check the 'aws.useFipsEndpoint' system property for 'true' or 'false'.
  2. + *
  3. Check the 'AWS_USE_FIPS_ENDPOINT' environment variable for 'true' or 'false'.
  4. + *
  5. Check the {user.home}/.aws/credentials and {user.home}/.aws/config files for the 'use_fips_endpoint' + * property set to 'true' or 'false'.
  6. + *
+ * + *

If the setting is not found in any of the locations above, 'false' will be used. + */ + public Builder fipsEnabled(Boolean isFipsEnabled) { + _fipsEnabled = isFipsEnabled; + return this; + } + + /** + * Specify overrides to the default SDK configuration that should be used for wrapped clients. + */ + public Builder overrideConfiguration(ClientOverrideConfiguration overrideConfiguration) { + _overrideConfiguration = overrideConfiguration; + return this; + } + + /** + * Retrieve the current override configuration. This allows further overrides across calls. Can be modified by first + * converting to a builder with {@link ClientOverrideConfiguration#toBuilder()}. + * + * @return The existing override configuration for the builder. + */ + public ClientOverrideConfiguration overrideConfiguration() { + return _overrideConfiguration; + } + + /** + * Configure the endpoint with which the SDK should communicate. + * NOTE: For the S3EncryptionClient, this ONLY overrides the endpoint for S3 clients. + * To set the endpointOverride for a KMS client, explicitly configure it and create a + * KmsKeyring instance for the encryption client to use. + *

+ * It is important to know that {@link EndpointProvider}s and the endpoint override on the client are not mutually + * exclusive. In all existing cases, the endpoint override is passed as a parameter to the provider and the provider *may* + * modify it. For example, the S3 provider may add the bucket name as a prefix to the endpoint override for virtual bucket + * addressing. + * + * @param endpointOverride + */ + public Builder endpointOverride(URI endpointOverride) { + _endpointOverride = endpointOverride; + return this; + } + + + /** + * Specify overrides to the default SDK async configuration that should be used for clients created by this builder. + * + * @param clientAsyncConfiguration + */ + @Override + public Builder asyncConfiguration(ClientAsyncConfiguration clientAsyncConfiguration) { + _clientAsyncConfiguration = clientAsyncConfiguration; + return this; + } + + /** + * Sets the {@link SdkAsyncHttpClient} that the SDK service client will use to make HTTP calls. This HTTP client may be + * shared between multiple SDK service clients to share a common connection pool. To create a client you must use an + * implementation specific builder. Note that this method is only recommended when you wish to share an HTTP client across + * multiple SDK service clients. If you do not wish to share HTTP clients, it is recommended to use + * {@link #httpClientBuilder(SdkAsyncHttpClient.Builder)} so that service specific default configuration may be applied. + * + *

+ * This client must be closed by the caller when it is ready to be disposed. The SDK will not close the HTTP client + * when the service client is closed. + *

+ * + * @param httpClient + * @return This builder for method chaining. + */ + @Override + public Builder httpClient(SdkAsyncHttpClient httpClient) { + _sdkAsyncHttpClient = httpClient; + return this; + } + + /** + * Sets a custom HTTP client builder that will be used to obtain a configured instance of {@link SdkAsyncHttpClient}. Any + * service specific HTTP configuration will be merged with the builder's configuration prior to creating the client. When + * there is no desire to share HTTP clients across multiple service clients, the client builder is the preferred way to + * customize the HTTP client as it benefits from service specific defaults. + * + *

+ * Clients created by the builder are managed by the SDK and will be closed when the service client is closed. + *

+ * + * @param httpClientBuilder + * @return This builder for method chaining. + */ + @Override + public Builder httpClientBuilder(SdkAsyncHttpClient.Builder httpClientBuilder) { + _sdkAsyncHttpClientBuilder = httpClientBuilder; + return this; + } + /** * Validates and builds the S3AsyncEncryptionClient according * to the configuration options passed to the Builder object. @@ -508,7 +680,17 @@ public S3AsyncEncryptionClient build() { } if (_wrappedClient == null) { - _wrappedClient = S3AsyncClient.create(); + _wrappedClient = S3AsyncClient.builder() + .credentialsProvider(_awsCredentialsProvider) + .region(_region) + .dualstackEnabled(_dualStackEnabled) + .fipsEnabled(_fipsEnabled) + .overrideConfiguration(_overrideConfiguration) + .endpointOverride(_endpointOverride) + .asyncConfiguration(_clientAsyncConfiguration != null ? _clientAsyncConfiguration : ClientAsyncConfiguration.builder().build()) + .httpClient(_sdkAsyncHttpClient) + .httpClientBuilder(_sdkAsyncHttpClientBuilder) + .build(); } if (_keyring == null) { @@ -525,7 +707,16 @@ public S3AsyncEncryptionClient build() { .secureRandom(_secureRandom) .build(); } else if (_kmsKeyId != null) { + KmsClient kmsClient = KmsClient.builder() + .credentialsProvider(_awsCredentialsProvider) + .region(_region) + .dualstackEnabled(_dualStackEnabled) + .fipsEnabled(_fipsEnabled) + .overrideConfiguration(_overrideConfiguration) + .build(); + _keyring = KmsKeyring.builder() + .kmsClient(kmsClient) .wrappingKeyId(_kmsKeyId) .enableLegacyWrappingAlgorithms(_enableLegacyWrappingAlgorithms) .secureRandom(_secureRandom) diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java index a5d401e62..cc7c97871 100644 --- a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -3,16 +3,22 @@ package software.amazon.encryption.s3; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.interceptor.ExecutionAttribute; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; import software.amazon.awssdk.services.s3.DelegatingS3Client; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; @@ -52,6 +58,7 @@ import javax.crypto.SecretKey; import java.io.IOException; +import java.net.URI; import java.security.KeyPair; import java.security.Provider; import java.security.SecureRandom; @@ -196,11 +203,11 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod try { CompletableFuture futurePut = pipeline.putObject(putObjectRequest, - AsyncRequestBody.fromInputStream( - requestBody.contentStreamProvider().newStream(), - requestBody.optionalContentLength().orElse(-1L), - singleThreadExecutor - ) + AsyncRequestBody.fromInputStream( + requestBody.contentStreamProvider().newStream(), + requestBody.optionalContentLength().orElse(-1L), + singleThreadExecutor + ) ); PutObjectResponse response = futurePut.join(); @@ -502,7 +509,7 @@ public void close() { // This is very similar to the S3EncryptionClient builder // Make sure to keep both clients in mind when adding new builder options - public static class Builder { + public static class Builder implements AwsClientBuilder { // The non-encrypted APIs will use a default client. private S3Client _wrappedClient; private S3AsyncClient _wrappedAsyncClient; @@ -521,6 +528,15 @@ public static class Builder { private boolean _enableLegacyUnauthenticatedModes = false; private long _bufferSize = -1L; + // generic AwsClient configuration to be shared by default clients + private AwsCredentialsProvider _awsCredentialsProvider = null; + private Region _region = null; + private boolean _dualStackEnabled = false; + private boolean _fipsEnabled = false; + private ClientOverrideConfiguration _overrideConfiguration = null; + // this should only be applied to S3 clients + private URI _endpointOverride = null; + private Builder() { } @@ -756,6 +772,110 @@ public Builder secureRandom(SecureRandom secureRandom) { return this; } + /** + * The credentials provider to use for all inner clients, including KMS, if a KMS key ID is provided. + * Note that if a wrapped client is configured, the wrapped client will take precedence over this option. + * @param awsCredentialsProvider + * @return + */ + @Override + public Builder credentialsProvider(AwsCredentialsProvider awsCredentialsProvider) { + _awsCredentialsProvider = awsCredentialsProvider; + return this; + } + + /** + * The AWS region to use for all inner clients, including KMS, if a KMS key ID is provided. + * @param region + * @return + */ + @Override + public Builder region(Region region) { + _region = region; + return this; + } + + /** + * Configure whether the SDK should use the AWS dualstack endpoint. + * + *

If this is not specified, the SDK will attempt to determine whether the dualstack endpoint should be used + * automatically using the following logic: + *

    + *
  1. Check the 'aws.useDualstackEndpoint' system property for 'true' or 'false'.
  2. + *
  3. Check the 'AWS_USE_DUALSTACK_ENDPOINT' environment variable for 'true' or 'false'.
  4. + *
  5. Check the {user.home}/.aws/credentials and {user.home}/.aws/config files for the 'use_dualstack_endpoint' + * property set to 'true' or 'false'.
  6. + *
+ * + *

If the setting is not found in any of the locations above, 'false' will be used. + */ + @Override + public Builder dualstackEnabled(Boolean isDualStackEnabled) { + _dualStackEnabled = isDualStackEnabled; + return this; + } + + /** + * Configure whether the wrapped SDK clients should use the AWS FIPS endpoints. + * Note that this option only enables FIPS for the service endpoints which the SDK clients use, + * it does not enable FIPS for the S3EC itself. Use a FIPS-enabled CryptoProvider for full FIPS support. + * + *

If this is not specified, the SDK will attempt to determine whether the FIPS endpoint should be used + * automatically using the following logic: + *

    + *
  1. Check the 'aws.useFipsEndpoint' system property for 'true' or 'false'.
  2. + *
  3. Check the 'AWS_USE_FIPS_ENDPOINT' environment variable for 'true' or 'false'.
  4. + *
  5. Check the {user.home}/.aws/credentials and {user.home}/.aws/config files for the 'use_fips_endpoint' + * property set to 'true' or 'false'.
  6. + *
+ * + *

If the setting is not found in any of the locations above, 'false' will be used. + */ + @Override + public Builder fipsEnabled(Boolean isFipsEnabled) { + _fipsEnabled = isFipsEnabled; + return this; + } + + /** + * Specify overrides to the default SDK configuration that should be used for clients created by this builder. + */ + @Override + public Builder overrideConfiguration(ClientOverrideConfiguration overrideConfiguration) { + _overrideConfiguration = overrideConfiguration; + return this; + } + + /** + * Retrieve the current override configuration. This allows further overrides across calls. Can be modified by first + * converting to a builder with {@link ClientOverrideConfiguration#toBuilder()}. + * + * @return The existing override configuration for the builder. + */ + @Override + public ClientOverrideConfiguration overrideConfiguration() { + return _overrideConfiguration; + } + + /** + * Configure the endpoint with which the SDK should communicate. + * NOTE: For the S3EncryptionClient, this ONLY overrides the endpoint for S3 clients. + * To set the endpointOverride for a KMS client, explicitly configure it and create a + * KmsKeyring instance for the encryption client to use. + *

+ * It is important to know that {@link EndpointProvider}s and the endpoint override on the client are not mutually + * exclusive. In all existing cases, the endpoint override is passed as a parameter to the provider and the provider *may* + * modify it. For example, the S3 provider may add the bucket name as a prefix to the endpoint override for virtual bucket + * addressing. + * + * @param endpointOverride + */ + @Override + public Builder endpointOverride(URI endpointOverride) { + _endpointOverride = endpointOverride; + return this; + } + /** * Validates and builds the S3EncryptionClient according * to the configuration options passed to the Builder object. @@ -775,11 +895,25 @@ public S3EncryptionClient build() { } if (_wrappedClient == null) { - _wrappedClient = S3Client.create(); + _wrappedClient = S3Client.builder() + .credentialsProvider(_awsCredentialsProvider) + .region(_region) + .dualstackEnabled(_dualStackEnabled) + .fipsEnabled(_fipsEnabled) + .overrideConfiguration(_overrideConfiguration) + .endpointOverride(_endpointOverride) + .build(); } if (_wrappedAsyncClient == null) { - _wrappedAsyncClient = S3AsyncClient.create(); + _wrappedAsyncClient = S3AsyncClient.builder() + .credentialsProvider(_awsCredentialsProvider) + .region(_region) + .dualstackEnabled(_dualStackEnabled) + .fipsEnabled(_fipsEnabled) + .overrideConfiguration(_overrideConfiguration) + .endpointOverride(_endpointOverride) + .build(); } if (_keyring == null) { @@ -796,7 +930,16 @@ public S3EncryptionClient build() { .secureRandom(_secureRandom) .build(); } else if (_kmsKeyId != null) { + KmsClient kmsClient = KmsClient.builder() + .credentialsProvider(_awsCredentialsProvider) + .region(_region) + .dualstackEnabled(_dualStackEnabled) + .fipsEnabled(_fipsEnabled) + .overrideConfiguration(_overrideConfiguration) + .build(); + _keyring = KmsKeyring.builder() + .kmsClient(kmsClient) .wrappingKeyId(_kmsKeyId) .enableLegacyWrappingAlgorithms(_enableLegacyWrappingAlgorithms) .secureRandom(_secureRandom) @@ -819,5 +962,6 @@ public S3EncryptionClient build() { return new S3EncryptionClient(this); } + } } diff --git a/src/test/java/software/amazon/encryption/s3/S3AsyncEncryptionClientTest.java b/src/test/java/software/amazon/encryption/s3/S3AsyncEncryptionClientTest.java index 5c6a487f4..7d7542612 100644 --- a/src/test/java/software/amazon/encryption/s3/S3AsyncEncryptionClientTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3AsyncEncryptionClientTest.java @@ -17,11 +17,17 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.KmsException; +import software.amazon.awssdk.services.kms.model.NotFoundException; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.CopyObjectResponse; @@ -31,7 +37,9 @@ import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.encryption.s3.materials.KmsKeyring; import software.amazon.encryption.s3.utils.BoundedInputStream; +import software.amazon.encryption.s3.utils.S3EncryptionClientTestResources; import software.amazon.encryption.s3.utils.TinyBufferAsyncRequestBody; import javax.crypto.KeyGenerator; @@ -52,7 +60,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.ALTERNATE_KMS_KEY; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; @@ -67,6 +79,240 @@ public static void setUp() throws NoSuchAlgorithmException { AES_KEY = keyGen.generateKey(); } + @Test + public void asyncCustomConfiguration() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-custom-credentials-async"); + + // use the default creds, but through an explicit credentials provider + AwsCredentialsProvider creds = DefaultCredentialsProvider.create(); + + S3AsyncClient wrappedAsyncClient = S3AsyncClient + .builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .build(); + KmsClient kmsClient = KmsClient + .builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .build(); + + KmsKeyring keyring = KmsKeyring + .builder() + .kmsClient(kmsClient) + .wrappingKeyId(KMS_KEY_ID) + .build(); + S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .wrappedClient(wrappedAsyncClient) + .keyring(keyring) + .build(); + + final String input = "SimpleTestOfV3EncryptionClientAsync"; + + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + + ResponseBytes objectResponse = s3Client.getObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), AsyncResponseTransformer.toBytes()).join(); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, s3Client); + wrappedAsyncClient.close(); + s3Client.close(); + } + + @Test + public void asyncTopLevelConfiguration() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-top-level-credentials-async"); + + // use the default creds, but through an explicit credentials provider + AwsCredentialsProvider creds = DefaultCredentialsProvider.create(); + + S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(KMS_KEY_ID) + .build(); + + final String input = "SimpleTestOfV3EncryptionClientAsync"; + + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + + ResponseBytes objectResponse = s3Client.getObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), AsyncResponseTransformer.toBytes()).join(); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, s3Client); + s3Client.close(); + } + + @Test + public void s3AsyncEncryptionClientTopLevelAlternateCredentials() { + final String objectKey = appendTestSuffix("wrapped-s3-async-client-with-top-level-alternate-credentials"); + final String input = "S3EncryptionClientTopLevelAlternateCredsTest"; + + // use alternate creds + AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + + S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(KMS_KEY_ID) + .build(); + + // using the original key fails + try { + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + fail("expected exception"); + } catch (KmsException exception) { + // expected + assertTrue(exception.getMessage().contains("is not authorized to perform")); + } finally { + s3Client.close(); + } + + // using the alternate key succeeds + S3AsyncClient s3ClientAltCreds = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(ALTERNATE_KMS_KEY) + .build(); + + s3ClientAltCreds.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + + ResponseBytes objectResponse = s3ClientAltCreds.getObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), AsyncResponseTransformer.toBytes()).join(); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, s3ClientAltCreds); + s3ClientAltCreds.close(); + } + + @Test + public void s3AsyncEncryptionClientMixedCredentials() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-mixed-credentials"); + final String input = "S3EncryptionClientTopLevelAlternateCredsTest"; + + // use alternate creds for KMS, + // default for S3 + AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + KmsClient kmsClient = KmsClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .build(); + KmsKeyring kmsKeyring = KmsKeyring.builder() + .kmsClient(kmsClient) + .wrappingKeyId(ALTERNATE_KMS_KEY) + .build(); + + S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .keyring(kmsKeyring) + .build(); + + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + + ResponseBytes objectResponse = s3Client.getObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), AsyncResponseTransformer.toBytes()).join(); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, s3Client); + s3Client.close(); + kmsClient.close(); + } + + @Test + public void asyncTopLevelConfigurationWrongRegion() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-wrong-region-credentials-async"); + + AwsCredentialsProvider creds = DefaultCredentialsProvider.create(); + + S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of("eu-west-1")) + .kmsKeyId(KMS_KEY_ID) + .build(); + + final String input = "SimpleTestOfV3EncryptionClientAsync"; + + try { + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + fail("expected exception"); + } catch (NotFoundException e) { + assertTrue(e.getMessage().contains("Invalid arn")); + } finally { + s3Client.close(); + } + } + + @Test + public void asyncTopLevelConfigurationNullCreds() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-null-credentials-async"); + + AwsCredentialsProvider creds = new S3EncryptionClientTestResources.NullCredentialsProvider(); + + S3AsyncClient s3Client = S3AsyncEncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(KMS_KEY_ID) + .build(); + + final String input = "SimpleTestOfV3EncryptionClientAsync"; + + try { + s3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .build(), + AsyncRequestBody.fromString(input)).join(); + fail("expected exception"); + } catch (NullPointerException npe) { + assertTrue(npe.getMessage().contains("Access key ID cannot be blank")); + } finally { + s3Client.close(); + } + } + @Test public void putAsyncGetDefault() { final String objectKey = appendTestSuffix("put-async-get-default"); diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java index 3e8fa78fe..e9f88d685 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 package software.amazon.encryption.s3; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; import com.amazonaws.services.s3.AmazonS3Encryption; @@ -43,6 +41,9 @@ 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; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; @@ -52,10 +53,6 @@ */ public class S3EncryptionClientCompatibilityTest { - private static final String BUCKET = System.getenv("AWS_S3EC_TEST_BUCKET"); - private static final String KMS_KEY_ID = System.getenv("AWS_S3EC_TEST_KMS_KEY_ID"); - private static final Region KMS_REGION = Region.getRegion(Regions.fromName(System.getenv("AWS_REGION"))); - private static SecretKey AES_KEY; private static KeyPair RSA_KEY_PAIR; diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientRangedGetCompatibilityTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientRangedGetCompatibilityTest.java index 5e23168bf..305dda8ee 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientRangedGetCompatibilityTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientRangedGetCompatibilityTest.java @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 package software.amazon.encryption.s3; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; import com.amazonaws.services.s3.AmazonS3Encryption; @@ -38,6 +36,7 @@ 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.KMS_KEY_ID; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; @@ -46,7 +45,6 @@ */ public class S3EncryptionClientRangedGetCompatibilityTest { - private static final Region KMS_REGION = Region.getRegion(Regions.fromName(System.getenv("AWS_REGION"))); private static SecretKey AES_KEY; private static KeyPair RSA_KEY_PAIR; diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java index 7b9843a9c..976c002fa 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java @@ -13,14 +13,18 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.KmsException; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.NoSuchBucketException; -import software.amazon.awssdk.services.s3.model.NoSuchKeyException; import software.amazon.awssdk.services.s3.model.NoSuchUploadException; import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -29,6 +33,7 @@ import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager; import software.amazon.encryption.s3.materials.KmsKeyring; import software.amazon.encryption.s3.utils.BoundedInputStream; +import software.amazon.encryption.s3.utils.S3EncryptionClientTestResources; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -48,15 +53,18 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.withSettings; import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.ALTERNATE_KMS_KEY; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ALIAS; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; @@ -391,8 +399,8 @@ public void s3EncryptionClientWithKeyringFromKmsKeyIdSucceeds() { KmsKeyring keyring = KmsKeyring.builder().wrappingKeyId(KMS_KEY_ID).build(); S3Client v3Client = S3EncryptionClient.builder() - .keyring(keyring) - .build(); + .keyring(keyring) + .build(); simpleV3RoundTrip(v3Client, objectKey); @@ -408,12 +416,12 @@ public void s3EncryptionClientWithCmmFromKmsKeyIdSucceeds() { KmsKeyring keyring = KmsKeyring.builder().wrappingKeyId(KMS_KEY_ID).build(); CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder() - .keyring(keyring) - .build(); + .keyring(keyring) + .build(); S3Client v3Client = S3EncryptionClient.builder() - .cryptoMaterialsManager(cmm) - .build(); + .cryptoMaterialsManager(cmm) + .build(); simpleV3RoundTrip(v3Client, objectKey); @@ -451,21 +459,21 @@ public void s3EncryptionClientWithWrappedS3ClientSucceeds() { @Test public void s3EncryptionClientWithWrappedS3EncryptionClientFails() { S3AsyncClient wrappedAsyncClient = S3AsyncEncryptionClient.builder() - .kmsKeyId(KMS_KEY_ID) - .build(); + .kmsKeyId(KMS_KEY_ID) + .build(); assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builder() - .wrappedAsyncClient(wrappedAsyncClient) - .kmsKeyId(KMS_KEY_ID) - .build()); + .wrappedAsyncClient(wrappedAsyncClient) + .kmsKeyId(KMS_KEY_ID) + .build()); } @Test public void s3EncryptionClientWithNullSecureRandomFails() { assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builder() - .aesKey(AES_KEY) - .secureRandom(null) - .build()); + .aesKey(AES_KEY) + .secureRandom(null) + .build()); } @Test @@ -475,8 +483,8 @@ public void s3EncryptionClientFromKMSKeyDoesNotUseUnprovidedSecureRandom() { final String objectKey = appendTestSuffix("no-secure-random-object-kms"); S3Client v3Client = S3EncryptionClient.builder() - .kmsKeyId(KMS_KEY_ID) - .build(); + .kmsKeyId(KMS_KEY_ID) + .build(); simpleV3RoundTrip(v3Client, objectKey); @@ -688,6 +696,182 @@ public void abortMultipartUploadFailure() { v3Client.close(); } + @Test + public void s3EncryptionClientWithCustomCredentials() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-custom-credentials"); + + // use the default creds, but through an explicit credentials provider + AwsCredentialsProvider creds = DefaultCredentialsProvider.create(); + + S3Client wrappedClient = S3Client + .builder() + .credentialsProvider(creds) + .build(); + S3AsyncClient wrappedAsyncClient = S3AsyncClient + .builder() + .credentialsProvider(creds) + .build(); + KmsClient kmsClient = KmsClient + .builder() + .credentialsProvider(creds) + .build(); + + KmsKeyring keyring = KmsKeyring + .builder() + .kmsClient(kmsClient) + .wrappingKeyId(KMS_KEY_ID) + .build(); + S3Client s3Client = S3EncryptionClient.builder() + .wrappedClient(wrappedClient) + .wrappedAsyncClient(wrappedAsyncClient) + .keyring(keyring) + .build(); + + simpleV3RoundTrip(s3Client, objectKey); + + // Cleanup + deleteObject(BUCKET, objectKey, s3Client); + wrappedClient.close(); + wrappedAsyncClient.close(); + s3Client.close(); + } + + @Test + public void s3EncryptionClientTopLevelCredentials() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-top-level-credentials"); + + // use the default creds, but through an explicit credentials provider + AwsCredentialsProvider creds = DefaultCredentialsProvider.create(); + + S3Client s3Client = S3EncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(KMS_KEY_ID) + .build(); + + simpleV3RoundTrip(s3Client, objectKey); + + // Cleanup + deleteObject(BUCKET, objectKey, s3Client); + s3Client.close(); + } + + @Test + public void s3EncryptionClientTopLevelCredentialsWrongRegion() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-top-level-credentials"); + + // use the default creds, but through an explicit credentials provider + AwsCredentialsProvider creds = DefaultCredentialsProvider.create(); + + S3Client s3Client = S3EncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of("eu-west-1")) + .kmsKeyId(KMS_KEY_ID) + .build(); + + try { + simpleV3RoundTrip(s3Client, objectKey); + fail("expected exception"); + } catch (S3EncryptionClientException exception) { + // expected + assertTrue(exception.getMessage().contains("Invalid arn")); + } finally { + // Cleanup + s3Client.close(); + } + } + + @Test + public void s3EncryptionClientTopLevelCredentialsNullCreds() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-null-credentials"); + + AwsCredentialsProvider creds = new S3EncryptionClientTestResources.NullCredentialsProvider(); + + S3Client s3Client = S3EncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(KMS_KEY_ID) + .build(); + + try { + simpleV3RoundTrip(s3Client, objectKey); + fail("expected exception"); + } catch (S3EncryptionClientException exception) { + // expected + assertTrue(exception.getMessage().contains("Access key ID cannot be blank")); + } finally { + // Cleanup + s3Client.close(); + } + } + + @Test + public void s3EncryptionClientTopLevelAlternateCredentials() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-top-level-credentials"); + + // use alternate creds + AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + + S3Client s3Client = S3EncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(KMS_KEY_ID) + .build(); + + // using the original key fails + try { + simpleV3RoundTrip(s3Client, objectKey); + fail("expected exception"); + } catch (S3EncryptionClientException exception) { + // expected + assertTrue(exception.getMessage().contains("is not authorized to perform")); + assertInstanceOf(KmsException.class, exception.getCause()); + } finally { + s3Client.close(); + } + + // using the alternate key succeeds + S3Client s3ClientAltCreds = S3EncryptionClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .kmsKeyId(ALTERNATE_KMS_KEY) + .build(); + + simpleV3RoundTrip(s3ClientAltCreds, objectKey); + + // Cleanup + deleteObject(BUCKET, objectKey, s3ClientAltCreds); + s3ClientAltCreds.close(); + } + + @Test + public void s3EncryptionClientMixedCredentials() { + final String objectKey = appendTestSuffix("wrapped-s3-client-with-mixed-credentials"); + + // use alternate creds for KMS, + // default for S3 + AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider(); + KmsClient kmsClient = KmsClient.builder() + .credentialsProvider(creds) + .region(Region.of(KMS_REGION.toString())) + .build(); + KmsKeyring kmsKeyring = KmsKeyring.builder() + .kmsClient(kmsClient) + .wrappingKeyId(ALTERNATE_KMS_KEY) + .build(); + + S3Client s3Client = S3EncryptionClient.builder() + .keyring(kmsKeyring) + .build(); + + simpleV3RoundTrip(s3Client, objectKey); + + // Cleanup + deleteObject(BUCKET, objectKey, s3Client); + s3Client.close(); + kmsClient.close(); + } + /** * A simple, reusable round-trip (encryption + decryption) using a given * S3Client. Useful for testing client configuration. @@ -710,5 +894,4 @@ private void simpleV3RoundTrip(final S3Client v3Client, final String objectKey) String output = objectResponse.asUtf8String(); assertEquals(input, output); } - } diff --git a/src/test/java/software/amazon/encryption/s3/examples/ClientConfigurationExampleTest.java b/src/test/java/software/amazon/encryption/s3/examples/ClientConfigurationExampleTest.java new file mode 100644 index 000000000..91077c643 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/examples/ClientConfigurationExampleTest.java @@ -0,0 +1,10 @@ +package software.amazon.encryption.s3.examples; + +import org.junit.jupiter.api.Test; + +public class ClientConfigurationExampleTest { + @Test + public void testClientConfigurationExamples() { + ClientConfigurationExample.main(new String[0]); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/utils/S3EncryptionClientTestResources.java b/src/test/java/software/amazon/encryption/s3/utils/S3EncryptionClientTestResources.java index 0aa498380..96099b4a9 100644 --- a/src/test/java/software/amazon/encryption/s3/utils/S3EncryptionClientTestResources.java +++ b/src/test/java/software/amazon/encryption/s3/utils/S3EncryptionClientTestResources.java @@ -2,11 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 package software.amazon.encryption.s3.utils; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.Credentials; import java.util.concurrent.CompletableFuture; @@ -19,6 +27,51 @@ public class S3EncryptionClientTestResources { public static final String KMS_KEY_ID = System.getenv("AWS_S3EC_TEST_KMS_KEY_ID"); // This alias must point to the same key as KMS_KEY_ID public static final String KMS_KEY_ALIAS = System.getenv("AWS_S3EC_TEST_KMS_KEY_ALIAS"); + // For now, these are the same. + public static final Region S3_REGION = Region.getRegion(Regions.fromName(System.getenv("AWS_REGION"))); + public static final Region KMS_REGION = Region.getRegion(Regions.fromName(System.getenv("AWS_REGION"))); + // Alternate role to test credential configuration and access denied behavior + public static final String ALTERNATE_ROLE_ARN = System.getenv("AWS_S3EC_TEST_ALT_ROLE_ARN"); + // Alternate KMS key, which only the alternate role has access to + public static final String ALTERNATE_KMS_KEY = System.getenv("AWS_S3EC_TEST_ALT_KMS_KEY_ARN"); + + + /** + * Creds provider for the "alternate" role which is useful for testing cred configuration + * and access denied behavior. + */ + public static class AlternateRoleCredentialsProvider implements AwsCredentialsProvider { + + StsClient stsClient_; + + public AlternateRoleCredentialsProvider() { + super(); + stsClient_ = StsClient.create(); + } + + @Override + public AwsCredentials resolveCredentials() { + String sessionName = "s3ec-test" + DateTimeFormat.forPattern("-yyMMdd-hhmmss").print(new DateTime()); + Credentials assumeRoleCreds = stsClient_.assumeRole(builder -> builder + .roleArn(ALTERNATE_ROLE_ARN).roleSessionName(sessionName).build()).credentials(); + return AwsSessionCredentials.create(assumeRoleCreds.accessKeyId(), + assumeRoleCreds.secretAccessKey(), + assumeRoleCreds.sessionToken()); + } + } + + public static class NullCredentialsProvider implements AwsCredentialsProvider { + + public NullCredentialsProvider() { + super(); + } + + @Override + public AwsCredentials resolveCredentials() { + return AwsBasicCredentials + .create(null, null); + } + } /** * For a given string, append a suffix to distinguish it from