Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Client side S3EncryptionClient #1033

Merged
merged 25 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions docs/src/main/asciidoc/s3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,8 @@ public class MyRsaProvider implements S3RsaProvider {
@Override
public KeyPair generateKeyPair() {
MatejNedic marked this conversation as resolved.
Show resolved Hide resolved
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
// 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;
Expand All @@ -199,9 +198,8 @@ public class MyAesProvider implements S3AesProvider {
@Override
public SecretKey generateSecretKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256);
return keyGenerator.generateKey();
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@

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 {
MatejNedic marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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;
Expand Down Expand Up @@ -120,28 +121,21 @@ else if (awsProperties.getEndpoint() != null) {
Optional.ofNullable(awsProperties.getDualstackEnabled()).ifPresent(builder::dualstackEnabled);
return builder.build();
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass(value = { "software.amazon.encryption.s3.S3EncryptionClient" })
S3Client s3Client(S3ClientBuilder s3ClientBuilder) {
return s3ClientBuilder.build();
}

@Conditional(S3EncryptionConditional.class)
@ConditionalOnClass(name = "software.amazon.encryption.s3.S3EncryptionClient")
@Configuration
@ConditionalOnClass(name = { "software.amazon.encryption.s3.S3EncryptionClient" })
static class S3EncryptionConfiguration {
public static class S3EncryptionConfiguration {

@Bean
@ConditionalOnMissingBean
S3Client s3EncryptionClient(S3EncryptionClient.Builder s3EncryptionBuilder, S3ClientBuilder s3ClientBuilder) {
public static S3Client s3EncryptionClient(S3EncryptionClient.Builder s3EncryptionBuilder, S3ClientBuilder s3ClientBuilder) {
MatejNedic marked this conversation as resolved.
Show resolved Hide resolved
s3EncryptionBuilder.wrappedClient(s3ClientBuilder.build());
return s3EncryptionBuilder.build();
}

@Bean
@ConditionalOnMissingBean
S3EncryptionClient.Builder s3EncrpytionClientBuilder(S3Properties properties,
public static S3EncryptionClient.Builder s3EncrpytionClientBuilder(S3Properties properties,
AwsClientBuilderConfigurer awsClientBuilderConfigurer,
ObjectProvider<AwsClientCustomizer<S3EncryptionClient.Builder>> configurer,
ObjectProvider<AwsConnectionDetails> connectionDetails,
Expand Down Expand Up @@ -186,6 +180,13 @@ private static void configureEncryptionProperties(S3Properties properties,
}
}

@Bean
@ConditionalOnMissingBean
S3Client s3Client(S3ClientBuilder s3ClientBuilder) {
return s3ClientBuilder.build();
}


@Configuration
@ConditionalOnClass(ObjectMapper.class)
static class Jackson2JsonS3ObjectConverterConfiguration {
Expand All @@ -204,5 +205,4 @@ S3OutputStreamProvider inMemoryBufferingS3StreamProvider(S3Client s3Client,
return new InMemoryBufferingS3OutputStreamProvider(s3Client,
contentTypeResolver.orElseGet(PropertiesS3ObjectContentTypeResolver::new));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@

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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ void propertyIsNotResolvedWhenIntegrationIsDisabled() {
"--spring.config.import=aws-parameterstore:/config/spring/",
"--spring.cloud.aws.parameterstore.enabled=false", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=eu-west-1")) {
assertThat(context.getEnvironment().getProperty("message")).isNull();
Expand Down Expand Up @@ -242,7 +241,6 @@ void serviceSpecificEndpointTakesPrecedenceOverGlobalAwsRegion() {
"--spring.config.import=aws-parameterstore:/config/spring/",
"--spring.cloud.aws.parameterstore.region=" + REGION,
"--spring.cloud.aws.endpoint=http://non-existing-host/",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.parameterstore.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=eu-west-1",
Expand All @@ -259,7 +257,6 @@ void parameterStoreClientUsesGlobalRegion() {
try (ConfigurableApplicationContext context = application.run(
"--spring.config.import=aws-parameterstore:/config/spring/",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=" + REGION,
"--logging.level.io.awspring.cloud.parameterstore=debug")) {
Expand Down Expand Up @@ -310,7 +307,6 @@ void reloadsPropertiesWhenPropertyValueChanges() {
try (ConfigurableApplicationContext context = application.run(
"--spring.config.import=aws-parameterstore:/config/spring/",
"--spring.cloud.aws.parameterstore.reload.strategy=refresh",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.parameterstore.reload.period=PT1S",
"--spring.cloud.aws.parameterstore.region=" + REGION,
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
Expand Down Expand Up @@ -338,7 +334,6 @@ void reloadsPropertiesWhenNewPropertyIsAdded() {
"--spring.config.import=aws-parameterstore:/config/spring/",
"--spring.cloud.aws.parameterstore.reload.strategy=refresh",
"--spring.cloud.aws.parameterstore.reload.period=PT1S",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.parameterstore.region=" + REGION,
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
Expand All @@ -364,7 +359,6 @@ void doesNotReloadPropertiesWhenReloadStrategyIsNotSet() {
try (ConfigurableApplicationContext context = application.run(
"--spring.config.import=aws-parameterstore:/config/spring/",
"--spring.cloud.aws.parameterstore.reload.period=PT1S",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.parameterstore.region=" + REGION,
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
Expand Down Expand Up @@ -392,7 +386,6 @@ void reloadsPropertiesWithRestartContextStrategy() {
"--spring.cloud.aws.parameterstore.reload.strategy=restart_context",
"--spring.cloud.aws.parameterstore.reload.period=PT1S",
"--spring.cloud.aws.parameterstore.region=" + REGION,
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--management.endpoint.restart.enabled=true", "--management.endpoints.web.exposure.include=restart",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
Expand All @@ -416,7 +409,6 @@ private ConfigurableApplicationContext runApplication(SpringApplication applicat
return application.run("--spring.config.import=" + springConfigImport,
"--spring.cloud.aws.parameterstore.region=" + REGION,
"--" + endpointProperty + "=" + localstack.getEndpoint(),
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=eu-west-1", "--logging.level.io.awspring.cloud.parameterstore=debug");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ void propertyIsNotResolvedWhenIntegrationIsDisabled() {
"--spring.config.import=aws-secretsmanager:/config/spring;/config/second",
"--spring.cloud.aws.secretsmanager.enabled=false", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=eu-west-1")) {
assertThat(context.getEnvironment().getProperty("message")).isNull();
Expand All @@ -313,7 +312,6 @@ void serviceSpecificEndpointTakesPrecedenceOverGlobalAwsRegion() {
"--spring.config.import=aws-secretsmanager:/config/spring;/config/second",
"--spring.cloud.aws.secretsmanager.region=" + REGION,
"--spring.cloud.aws.endpoint=http://non-existing-host/",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.secretsmanager.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=eu-west-1")) {
Expand All @@ -333,7 +331,6 @@ void secretsManagerClientUsesStsCredentials() throws IOException {
"--spring.config.import=optional:aws-secretsmanager:/config/spring;/config/second",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(), "--spring.cloud.aws.region.static=" + REGION,
"--spring.cloud.aws.credentials.sts.role-arn=develop",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.sts.enabled=true",
"--spring.cloud.aws.credentials.sts.web-identity-token-file=" + tempFile.getAbsolutePath())) {
assertThat(context.getBean(AwsCredentialsProvider.class))
Expand All @@ -349,7 +346,6 @@ void secretsManagerClientUsesGlobalRegion() {
try (ConfigurableApplicationContext context = application.run(
"--spring.config.import=aws-secretsmanager:/config/spring;/config/second",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=" + REGION)) {
assertThat(context.getEnvironment().getProperty("message")).isEqualTo("value from tests");
Expand All @@ -374,7 +370,6 @@ void reloadsProperties() {
"--spring.config.import=aws-secretsmanager:/config/spring;/config/second",
"--spring.cloud.aws.secretsmanager.region=" + REGION,
"--spring.cloud.aws.secretsmanager.reload.strategy=refresh",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.secretsmanager.reload.period=PT1S",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
Expand All @@ -401,7 +396,6 @@ void doesNotReloadPropertiesWhenMonitoringIsDisabled() {
try (ConfigurableApplicationContext context = application.run(
"--spring.config.import=aws-secretsmanager:/config/spring;/config/second",
"--spring.cloud.aws.secretsmanager.region=" + REGION,
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.secretsmanager.reload.period=PT1S",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
Expand Down Expand Up @@ -430,7 +424,6 @@ void reloadsPropertiesWithRestartContextStrategy() {
"--spring.cloud.aws.secretsmanager.region=" + REGION,
"--spring.cloud.aws.secretsmanager.reload.strategy=RESTART_CONTEXT",
"--spring.cloud.aws.secretsmanager.reload.period=PT1S",
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.secretsmanager.reload.max-wait-for-restart=PT1S",
"--management.endpoint.restart.enabled=true", "--management.endpoints.web.exposure.include=restart",
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
Expand Down Expand Up @@ -459,7 +452,6 @@ private ConfigurableApplicationContext runApplication(SpringApplication applicat
String endpointProperty) {
return application.run("--spring.config.import=" + springConfigImport,
"--spring.cloud.aws.secretsmanager.region=" + REGION,
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--" + endpointProperty + "=" + localstack.getEndpoint(),
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=eu-west-1", "--logging.level.io.awspring.cloud.secretsmanager=debug");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ void testCounter() {

try (ConfigurableApplicationContext context = application.run(
"--spring.cloud.aws.endpoint=" + localstack.getEndpoint(),
"--spring.cloud.aws.s3.encryption.keyId=234abcd-12ab-34cd-56ef-1234567890ab",
"--spring.cloud.aws.credentials.access-key=noop", "--spring.cloud.aws.credentials.secret-key=noop",
"--spring.cloud.aws.region.static=us-east-1", "--management.cloudwatch.metrics.export.step=5s",
"--management.cloudwatch.metrics.export.namespace=awspring/spring-cloud-aws",
Expand Down
4 changes: 2 additions & 2 deletions spring-cloud-aws-dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<spotless.version>2.31.0</spotless.version>
<awssdk-v2.version>2.28.2</awssdk-v2.version>
<amazon.dax.version>2.0.4</amazon.dax.version>
<amazon.encryption.s3>3.2.3</amazon.encryption.s3>
<amazon.encryption.s3.version>3.2.3</amazon.encryption.s3.version>
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
<spring-cloud-commons.version>4.1.4</spring-cloud-commons.version>
<jakarta.mail.version>2.1.0</jakarta.mail.version>
Expand Down Expand Up @@ -73,7 +73,7 @@
<dependency>
<groupId>software.amazon.encryption.s3</groupId>
<artifactId>amazon-s3-encryption-client-java</artifactId>
<version>${amazon.encryption.s3}</version>
<version>${amazon.encryption.s3.version}</version>
<optional>true</optional>
</dependency>

Expand Down