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