using System;
using System.Collections.Generic;
using System.Linq;
using Amazon.Runtime;
using Amazon.S3.Model;
using Amazon.Runtime.Internal;
using Amazon.S3;
using GetObjectResponse = Amazon.S3.Model.GetObjectResponse;
namespace Amazon.Extensions.S3.Encryption.Internal
{
///
/// Custom the pipeline handler to decrypt objects.
///
public abstract class SetupDecryptionHandler : PipelineHandler
{
///
/// Construct instance of SetupDecryptionHandler.
///
///
public SetupDecryptionHandler(AmazonS3EncryptionClientBase encryptionClient)
{
this.EncryptionClient = encryptionClient;
}
///
/// Gets the EncryptionClient property which is the AmazonS3EncryptionClientBase that is decrypting the object.
///
public AmazonS3EncryptionClientBase EncryptionClient
{
get;
private set;
}
///
/// Calls the post invoke logic after calling the next handler
/// in the pipeline.
///
/// The execution context which contains both the
/// requests and response context.
public override void InvokeSync(IExecutionContext executionContext)
{
base.InvokeSync(executionContext);
PostInvoke(executionContext);
}
///
/// Decrypt the object being downloaded.
///
///
protected void PostInvoke(IExecutionContext executionContext)
{
byte[] encryptedKMSEnvelopeKey;
Dictionary encryptionContext;
byte[] decryptedEnvelopeKeyKMS = null;
if (KMSEnvelopeKeyIsPresent(executionContext, out encryptedKMSEnvelopeKey, out encryptionContext))
{
#if BCL
decryptedEnvelopeKeyKMS = DecryptedEnvelopeKeyKms(encryptedKMSEnvelopeKey, encryptionContext);
#else
decryptedEnvelopeKeyKMS = DecryptedEnvelopeKeyKmsAsync(encryptedKMSEnvelopeKey, encryptionContext).GetAwaiter().GetResult();
#endif
}
var getObjectResponse = executionContext.ResponseContext.Response as GetObjectResponse;
if (getObjectResponse != null)
{
#if BCL
DecryptObject(decryptedEnvelopeKeyKMS, getObjectResponse);
#else
DecryptObjectAsync(decryptedEnvelopeKeyKMS, getObjectResponse).GetAwaiter().GetResult();
#endif
}
var completeMultiPartUploadRequest = executionContext.RequestContext.Request.OriginalRequest as CompleteMultipartUploadRequest;
var completeMultipartUploadResponse = executionContext.ResponseContext.Response as CompleteMultipartUploadResponse;
if (completeMultipartUploadResponse != null)
{
#if BCL
CompleteMultipartUpload(completeMultiPartUploadRequest);
#else
CompleteMultipartUploadAsync(completeMultiPartUploadRequest).GetAwaiter().GetResult();
#endif
}
PostInvokeSynchronous(executionContext, decryptedEnvelopeKeyKMS);
}
#if BCL
///
/// Decrypts envelope key using KMS client
///
/// Encrypted key byte array to be decrypted
/// Encryption context for KMS
///
protected abstract byte[] DecryptedEnvelopeKeyKms(byte[] encryptedKMSEnvelopeKey, Dictionary encryptionContext);
#endif
#if AWS_ASYNC_API
///
/// Decrypts envelope key using KMS client asynchronously
///
/// Encrypted key byte array to be decrypted
/// Encryption context for KMS
///
protected abstract System.Threading.Tasks.Task DecryptedEnvelopeKeyKmsAsync(byte[] encryptedKMSEnvelopeKey, Dictionary encryptionContext);
#endif
#if AWS_ASYNC_API
///
/// Calls the and post invoke logic after calling the next handler
/// in the pipeline.
///
/// The response type for the current request.
/// The execution context, it contains the
/// request and response context.
/// A task that represents the asynchronous operation.
public override async System.Threading.Tasks.Task InvokeAsync(IExecutionContext executionContext)
{
var response = await base.InvokeAsync(executionContext).ConfigureAwait(false);
await PostInvokeAsync(executionContext).ConfigureAwait(false);
return response;
}
///
/// Decrypt the object being downloaded.
///
/// The execution context, it contains the request and response context.
protected async System.Threading.Tasks.Task PostInvokeAsync(IExecutionContext executionContext)
{
byte[] encryptedKMSEnvelopeKey;
Dictionary encryptionContext;
byte[] decryptedEnvelopeKeyKMS = null;
if (KMSEnvelopeKeyIsPresent(executionContext, out encryptedKMSEnvelopeKey, out encryptionContext))
{
decryptedEnvelopeKeyKMS = await DecryptedEnvelopeKeyKmsAsync(encryptedKMSEnvelopeKey, encryptionContext).ConfigureAwait(false);
}
var getObjectResponse = executionContext.ResponseContext.Response as GetObjectResponse;
if (getObjectResponse != null)
{
await DecryptObjectAsync(decryptedEnvelopeKeyKMS, getObjectResponse).ConfigureAwait(false);
}
var completeMultiPartUploadRequest = executionContext.RequestContext.Request.OriginalRequest as CompleteMultipartUploadRequest;
var completeMultipartUploadResponse = executionContext.ResponseContext.Response as CompleteMultipartUploadResponse;
if (completeMultipartUploadResponse != null)
{
await CompleteMultipartUploadAsync(completeMultiPartUploadRequest).ConfigureAwait(false);
}
PostInvokeSynchronous(executionContext, decryptedEnvelopeKeyKMS);
}
///
/// Mark multipart upload operation as completed and free resources asynchronously
///
/// CompleteMultipartUploadRequest request which needs to marked as completed
///
protected abstract System.Threading.Tasks.Task CompleteMultipartUploadAsync(CompleteMultipartUploadRequest completeMultiPartUploadRequest);
///
/// Decrypt GetObjectResponse asynchronously
/// Find which mode of encryption is used which can be either metadata (including KMS) or instruction file mode and
/// use these instructions to decrypt GetObjectResponse asynchronously
///
/// decrypted envelope key for KMS
/// GetObjectResponse which needs to be decrypted
///
/// Exception thrown if GetObjectResponse decryption fails
protected async System.Threading.Tasks.Task DecryptObjectAsync(byte[] decryptedEnvelopeKeyKMS, GetObjectResponse getObjectResponse)
{
if (EncryptionUtils.IsEncryptionInfoInMetadata(getObjectResponse))
{
DecryptObjectUsingMetadata(getObjectResponse, decryptedEnvelopeKeyKMS);
}
else
{
GetObjectResponse instructionFileResponse = null;
try
{
var instructionFileRequest = EncryptionUtils.GetInstructionFileRequest(getObjectResponse, EncryptionUtils.EncryptionInstructionFileV2Suffix);
instructionFileResponse = await GetInstructionFileAsync(instructionFileRequest).ConfigureAwait(false);
}
catch (AmazonS3Exception amazonS3Exception) when (amazonS3Exception.ErrorCode == EncryptionUtils.NoSuchKey)
{
Logger.InfoFormat($"New instruction file with suffix {EncryptionUtils.EncryptionInstructionFileV2Suffix} doesn't exist. " +
$"Try to get old instruction file with suffix {EncryptionUtils.EncryptionInstructionFileSuffix}. {amazonS3Exception.Message}");
try
{
var instructionFileRequest = EncryptionUtils.GetInstructionFileRequest(getObjectResponse, EncryptionUtils.EncryptionInstructionFileSuffix);
instructionFileResponse = await GetInstructionFileAsync(instructionFileRequest).ConfigureAwait(false);
}
catch (AmazonServiceException ace)
{
throw new AmazonServiceException($"Unable to decrypt data for object {getObjectResponse.Key} in bucket {getObjectResponse.BucketName}", ace);
}
}
catch (AmazonServiceException ace)
{
throw new AmazonServiceException($"Unable to decrypt data for object {getObjectResponse.Key} in bucket {getObjectResponse.BucketName}", ace);
}
DecryptObjectUsingInstructionFile(getObjectResponse, instructionFileResponse);
}
}
private async System.Threading.Tasks.Task GetInstructionFileAsync(GetObjectRequest instructionFileRequest)
{
var instructionFileResponse = await EncryptionClient.S3ClientForInstructionFile.GetObjectAsync(instructionFileRequest)
.ConfigureAwait(false);
return instructionFileResponse;
}
#elif AWS_APM_API
///
/// Calls the PostInvoke methods after calling the next handler
/// in the pipeline.
///
/// The execution context, it contains the
/// request and response context.
protected override void InvokeAsyncCallback(IAsyncExecutionContext executionContext)
{
IExecutionContext syncExecutionContext = ExecutionContext.CreateFromAsyncContext(executionContext);
// Process the response if an exception hasn't occured
if (executionContext.ResponseContext.AsyncResult.Exception == null)
{
byte[] encryptedKMSEnvelopeKey;
Dictionary encryptionContext;
if (KMSEnvelopeKeyIsPresent(syncExecutionContext, out encryptedKMSEnvelopeKey, out encryptionContext))
throw new NotSupportedException("The AWS SDK for .NET Framework 3.5 version of " +
EncryptionClient.GetType().Name + " does not support KMS key wrapping via the async programming model. " +
"Please use the synchronous version instead.");
var getObjectResponse = executionContext.ResponseContext.Response as GetObjectResponse;
if (getObjectResponse != null)
{
DecryptObject(encryptedKMSEnvelopeKey, getObjectResponse);
}
var completeMultiPartUploadRequest = executionContext.RequestContext.Request.OriginalRequest as CompleteMultipartUploadRequest;
var completeMultipartUploadResponse = executionContext.ResponseContext.Response as CompleteMultipartUploadResponse;
if (completeMultipartUploadResponse != null)
{
CompleteMultipartUpload(completeMultiPartUploadRequest);
}
PostInvokeSynchronous(syncExecutionContext, null);
}
base.InvokeAsyncCallback(executionContext);
}
#endif
///
/// Verify whether envelope is KMS or not
/// Populate envelope key and encryption context
///
/// The execution context, it contains the request and response context.
/// Encrypted KMS envelope key
/// KMS encryption context used for encryption and decryption
///
protected bool KMSEnvelopeKeyIsPresent(IExecutionContext executionContext,
out byte[] encryptedKMSEnvelopeKey, out Dictionary encryptionContext)
{
var response = executionContext.ResponseContext.Response;
var getObjectResponse = response as GetObjectResponse;
encryptedKMSEnvelopeKey = null;
encryptionContext = null;
if (getObjectResponse != null)
{
var metadata = getObjectResponse.Metadata;
EncryptionUtils.EnsureSupportedAlgorithms(metadata);
var base64EncodedEncryptedKmsEnvelopeKey = metadata[EncryptionUtils.XAmzKeyV2];
if (base64EncodedEncryptedKmsEnvelopeKey != null)
{
var wrapAlgorithm = metadata[EncryptionUtils.XAmzWrapAlg];
if (!(EncryptionUtils.XAmzWrapAlgKmsContextValue.Equals(wrapAlgorithm) || EncryptionUtils.XAmzWrapAlgKmsValue.Equals(wrapAlgorithm)))
{
return false;
}
encryptedKMSEnvelopeKey = Convert.FromBase64String(base64EncodedEncryptedKmsEnvelopeKey);
encryptionContext = EncryptionUtils.GetMaterialDescriptionFromMetaData(metadata);
return true;
}
}
return false;
}
///
/// Decrypt the object being downloaded.
///
/// The execution context, it contains the request and response context.
/// Decrypted KMS envelope key
protected void PostInvokeSynchronous(IExecutionContext executionContext, byte[] decryptedEnvelopeKeyKMS)
{
var request = executionContext.RequestContext.Request;
var response = executionContext.ResponseContext.Response;
var initiateMultiPartUploadRequest = request.OriginalRequest as InitiateMultipartUploadRequest;
var initiateMultiPartResponse = response as InitiateMultipartUploadResponse;
if (initiateMultiPartResponse != null)
{
AddMultipartUploadEncryptionContext(initiateMultiPartUploadRequest, initiateMultiPartResponse);
}
var uploadPartRequest = request.OriginalRequest as UploadPartRequest;
var uploadPartResponse = response as UploadPartResponse;
if (uploadPartResponse != null)
{
UpdateMultipartUploadEncryptionContext(uploadPartRequest);
}
var abortMultipartUploadRequest = request.OriginalRequest as AbortMultipartUploadRequest;
var abortMultipartUploadResponse = response as AbortMultipartUploadResponse;
if (abortMultipartUploadResponse != null)
{
//Clear Context data since encryption is aborted
EncryptionClient.CurrentMultiPartUploadKeys.TryRemove(abortMultipartUploadRequest.UploadId, out _);
}
}
#if BCL
///
/// Mark multipart upload operation as completed and free resources
///
/// CompleteMultipartUploadRequest request which needs to marked as completed
///
protected abstract void CompleteMultipartUpload(CompleteMultipartUploadRequest completeMultiPartUploadRequest);
///
/// Find mode of encryption and decrypt GetObjectResponse
///
/// decrypted envelope key for KMS
/// GetObjectResponse which needs to be decrypted
///
/// Exception thrown if GetObjectResponse decryption fails
protected void DecryptObject(byte[] decryptedEnvelopeKeyKMS, GetObjectResponse getObjectResponse)
{
if (EncryptionUtils.IsEncryptionInfoInMetadata(getObjectResponse))
{
DecryptObjectUsingMetadata(getObjectResponse, decryptedEnvelopeKeyKMS);
}
else
{
GetObjectResponse instructionFileResponse = null;
try
{
var instructionFileRequest = EncryptionUtils.GetInstructionFileRequest(getObjectResponse, EncryptionUtils.EncryptionInstructionFileV2Suffix);
instructionFileResponse = GetInstructionFile(instructionFileRequest);
}
catch (AmazonS3Exception amazonS3Exception) when (amazonS3Exception.ErrorCode == EncryptionUtils.NoSuchKey)
{
Logger.InfoFormat($"New instruction file with suffix {EncryptionUtils.EncryptionInstructionFileV2Suffix} doesn't exist. " +
$"Try to get old instruction file with suffix {EncryptionUtils.EncryptionInstructionFileSuffix}. {amazonS3Exception.Message}");
try
{
var instructionFileRequest = EncryptionUtils.GetInstructionFileRequest(getObjectResponse, EncryptionUtils.EncryptionInstructionFileSuffix);
instructionFileResponse = GetInstructionFile(instructionFileRequest);
}
catch (AmazonServiceException ace)
{
throw new AmazonServiceException($"Unable to decrypt data for object {getObjectResponse.Key} in bucket {getObjectResponse.BucketName}", ace);
}
}
catch (AmazonServiceException ace)
{
throw new AmazonServiceException($"Unable to decrypt data for object {getObjectResponse.Key} in bucket {getObjectResponse.BucketName}", ace);
}
DecryptObjectUsingInstructionFile(getObjectResponse, instructionFileResponse);
}
}
private GetObjectResponse GetInstructionFile(GetObjectRequest instructionFileRequest)
{
var instructionFileResponse = EncryptionClient.S3ClientForInstructionFile.GetObject(instructionFileRequest);
return instructionFileResponse;
}
#endif
///
/// Updates object where the object input stream contains the decrypted contents.
///
/// The getObject response of InstructionFile.
/// The getObject response whose contents are to be decrypted.
protected void DecryptObjectUsingInstructionFile(GetObjectResponse getObjectResponse, GetObjectResponse instructionFileResponse)
{
// Create an instruction object from the instruction file response
var instructions = EncryptionUtils.BuildInstructionsUsingInstructionFileV2(instructionFileResponse, EncryptionClient.EncryptionMaterials);
if (EncryptionUtils.XAmzAesGcmCekAlgValue.Equals(instructions.CekAlgorithm))
{
// Decrypt the object with V2 instructions
EncryptionUtils.DecryptObjectUsingInstructionsGcm(getObjectResponse, instructions);
}
else
{
ThrowIfLegacyReadIsDisabled();
// Decrypt the object with V1 instructions
EncryptionUtils.DecryptObjectUsingInstructions(getObjectResponse, instructions);
}
}
///
/// Updates object where the object input stream contains the decrypted contents.
///
/// The getObject response whose contents are to be decrypted.
/// The decrypted envelope key to be use if KMS key wrapping is being used. Or null if non-KMS key wrapping is being used.
protected void DecryptObjectUsingMetadata(GetObjectResponse getObjectResponse, byte[] decryptedEnvelopeKeyKMS)
{
// Create an instruction object from the object metadata
EncryptionInstructions instructions = EncryptionUtils.BuildInstructionsFromObjectMetadata(getObjectResponse, EncryptionClient.EncryptionMaterials, decryptedEnvelopeKeyKMS);
if (decryptedEnvelopeKeyKMS != null)
{
// Check if encryption context is present for KMS+context (v2) objects
if (getObjectResponse.Metadata[EncryptionUtils.XAmzCekAlg] != null
&& instructions.MaterialsDescription.ContainsKey(EncryptionUtils.XAmzEncryptionContextCekAlg))
{
// If encryption context is present, ensure that the GCM algorithm name is in the EC as expected in v2
if (EncryptionUtils.XAmzAesGcmCekAlgValue.Equals(getObjectResponse.Metadata[EncryptionUtils.XAmzCekAlg])
&& EncryptionUtils.XAmzAesGcmCekAlgValue.Equals(instructions.MaterialsDescription[EncryptionUtils.XAmzEncryptionContextCekAlg]))
{
// Decrypt the object with V2 instruction
EncryptionUtils.DecryptObjectUsingInstructionsGcm(getObjectResponse, instructions);
}
else
{
throw new AmazonCryptoException($"The content encryption algorithm used at encryption time does not match the algorithm stored for decryption time." +
" The object may be altered or corrupted.");
}
}
// Handle legacy KMS (v1) mode with GCM content encryption
// See https://github.com/aws/amazon-s3-encryption-client-dotnet/issues/26 for context. It fixes AWS SES encryption/decryption bug
else if (EncryptionUtils.XAmzAesGcmCekAlgValue.Equals(instructions.CekAlgorithm))
{
// KMS (v1) without Encryption Context requires legacy mode to be enabled even when GCM is used for content encryption
ThrowIfLegacyReadIsDisabled();
EncryptionUtils.DecryptObjectUsingInstructionsGcm(getObjectResponse, instructions);
}
else if (EncryptionUtils.XAmzAesCbcPaddingCekAlgValue.Equals(instructions.CekAlgorithm))
{
ThrowIfLegacyReadIsDisabled();
// Decrypt the object with V1 instruction
EncryptionUtils.DecryptObjectUsingInstructions(getObjectResponse, instructions);
}
else
{
throw new AmazonCryptoException($"The content encryption algorithm used at encryption time does not match the algorithm stored for decryption time." +
" The object may be altered or corrupted.");
}
}
else if (EncryptionUtils.XAmzAesGcmCekAlgValue.Equals(getObjectResponse.Metadata[EncryptionUtils.XAmzCekAlg]))
{
// Decrypt the object with V2 instruction
EncryptionUtils.DecryptObjectUsingInstructionsGcm(getObjectResponse, instructions);
}
// It is safe to assume, this is either non KMS encryption with V1 client or AES CBC
// We don't need to check cek algorithm to be AES CBC, because non KMS encryption with V1 client doesn't set it
else
{
ThrowIfLegacyReadIsDisabled();
EncryptionUtils.DecryptObjectUsingInstructions(getObjectResponse, instructions);
}
}
///
/// Throws if legacy security profile is disabled
///
protected abstract void ThrowIfLegacyReadIsDisabled();
///
/// Update multipart upload encryption context for the given UploadPartRequest
///
/// UploadPartRequest whose context needs to be updated
protected abstract void UpdateMultipartUploadEncryptionContext(UploadPartRequest uploadPartRequest);
///
/// Add multipart UploadId and encryption context to the current known multipart operations
/// Encryption context is used decided the encryption instructions for next UploadPartRequest
///
/// InitiateMultipartUploadRequest whose encryption context needs to be saved
/// InitiateMultipartUploadResponse whose UploadId needs to be saved
protected void AddMultipartUploadEncryptionContext(InitiateMultipartUploadRequest initiateMultiPartUploadRequest, InitiateMultipartUploadResponse initiateMultiPartResponse)
{
if (!EncryptionClient.AllMultiPartUploadRequestContexts.ContainsKey(initiateMultiPartUploadRequest))
{
throw new AmazonServiceException($"Failed to find encryption context required to start multipart uploads for request {initiateMultiPartUploadRequest}");
}
EncryptionClient.CurrentMultiPartUploadKeys.TryAdd(initiateMultiPartResponse.UploadId, EncryptionClient.AllMultiPartUploadRequestContexts[initiateMultiPartUploadRequest]);
// It is safe to remove the request as it has been already added to the CurrentMultiPartUploadKeys
EncryptionClient.AllMultiPartUploadRequestContexts.TryRemove(initiateMultiPartUploadRequest, out _);
}
}
}