using System.Collections.Generic;
using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Globalization;
using System.Security.Policy;
namespace FastReport.Cloud.StorageClient.S3
{
///
/// Class for request sign to S3. Doc
///
class S3Signer
{
private readonly string _accessKeyId;
private readonly string _secretAccessKey;
private readonly string _region;
private readonly string _service;
private static readonly string EmptySha256 = sha256_hash("");
public S3Signer(string accessKeyId, string secretAccessKey, string region, string service = "s3")
{
_accessKeyId = accessKeyId;
_secretAccessKey = secretAccessKey;
_region = region;
_service = service;
}
public void Sign(HttpWebRequest request, Stream contentStream = null, DateTimeOffset? signDate = null)
{
// article about S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
string nowDate = dateToUse.ToString("yyyyMMdd");
string amzNowDate = GetAmzDate(dateToUse);
request.Headers.Add("x-amz-date", amzNowDate);
string payloadHash = CalcPayloadHash(request, contentStream);
request.Headers.Add("x-amz-content-sha256", payloadHash);
// 1. Create a canonical request
/*
* \n
* \n
* \n
* \n
* \n
*
*/
string canonicalRequest = request.Method + "\n" +
GetCanonicalUri(request) + "\n" + // CanonicalURI
GetCanonicalQueryString(request) + "\n" +
GetCanonicalHeaders(request, out string signedHeaders) + "\n" + // ends up with two newlines which is expected
signedHeaders + "\n" +
payloadHash;
// 2. Create a string to sign
// step by step instructions: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
/*
* StringToSign =
* Algorithm + \n +
* RequestDateTime + \n +
* CredentialScope + \n +
* HashedCanonicalRequest
*/
string stringToSign = "AWS4-HMAC-SHA256\n" +
amzNowDate + "\n" +
nowDate + "/" + _region + "/s3/aws4_request\n" +
sha256_hash(canonicalRequest);
// 3. Calculate Signature
string signature = CalcSignature(stringToSign, nowDate);
string auth = $"Credential={_accessKeyId}/{nowDate}/{_region}/s3/aws4_request,SignedHeaders={signedHeaders},Signature={signature}";
request.Headers.Add(HttpRequestHeader.Authorization, "AWS4-HMAC-SHA256" + " " + auth);
}
public string SignSeed(HttpWebRequest request, long contentLength, DateTimeOffset? signDate = null)
{
// article about S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
string nowDate = dateToUse.ToString("yyyyMMdd");
string amzNowDate = GetAmzDate(dateToUse);
request.Headers.Add("x-amz-date", amzNowDate);
// 1. Create a canonical request
/*
* \n
* \n
* \n
* \n
* \n
* "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
*/
request.Headers.Add("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD");
request.Headers.Add("Content-Encoding", "aws-chunked");
request.Headers.Add("x-amz-decoded-content-length", contentLength.ToString());
string canonicalRequest = request.Method + "\n" +
GetCanonicalUri(request) + "\n" + // CanonicalURI
GetCanonicalQueryString(request) + "\n" +
GetCanonicalHeaders(request, out string signedHeaders) + "\n" +
signedHeaders + "\n" +
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
// 2. Create a string to sign
/*
* StringToSign =
* Algorithm + \n +
* RequestDateTime + \n +
* CredentialScope + \n +
* HashedCanonicalRequest
*/
string stringToSign = "AWS4-HMAC-SHA256\n" +
amzNowDate + "\n" +
nowDate + "/" + _region + "/s3/aws4_request\n" +
sha256_hash(canonicalRequest);
// 3. Calculate Signature
string signature = CalcSignature(stringToSign, nowDate);
string auth = $"Credential={_accessKeyId}/{nowDate}/{_region}/s3/aws4_request,SignedHeaders={signedHeaders},Signature={signature}";
request.Headers.Add(HttpRequestHeader.Authorization, "AWS4-HMAC-SHA256" + " " + auth);
return signature;
}
public string SignChunk(HttpWebRequest request, string previousSignature, Stream contentStream, Stream requestStream, DateTimeOffset? signDate = null)
{
// article about S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
string nowDate = dateToUse.ToString("yyyyMMdd");
string amzNowDate = GetAmzDate(dateToUse);
string payloadHash = CalcPayloadHash(request, contentStream);
// 1. Create a string to sign
/*
* StringToSign =
* Algorithm + \n +
* RequestDateTime + \n +
* CredentialScope + \n +
* PreviousSignature + \n +
* Hex(Sha256Hash("")) + \n +
* Hex(Sha256Hash("current-chunk-data"))
*/
string stringToSign = "AWS4-HMAC-SHA256-PAYLOAD\n" +
amzNowDate + "\n" +
nowDate + "/" + _region + "/s3/aws4_request\n" +
previousSignature + "\n" +
EmptySha256 + "\n" +
payloadHash;
// 2. Calculate Signature
string signature = CalcSignature(stringToSign, nowDate);
// prepare chunk
string headChunk = ToHEX(contentStream == null ? 0 : contentStream.Length) + ";chunk-signature=" + signature + "\r\n";
MemoryStream ms = new MemoryStream();
byte[] temp = Encoding.UTF8.GetBytes(headChunk);
ms.Write(temp, 0, temp.Length);
if (contentStream != null)
{
contentStream.Position = 0;
contentStream.CopyTo(ms);
temp = Encoding.UTF8.GetBytes("\r\n");
ms.Write(temp, 0, temp.Length);
}
temp = ms.ToArray();
try
{
requestStream.Write(temp, 0, temp.Length); // send chunk
}
finally
{
ms.Close();
}
return signature;
}
private string CalcSignature(string stringToSign, string nowDate)
{
/*
* DateKey = HMAC-SHA256("AWS4"+"", "")
* DateRegionKey = HMAC-SHA256(, "")
* DateRegionServiceKey = HMAC-SHA256(, "")
* SigningKey = HMAC-SHA256(, "aws4_request")
*/
byte[] kSecret = Encoding.UTF8.GetBytes(("AWS4" + _secretAccessKey).ToCharArray());
byte[] kDate = HmacSha256(nowDate, kSecret);
byte[] kRegion = HmacSha256(_region, kDate);
byte[] kService = HmacSha256(_service, kRegion);
byte[] kSigning = HmacSha256("aws4_request", kService);
// final signature
byte[] signatureRaw = HmacSha256(stringToSign, kSigning);
string signature = ToHEX(signatureRaw);
return signature;
}
private static string GetAmzDate(DateTimeOffset date)
{
return date.ToString("yyyyMMddTHHmmssZ");
}
private string GetCanonicalUri(HttpWebRequest request)
{
string path = UrlEncode(request.RequestUri.AbsolutePath.TrimStart('/')).Replace("%2F", "/");
return "/" + path;
}
private string ToHEX(byte[] str)
{
var sb = new StringBuilder();
foreach (var t in str)
{
sb.Append(t.ToString("x2"));
}
return sb.ToString();
}
private string ToHEX(long value)
{
return value.ToString("X", CultureInfo.InvariantCulture);
}
public static string sha256_hash(string value)
{
StringBuilder Sb = new StringBuilder();
using (SHA256 hash = SHA256Managed.Create())
{
Encoding enc = Encoding.UTF8;
Byte[] result = hash.ComputeHash(enc.GetBytes(value));
foreach (Byte b in result)
Sb.Append(b.ToString("x2"));
}
return Sb.ToString();
}
public static string sha256_hash(byte[] value)
{
StringBuilder Sb = new StringBuilder();
using (SHA256 hash = SHA256Managed.Create())
{
Byte[] result = hash.ComputeHash(value);
foreach (Byte b in result)
Sb.Append(b.ToString("x2"));
}
return Sb.ToString();
}
private string GetCanonicalQueryString(HttpWebRequest request)
{
NameValueCollection values = ParseQueryString(request.RequestUri.Query);
var sb = new StringBuilder();
foreach (string key in values.AllKeys.OrderBy(k => k))
{
if (sb.Length > 0)
{
sb.Append('&');
}
string value = UrlEncode(values[key]);
if (key == null)
{
sb
.Append(value)
.Append("=");
}
else
{
sb
.Append(UrlEncode(key.ToLower()))
.Append("=")
.Append(value);
}
}
return sb.ToString();
}
public static NameValueCollection ParseQueryString(string query)
{
NameValueCollection result = new NameValueCollection();
string[] queries = query.Split('&');
for (int i = 0; i < queries.Length; i++)
{
string[] kvp = queries[i].Split('=');
if (kvp.Length > 1)
result.Add(kvp[0].Replace("?", ""), kvp[1]);
}
return result;
}
public static string UrlEncode(string str)
{
StringBuilder sb = new StringBuilder();
StringReader reader = new StringReader(str);
int charCode = reader.Read();
while (charCode != -1)
{
if (((charCode >= 'A') && (charCode <= 'Z')) || ((charCode >= 'a') && (charCode <= 'z')) || ((charCode >= '0') && (charCode <= '9'))
|| (charCode == '-') || (charCode == '_') || (charCode == '.') || (charCode == '~'))
{
sb.Append((char)charCode);
}
else
{
sb.AppendFormat("%{0:X2}", charCode);
}
charCode = reader.Read();
}
return sb.ToString().Replace("%2520", "%20");
}
private string GetCanonicalHeaders(HttpWebRequest request, out string signedHeaders)
{
// List of request headers with their values.
// Individual header name and value pairs are separated by the newline character ("\n").
// Header names must be in lowercase.
Dictionary headers = new Dictionary();
foreach (string header in request.Headers.AllKeys.OrderBy(x => x).ToList())
if (header.StartsWith("x-amz-", StringComparison.OrdinalIgnoreCase))
headers.Add(header.ToLowerInvariant(), request.Headers.GetValues(header).First());
var sb = new StringBuilder();
var signedHeadersList = new List();
if (request.Headers.AllKeys.Contains("date"))
{
sb.Append("date:").Append(request.Headers.GetValues("date").First()).Append("\n");
signedHeadersList.Add("date");
}
sb.Append("host:").Append(request.RequestUri.Host).Append("\n");
signedHeadersList.Add("host");
string contentType = request.ContentType;
if (contentType != null)
{
sb.Append("content-type:").Append(contentType).Append("\n");
signedHeadersList.Add("content-type");
}
if (request.Headers.AllKeys.Contains("range"))
{
sb.Append("range:").Append(request.Headers.GetValues("range").First()).Append("\n");
signedHeadersList.Add("range");
}
// Create the string in the right format; this is what makes the headers "canonicalized" --
// it means put in a standard format. http://en.wikipedia.org/wiki/Canonicalization
foreach (var kvp in headers)
{
sb.Append(kvp.Key).Append(":");
signedHeadersList.Add(kvp.Key);
foreach (string hv in request.Headers.GetValues(kvp.Key))
{
sb.Append(hv);
}
sb.Append("\n");
}
signedHeaders = string.Join(";", signedHeadersList);
return sb.ToString();
}
///
/// Calculate hash sha256 of content and convert it to hexadecimal string. This string will be returned and written to x-amz-content-sha256 header.
///
/// Request to which header will be added
/// Stream with content of request
/// String containing the encrypted and hexadecimal stream of the request content
private string CalcPayloadHash(HttpWebRequest request, Stream contentStream)
{
string hash;
if (request.Method == "PUT" && contentStream != null)
{
using (var memoryStream = new MemoryStream())
{
contentStream.CopyTo(memoryStream);
byte[] content = memoryStream.ToArray();
hash = sha256_hash(content);
}
}
else
{
hash = EmptySha256;
}
return hash;
}
private static byte[] HmacSha256(string data, byte[] key)
{
var alg = KeyedHashAlgorithm.Create("HmacSHA256");
alg.Key = key;
return alg.ComputeHash(Encoding.UTF8.GetBytes(data));
}
}
}