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 _); } } }