diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddfb..702dda0cc 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - - - - \ No newline at end of file + + + + diff --git a/docs/src/main/asciidoc/s3.adoc b/docs/src/main/asciidoc/s3.adoc index 055a33c1d..702873605 100644 --- a/docs/src/main/asciidoc/s3.adoc +++ b/docs/src/main/asciidoc/s3.adoc @@ -127,6 +127,88 @@ try (OutputStream outputStream = s3Resource.getOutputStream()) { } ---- +=== S3 Client Side Encryption + +AWS offers encryption library which is integrated inside of S3 Client called https://docs.aws.amazon.com/amazon-s3-encryption-client/latest/developerguide/what-is-s3-encryption-client.html [S3EncryptionClient]. +With encryption client you are going to encrypt your files before sending them to S3 bucket. + +To autoconfigure Encryption Client simply add the following dependency. + +[source,xml] +---- + + software.amazon.encryption.s3 + amazon-s3-encryption-client-java + +---- + + +We are supporting 3 types of encryption. + +1. To configure encryption via KMS key specify 'spring.cloud.aws.s3.encryption.keyId' with KMS key arn and this key will be used to encrypt your files. + +Also, following dependency is required. +[source,xml] +---- + + software.amazon.awssdk + kms + true + +---- + + +2. Asymmetric encryption is possible via RSA to enable it you will have to implement 'io.awspring.cloud.autoconfigure.s3.S3RsaProvider' + +!Note you will have to manage storing private and public keys yourself otherwise you won't be able to decrypt the data later. +Example of simple RSAProvider: + +[source,java,indent=0] +---- +import io.awspring.cloud.autoconfigure.s3.S3RsaProvider; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +public class MyRsaProvider implements S3RsaProvider { + @Override + public KeyPair generateKeyPair() { + try { + // fetch key pair from secure location such as Secrets Manager + // access to KeyPair is required to decrypt objects when fetching, so it is advised to keep them stored securely + } + catch (Exception e) { + return null; + } + } +} +---- + +3. Last option is if you want to use symmetric algorithm, this is possible via `io.awspring.cloud.autoconfigure.s3.S3AesProvider` + +!Note you will have to manage storing storing private key! +Example of simple AESProvider: + +[source,java,indent=0] +---- +import io.awspring.cloud.autoconfigure.s3.S3AesProvider; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +public class MyAesProvider implements S3AesProvider { + @Override + public SecretKey generateSecretKey() { + try { + // fetch secret key from secure location such as Secrets Manager + // access to secret key is required to decrypt objects when fetching, so it is advised to keep them stored securely + } + catch (Exception e) { + return null; + } + } +} +---- + + ==== S3 Output Stream Under the hood by default `S3Resource` uses a `io.awspring.cloud.s3.InMemoryBufferingS3OutputStream`. When data is written to the resource, is gets sent to S3 using multipart upload. diff --git a/spring-cloud-aws-autoconfigure/pom.xml b/spring-cloud-aws-autoconfigure/pom.xml index 7f17e16ee..cd6f485c7 100644 --- a/spring-cloud-aws-autoconfigure/pom.xml +++ b/spring-cloud-aws-autoconfigure/pom.xml @@ -166,5 +166,15 @@ sts true + + software.amazon.awssdk + kms + true + + + software.amazon.encryption.s3 + amazon-s3-encryption-client-java + true + diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AesProvider.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AesProvider.java new file mode 100644 index 000000000..44d01084e --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AesProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3; + +import javax.crypto.SecretKey; + +/** + * Interface for providing {@link SecretKey} when configuring {@link software.amazon.encryption.s3.S3EncryptionClient}. + * Required when encrypting files server side with AES. Secret Key should be stored in secure storage, for example AWS + * Secrets Manager. + * @author Matej Nedic + * @since 3.3.0 + */ +public interface S3AesProvider { + + /** + * Provides SecretKey that will be used to configure {@link software.amazon.encryption.s3.S3EncryptionClient}. + * Advised to fetch and return SecretKey in this method from Secured Storage. + * @return KeyPair that will be used for encryption/decryption. + */ + SecretKey generateSecretKey(); +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java index dee0e3234..e2a91f384 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java @@ -35,26 +35,28 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.*; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.providers.AwsRegionProvider; import software.amazon.awssdk.s3accessgrants.plugin.S3AccessGrantsPlugin; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3ClientBuilder; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.encryption.s3.S3EncryptionClient; /** * {@link EnableAutoConfiguration} for {@link S3Client} and {@link S3ProtocolResolver}. * * @author Maciej Walkowiak + * @author Matej Nedic */ @AutoConfiguration @ConditionalOnClass({ S3Client.class, S3OutputStreamProvider.class }) @@ -85,6 +87,7 @@ S3ClientBuilder s3ClientBuilder(AwsClientBuilderConfigurer awsClientBuilderConfi .enableFallback(properties.getPlugin().getEnableFallback()).build(); builder.addPlugin(s3AccessGrantsPlugin); } + Optional.ofNullable(this.properties.getCrossRegionEnabled()).ifPresent(builder::crossRegionAccessEnabled); builder.serviceConfiguration(this.properties.toS3Configuration()); @@ -119,6 +122,65 @@ else if (awsProperties.getEndpoint() != null) { return builder.build(); } + @Conditional(S3EncryptionConditional.class) + @ConditionalOnClass(name = "software.amazon.encryption.s3.S3EncryptionClient") + @Configuration + public static class S3EncryptionConfiguration { + + @Bean + @ConditionalOnMissingBean + S3Client s3EncryptionClient(S3EncryptionClient.Builder s3EncryptionBuilder, S3ClientBuilder s3ClientBuilder) { + s3EncryptionBuilder.wrappedClient(s3ClientBuilder.build()); + return s3EncryptionBuilder.build(); + } + + @Bean + @ConditionalOnMissingBean + S3EncryptionClient.Builder s3EncrpytionClientBuilder(S3Properties properties, + AwsClientBuilderConfigurer awsClientBuilderConfigurer, + ObjectProvider> configurer, + ObjectProvider connectionDetails, + ObjectProvider s3ClientCustomizers, + ObjectProvider awsSyncClientCustomizers, + ObjectProvider rsaProvider, ObjectProvider aesProvider) { + S3EncryptionClient.Builder builder = awsClientBuilderConfigurer.configureSyncClient( + S3EncryptionClient.builder(), properties, connectionDetails.getIfAvailable(), + configurer.getIfAvailable(), s3ClientCustomizers.orderedStream(), + awsSyncClientCustomizers.orderedStream()); + + Optional.ofNullable(properties.getCrossRegionEnabled()).ifPresent(builder::crossRegionAccessEnabled); + builder.serviceConfiguration(properties.toS3Configuration()); + + configureEncryptionProperties(properties, rsaProvider, aesProvider, builder); + return builder; + } + + private static void configureEncryptionProperties(S3Properties properties, + ObjectProvider rsaProvider, ObjectProvider aesProvider, + S3EncryptionClient.Builder builder) { + PropertyMapper propertyMapper = PropertyMapper.get(); + var encryptionProperties = properties.getEncryption(); + + propertyMapper.from(encryptionProperties::isEnableDelayedAuthenticationMode) + .to(builder::enableDelayedAuthenticationMode); + propertyMapper.from(encryptionProperties::isEnableLegacyUnauthenticatedModes) + .to(builder::enableLegacyUnauthenticatedModes); + propertyMapper.from(encryptionProperties::isEnableMultipartPutObject).to(builder::enableMultipartPutObject); + + if (!StringUtils.hasText(properties.getEncryption().getKeyId())) { + if (aesProvider.getIfAvailable() != null) { + builder.aesKey(aesProvider.getObject().generateSecretKey()); + } + else { + builder.rsaKeyPair(rsaProvider.getObject().generateKeyPair()); + } + } + else { + propertyMapper.from(encryptionProperties::getKeyId).to(builder::kmsKeyId); + } + } + } + @Bean @ConditionalOnMissingBean S3Client s3Client(S3ClientBuilder s3ClientBuilder) { @@ -143,5 +205,4 @@ S3OutputStreamProvider inMemoryBufferingS3StreamProvider(S3Client s3Client, return new InMemoryBufferingS3OutputStreamProvider(s3Client, contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new)); } - } diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3EncryptionClientCustomizer.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3EncryptionClientCustomizer.java new file mode 100644 index 000000000..0367badd0 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3EncryptionClientCustomizer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3; + +import io.awspring.cloud.autoconfigure.AwsClientCustomizer; +import software.amazon.encryption.s3.S3EncryptionClient; + +/** + * Callback interface that can be used to customize a {@link S3EncryptionClient.Builder}. + * + * @author Matej Nedic + * @since 3.3.0 + */ +@FunctionalInterface +public interface S3EncryptionClientCustomizer extends AwsClientCustomizer { +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3EncryptionConditional.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3EncryptionConditional.java new file mode 100644 index 000000000..f49d0e4ad --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3EncryptionConditional.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Conditional for creating {@link software.amazon.encryption.s3.S3EncryptionClient}. Will only create + * S3EncryptionClient if one of following is true. + * @author Matej Nedic + * @since 3.3.0 + */ +public class S3EncryptionConditional extends AnyNestedCondition { + public S3EncryptionConditional() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(S3RsaProvider.class) + static class RSAProviderCondition { + } + + @ConditionalOnBean(S3AesProvider.class) + static class AESProviderCondition { + } + + @ConditionalOnProperty(name = "spring.cloud.aws.s3.encryption.keyId") + static class KmsKeyProperty { + } +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3RsaProvider.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3RsaProvider.java new file mode 100644 index 000000000..cc3daa08a --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3RsaProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3; + +import java.security.KeyPair; + +/** + * Interface for providing {@link KeyPair} when configuring {@link software.amazon.encryption.s3.S3EncryptionClient}. + * Required for encrypting/decrypting files server side with RSA. Key pair should be stored in secure storage, for + * example AWS Secrets Manager. + * @author Matej Nedic + * @since 3.3.0 + */ +public interface S3RsaProvider { + + /** + * Provides KeyPair that will be used to configure {@link software.amazon.encryption.s3.S3EncryptionClient}. Advised + * to fetch and return KeyPair in this method from Secured Storage. + * @return KeyPair that will be used for encryption/decryption. + */ + KeyPair generateKeyPair(); +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3EncryptionProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3EncryptionProperties.java new file mode 100644 index 000000000..e4b910b79 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3EncryptionProperties.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3.properties; + +/** + * Properties to configure {@link software.amazon.encryption.s3.S3EncryptionClient} + * @author Matej Nedic + */ +public class S3EncryptionProperties { + private boolean enableLegacyUnauthenticatedModes = false; + private boolean enableDelayedAuthenticationMode = false; + private boolean enableMultipartPutObject = false; + private String keyId; + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public boolean isEnableLegacyUnauthenticatedModes() { + return enableLegacyUnauthenticatedModes; + } + + public void setEnableLegacyUnauthenticatedModes(boolean enableLegacyUnauthenticatedModes) { + this.enableLegacyUnauthenticatedModes = enableLegacyUnauthenticatedModes; + } + + public boolean isEnableDelayedAuthenticationMode() { + return enableDelayedAuthenticationMode; + } + + public void setEnableDelayedAuthenticationMode(boolean enableDelayedAuthenticationMode) { + this.enableDelayedAuthenticationMode = enableDelayedAuthenticationMode; + } + + public boolean isEnableMultipartPutObject() { + return enableMultipartPutObject; + } + + public void setEnableMultipartPutObject(boolean enableMultipartPutObject) { + this.enableMultipartPutObject = enableMultipartPutObject; + } + +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3PluginProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3PluginProperties.java index c636d89db..15c4afdaa 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3PluginProperties.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3PluginProperties.java @@ -1,3 +1,18 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.awspring.cloud.autoconfigure.s3.properties; public class S3PluginProperties { diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3Properties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3Properties.java index 307caeac9..3f528ca51 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3Properties.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3Properties.java @@ -23,11 +23,13 @@ import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.internal.crt.S3CrtAsyncClient; import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.encryption.s3.S3EncryptionClient; /** * Properties related to AWS S3. * * @author Maciej Walkowiak + * @author Matej Nedic */ @ConfigurationProperties(prefix = S3Properties.PREFIX) public class S3Properties extends AwsClientProperties { @@ -96,6 +98,20 @@ public class S3Properties extends AwsClientProperties { @NestedConfigurationProperty private S3PluginProperties plugin = new S3PluginProperties(); + /** + * Configuration properties for {@link S3EncryptionClient} integration + */ + @NestedConfigurationProperty + private S3EncryptionProperties encryption = new S3EncryptionProperties(); + + public S3EncryptionProperties getEncryption() { + return encryption; + } + + public void setEncryption(S3EncryptionProperties encryption) { + this.encryption = encryption; + } + @Nullable public Boolean getAccelerateModeEnabled() { return this.accelerateModeEnabled; diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java index edfb39392..c1a1ca055 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java @@ -26,6 +26,8 @@ import io.awspring.cloud.autoconfigure.core.CredentialsProviderAutoConfiguration; import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration; import io.awspring.cloud.autoconfigure.s3.properties.S3Properties; +import io.awspring.cloud.autoconfigure.s3.provider.MyAesProvider; +import io.awspring.cloud.autoconfigure.s3.provider.MyRsaProvider; import io.awspring.cloud.s3.InMemoryBufferingS3OutputStreamProvider; import io.awspring.cloud.s3.ObjectMetadata; import io.awspring.cloud.s3.S3ObjectConverter; @@ -38,7 +40,6 @@ import java.util.Objects; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.internal.util.MockUtil; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -58,15 +59,24 @@ import software.amazon.awssdk.services.s3.S3ClientBuilder; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.encryption.s3.S3EncryptionClient; /** * Tests for {@link S3AutoConfiguration}. * * @author Maciej Walkowiak + * @author Matej Nedic */ class S3AutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.cloud.aws.region.static:eu-west-1") + .withClassLoader(new FilteredClassLoader(S3EncryptionClient.class)) + .withConfiguration(AutoConfigurations.of(AwsAutoConfiguration.class, RegionProviderAutoConfiguration.class, + CredentialsProviderAutoConfiguration.class, S3AutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(S3EncryptionClient.class)); + + private final ApplicationContextRunner contextRunnerEncryption = new ApplicationContextRunner() .withPropertyValues("spring.cloud.aws.region.static:eu-west-1") .withConfiguration(AutoConfigurations.of(AwsAutoConfiguration.class, RegionProviderAutoConfiguration.class, CredentialsProviderAutoConfiguration.class, S3AutoConfiguration.class)); @@ -75,7 +85,7 @@ class S3AutoConfigurationTests { .withPropertyValues("spring.cloud.aws.region.static:eu-west-1") .withConfiguration(AutoConfigurations.of(AwsAutoConfiguration.class, RegionProviderAutoConfiguration.class, CredentialsProviderAutoConfiguration.class, S3AutoConfiguration.class)) - .withClassLoader(new FilteredClassLoader(S3AccessGrantsPlugin.class)); + .withClassLoader(new FilteredClassLoader(S3AccessGrantsPlugin.class, S3EncryptionClient.class)); @Test void setsS3AccessGrantIdentityProvider() { @@ -123,12 +133,40 @@ void autoconfigurationIsNotTriggeredWhenS3ModuleIsNotOnClasspath() { }); } - @Test - void s3ClientCanBeOverwritten() { - contextRunner.withUserConfiguration(CustomS3ClientConfiguration.class).run(context -> { - assertThat(context).hasSingleBean(S3Client.class); - assertThat(MockUtil.isMock(context.getBean(S3Client.class))).isTrue(); - }); + @Nested + class S3ClientTests { + @Test + void s3ClientCanBeOverwritten() { + contextRunnerEncryption + .withPropertyValues("spring.cloud.aws.s3.encryption.keyId:234abcd-12ab-34cd-56ef-1234567890ab") + .withUserConfiguration(CustomS3ClientConfiguration.class).run(context -> { + assertThat(context).hasSingleBean(S3Client.class); + }); + } + + @Test + void createsStandardClientWhenCrossRegionAndEncryptionModuleIsNotInClasspath() { + contextRunnerEncryption.withClassLoader(new FilteredClassLoader(S3EncryptionClient.class)).run(context -> { + assertThat(context).doesNotHaveBean(S3EncryptionClient.class); + assertThat(context).hasSingleBean(S3Client.class); + }); + } + + @Test + void createsEncryptionClientBackedByRsa() { + contextRunnerEncryption.withPropertyValues().withUserConfiguration(CustomRsaProvider.class).run(context -> { + assertThat(context).hasSingleBean(S3EncryptionClient.class); + assertThat(context).hasSingleBean(S3RsaProvider.class); + }); + } + + @Test + void createsEncryptionClientBackedByAes() { + contextRunnerEncryption.withPropertyValues().withUserConfiguration(CustomAesProvider.class).run(context -> { + assertThat(context).hasSingleBean(S3EncryptionClient.class); + assertThat(context).hasSingleBean(S3AesProvider.class); + }); + } } @Nested @@ -195,10 +233,11 @@ void withJacksonOnClasspathAutoconfiguresObjectConverter() { @Test void withoutJacksonOnClasspathDoesNotConfigureObjectConverter() { - contextRunner.withClassLoader(new FilteredClassLoader(ObjectMapper.class)).run(context -> { - assertThat(context).doesNotHaveBean(S3ObjectConverter.class); - assertThat(context).doesNotHaveBean(S3Template.class); - }); + contextRunner.withClassLoader(new FilteredClassLoader(ObjectMapper.class, S3EncryptionClient.class)) + .run(context -> { + assertThat(context).doesNotHaveBean(S3ObjectConverter.class); + assertThat(context).doesNotHaveBean(S3Template.class); + }); } @Test @@ -319,6 +358,22 @@ S3Client customS3Client() { } + @Configuration + static class CustomRsaProvider { + @Bean + S3RsaProvider rsaProvider() { + return new MyRsaProvider(); + } + } + + @Configuration + static class CustomAesProvider { + @Bean + S3AesProvider aesProvider() { + return new MyAesProvider(); + } + } + @Configuration(proxyBeanMethods = false) static class CustomAwsConfigurerClient { diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3ClientCustomizerTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3ClientCustomizerTests.java index 2679effd9..1b5208169 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3ClientCustomizerTests.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3ClientCustomizerTests.java @@ -25,12 +25,14 @@ import java.time.Duration; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.S3EncryptionClient; /** * Tests for {@link S3ClientCustomizer}. @@ -43,7 +45,8 @@ class S3ClientCustomizerTests { .withPropertyValues("spring.cloud.aws.region.static:eu-west-1", "spring.cloud.aws.credentials.access-key:noop", "spring.cloud.aws.credentials.secret-key:noop") .withConfiguration(AutoConfigurations.of(AwsAutoConfiguration.class, RegionProviderAutoConfiguration.class, - CredentialsProviderAutoConfiguration.class, S3AutoConfiguration.class)); + CredentialsProviderAutoConfiguration.class, S3AutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(S3EncryptionClient.class)); @Test void customClientCustomizer() { diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/provider/MyAesProvider.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/provider/MyAesProvider.java new file mode 100644 index 000000000..a4d407bab --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/provider/MyAesProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3.provider; + +import io.awspring.cloud.autoconfigure.s3.S3AesProvider; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +public class MyAesProvider implements S3AesProvider { + @Override + public SecretKey generateSecretKey() { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(256); + return keyGenerator.generateKey(); + } + catch (Exception e) { + return null; + } + } +} diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/provider/MyRsaProvider.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/provider/MyRsaProvider.java new file mode 100644 index 000000000..d9f5bdfcc --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/provider/MyRsaProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.autoconfigure.s3.provider; + +import io.awspring.cloud.autoconfigure.s3.S3RsaProvider; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +public class MyRsaProvider implements S3RsaProvider { + @Override + public KeyPair generateKeyPair() { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + catch (Exception e) { + return null; + } + } +} diff --git a/spring-cloud-aws-dependencies/pom.xml b/spring-cloud-aws-dependencies/pom.xml index 9d44607a8..c032e06e0 100644 --- a/spring-cloud-aws-dependencies/pom.xml +++ b/spring-cloud-aws-dependencies/pom.xml @@ -26,6 +26,7 @@ 2.31.0 2.29.6 2.0.5 + 3.2.3 1.6 4.2.0-M2 2.1.0 @@ -69,6 +70,13 @@ true + + software.amazon.encryption.s3 + amazon-s3-encryption-client-java + ${amazon.encryption.s3.version} + true + + software.amazon.s3.accessgrants aws-s3-accessgrants-java-plugin diff --git a/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/ObjectMetadataTests.java b/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/ObjectMetadataTests.java index f6213dc3d..568f88ed6 100644 --- a/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/ObjectMetadataTests.java +++ b/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/ObjectMetadataTests.java @@ -81,7 +81,7 @@ void mapsEnumsToString() { @Test void doesNotApplyContentLengthForPartUpload() { - long objectContentLength = 16L; + long objectContentLength = 16L; long partContentLength = 8L; ObjectMetadata metadata = ObjectMetadata.builder().contentLength(objectContentLength).build(); diff --git a/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java b/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java index 7f4b68e52..2821f7a36 100644 --- a/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java +++ b/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3ResourceIntegrationTests.java @@ -236,7 +236,7 @@ void contentLengthCanBeSetForLargeFiles(S3OutputStreamProvider s3OutputStreamPro outputStream.write(Files.toByteArray(file)); } GetObjectResponse result = client - .getObject(request -> request.bucket("first-bucket").key("new-file" + i + ".txt").build()).response(); + .getObject(request -> request.bucket("first-bucket").key("new-file" + i + ".txt").build()).response(); assertThat(result.contentType()).isEqualTo("text/plain"); }