Keywords: Password Hashing | C# Security | PBKDF2 | Salt | Password Storage
Abstract: This article provides an in-depth exploration of secure password hashing implementation in C#, analyzing the security flaws of traditional hashing algorithms like MD5 and SHA1, and detailing modern password hashing schemes based on PBKDF2. Through comprehensive code examples, it demonstrates the complete process of salt generation, key derivation, hash storage, and verification, while discussing critical security considerations such as iteration count selection and algorithm upgrade strategies. The article also presents a practical SecurePasswordHasher class implementation to help developers build more secure password storage systems.
Fundamental Concepts and Security Requirements of Password Hashing
In modern application development, secure password storage forms the foundation of system security. Unlike encryption, hashing is a one-way function designed to generate fixed-length output from input data while preventing reverse computation from output to input. This characteristic makes hashing an ideal choice for password storage.
However, simple hash functions like MD5 and SHA1 are no longer secure in modern computing environments. These algorithms suffer from several critical issues: excessive computational speed makes brute-force attacks feasible; lack of salt mechanisms makes them vulnerable to rainbow table attacks; fixed output lengths limit security scalability.
Security Flaws in Traditional Hashing Methods
Early password hashing typically employed MD5 or SHA1 algorithms, as shown in the following code:
// Insecure MD5 hashing example
var md5 = new MD5CryptoServiceProvider();
var data = Encoding.ASCII.GetBytes(password);
var md5data = md5.ComputeHash(data);
var hashedPassword = ASCIIEncoding.GetString(md5data);
This approach contains serious security vulnerabilities. First, the MD5 algorithm has been proven to have collision vulnerabilities, allowing attackers to find different inputs that produce identical hash values. Second, the absence of salt means identical passwords always produce identical hashes, enabling attackers to use precomputed rainbow tables for rapid password cracking. Finally, ASCII encoding may cause data loss, further reducing security.
Best Practices for Modern Password Hashing
Based on current security standards, Password-Based Key Derivation Function 2 (PBKDF2) is recommended. This algorithm enhances security through several mechanisms: using cryptographically secure random number generators to generate salts; increasing computational cost through numerous iterations; combining salt and password to generate the final hash.
In C#, PBKDF2 is implemented through the Rfc2898DeriveBytes class. The following demonstrates the basic implementation steps:
// Step 1: Generate cryptographically secure salt
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);
// Step 2: Generate hash using PBKDF2
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
// Step 3: Combine salt and hash for storage
byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);
// Step 4: Convert to Base64 string for storage
string savedPasswordHash = Convert.ToBase64String(hashBytes);
Complete Password Hashing Utility Class Implementation
For practical use in real projects, a comprehensive password hashing utility class can be encapsulated:
public static class SecurePasswordHasher
{
private const int SaltSize = 16;
private const int HashSize = 20;
public static string Hash(string password, int iterations)
{
// Generate cryptographically secure salt
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);
// Generate hash using PBKDF2
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
var hash = pbkdf2.GetBytes(HashSize);
// Combine salt and hash
var hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
// Convert to Base64 and add metadata
var base64Hash = Convert.ToBase64String(hashBytes);
return string.Format("&MYHASH&V1&{0}&{1}", iterations, base64Hash);
}
public static string Hash(string password)
{
return Hash(password, 10000);
}
public static bool Verify(string password, string hashedPassword)
{
// Check hash format support
if (!hashedPassword.Contains("&MYHASH&V1&"))
{
throw new NotSupportedException("The hashtype is not supported");
}
// Extract iteration count and Base64 hash
var splittedHashString = hashedPassword.Replace("&MYHASH&V1&", "").Split('&');
var iterations = int.Parse(splittedHashString[0]);
var base64Hash = splittedHashString[1];
// Get hash byte array
var hashBytes = Convert.FromBase64String(base64Hash);
// Extract salt
var salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);
// Compute hash for input password with same parameters
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
byte[] hash = pbkdf2.GetBytes(HashSize);
// Compare hash values
for (var i = 0; i < HashSize; i++)
{
if (hashBytes[i + SaltSize] != hash[i])
{
return false;
}
}
return true;
}
}
Secure Implementation of Password Verification
The password verification process must strictly correspond to the hash generation process:
// Retrieve hash value from storage
string savedPasswordHash = DBContext.GetUser(u => u.UserName == user).Password;
// Extract byte array
byte[] hashBytes = Convert.FromBase64String(savedPasswordHash);
// Extract salt
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
// Compute hash for user-input password
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
// Securely compare hash values
for (int i = 0; i < 20; i++)
if (hashBytes[i + 16] != hash[i])
throw new UnauthorizedAccessException();
This byte-by-byte comparison approach effectively prevents timing attacks, avoiding the possibility of inferring password correctness through comparison time differences.
Iteration Count Selection and Performance Considerations
The iteration count in PBKDF2 represents a critical balance between security and performance. Higher iteration counts significantly increase the cost of brute-force attacks but also increase verification time for legitimate users.
Based on current computing capabilities, recommended iteration counts range from 10,000 to 100,000. Specific selection should consider: application security requirements, target platform performance, and user experience tolerance. On mobile devices like Windows Phone 7, lower iteration counts (such as 10,000) may be necessary to ensure responsive performance.
The utility class design includes iteration counts in the hash output, providing convenience for future algorithm upgrades. When computing power increases or security standards change, iteration counts can be increased without affecting existing user password verification.
Security Storage and Transmission Considerations
Beyond proper hash algorithm selection, the following security practices should be observed: never log passwords or hash values in logs or error messages; use secure transmission protocols (like HTTPS) for password transmission; regularly review and update security configurations; consider using professional password management libraries or services.
By adopting modern password hashing schemes based on PBKDF2, combined with appropriate salt management and iteration count configuration, password storage security can be significantly enhanced, effectively defending against various password cracking attacks.