Skip to content

Commit

Permalink
Introduce Client side S3EncryptionClient (#1033)
Browse files Browse the repository at this point in the history
Introduce KMS with S3EncryptionClient
  • Loading branch information
MatejNedic authored Nov 6, 2024
1 parent 29edb24 commit dc67ca3
Show file tree
Hide file tree
Showing 18 changed files with 545 additions and 24 deletions.
8 changes: 4 additions & 4 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 82 additions & 0 deletions docs/src/main/asciidoc/s3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
----
<dependency>
<groupId>software.amazon.encryption.s3</groupId>
<artifactId>amazon-s3-encryption-client-java</artifactId>
</dependency>
----


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]
----
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>kms</artifactId>
<optional>true</optional>
</dependency>
----


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.
Expand Down
10 changes: 10 additions & 0 deletions spring-cloud-aws-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,15 @@
<artifactId>sts</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>kms</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>software.amazon.encryption.s3</groupId>
<artifactId>amazon-s3-encryption-client-java</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<AwsClientCustomizer<S3EncryptionClient.Builder>> configurer,
ObjectProvider<AwsConnectionDetails> connectionDetails,
ObjectProvider<S3EncryptionClientCustomizer> s3ClientCustomizers,
ObjectProvider<AwsSyncClientCustomizer> awsSyncClientCustomizers,
ObjectProvider<S3RsaProvider> rsaProvider, ObjectProvider<S3AesProvider> 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<S3RsaProvider> rsaProvider, ObjectProvider<S3AesProvider> 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) {
Expand All @@ -143,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 @@
/*
* 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<S3EncryptionClient.Builder> {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit dc67ca3

Please sign in to comment.