123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- 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
- {
- /// <summary>
- /// Class for request sign to S3. Doc <see href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html"/>
- /// </summary>
- 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
- /*
- * <HTTPMethod>\n
- * <CanonicalURI>\n
- * <CanonicalQueryString>\n
- * <CanonicalHeaders>\n
- * <SignedHeaders>\n
- * <HashedPayload>
- */
- 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
- /*
- * <HTTPMethod>\n
- * <CanonicalURI>\n
- * <CanonicalQueryString>\n
- * <CanonicalHeaders>\n
- * <SignedHeaders>\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"+"<SecretAccessKey>", "<YYYYMMDD>")
- * DateRegionKey = HMAC-SHA256(<DateKey>, "<aws-region>")
- * DateRegionServiceKey = HMAC-SHA256(<DateRegionKey>, "<aws-service>")
- * SigningKey = HMAC-SHA256(<DateRegionServiceKey>, "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<string, string> headers = new Dictionary<string, string>();
- 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<string>();
- 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();
- }
- /// <summary>
- /// Calculate hash sha256 of content and convert it to hexadecimal string. This string will be returned and written to x-amz-content-sha256 header.
- /// </summary>
- /// <param name="request">Request to which header will be added</param>
- /// <param name="contentStream">Stream with content of request</param>
- /// <returns>String containing the encrypted and hexadecimal stream of the request content</returns>
- 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));
- }
- }
- }
|