/* * Copyright 2013-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.s3; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.services.s3.model.CryptoRangeGetMode; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; import com.amazonaws.SdkClientException; import com.amazonaws.annotation.SdkInternalApi; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.metrics.RequestMetricCollector; import com.amazonaws.regions.Region; import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; import com.amazonaws.services.s3.internal.MultiFileOutputStream; import com.amazonaws.services.s3.internal.S3Direct; import com.amazonaws.services.s3.internal.crypto.v2.S3CryptoModule; import com.amazonaws.services.s3.internal.crypto.v2.S3CryptoModuleAE; import com.amazonaws.services.s3.internal.crypto.v2.S3CryptoModuleAEStrict; import com.amazonaws.services.s3.model.AbortMultipartUploadRequest; import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; import com.amazonaws.services.s3.model.CopyPartRequest; import com.amazonaws.services.s3.model.CopyPartResult; import com.amazonaws.services.s3.model.CryptoConfigurationV2; import com.amazonaws.services.s3.model.CryptoMode; import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.EncryptedInitiateMultipartUploadRequest; import com.amazonaws.services.s3.model.EncryptedPutObjectRequest; import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.GetObjectMetadataRequest; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; import com.amazonaws.services.s3.model.InstructionFileId; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.PutInstructionFileRequest; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectId; import com.amazonaws.services.s3.model.UploadObjectRequest; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; import com.amazonaws.util.VersionInfoUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Used to perform client-side encryption for storing data securely in S3. Data * encryption is done using a one-time randomly generated content encryption * key (CEK) per S3 object. *
* The encryption materials specified in the constructor will be used to * protect the CEK which is then stored along side with the S3 object. *
* For some code examples, see: *
* https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-crypto.html
*
* https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-crypto-kms.html
*
* https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-crypto-masterkey.html
*/
public class AmazonS3EncryptionClientV2 extends AmazonS3Client implements AmazonS3EncryptionV2 {
private static final String USER_AGENT_V2 = "S3CryptoV2/" + VersionInfoUtils.getVersion();
private static final Log log = LogFactory.getLog(AmazonS3EncryptionClientV2.class);
private final S3CryptoModule> crypto;
private final AWSKMS kmsClient;
/**
* True if the a default KMS client is constructed, which will be shut down
* when this instance of S3 encryption client is shutdown. False otherwise,
* which means the users who provided the KMS client would be responsible
* to shut down the KMS client.
*/
private final boolean isKMSClientInternal;
public static AmazonS3EncryptionClientV2Builder encryptionBuilder() {
return AmazonS3EncryptionClientV2Builder.standard();
}
@SdkInternalApi
AmazonS3EncryptionClientV2(AmazonS3EncryptionClientV2Params params) {
super(params);
validateParameters(params);
CryptoConfigurationV2 readOnlyCryptoConfig = validateConfigAndCreateReadOnlyCopy(
params.getCryptoConfiguration());
this.isKMSClientInternal = params.getKmsClient() == null;
this.kmsClient = isKMSClientInternal ?
newAWSKMSClient(params.getClientParams().getCredentialsProvider(),
params.getClientParams().getClientConfiguration(),
readOnlyCryptoConfig,
params.getClientParams().getRequestMetricCollector()) :
params.getKmsClient();
this.crypto = createCryptoModule(readOnlyCryptoConfig, this.kmsClient,
params.getEncryptionMaterialsProvider(), params.getClientParams().getCredentialsProvider());
warnOnLegacyCryptoMode(params.getCryptoConfiguration().getCryptoMode());
warnOnRangeGetsEnabled(params);
}
private void validateParameters(AmazonS3EncryptionClientV2Params params) {
assertParameterNotNull(params.getEncryptionMaterialsProvider(),
"EncryptionMaterialsProvider parameter must not be null.");
assertParameterNotNull(params.getCryptoConfiguration(),
"CryptoConfiguration parameter must not be null.");
}
private S3CryptoModule> createCryptoModule(CryptoConfigurationV2 cryptoConfig,
AWSKMS kmsClient,
EncryptionMaterialsProvider encryptionMaterialsProvider,
AWSCredentialsProvider credentialsProvider) {
if (cryptoConfig.getCryptoMode() == CryptoMode.AuthenticatedEncryption) {
return new S3CryptoModuleAE(
kmsClient,
new S3DirectImpl(),
credentialsProvider,
encryptionMaterialsProvider,
cryptoConfig);
} else if (cryptoConfig.getCryptoMode() == CryptoMode.StrictAuthenticatedEncryption) {
return new S3CryptoModuleAEStrict(
kmsClient,
new S3DirectImpl(),
credentialsProvider,
encryptionMaterialsProvider,
cryptoConfig);
} else {
throw new UnsupportedOperationException("Cannot encrypt using mode " + cryptoConfig.getCryptoMode());
}
}
private CryptoConfigurationV2 validateConfigAndCreateReadOnlyCopy(CryptoConfigurationV2 cryptoConfig) {
CryptoConfigurationV2 clonedCryptoConfig = cryptoConfig.clone();
if (clonedCryptoConfig.getCryptoMode() == null) {
clonedCryptoConfig.setCryptoMode(CryptoMode.StrictAuthenticatedEncryption);
}
if (CryptoMode.AuthenticatedEncryption != clonedCryptoConfig.getCryptoMode() &&
CryptoMode.StrictAuthenticatedEncryption != clonedCryptoConfig.getCryptoMode()) {
throw new IllegalArgumentException("Invalid value for CryptoMode : " + clonedCryptoConfig.getCryptoMode());
}
if (cryptoConfig.isUnsafeUndecryptableObjectPassthrough() && CryptoMode.StrictAuthenticatedEncryption == cryptoConfig.getCryptoMode()) {
throw new IllegalArgumentException(String.format("unsafeUndecryptableObjectPassthrough must not be enabled in %s "
+ "mode", CryptoMode.StrictAuthenticatedEncryption));
}
return clonedCryptoConfig.readOnly();
}
/**
* Creates and returns a new instance of Amazon Web Services KMS client in the case when
* an explicit Amazon Web Services KMS client is not specified.
*/
private AWSKMS newAWSKMSClient(
AWSCredentialsProvider credentialsProvider,
ClientConfiguration clientConfig,
CryptoConfigurationV2 cryptoConfig,
RequestMetricCollector requestMetricCollector) {
AWSKMSClientBuilder kmsClientBuilder = AWSKMSClientBuilder.standard()
.withCredentials(credentialsProvider)
.withClientConfiguration(clientConfig)
.withMetricsCollector(requestMetricCollector);
final Region kmsRegion = cryptoConfig.getAwsKmsRegion();
if (kmsRegion != null) {
kmsClientBuilder.withRegion(kmsRegion.getName());
}
return kmsClientBuilder.build();
}
private void assertParameterNotNull(Object parameterValue, String errorMessage) {
if (parameterValue == null) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
* Returns the kmsClient that was supplied to this encryption client, or
* null if it wasn't set.
*
* @return an Amazon Web Services KMS client
*/
public AWSKMS getKmsClient() {
return isKMSClientInternal? null : kmsClient;
}
public EncryptionMaterialsProvider getEncryptionMaterialsProvider() {
return crypto.getEncryptionMaterialsProvider();
}
public CryptoConfigurationV2 getCryptoConfiguration() {
return crypto.getCryptoConfiguration();
}
/**
* {@inheritDoc}
*
* Use {@link EncryptedPutObjectRequest} to specify materialsDescription for the EncryptionMaterials to be used for * this request.AmazonS3EncryptionClient would use * {@link EncryptionMaterialsProvider#getEncryptionMaterials(java.util.Map)} to * retrieve encryption materials corresponding to the materialsDescription specified in the current request. *
* */ @Override public PutObjectResult putObject(PutObjectRequest req) { return crypto.putObjectSecurely(req.clone()); } @Override public S3Object getObject(GetObjectRequest req) { return crypto.getObjectSecurely(req); } @Override public ObjectMetadata getObject(GetObjectRequest req, File dest) { return crypto.getObjectSecurely(req, dest); } @Override public void deleteObject(DeleteObjectRequest req) { req.getRequestClientOptions().appendUserAgent(USER_AGENT_V2); super.deleteObject(req); // If it exists, delete the instruction file. InstructionFileId ifid = new S3ObjectId(req.getBucketName(), req.getKey()).instructionFileId(); DeleteObjectRequest instructionDeleteRequest = (DeleteObjectRequest) req.clone(); instructionDeleteRequest.withBucketName(ifid.getBucket()).withKey(ifid.getKey()); super.deleteObject(instructionDeleteRequest); } @Override public CompleteMultipartUploadResult completeMultipartUpload(CompleteMultipartUploadRequest req) { return crypto.completeMultipartUploadSecurely(req); } /** * {@inheritDoc} ** Use {@link EncryptedInitiateMultipartUploadRequest} to specify materialsDescription for the * EncryptionMaterials to be used for this request. AmazonS3EncryptionClient would use * {@link EncryptionMaterialsProvider#getEncryptionMaterials(java.util.Map)} to retrieve encryption materials * corresponding to the materialsDescription specified in the current request. *
*/ @Override public InitiateMultipartUploadResult initiateMultipartUpload(InitiateMultipartUploadRequest req) { boolean isCreateEncryptionMaterial = true; if (req instanceof EncryptedInitiateMultipartUploadRequest) { EncryptedInitiateMultipartUploadRequest cryptoReq = (EncryptedInitiateMultipartUploadRequest) req; isCreateEncryptionMaterial = cryptoReq.isCreateEncryptionMaterial(); } return isCreateEncryptionMaterial ? crypto.initiateMultipartUploadSecurely(req) : super.initiateMultipartUpload(req) ; } /** * {@inheritDoc} * *
* NOTE: Because the encryption process requires context from block
* N-1 in order to encrypt block N, parts uploaded with the
* AmazonS3EncryptionClient (as opposed to the normal AmazonS3Client) must
* be uploaded serially, and in order. Otherwise, the previous encryption
* context isn't available to use when encrypting the current part.
*/
@Override
public UploadPartResult uploadPart(UploadPartRequest uploadPartRequest)
throws SdkClientException, AmazonServiceException {
return crypto.uploadPartSecurely(uploadPartRequest);
}
@Override
public CopyPartResult copyPart(CopyPartRequest copyPartRequest) {
return crypto.copyPartSecurely(copyPartRequest);
}
@Override
public void abortMultipartUpload(AbortMultipartUploadRequest req) {
crypto.abortMultipartUploadSecurely(req);
}
@Override
public PutObjectResult putInstructionFile(PutInstructionFileRequest req) {
return crypto.putInstructionFileSecurely(req);
}
@Override
public CompleteMultipartUploadResult uploadObject(final UploadObjectRequest req)
throws IOException, InterruptedException, ExecutionException {
// Set up the pipeline for concurrent encrypt and upload
// Set up a thread pool for this pipeline
ExecutorService es = req.getExecutorService();
final boolean defaultExecutorService = es == null;
if (es == null) {
es = Executors.newFixedThreadPool(clientConfiguration.getMaxConnections());
}
UploadObjectObserver observer = req.getUploadObjectObserver();
if (observer == null) {
observer = new UploadObjectObserver();
}
observer.init(req, new S3DirectImpl(), this, es);
final String uploadId = observer.onUploadInitiation(req);
final List
* If the a default internal KMS client has been constructed, it will also be
* shut down by calling this method.
* Otherwise, users who provided the KMS client would be responsible to
* shut down the KMS client extrinsic to this method.
*/
@Override
public void shutdown() {
super.shutdown();
if (isKMSClientInternal) {
kmsClient.shutdown();
}
}
/**
* Convenient method to notifies the observer to abort the multi-part
* upload, and returns the original exception.
*/
private