.NET Core驗證ASP.NET密碼
隨着.NET Core
的持續更新和完善,越來越多的機構已經選擇或者升級為.NET Core
。但由於技術不完全相同,不可能所有應用/數據庫都能無縫遷移,因此ASP.NET Core
和傳統ASP.NET
之間多少會存在一些挑戰,需要更多的漸進升級方法和交互。
其中,密碼共享就是升級到ASP.NET Core
一個很容易想到的漸進升級方式,也是一個需要解決的問題。
啥?什么都不用做?
其實如果堅持走ASP.NET Core Identity
這一套,代碼是寫了一個if/else
,能兼容老ASP.NET Identity
生成的密碼的,Github
鏈接:https://github.com/dotnet/aspnetcore/blob/8b7f6621695d93b2a55fb8a5b1be99c4af867ae4/src/Identity/Extensions.Core/src/PasswordHasher.cs#L185-L207,核心代碼如下:
/// <summary>
/// Returns a <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.
/// </summary>
/// <param name="user">The user whose password should be verified.</param>
/// <param name="hashedPassword">The hash value for a user's stored password.</param>
/// <param name="providedPassword">The password supplied for comparison.</param>
/// <returns>A <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.</returns>
/// <remarks>Implementations of this method should be time consistent.</remarks>
public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
{
if (hashedPassword == null)
{
throw new ArgumentNullException(nameof(hashedPassword));
}
if (providedPassword == null)
{
throw new ArgumentNullException(nameof(providedPassword));
}
byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword);
// read the format marker from the hashed password
if (decodedHashedPassword.Length == 0)
{
return PasswordVerificationResult.Failed;
}
switch (decodedHashedPassword[0])
{
case 0x00:
if (VerifyHashedPasswordV2(decodedHashedPassword, providedPassword))
{
// This is an old password hash format - the caller needs to rehash if we're not running in an older compat mode.
return (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3)
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
case 0x01:
int embeddedIterCount;
if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount))
{
// If this hasher was configured with a higher iteration count, change the entry now.
return (embeddedIterCount < _iterCount)
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
default:
return PasswordVerificationResult.Failed; // unknown format marker
}
}
它根據Base64
解碼后的第一個字節,來判斷是老版本還是新版本,然后調用各自不同的驗證函數。
但時代在變化,很多人已經不用官方提供的這一套Identity
驗證密碼,那么有什么“騷”操作,可以解密嗎?
和密碼剛正面
傳統的ASP.NET MVC
模板項目是通過ASP.NET Identity
管理的密碼,它由Rfc2898DeriveBytes
實現,該算法進行了1000
次循環SHA1
哈希、並加鹽,以確保難以通過傳統的哈希碰撞來確保密碼的安全性,其核心代碼如下(Github
鏈接:https://github.com/aspnet/AspNetIdentity/blob/9c48993a446288032f9824633e6dae81257da06e/src/Microsoft.AspNet.Identity.Core/Crypto.cs#L26-L46):
private const int PBKDF2IterCount = 1000; // default for Rfc2898DeriveBytes
private const int PBKDF2SubkeyLength = 256/8; // 256 bits
private const int SaltSize = 128/8; // 128 bits
/* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 0:
* PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
* (See also: SDL crypto guidelines v5.1, Part III)
* Format: { 0x00, salt, subkey }
*/
public static string HashPassword(string password)
{
if (password == null)
{
throw new ArgumentNullException("password");
}
// Produce a version 0 (see comment above) text hash.
byte[] salt;
byte[] subkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, PBKDF2IterCount))
{
salt = deriveBytes.Salt;
subkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
var outputBytes = new byte[1 + SaltSize + PBKDF2SubkeyLength];
Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, PBKDF2SubkeyLength);
return Convert.ToBase64String(outputBytes);
}
值得一提的是,代碼用到了Rfc2898DeriveBytes
,經常讀我的博客的知道,這個類是老朋友了,通過傳入明文密碼、鹽、迭代次數和算法,可以在不依賴於哈希算法安全性的前提下做單向加密,確保了密碼的不可(難以)破解性。
可見,密碼的原始字節由{0, salt, subkey}
三部分組成,其中鹽為128
位,即16
字節,subkey
為256
位,即32
字節,總共1+16+32=49
字節,密文需要轉換為Base64
,根據Base64
的信息量計算公式
Math.Log(64, 256)=0.75
可知,Base64
編碼相對原始字節的比例為0.75:1
,因此計算可得轉換為Base64
之后,其字符串長度為
Math.Ceiling(49 / 0.75 / 4) * 4 = 68
我們來從數據庫隨便查詢一個由ASP.NET Identity
創建的密碼:
SELECT TOP 1
PasswordHash,
LEN(PasswordHash) AS Len
FROM [User]
結果如下,可見結果長度真的為68
:
所以,說了這么多,怎么把密碼遷移到ASP.NET Core
呢?
注意看,上面的代碼沒什么特別,就是依賴於Rfc2898DeriveBytes
這個類。
.NET Core
內置了Rfc2898DeriveBytes
這個類,可以直接使用,不需要安裝任何NuGet
包,因此……直接復制粘貼上文中的【核心代碼】即可。
ASP.NET Core
中的Identity
有什么區別?
我就從簡引用一下關鍵注釋:
/* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 2:
* PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
* (See also: SDL crypto guidelines v5.1, Part III)
* Format: { 0x00, salt, subkey }
*
* Version 3:
* PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
原來,老ASP.NET Identity
中的密碼版本為V2
,當前ASP.NET Core Identity
中的密碼版本為V3
,首字節(版本號)從0x00
改成了0x01
,算法從HMACSHA1
升級為了HMACSHA256
,另外V3
版本還將迭代次數從1000
升級為10000
,另外還將算法名、迭代次數、鹽的長度信息保存在了密碼中。
另外需要注意的是,新ASP.NET Core Identity
的實現很有彈性,它通過依賴注入IOptions<T>
的形式,使得所有選項都是可以配置的。
解密示例
假如我有一個密碼:myF&TB9vhTx7
,我使用傳統的ASP.NET MVC
創建項目,然后用這個密碼注冊一個帳號:
然后在數據庫中將哈希值取出來:
SELECT PasswordHash
FROM [AspNetUsers]
WHERE [Email] = N'sdflysha@qq.com'
查得結果如下,結果為AJRvdJ1/Ii+58zU6yBrPJH4hCkMagxqK/W6oejAuG1hIrNPEQMAAyYynsXWwat9Huw==
:
我將上述代碼稍作簡化,用最簡單的代碼表達驗證密碼的過程,除去兩行注釋代碼(用於做斷言),整個過程只需7行代碼,代碼如下:
string pwdHashText = "AJRvdJ1/Ii+58zU6yBrPJH4hCkMagxqK/W6oejAuG1hIrNPEQMAAyYynsXWwat9Huw==";
string pwd = "myF&TB9vhTx7";
byte[] pwdHash = Convert.FromBase64String(pwdHashText);
// Debug.Assert(pwdHash.Length == 49);
// Debug.Assert(pwdHash[0] == 0);
byte[] salt = pwdHash[1..17];
byte[] hash = pwdHash[17..49];
using var r = new Rfc2898DeriveBytes(pwd, salt, 1000);
Console.WriteLine(r.GetBytes(32).SequenceEqual(hash));
此時,運行結果如下,運行顯示True
,表示驗證解碼成功:
總結
我總結一個表格如下所示:
ASP.NET Identity |
ASP.NET Core Identity |
|
---|---|---|
版本 | V2 | V3 |
首字節 | 0x00 |
0x01 |
默認算法 | HMACSHA1 |
HMACSHA256 |
迭代次數 | 1000 |
10000 |
密碼信息 | 版本+鹽+哈希值 | 版本+算法+迭代次數+鹽長度+鹽+哈希值 |
原始長度 | 1+16+32=49 | 1+4+4+4+16+32=61 |
Base64長度 | 68 | 84 |
可配置性 | 不可配置 | 可自由配置 |
安全性 | 好 | 非常好 |
密碼解密其實是前往ASP.NET Core
上較為輕松的一步,核心代碼也就7
行。
其實更有挑戰、也更有意思的是如何解傳統ASP.NET Identity
中的Cookie
,我將在下一篇中詳情分析這個主題,敬請期待!
喜歡的朋友請關注我的微信公眾號:【DotNet騷操作】
新年快到了,祝大家闔家歡樂,鼠年大吉!