S3Signer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. using System.Collections.Generic;
  2. using System;
  3. using System.Collections.Specialized;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Security.Cryptography;
  8. using System.Text;
  9. using System.Globalization;
  10. using System.Security.Policy;
  11. namespace FastReport.Cloud.StorageClient.S3
  12. {
  13. /// <summary>
  14. /// Class for request sign to S3. Doc <see href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html"/>
  15. /// </summary>
  16. class S3Signer
  17. {
  18. private readonly string _accessKeyId;
  19. private readonly string _secretAccessKey;
  20. private readonly string _region;
  21. private readonly string _service;
  22. private static readonly string EmptySha256 = sha256_hash("");
  23. public S3Signer(string accessKeyId, string secretAccessKey, string region, string service = "s3")
  24. {
  25. _accessKeyId = accessKeyId;
  26. _secretAccessKey = secretAccessKey;
  27. _region = region;
  28. _service = service;
  29. }
  30. public void Sign(HttpWebRequest request, Stream contentStream = null, DateTimeOffset? signDate = null)
  31. {
  32. // article about S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
  33. DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
  34. string nowDate = dateToUse.ToString("yyyyMMdd");
  35. string amzNowDate = GetAmzDate(dateToUse);
  36. request.Headers.Add("x-amz-date", amzNowDate);
  37. string payloadHash = CalcPayloadHash(request, contentStream);
  38. request.Headers.Add("x-amz-content-sha256", payloadHash);
  39. // 1. Create a canonical request
  40. /*
  41. * <HTTPMethod>\n
  42. * <CanonicalURI>\n
  43. * <CanonicalQueryString>\n
  44. * <CanonicalHeaders>\n
  45. * <SignedHeaders>\n
  46. * <HashedPayload>
  47. */
  48. string canonicalRequest = request.Method + "\n" +
  49. GetCanonicalUri(request) + "\n" + // CanonicalURI
  50. GetCanonicalQueryString(request) + "\n" +
  51. GetCanonicalHeaders(request, out string signedHeaders) + "\n" + // ends up with two newlines which is expected
  52. signedHeaders + "\n" +
  53. payloadHash;
  54. // 2. Create a string to sign
  55. // step by step instructions: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
  56. /*
  57. * StringToSign =
  58. * Algorithm + \n +
  59. * RequestDateTime + \n +
  60. * CredentialScope + \n +
  61. * HashedCanonicalRequest
  62. */
  63. string stringToSign = "AWS4-HMAC-SHA256\n" +
  64. amzNowDate + "\n" +
  65. nowDate + "/" + _region + "/s3/aws4_request\n" +
  66. sha256_hash(canonicalRequest);
  67. // 3. Calculate Signature
  68. string signature = CalcSignature(stringToSign, nowDate);
  69. string auth = $"Credential={_accessKeyId}/{nowDate}/{_region}/s3/aws4_request,SignedHeaders={signedHeaders},Signature={signature}";
  70. request.Headers.Add(HttpRequestHeader.Authorization, "AWS4-HMAC-SHA256" + " " + auth);
  71. }
  72. public string SignSeed(HttpWebRequest request, long contentLength, DateTimeOffset? signDate = null)
  73. {
  74. // article about S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
  75. DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
  76. string nowDate = dateToUse.ToString("yyyyMMdd");
  77. string amzNowDate = GetAmzDate(dateToUse);
  78. request.Headers.Add("x-amz-date", amzNowDate);
  79. // 1. Create a canonical request
  80. /*
  81. * <HTTPMethod>\n
  82. * <CanonicalURI>\n
  83. * <CanonicalQueryString>\n
  84. * <CanonicalHeaders>\n
  85. * <SignedHeaders>\n
  86. * "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
  87. */
  88. request.Headers.Add("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD");
  89. request.Headers.Add("Content-Encoding", "aws-chunked");
  90. request.Headers.Add("x-amz-decoded-content-length", contentLength.ToString());
  91. string canonicalRequest = request.Method + "\n" +
  92. GetCanonicalUri(request) + "\n" + // CanonicalURI
  93. GetCanonicalQueryString(request) + "\n" +
  94. GetCanonicalHeaders(request, out string signedHeaders) + "\n" +
  95. signedHeaders + "\n" +
  96. "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
  97. // 2. Create a string to sign
  98. /*
  99. * StringToSign =
  100. * Algorithm + \n +
  101. * RequestDateTime + \n +
  102. * CredentialScope + \n +
  103. * HashedCanonicalRequest
  104. */
  105. string stringToSign = "AWS4-HMAC-SHA256\n" +
  106. amzNowDate + "\n" +
  107. nowDate + "/" + _region + "/s3/aws4_request\n" +
  108. sha256_hash(canonicalRequest);
  109. // 3. Calculate Signature
  110. string signature = CalcSignature(stringToSign, nowDate);
  111. string auth = $"Credential={_accessKeyId}/{nowDate}/{_region}/s3/aws4_request,SignedHeaders={signedHeaders},Signature={signature}";
  112. request.Headers.Add(HttpRequestHeader.Authorization, "AWS4-HMAC-SHA256" + " " + auth);
  113. return signature;
  114. }
  115. public string SignChunk(HttpWebRequest request, string previousSignature, Stream contentStream, Stream requestStream, DateTimeOffset? signDate = null)
  116. {
  117. // article about S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
  118. DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
  119. string nowDate = dateToUse.ToString("yyyyMMdd");
  120. string amzNowDate = GetAmzDate(dateToUse);
  121. string payloadHash = CalcPayloadHash(request, contentStream);
  122. // 1. Create a string to sign
  123. /*
  124. * StringToSign =
  125. * Algorithm + \n +
  126. * RequestDateTime + \n +
  127. * CredentialScope + \n +
  128. * PreviousSignature + \n +
  129. * Hex(Sha256Hash("")) + \n +
  130. * Hex(Sha256Hash("current-chunk-data"))
  131. */
  132. string stringToSign = "AWS4-HMAC-SHA256-PAYLOAD\n" +
  133. amzNowDate + "\n" +
  134. nowDate + "/" + _region + "/s3/aws4_request\n" +
  135. previousSignature + "\n" +
  136. EmptySha256 + "\n" +
  137. payloadHash;
  138. // 2. Calculate Signature
  139. string signature = CalcSignature(stringToSign, nowDate);
  140. // prepare chunk
  141. string headChunk = ToHEX(contentStream == null ? 0 : contentStream.Length) + ";chunk-signature=" + signature + "\r\n";
  142. MemoryStream ms = new MemoryStream();
  143. byte[] temp = Encoding.UTF8.GetBytes(headChunk);
  144. ms.Write(temp, 0, temp.Length);
  145. if (contentStream != null)
  146. {
  147. contentStream.Position = 0;
  148. contentStream.CopyTo(ms);
  149. temp = Encoding.UTF8.GetBytes("\r\n");
  150. ms.Write(temp, 0, temp.Length);
  151. }
  152. temp = ms.ToArray();
  153. try
  154. {
  155. requestStream.Write(temp, 0, temp.Length); // send chunk
  156. }
  157. finally
  158. {
  159. ms.Close();
  160. }
  161. return signature;
  162. }
  163. private string CalcSignature(string stringToSign, string nowDate)
  164. {
  165. /*
  166. * DateKey = HMAC-SHA256("AWS4"+"<SecretAccessKey>", "<YYYYMMDD>")
  167. * DateRegionKey = HMAC-SHA256(<DateKey>, "<aws-region>")
  168. * DateRegionServiceKey = HMAC-SHA256(<DateRegionKey>, "<aws-service>")
  169. * SigningKey = HMAC-SHA256(<DateRegionServiceKey>, "aws4_request")
  170. */
  171. byte[] kSecret = Encoding.UTF8.GetBytes(("AWS4" + _secretAccessKey).ToCharArray());
  172. byte[] kDate = HmacSha256(nowDate, kSecret);
  173. byte[] kRegion = HmacSha256(_region, kDate);
  174. byte[] kService = HmacSha256(_service, kRegion);
  175. byte[] kSigning = HmacSha256("aws4_request", kService);
  176. // final signature
  177. byte[] signatureRaw = HmacSha256(stringToSign, kSigning);
  178. string signature = ToHEX(signatureRaw);
  179. return signature;
  180. }
  181. private static string GetAmzDate(DateTimeOffset date)
  182. {
  183. return date.ToString("yyyyMMddTHHmmssZ");
  184. }
  185. private string GetCanonicalUri(HttpWebRequest request)
  186. {
  187. string path = UrlEncode(request.RequestUri.AbsolutePath.TrimStart('/')).Replace("%2F", "/");
  188. return "/" + path;
  189. }
  190. private string ToHEX(byte[] str)
  191. {
  192. var sb = new StringBuilder();
  193. foreach (var t in str)
  194. {
  195. sb.Append(t.ToString("x2"));
  196. }
  197. return sb.ToString();
  198. }
  199. private string ToHEX(long value)
  200. {
  201. return value.ToString("X", CultureInfo.InvariantCulture);
  202. }
  203. public static string sha256_hash(string value)
  204. {
  205. StringBuilder Sb = new StringBuilder();
  206. using (SHA256 hash = SHA256Managed.Create())
  207. {
  208. Encoding enc = Encoding.UTF8;
  209. Byte[] result = hash.ComputeHash(enc.GetBytes(value));
  210. foreach (Byte b in result)
  211. Sb.Append(b.ToString("x2"));
  212. }
  213. return Sb.ToString();
  214. }
  215. public static string sha256_hash(byte[] value)
  216. {
  217. StringBuilder Sb = new StringBuilder();
  218. using (SHA256 hash = SHA256Managed.Create())
  219. {
  220. Byte[] result = hash.ComputeHash(value);
  221. foreach (Byte b in result)
  222. Sb.Append(b.ToString("x2"));
  223. }
  224. return Sb.ToString();
  225. }
  226. private string GetCanonicalQueryString(HttpWebRequest request)
  227. {
  228. NameValueCollection values = ParseQueryString(request.RequestUri.Query);
  229. var sb = new StringBuilder();
  230. foreach (string key in values.AllKeys.OrderBy(k => k))
  231. {
  232. if (sb.Length > 0)
  233. {
  234. sb.Append('&');
  235. }
  236. string value = UrlEncode(values[key]);
  237. if (key == null)
  238. {
  239. sb
  240. .Append(value)
  241. .Append("=");
  242. }
  243. else
  244. {
  245. sb
  246. .Append(UrlEncode(key.ToLower()))
  247. .Append("=")
  248. .Append(value);
  249. }
  250. }
  251. return sb.ToString();
  252. }
  253. public static NameValueCollection ParseQueryString(string query)
  254. {
  255. NameValueCollection result = new NameValueCollection();
  256. string[] queries = query.Split('&');
  257. for (int i = 0; i < queries.Length; i++)
  258. {
  259. string[] kvp = queries[i].Split('=');
  260. if (kvp.Length > 1)
  261. result.Add(kvp[0].Replace("?", ""), kvp[1]);
  262. }
  263. return result;
  264. }
  265. public static string UrlEncode(string str)
  266. {
  267. StringBuilder sb = new StringBuilder();
  268. StringReader reader = new StringReader(str);
  269. int charCode = reader.Read();
  270. while (charCode != -1)
  271. {
  272. if (((charCode >= 'A') && (charCode <= 'Z')) || ((charCode >= 'a') && (charCode <= 'z')) || ((charCode >= '0') && (charCode <= '9'))
  273. || (charCode == '-') || (charCode == '_') || (charCode == '.') || (charCode == '~'))
  274. {
  275. sb.Append((char)charCode);
  276. }
  277. else
  278. {
  279. sb.AppendFormat("%{0:X2}", charCode);
  280. }
  281. charCode = reader.Read();
  282. }
  283. return sb.ToString().Replace("%2520", "%20");
  284. }
  285. private string GetCanonicalHeaders(HttpWebRequest request, out string signedHeaders)
  286. {
  287. // List of request headers with their values.
  288. // Individual header name and value pairs are separated by the newline character ("\n").
  289. // Header names must be in lowercase.
  290. Dictionary<string, string> headers = new Dictionary<string, string>();
  291. foreach (string header in request.Headers.AllKeys.OrderBy(x => x).ToList())
  292. if (header.StartsWith("x-amz-", StringComparison.OrdinalIgnoreCase))
  293. headers.Add(header.ToLowerInvariant(), request.Headers.GetValues(header).First());
  294. var sb = new StringBuilder();
  295. var signedHeadersList = new List<string>();
  296. if (request.Headers.AllKeys.Contains("date"))
  297. {
  298. sb.Append("date:").Append(request.Headers.GetValues("date").First()).Append("\n");
  299. signedHeadersList.Add("date");
  300. }
  301. sb.Append("host:").Append(request.RequestUri.Host).Append("\n");
  302. signedHeadersList.Add("host");
  303. string contentType = request.ContentType;
  304. if (contentType != null)
  305. {
  306. sb.Append("content-type:").Append(contentType).Append("\n");
  307. signedHeadersList.Add("content-type");
  308. }
  309. if (request.Headers.AllKeys.Contains("range"))
  310. {
  311. sb.Append("range:").Append(request.Headers.GetValues("range").First()).Append("\n");
  312. signedHeadersList.Add("range");
  313. }
  314. // Create the string in the right format; this is what makes the headers "canonicalized" --
  315. // it means put in a standard format. http://en.wikipedia.org/wiki/Canonicalization
  316. foreach (var kvp in headers)
  317. {
  318. sb.Append(kvp.Key).Append(":");
  319. signedHeadersList.Add(kvp.Key);
  320. foreach (string hv in request.Headers.GetValues(kvp.Key))
  321. {
  322. sb.Append(hv);
  323. }
  324. sb.Append("\n");
  325. }
  326. signedHeaders = string.Join(";", signedHeadersList);
  327. return sb.ToString();
  328. }
  329. /// <summary>
  330. /// Calculate hash sha256 of content and convert it to hexadecimal string. This string will be returned and written to x-amz-content-sha256 header.
  331. /// </summary>
  332. /// <param name="request">Request to which header will be added</param>
  333. /// <param name="contentStream">Stream with content of request</param>
  334. /// <returns>String containing the encrypted and hexadecimal stream of the request content</returns>
  335. private string CalcPayloadHash(HttpWebRequest request, Stream contentStream)
  336. {
  337. string hash;
  338. if (request.Method == "PUT" && contentStream != null)
  339. {
  340. using (var memoryStream = new MemoryStream())
  341. {
  342. contentStream.CopyTo(memoryStream);
  343. byte[] content = memoryStream.ToArray();
  344. hash = sha256_hash(content);
  345. }
  346. }
  347. else
  348. {
  349. hash = EmptySha256;
  350. }
  351. return hash;
  352. }
  353. private static byte[] HmacSha256(string data, byte[] key)
  354. {
  355. var alg = KeyedHashAlgorithm.Create("HmacSHA256");
  356. alg.Key = key;
  357. return alg.ComputeHash(Encoding.UTF8.GetBytes(data));
  358. }
  359. }
  360. }