.NET 6 中哈希算法的簡化用法
本文轉(zhuǎn)載自微信公眾號「amazingdotnet」,作者 WeihanLi 。轉(zhuǎn)載本文請聯(lián)系amazingdotnet公眾號。
Intro
微軟在 .NET 6 中引入一些更簡單的 API 來使用 HMAC 哈希算法(MD5/SHA1/SHA256/SHA384/SHA512)
微軟的叫法叫做 HMAC One-Shoot method, HMAC 算法在普通的哈希算法基礎(chǔ)上增加了一個 key,通過 key 提升了安全性,能夠有效避免密碼泄露被彩虹表反推出真實密碼, JWT(Json Web Token) 除了可以使用 RSA 方式外也支持使用 HMAC 。
New API
新增的 API 定義如下:
- namespace System.Security.Cryptography {
- public partial class HMACMD5 {
- public static byte[] HashData(byte[] key, byte[] source);
- public static byte[] HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source);
- public static int HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination);
- public static bool TryHashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);
- }
- public partial class HMACSHA1 {
- public static byte[] HashData(byte[] key, byte[] source);
- public static byte[] HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source);
- public static int HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination);
- public static bool TryHashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);
- }
- public partial class HMACSHA256 {
- public static byte[] HashData(byte[] key, byte[] source);
- public static byte[] HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source);
- public static int HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination);
- public static bool TryHashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);
- }
- public partial class HMACSHA384 {
- public static byte[] HashData(byte[] key, byte[] source);
- public static byte[] HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source);
- public static int HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination);
- public static bool TryHashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);
- }
- public partial class HMACSHA512 {
- public static byte[] HashData(byte[] key, byte[] source);
- public static byte[] HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source);
- public static int HashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination);
- public static bool TryHashData(ReadOnlySpan<byte> key, ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);
- }
- }
Sample Before
在之前的版本中想要實現(xiàn)計算 HMAC 算法會比較復(fù)雜,之前實現(xiàn)了一個 HashHelper 來封裝了常用的 Hash 算法和 HMAC 算法,HashHelper 部分代碼如下,完整代碼可以從 Github 獲取:https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Helpers/HashHelper.cs
- /// <summary>
- /// 獲取哈希之后的字符串
- /// </summary>
- /// <param name="type">哈希類型</param>
- /// <param name="source">源</param>
- /// <param name="key">key</param>
- /// <param name="isLower">是否是小寫</param>
- /// <returns>哈希算法處理之后的字符串</returns>
- public static string GetHashedString(HashType type, byte[] source, byte[]? key, bool isLower = false)
- {
- Guard.NotNull(source, nameof(source));
- if (source.Length == 0)
- {
- return string.Empty;
- }
- var hashedBytes = GetHashedBytes(type, source, key);
- var sbText = new StringBuilder();
- if (isLower)
- {
- foreach (var b in hashedBytes)
- {
- sbText.Append(b.ToString("x2"));
- }
- }
- else
- {
- foreach (var b in hashedBytes)
- {
- sbText.Append(b.ToString("X2"));
- }
- }
- return sbText.ToString();
- }
- /// <summary>
- /// 計算字符串Hash值
- /// </summary>
- /// <param name="type">hash類型</param>
- /// <param name="str">要hash的字符串</param>
- /// <returns>hash過的字節(jié)數(shù)組</returns>
- public static byte[] GetHashedBytes(HashType type, string str) => GetHashedBytes(type, str, Encoding.UTF8);
- /// <summary>
- /// 計算字符串Hash值
- /// </summary>
- /// <param name="type">hash類型</param>
- /// <param name="str">要hash的字符串</param>
- /// <param name="encoding">編碼類型</param>
- /// <returns>hash過的字節(jié)數(shù)組</returns>
- public static byte[] GetHashedBytes(HashType type, string str, Encoding encoding)
- {
- Guard.NotNull(str, nameof(str));
- if (str == string.Empty)
- {
- return Array.Empty<byte>();
- }
- var bytes = encoding.GetBytes(str);
- return GetHashedBytes(type, bytes);
- }
- /// <summary>
- /// 獲取Hash后的字節(jié)數(shù)組
- /// </summary>
- /// <param name="type">哈希類型</param>
- /// <param name="bytes">原字節(jié)數(shù)組</param>
- /// <returns></returns>
- public static byte[] GetHashedBytes(HashType type, byte[] bytes) => GetHashedBytes(type, bytes, null);
- /// <summary>
- /// 獲取Hash后的字節(jié)數(shù)組
- /// </summary>
- /// <param name="type">哈希類型</param>
- /// <param name="key">key</param>
- /// <param name="bytes">原字節(jié)數(shù)組</param>
- /// <returns></returns>
- public static byte[] GetHashedBytes(HashType type, byte[] bytes, byte[]? key)
- {
- Guard.NotNull(bytes, nameof(bytes));
- if (bytes.Length == 0)
- {
- return bytes;
- }
- HashAlgorithm algorithm = null!;
- try
- {
- if (key == null)
- {
- algorithm = type switch
- {
- HashType.SHA1 => new SHA1Managed(),
- HashType.SHA256 => new SHA256Managed(),
- HashType.SHA384 => new SHA384Managed(),
- HashType.SHA512 => new SHA512Managed(),
- _ => MD5.Create()
- };
- }
- else
- {
- algorithm = type switch
- {
- HashType.SHA1 => new HMACSHA1(key),
- HashType.SHA256 => new HMACSHA256(key),
- HashType.SHA384 => new HMACSHA384(key),
- HashType.SHA512 => new HMACSHA512(key),
- _ => new HMACMD5(key)
- };
- }
- return algorithm.ComputeHash(bytes);
- }
- finally
- {
- algorithm.Dispose();
- }
- }
使用示例如下:
- HashHelper.GetHashedBytes(HashType.MD5, "test");
- HashHelper.GetHashedBytes(HashType.MD5, "test".GetBytes());
- HashHelper.GetHashedBytes(HashType.MD5, "test", "testKey");
- HashHelper.GetHashedBytes(HashType.MD5, "test".GetBytes(), "testKey".GetBytes());
- HashHelper.GetHashedString(HashType.MD5, "test");
- HashHelper.GetHashedString(HashType.SHA1, "test".GetBytes());
- HashHelper.GetHashedString(HashType.SHA256, "test", "testKey");
- HashHelper.GetHashedString(HashType.MD5, "test".GetBytes(), "testKey".GetBytes());
New API Sample
有了新的 API 以后可以怎么簡化呢,來看下面的示例:
- var bytes = "test".GetBytes();
- var keyBytes = "test-key".GetBytes();
- // HMACMD5
- var hmd5V1 = HMACMD5.HashData(keyBytes, bytes);
- var hmd5V2 = HashHelper.GetHashedBytes(HashType.MD5, bytes, keyBytes);
- Console.WriteLine(hmd5V2.SequenceEqual(hmd5V1));
- // HMACSHA1
- var hsha1V1 = HMACSHA1.HashData(keyBytes, bytes);
- var hsha1V2 = HashHelper.GetHashedBytes(HashType.SHA1, bytes, keyBytes);
- Console.WriteLine(hsha1V2.SequenceEqual(hsha1V1));
- // HMACSHA256
- var hsha256V1 = HMACSHA256.HashData(keyBytes, bytes);
- var hsha256V2 = HashHelper.GetHashedBytes(HashType.SHA256, bytes, keyBytes);
- Console.WriteLine(hsha256V2.SequenceEqual(hsha256V1));
- // HMACSHA384
- var hsha384V1 = HMACSHA384.HashData(keyBytes ,bytes);
- var hsha384V2 = HashHelper.GetHashedBytes(HashType.SHA384, bytes, keyBytes);
- Console.WriteLine(hsha384V2.SequenceEqual(hsha384V1));
- // HMACSHA512
- var hsha512V1 = HMACSHA512.HashData(keyBytes ,bytes);
- var hsha512V2 = HashHelper.GetHashedBytes(HashType.SHA512, bytes, keyBytes);
- Console.WriteLine(hsha512V2.SequenceEqual(hsha512V1));
直接使用對應(yīng)的 HMAC 哈希算法的 HashData 方法即可,傳入對應(yīng)的 key 和 原始內(nèi)容就可以了,上面是和我們 HashHelper 封裝的方法進(jìn)行對比,看結(jié)果是否一致,都是一致的,輸出結(jié)果如下:
More
對于普通的哈希算法,微軟其實在 .NET 5 就已經(jīng)支持了上面的用法,可以嘗試一下下面的代碼:
- var bytes = "test".GetBytes();
- // MD5
- var md5V1 = MD5.HashData(bytes);
- var md5V2 = HashHelper.GetHashedBytes(HashType.MD5, bytes);
- Console.WriteLine(md5V2.SequenceEqual(md5V1));
- // SHA1
- var sha1V1 = SHA1.HashData(bytes);
- var sha1V2 = HashHelper.GetHashedBytes(HashType.SHA1, bytes);
- Console.WriteLine(sha1V2.SequenceEqual(sha1V1));
- // SHA256
- var sha256V1 = SHA256.HashData(bytes);
- var sha256V2 = HashHelper.GetHashedBytes(HashType.SHA256, bytes);
- Console.WriteLine(sha256V2.SequenceEqual(sha256V1));
- // SHA384
- var sha384V1 = SHA384.HashData(bytes);
- var sha384V2 = HashHelper.GetHashedBytes(HashType.SHA384, bytes);
- Console.WriteLine(sha384V2.SequenceEqual(sha384V1));
- // SHA512
- var sha512V1 = SHA512.HashData(bytes);
- var sha512V2 = HashHelper.GetHashedBytes(HashType.SHA512, bytes);
- Console.WriteLine(sha512V2.SequenceEqual(sha512V1));
很多時候我們可能都會要使用 MD5 或者 SHA1 之后的字符串,不知道為什么微軟沒有直接獲取一個字符串的方法,如果有這樣一個方法,就會更方便了,相比之后,感覺還是自己封裝的 HashHelper 使用起來更舒服一些,哈哈,這樣的靜態(tài)方法不夠抽象如果要動態(tài)替換哈希算法代碼可能就有點...
References
- https://github.com/dotnet/runtime/pull/53487
- https://github.com/dotnet/runtime/issues/40012
- https://github.com/dotnet/core/issues/6569#issuecomment-913876347
- https://baike.baidu.com/item/hmac/7307543?fr=aladdin
- https://github.com/WeihanLi/SamplesInPractice/blob/master/net6sample/HashSample/Program.cs
- https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Helpers/HashHelper.cs