/*
* Copyright 2013-2016 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.internal.crypto;
import static com.amazonaws.services.s3.AmazonS3EncryptionClient.USER_AGENT;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.createSymmetricCipher;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.encryptRequestUsingInstruction;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.generateInstruction;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.generateOneTimeUseSymmetricKey;
import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.getEncryptedSymmetricKey;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.s3.internal.S3Direct;
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.CryptoConfiguration;
import com.amazonaws.services.s3.model.CryptoStorageMode;
import com.amazonaws.services.s3.model.EncryptedInitiateMultipartUploadRequest;
import com.amazonaws.services.s3.model.EncryptionMaterials;
import com.amazonaws.services.s3.model.EncryptionMaterialsProvider;
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.MaterialsDescriptionProvider;
import com.amazonaws.services.s3.model.ObjectMetadata;
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.UploadPartRequest;
import com.amazonaws.services.s3.model.UploadPartResult;
import java.io.File;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* Encryption only (EO) cryptographic module for the S3 encryption client.
*/
class S3CryptoModuleEO extends S3CryptoModuleBase<EncryptedUploadContext> {
S3CryptoModuleEO(S3Direct s3,
AWSCredentialsProvider credentialsProvider,
EncryptionMaterialsProvider encryptionMaterialsProvider,
ClientConfiguration clientConfig, CryptoConfiguration cryptoConfig) {
super(s3, credentialsProvider, encryptionMaterialsProvider,
clientConfig, cryptoConfig,
new S3CryptoScheme(ContentCryptoScheme.AES_CBC));
}
/**
* Used for testing purposes only.
*/
S3CryptoModuleEO(S3Direct s3,
EncryptionMaterialsProvider encryptionMaterialsProvider,
CryptoConfiguration cryptoConfig) {
this(s3, new DefaultAWSCredentialsProviderChain(),
encryptionMaterialsProvider, new ClientConfiguration(),
cryptoConfig);
}
@Override
public PutObjectResult putObjectSecurely(PutObjectRequest putObjectRequest)
throws AmazonClientException, AmazonServiceException {
appendUserAgent(putObjectRequest, USER_AGENT);
if (this.cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) {
return putObjectUsingInstructionFile(putObjectRequest);
} else {
return putObjectUsingMetadata(putObjectRequest);
}
}
@Override
public S3Object getObjectSecurely(GetObjectRequest getObjectRequest)
throws AmazonClientException, AmazonServiceException {
// Should never get here, as S3 object encrypted in either EO or AE
// format should all be handled by the AE module.
throw new IllegalStateException();
}
@Override
public ObjectMetadata getObjectSecurely(GetObjectRequest getObjectRequest, File destinationFile)
throws AmazonClientException, AmazonServiceException {
// Should never get here, as S3 object encrypted in either EO or AE
// format should all be handled by the AE module.
throw new IllegalStateException();
}
@Override
public CompleteMultipartUploadResult completeMultipartUploadSecurely(
CompleteMultipartUploadRequest completeMultipartUploadRequest)
throws AmazonClientException, AmazonServiceException {
appendUserAgent(completeMultipartUploadRequest, USER_AGENT);
String uploadId = completeMultipartUploadRequest.getUploadId();
EncryptedUploadContext encryptedUploadContext = multipartUploadContexts.get(uploadId);
if (encryptedUploadContext.hasFinalPartBeenSeen() == false) {
throw new AmazonClientException(
"Unable to complete an encrypted multipart upload without being told which part was the last. "
+
"Without knowing which part was the last, the encrypted data in Amazon S3 is incomplete and corrupt.");
}
CompleteMultipartUploadResult result = s3
.completeMultipartUpload(completeMultipartUploadRequest);
// In InstructionFile mode, we want to write the instruction file only
// after the whole upload has completed correctly.
if (cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) {
Cipher symmetricCipher = createSymmetricCipher(
encryptedUploadContext.getEnvelopeEncryptionKey(),
Cipher.ENCRYPT_MODE, cryptoConfig.getCryptoProvider(),
encryptedUploadContext.getFirstInitializationVector());
EncryptionMaterials encryptionMaterials;
if (encryptedUploadContext.getMaterialsDescription() != null) {
encryptionMaterials = kekMaterialsProvider
.getEncryptionMaterials(encryptedUploadContext.getMaterialsDescription());
} else {
encryptionMaterials = kekMaterialsProvider.getEncryptionMaterials();
}
// Encrypt the envelope symmetric key
byte[] encryptedEnvelopeSymmetricKey = getEncryptedSymmetricKey(
encryptedUploadContext.getEnvelopeEncryptionKey(), encryptionMaterials,
cryptoConfig.getCryptoProvider());
EncryptionInstruction instruction = new EncryptionInstruction(
encryptionMaterials.getMaterialsDescription(), encryptedEnvelopeSymmetricKey,
encryptedUploadContext.getEnvelopeEncryptionKey(), symmetricCipher);
// Put the instruction file into S3
s3.putObject(EncryptionUtils.createInstructionPutRequest(
encryptedUploadContext.getBucketName(), encryptedUploadContext.getKey(),
instruction));
}
multipartUploadContexts.remove(uploadId);
return result;
}
@Override
public InitiateMultipartUploadResult initiateMultipartUploadSecurely(
InitiateMultipartUploadRequest initiateMultipartUploadRequest)
throws AmazonClientException, AmazonServiceException {
appendUserAgent(initiateMultipartUploadRequest, USER_AGENT);
// Generate a one-time use symmetric key and initialize a cipher to
// encrypt object data
SecretKey envelopeSymmetricKey = generateOneTimeUseSymmetricKey();
Cipher symmetricCipher = createSymmetricCipher(envelopeSymmetricKey, Cipher.ENCRYPT_MODE,
cryptoConfig.getCryptoProvider(), null);
if (cryptoConfig.getStorageMode() == CryptoStorageMode.ObjectMetadata) {
EncryptionMaterials encryptionMaterials = null;
if (initiateMultipartUploadRequest instanceof EncryptedInitiateMultipartUploadRequest) {
encryptionMaterials = kekMaterialsProvider
.getEncryptionMaterials(((EncryptedInitiateMultipartUploadRequest) initiateMultipartUploadRequest)
.getMaterialsDescription());
} else {
encryptionMaterials = kekMaterialsProvider.getEncryptionMaterials();
}
// Encrypt the envelope symmetric key
byte[] encryptedEnvelopeSymmetricKey = getEncryptedSymmetricKey(envelopeSymmetricKey,
encryptionMaterials, cryptoConfig.getCryptoProvider());
// Store encryption info in metadata
ObjectMetadata metadata = EncryptionUtils.updateMetadataWithEncryptionInfo(
initiateMultipartUploadRequest, encryptedEnvelopeSymmetricKey, symmetricCipher,
encryptionMaterials.getMaterialsDescription());
// Update the request's metadata to the updated metadata
initiateMultipartUploadRequest.setObjectMetadata(metadata);
}
InitiateMultipartUploadResult result = s3
.initiateMultipartUpload(initiateMultipartUploadRequest);
EncryptedUploadContext encryptedUploadContext = new EncryptedUploadContext(
initiateMultipartUploadRequest.getBucketName(),
initiateMultipartUploadRequest.getKey(), envelopeSymmetricKey);
encryptedUploadContext.setNextInitializationVector(symmetricCipher.getIV());
encryptedUploadContext.setFirstInitializationVector(symmetricCipher.getIV());
if (initiateMultipartUploadRequest instanceof EncryptedInitiateMultipartUploadRequest) {
encryptedUploadContext
.setMaterialsDescription(((EncryptedInitiateMultipartUploadRequest) initiateMultipartUploadRequest)
.getMaterialsDescription());
}
multipartUploadContexts.put(result.getUploadId(), encryptedUploadContext);
return result;
}
/**
* {@inheritDoc}
* <p>
* <b>NOTE:</b> 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 uploadPartSecurely(UploadPartRequest uploadPartRequest)
throws AmazonClientException, AmazonServiceException {
appendUserAgent(uploadPartRequest, USER_AGENT);
boolean isLastPart = uploadPartRequest.isLastPart();
String uploadId = uploadPartRequest.getUploadId();
boolean partSizeMultipleOfCipherBlockSize = uploadPartRequest.getPartSize()
% JceEncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE == 0;
if (!isLastPart && !partSizeMultipleOfCipherBlockSize) {
throw new AmazonClientException(
"Invalid part size: part sizes for encrypted multipart uploads must be multiples "
+
"of the cipher block size ("
+ JceEncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE
+ ") with the exception of the last part. "
+
"Otherwise encryption adds extra padding that will corrupt the final object.");
}
// Generate the envelope symmetric key and initialize a cipher to
// encrypt the object's data
EncryptedUploadContext encryptedUploadContext = multipartUploadContexts.get(uploadId);
if (encryptedUploadContext == null)
throw new AmazonClientException("No client-side information available on upload ID "
+ uploadId);
SecretKey envelopeSymmetricKey = encryptedUploadContext.getEnvelopeEncryptionKey();
byte[] iv = encryptedUploadContext.getNextInitializationVector();
CipherFactory cipherFactory = new CipherFactory(envelopeSymmetricKey, Cipher.ENCRYPT_MODE,
iv, this.cryptoConfig.getCryptoProvider());
// Create encrypted input stream
ByteRangeCapturingInputStream encryptedInputStream = EncryptionUtils
.getEncryptedInputStream(uploadPartRequest, cipherFactory);
uploadPartRequest.setInputStream(encryptedInputStream);
// The last part of the multipart upload will contain extra padding from
// the encryption process
if (uploadPartRequest.isLastPart()) {
// We only change the size of the last part
long cryptoContentLength = EncryptionUtils.calculateCryptoContentLength(
cipherFactory.createCipher(), uploadPartRequest);
if (cryptoContentLength > 0)
uploadPartRequest.setPartSize(cryptoContentLength);
if (encryptedUploadContext.hasFinalPartBeenSeen()) {
throw new AmazonClientException(
"This part was specified as the last part in a multipart upload, but a previous part was already marked as the last part. "
+
"Only the last part of the upload should be marked as the last part, otherwise it will cause the encrypted data to be corrupted.");
}
encryptedUploadContext.setHasFinalPartBeenSeen(true);
}
// Treat all encryption requests as input stream upload requests, not as
// file upload requests.
uploadPartRequest.setFile(null);
uploadPartRequest.setFileOffset(0);
UploadPartResult result = s3.uploadPart(uploadPartRequest);
encryptedUploadContext.setNextInitializationVector(encryptedInputStream.getBlock());
return result;
}
@Override
public CopyPartResult copyPartSecurely(CopyPartRequest copyPartRequest) {
String uploadId = copyPartRequest.getUploadId();
EncryptedUploadContext encryptedUploadContext = multipartUploadContexts.get(uploadId);
if (!encryptedUploadContext.hasFinalPartBeenSeen()) {
encryptedUploadContext.setHasFinalPartBeenSeen(true);
}
return s3.copyPart(copyPartRequest);
}
/*
* Private helper methods
*/
/**
* Puts an encrypted object into S3 and stores encryption info in the object
* metadata.
*
* @param putObjectRequest The request object containing all the parameters
* to upload a new object to Amazon S3.
* @return A {@link PutObjectResult} object containing the information
* returned by Amazon S3 for the new, created object.
* @throws AmazonClientException If any errors are encountered on the client
* while making the request or handling the response.
* @throws AmazonServiceException If any errors occurred in Amazon S3 while
* processing the request.
*/
private PutObjectResult putObjectUsingMetadata(PutObjectRequest putObjectRequest)
throws AmazonClientException, AmazonServiceException {
// Create instruction
EncryptionInstruction instruction = encryptionInstructionOf(putObjectRequest);
// Encrypt the object data with the instruction
PutObjectRequest encryptedObjectRequest = encryptRequestUsingInstruction(putObjectRequest,
instruction);
// Update the metadata
EncryptionUtils.updateMetadataWithEncryptionInstruction(putObjectRequest, instruction);
// Put the encrypted object into S3
return s3.putObject(encryptedObjectRequest);
}
/**
* Puts an encrypted object into S3, and puts an instruction file into S3.
* Encryption info is stored in the instruction file.
*
* @param putObjectRequest The request object containing all the parameters
* to upload a new object to Amazon S3.
* @return A {@link PutObjectResult} object containing the information
* returned by Amazon S3 for the new, created object.
* @throws AmazonClientException If any errors are encountered on the client
* while making the request or handling the response.
* @throws AmazonServiceException If any errors occurred in Amazon S3 while
* processing the request.
*/
private PutObjectResult putObjectUsingInstructionFile(PutObjectRequest putObjectRequest)
throws AmazonClientException, AmazonServiceException {
// Create instruction
EncryptionInstruction instruction = encryptionInstructionOf(putObjectRequest);
// Encrypt the object data with the instruction
PutObjectRequest encryptedObjectRequest = encryptRequestUsingInstruction(putObjectRequest,
instruction);
// Put the encrypted object into S3
PutObjectResult encryptedObjectResult = s3.putObject(encryptedObjectRequest);
// Put the instruction file into S3
PutObjectRequest instructionRequest = EncryptionUtils.createInstructionPutRequest(
putObjectRequest, instruction);
s3.putObject(instructionRequest);
// Return the result of the encrypted object PUT.
return encryptedObjectResult;
}
private EncryptionInstruction encryptionInstructionOf(
AmazonWebServiceRequest req) {
EncryptionInstruction instruction;
if (req instanceof MaterialsDescriptionProvider) {
MaterialsDescriptionProvider p = (MaterialsDescriptionProvider) req;
instruction = generateInstruction(this.kekMaterialsProvider,
p.getMaterialsDescription(),
this.cryptoConfig.getCryptoProvider());
} else {
instruction = generateInstruction(this.kekMaterialsProvider,
this.cryptoConfig.getCryptoProvider());
}
return instruction;
}
@Override
protected final long ciphertextLength(long plaintextLength) {
long cipherBlockSize = contentCryptoScheme.getBlockSizeInBytes();
long offset = cipherBlockSize - (plaintextLength % cipherBlockSize);
return plaintextLength + offset;
}
}