/*
* Copyright 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.
*/
using Amazon.Runtime.Internal.Transform;
using Amazon.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using ThirdParty.MD5;
namespace Amazon.Runtime.Internal.Util
{
///
/// Utilities for working with the checksums used to validate request/response integrity
///
public static class ChecksumUtils
{
///
/// Generates the name of the header key to use for a given checksum algorithm
///
/// Checksum algorithm
/// Name of the HTTP header key for the given algorithm
internal static string GetChecksumHeaderKey(CoreChecksumAlgorithm checksumAlgorithm)
{
return $"x-amz-checksum-{checksumAlgorithm.ToString().ToLower()}";
}
///
/// Attempts to select and then calculate the checksum for a request
///
/// Request to calculate the checksum for
/// Checksum algorithm to use, specified on the request using a service-specific enum
/// If checksumAlgorithm is ,
/// this flag controls whether or not to fallback to using a MD5 to generate a checksum.
///
public static void SetRequestChecksum(IRequest request, string checksumAlgorithm, bool fallbackToMD5 = true)
{
var coreChecksumAlgoritm = ChecksumUtils.ConvertToCoreChecksumAlgorithm(checksumAlgorithm);
if (coreChecksumAlgoritm == CoreChecksumAlgorithm.NONE)
{
if (fallbackToMD5)
SetRequestChecksumMD5(request);
return;
}
var checksumHeaderKey = GetChecksumHeaderKey(coreChecksumAlgoritm);
// If the user provided a precalculated header for this checksum, don't recalculate it
if (request.Headers.TryGetValue(checksumHeaderKey, out var checksumHeaderValue))
{
if (!string.IsNullOrEmpty(checksumHeaderValue))
{
return;
}
}
if (request.UseChunkEncoding || (request.DisablePayloadSigning ?? false))
{
// Add the checksum key to the trailing headers, but not the value
// because we won't know it until the wrapper stream is fully read,
// and we only want to read the wrapper stream once.
//
// The header key is required upfront for calculating the total length of
// the wrapper stream, which we need to send as the Content-Length header
// before the wrapper stream is transmitted.
request.TrailingHeaders.Add(checksumHeaderKey, string.Empty);
request.SelectedChecksum = coreChecksumAlgoritm;
}
else // calculate and set the checksum in the request headers
{
request.Headers[checksumHeaderKey] = CalculateChecksumForRequest(CryptoUtilFactory.GetChecksumInstance(coreChecksumAlgoritm), request);
}
}
///
/// Attempts to select and then calculate a MD5 checksum for a request
///
/// Request to calculate the checksum for
public static void SetRequestChecksumMD5(IRequest request)
{
// If the user provided a precalculated MD5 header, don't recalculate it
if (request.Headers.TryGetValue(HeaderKeys.ContentMD5Header, out var md5HeaderValue))
{
if (!string.IsNullOrEmpty(md5HeaderValue))
{
return;
}
}
request.Headers[HeaderKeys.ContentMD5Header] = request.ContentStream != null ?
AWSSDKUtils.GenerateMD5ChecksumForStream(request.ContentStream) :
AWSSDKUtils.GenerateChecksumForBytes(request.Content, true);
}
///
/// Calculates the given checksum for a marshalled request
///
/// Checksum algorithm to calculate
/// Request whose content to calculate the checksum for
/// Calculated checksum for the given request
private static string CalculateChecksumForRequest(HashAlgorithm algorithm, IRequest request)
{
if (request.ContentStream != null)
{
var seekableStream = WrapperStream.SearchWrappedStream(request.ContentStream, s => s.CanSeek);
if (seekableStream != null)
{
var position = seekableStream.Position;
var checksumBytes = algorithm.ComputeHash(seekableStream);
seekableStream.Seek(position, SeekOrigin.Begin);
return Convert.ToBase64String(checksumBytes);
}
else
{
throw new ArgumentException("Request must have a seekable content stream to calculate checksum");
}
}
else if (request.Content != null)
{
var checksumBytes = algorithm.ComputeHash(request.Content);
return Convert.ToBase64String(checksumBytes);
}
else // request doesn't have content
{
return string.Empty;
}
}
///
/// Selects which checksum to use to validate the integrity of a response
///
/// List of checksums supported by the service operation
/// Response from the service, which potentially contains x-amz-checksum-* headers
/// Single checksum algorithm to use for validating the response, or NONE if checksum validation should be skipped
public static CoreChecksumAlgorithm SelectChecksumForResponseValidation(ICollection operationSupportedChecksums, IWebResponseData responseData)
{
if (operationSupportedChecksums == null || operationSupportedChecksums.Count == 0 || responseData == null)
{
return CoreChecksumAlgorithm.NONE;
}
// Checksums to use for validation in order of speed (via CRT profiling)
CoreChecksumAlgorithm[] checksumsInPriorityOrder =
{
CoreChecksumAlgorithm.CRC32C,
CoreChecksumAlgorithm.CRC32,
CoreChecksumAlgorithm.SHA1,
CoreChecksumAlgorithm.SHA256
};
foreach (var algorithm in checksumsInPriorityOrder)
{
if (operationSupportedChecksums.Contains(algorithm))
{
var headerKey = GetChecksumHeaderKey(algorithm);
if (responseData.IsHeaderPresent(headerKey) && !IsChecksumValueMultipartGet(responseData.GetHeaderValue(headerKey)))
{
return algorithm;
}
}
}
return CoreChecksumAlgorithm.NONE;
}
///
/// Determines if a checksum value is for a whole S3 multipart object, which must skip validation.
/// These checksums end with `-#`, where # is an integer between 1 and 10000
///
/// Base 64 checksum value
/// True if the checksum is for an S3 multipart object, false otherwise
private static bool IsChecksumValueMultipartGet(string checksumValue)
{
if (string.IsNullOrEmpty(checksumValue))
{
return false;
}
var lastDashIndex = checksumValue.LastIndexOf('-');
if (lastDashIndex == -1)
{
return false;
}
int partNumber;
var isInteger = int.TryParse(checksumValue.Substring(lastDashIndex + 1), out partNumber);
if (!isInteger)
{
return false;
}
if (partNumber >= 1 && partNumber <= 10000)
{
return true;
}
return false;
}
///
/// Translates a string representation of a flexible checksum algorithm to the
/// corresponding enum value.
///
/// Checksum algorithm as a string
/// Enum value reprsenting the checksum algorithm
/// Thrown if a matching enum value could not be found
private static CoreChecksumAlgorithm ConvertToCoreChecksumAlgorithm(string selectedServiceChecksum)
{
if (string.IsNullOrEmpty(selectedServiceChecksum))
{
return CoreChecksumAlgorithm.NONE;
}
CoreChecksumAlgorithm selectedCoreChecksumAlgorithm;
#if BCL35
try
{
selectedCoreChecksumAlgorithm = (CoreChecksumAlgorithm)Enum.Parse(typeof(CoreChecksumAlgorithm), selectedServiceChecksum, true);
}
catch (Exception ex)
{
// Service checksum options should always be a subset of the core, but in case not
throw new AmazonClientException($"Attempted to sign a request with an unknown checksum algorithm {selectedServiceChecksum}", ex);
}
#else
if (!Enum.TryParse(selectedServiceChecksum, true, out selectedCoreChecksumAlgorithm))
{
// Service checksum options should always be a subset of the core, but in case not
throw new AmazonClientException($"Attempted to sign a request with an unknown checksum algorithm {selectedServiceChecksum}");
}
#endif
return selectedCoreChecksumAlgorithm;
}
}
}