Skip to content

Commit

Permalink
HADOOP-18708: Support S3 Client Side Encryption(CSE) With AWS SDK V2
Browse files Browse the repository at this point in the history
  • Loading branch information
shameersss1 committed Jul 13, 2024
1 parent 4f0ee9d commit 838dc24
Show file tree
Hide file tree
Showing 25 changed files with 1,191 additions and 84 deletions.
12 changes: 12 additions & 0 deletions hadoop-project/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
<surefire.fork.timeout>900</surefire.fork.timeout>
<aws-java-sdk.version>1.12.720</aws-java-sdk.version>
<aws-java-sdk-v2.version>2.25.53</aws-java-sdk-v2.version>
<amazon-s3-encryption-client-java.version>3.1.1</amazon-s3-encryption-client-java.version>
<aws.eventstream.version>1.0.1</aws.eventstream.version>
<hsqldb.version>2.7.1</hsqldb.version>
<frontend-maven-plugin.version>1.11.2</frontend-maven-plugin.version>
Expand Down Expand Up @@ -1154,6 +1155,17 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>software.amazon.encryption.s3</groupId>
<artifactId>amazon-s3-encryption-client-java</artifactId>
<version>${amazon-s3-encryption-client-java.version}</version>
<exclusions>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
Expand Down
15 changes: 15 additions & 0 deletions hadoop-tools/hadoop-aws/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,16 @@
<bannedImport>org.apache.hadoop.mapred.**</bannedImport>
</bannedImports>
</restrictImports>
<restrictImports>
<includeTestCode>false</includeTestCode>
<reason>Restrict encryption client imports to encryption client factory</reason>
<exclusions>
<exclusion>org.apache.hadoop.fs.s3a.EncryptionS3ClientFactory</exclusion>
</exclusions>
<bannedImports>
<bannedImport>software.amazon.encryption.s3.**</bannedImport>
</bannedImports>
</restrictImports>
</rules>
</configuration>
</execution>
Expand Down Expand Up @@ -511,6 +521,11 @@
<artifactId>bundle</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>software.amazon.encryption.s3</groupId>
<artifactId>amazon-s3-encryption-client-java</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,61 @@ private Constants() {
public static final String S3_ENCRYPTION_KEY =
"fs.s3a.encryption.key";

/**
* Client side encryption (CSE-CUSTOM) with custom cryptographic material manager class name.
*/
public static final String S3_ENCRYPTION_CSE_CUSTOM_KEYRING_CLASS_NAME =
"fs.s3a.encryption.cse.custom.keyring.class.name";

/**
* This config initializes unencrypted s3 client will be used to access unencrypted
* s3 object. This is to provide backward compatibility.
*/
public static final String S3_ENCRYPTION_CSE_READ_UNENCRYPTED_OBJECTS =
"fs.s3a.encryption.cse.read.unencrypted.objects";

/**
* Default value : {@value}.
*/
public static final boolean S3_ENCRYPTION_CSE_READ_UNENCRYPTED_OBJECTS_DEFAULT = false;

/**
* Config to calculate the size of unencrypted object size using ranged S3 calls.
* This is to provide backward compatability with objects encrypted with V1 client.
* Unlike V2 and V3 client which always pads 16 bytes, V1 client pads bytes till the
* object size reaches next multiple of 16.
* * This is to provide backward compatibility.
*/
public static final String S3_ENCRYPTION_CSE_OBJECT_SIZE_FROM_RANGED_GET_ENABLED =
"fs.s3a.encryption.cse.object.size.ranged.get.enabled";

/**
* Default value : {@value}.
*/
public static final boolean S3_ENCRYPTION_CSE_OBJECT_SIZE_FROM_RANGED_GET_ENABLED_DEFAULT = false;

/**
* Config to control whether to skip file named with suffix
* {@link #S3_ENCRYPTION_CSE_INSTRUCTION_FILE_SUFFIX}. Encryption V1 client supports storing
* encryption metadata in an instruction file which should be skipped while listing for the files.
* This is to provide backward compatibility.
*/
public static final String S3_ENCRYPTION_CSE_SKIP_INSTRUCTION_FILE =
"fs.s3a.encryption.cse.skip.instruction.file";

/**
* Default value : {@value}.
*/
public static final boolean S3_ENCRYPTION_CSE_SKIP_INSTRUCTION_FILE_DEFAULT = false;

/**
* Suffix of instruction file : {@value}.
*/
public static final String S3_ENCRYPTION_CSE_INSTRUCTION_FILE_SUFFIX = ".instruction";




/**
* List of custom Signers. The signer class will be loaded, and the signer
* name will be associated with this signer class in the S3 SDK.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.s3accessgrants.plugin.S3AccessGrantsPlugin;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3AsyncClientBuilder;
import software.amazon.awssdk.services.s3.S3BaseClientBuilder;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
Expand Down Expand Up @@ -153,11 +154,16 @@ public S3AsyncClient createS3AsyncClient(
.thresholdInBytes(parameters.getMultiPartThreshold())
.build();

return configureClientBuilder(S3AsyncClient.builder(), parameters, conf, bucket)
.httpClientBuilder(httpClientBuilder)
.multipartConfiguration(multipartConfiguration)
.multipartEnabled(parameters.isMultipartCopy())
.build();
S3AsyncClientBuilder s3AsyncClientBuilder =
configureClientBuilder(S3AsyncClient.builder(), parameters, conf, bucket)
.httpClientBuilder(httpClientBuilder);

if (!parameters.isClientSideEncryptionEnabled()) {
s3AsyncClientBuilder.multipartConfiguration(multipartConfiguration)
.multipartEnabled(parameters.isMultipartCopy());
}

return s3AsyncClientBuilder.build();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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 org.apache.hadoop.fs.s3a;

import java.io.IOException;
import java.net.URI;

import org.apache.hadoop.thirdparty.com.google.common.base.Strings;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.encryption.s3.S3AsyncEncryptionClient;
import software.amazon.encryption.s3.S3EncryptionClient;
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
import software.amazon.encryption.s3.materials.Keyring;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.s3a.impl.CSEMaterials;
import org.apache.hadoop.util.ReflectionUtils;

import static org.apache.hadoop.fs.s3a.impl.InstantiationIOException.unavailable;

public class EncryptionS3ClientFactory extends DefaultS3ClientFactory {

private static final String ENCRYPTION_CLIENT_CLASSNAME =
"software.amazon.encryption.s3.S3EncryptionClient";

/**
* Encryption client availability.
*/
private static final boolean ENCRYPTION_CLIENT_FOUND = checkForEncryptionClient();

/**
* S3Client to be wrapped by encryption client.
*/
private S3Client s3Client;

/**
* S3AsyncClient to be wrapped by encryption client.
*/
private S3AsyncClient s3AsyncClient;

private static boolean checkForEncryptionClient() {
try {
ClassLoader cl = EncryptionS3ClientFactory.class.getClassLoader();
cl.loadClass(ENCRYPTION_CLIENT_CLASSNAME);
LOG.debug("encryption client class {} found", ENCRYPTION_CLIENT_CLASSNAME);
return true;
} catch (Exception e) {
LOG.debug("encryption client class {} not found", ENCRYPTION_CLIENT_CLASSNAME, e);
return false;
}
}

/**
* Is the Encryption client available?
* @return true if it was found in the classloader
*/
private static synchronized boolean isEncryptionClientAvailable() {
return ENCRYPTION_CLIENT_FOUND;
}

/**
* Create encrypted s3 client.
* @param uri S3A file system URI
* @param parameters parameter object
* @return encrypted s3 client
* @throws IOException IO failures
*/
@Override
public S3Client createS3Client(URI uri, S3ClientCreationParameters parameters)
throws IOException {
if (!isEncryptionClientAvailable()) {
throw unavailable(uri, ENCRYPTION_CLIENT_CLASSNAME, null,
"No encryption client available");
}

s3Client = super.createS3Client(uri, parameters);
s3AsyncClient = super.createS3AsyncClient(uri, parameters);

return createS3EncryptionClient(parameters.getClientSideEncryptionMaterials());
}

/**
* Create async encrypted s3 client.
* @param uri S3A file system URI
* @param parameters parameter object
* @return async encrypted s3 client
* @throws IOException IO failures
*/
@Override
public S3AsyncClient createS3AsyncClient(URI uri, S3ClientCreationParameters parameters)
throws IOException {
if (!isEncryptionClientAvailable()) {
throw unavailable(uri, ENCRYPTION_CLIENT_CLASSNAME, null,
"No encryption client available");
}
return createS3AsyncEncryptionClient(parameters.getClientSideEncryptionMaterials());
}

private S3Client createS3EncryptionClient(final CSEMaterials cseMaterials) {
S3EncryptionClient.Builder s3EncryptionClientBuilder =
S3EncryptionClient.builder().wrappedAsyncClient(s3AsyncClient).wrappedClient(s3Client)
// this is required for doing S3 ranged GET calls
.enableLegacyUnauthenticatedModes(true)
// this is required for backward compatibility with older encryption clients
.enableLegacyWrappingAlgorithms(true);

switch (cseMaterials.getCseKeyType()) {
case KMS:
s3EncryptionClientBuilder.kmsKeyId(cseMaterials.getKmsKeyId());
break;
case CUSTOM:
Keyring keyring = getKeyringProvider(cseMaterials.getCustomKeyringClassName(),
cseMaterials.getConf());
CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder()
.keyring(keyring)
.build();
s3EncryptionClientBuilder.cryptoMaterialsManager(cmm);
break;
default:
break;
}

return s3EncryptionClientBuilder.build();
}

private S3AsyncClient createS3AsyncEncryptionClient(final CSEMaterials cseMaterials) {
S3AsyncEncryptionClient.Builder s3EncryptionAsyncClientBuilder =
S3AsyncEncryptionClient.builder().wrappedClient(s3AsyncClient)
// this is required for doing S3 ranged GET calls
.enableLegacyUnauthenticatedModes(true)
// this is required for backward compatibility with older encryption clients
.enableLegacyWrappingAlgorithms(true);

switch (cseMaterials.getCseKeyType()) {
case KMS:
s3EncryptionAsyncClientBuilder.kmsKeyId(cseMaterials.getKmsKeyId());
break;
case CUSTOM:
Keyring keyring = getKeyringProvider(cseMaterials.getCustomKeyringClassName(),
cseMaterials.getConf());
CryptographicMaterialsManager cmm = DefaultCryptoMaterialsManager.builder()
.keyring(keyring)
.build();
s3EncryptionAsyncClientBuilder.cryptoMaterialsManager(cmm);
break;
default:
break;
}

return s3EncryptionAsyncClientBuilder.build();
}

/**
* Get the custom Keyring class.
* @param className
* @param conf
* @return custom keyring class
*/
private Keyring getKeyringProvider(String className,
Configuration conf) {
try {
return ReflectionUtils.newInstance(getCustomKeyringProviderClass(className), conf);
} catch (Exception e) {
// this is for testing purpose to support CustomKeyring.java
return ReflectionUtils.newInstance(getCustomKeyringProviderClass(className), conf,
new Class[] {Configuration.class}, conf);
}
}

private Class<? extends Keyring> getCustomKeyringProviderClass(String className) {
if (Strings.isNullOrEmpty(className)) {
throw new IllegalArgumentException(
"Custom Keyring class name is null or empty");
}
try {
return Class.forName(className).asSubclass(Keyring.class);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
"Custom CryptographicMaterialsManager class " + className + "not found", e);
}
}
}
Loading

0 comments on commit 838dc24

Please sign in to comment.