Keywords: ASP.NET Identity | Password Hashing | PBKDF2 | Salt | Security
Abstract: This article provides an in-depth exploration of the implementation mechanisms and security of the default password hasher in the ASP.NET Identity framework. By analyzing its implementation based on the RFC 2898 key derivation function (PBKDF2), it explains in detail the generation and storage of random salts, the hash verification process, and evaluates its resistance to brute-force and rainbow table attacks. Code examples illustrate the specific steps of hash generation and verification, helping developers understand how to securely store user passwords.
Fundamentals of Password Hashing and Security Requirements
In modern web applications, securely storing user passwords is a core task of authentication systems. Storing plaintext passwords directly is highly insecure, as a database breach would allow attackers to obtain all user credentials. Therefore, passwords must be hashed—transformed into fixed-length strings via one-way functions, making it computationally infeasible to reverse the hash to the original password.
However, simple hash functions (e.g., MD5, SHA-1) are vulnerable to rainbow table attacks, where attackers precompute hashes of common passwords for matching. To counter this, modern password hashing schemes introduce a salt—a randomly generated string combined with the password before hashing, ensuring that even identical passwords produce different hashes. Additionally, key derivation functions (e.g., PBKDF2, bcrypt, scrypt) increase computational cost through multiple iterations, effectively slowing down brute-force attacks.
Implementation Mechanism of ASP.NET Identity's Default Password Hasher
The ASP.NET Identity framework provides password hashing functionality through the IPasswordHasher interface, with its default implementation based on the PBKDF2 algorithm defined in RFC 2898. This algorithm uses HMAC-SHA1 as a pseudorandom function, enhancing security through multiple iterations. The following code illustrates the core logic of the hash generation process:
public static string HashPassword(string password)
{
byte[] salt;
byte[] buffer2;
if (password == null)
{
throw new ArgumentNullException("password");
}
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
{
salt = bytes.Salt;
buffer2 = bytes.GetBytes(0x20);
}
byte[] dst = new byte[0x31];
Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
return Convert.ToBase64String(dst);
}
In this implementation, the salt is not static but randomly generated each time hashing occurs. Specifically, the Rfc2898DeriveBytes constructor automatically generates a 16-byte (0x10) random salt when no salt parameter is provided. The hash output consists of three parts: a version identifier byte (value 0), 16 bytes of salt, and 32 bytes (0x20) of derived key, totaling 49 bytes (0x31). This design bundles the salt with the hash result, eliminating the need for separate salt database management.
Detailed Hash Verification Process
During password verification, the system extracts the salt from the stored hash string, recalculates the derived key, and compares it with the stored value. The verification code is as follows:
public static bool VerifyHashedPassword(string hashedPassword, string password)
{
byte[] buffer4;
if (hashedPassword == null)
{
return false;
}
if (password == null)
{
throw new ArgumentNullException("password");
}
byte[] src = Convert.FromBase64String(hashedPassword);
if ((src.Length != 0x31) || (src[0] != 0))
{
return false;
}
byte[] dst = new byte[0x10];
Buffer.BlockCopy(src, 1, dst, 0, 0x10);
byte[] buffer3 = new byte[0x20];
Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8))
{
buffer4 = bytes.GetBytes(0x20);
}
return ByteArraysEqual(buffer3, buffer4);
}
The verification process first checks the format and version identifier of the hash string to ensure compatibility. It then extracts the salt (positions 1-16) and stored derived key (positions 17-48), recalculating the key using the same iteration count (1000 iterations, i.e., 0x3e8). If the newly generated key matches the stored value, password verification succeeds. This design ensures that even if attackers obtain the hash database, they cannot directly crack passwords, as each password has a unique salt, and PBKDF2's high iteration count significantly increases cracking costs.
Security Evaluation and Best Practices
ASP.NET Identity's default hasher performs well in terms of security, primarily体现在以下几个方面:首先,随机盐值有效防御彩虹表攻击,确保相同密码产生不同哈希值。其次,PBKDF2算法通过1000次迭代增加计算开销,减缓暴力破解速度。此外,输出格式包含版本标识,便于未来算法升级时保持向后兼容。
However, as hardware performance improves, 1000 iterations may no longer suffice against high-end GPU or ASIC attacks. It is recommended to increase the iteration count where resources allow, or consider migrating to more hardware-resistant algorithms (e.g., Argon2). Developers can adjust parameters by implementing a custom IPasswordHasher, but must ensure new configurations do not affect existing user verification.
In actual deployments, other security measures should be combined, such as enforcing strong password policies, implementing account lockout mechanisms, and monitoring abnormal login attempts. Password hashing is only one layer of defense-in-depth; a comprehensive security strategy is essential to effectively protect user data.