ASP.NET Core 8.0 - Password Hasher
This article will describe the implementation of a custom password hasher. You should review the earlier articles of the Cookies And Claims Project series. Registered users can download the ASP.NET Core 8.0 - Cookies And Claims Project for free.
Cookies And Claims Project and Article Series
Free project download for registered users!
I developed the Cookies And Claims Project (CACP) to demonstrate a simple cookie authentication scheme and claim-based authorization with a clear and modifiable design. The CACP is developed with Visual Studio 2022 and the MS Long Term Support (LTS) version .NET 8.0 framework. All Errors, Warnings, and Messages from Code Analysis have been mitigated. The CACP implements utilities like an AES Cipher, Password Hasher, native JavaScript client form validation, password requirements UI, Bootstrap Native message modal generator, loading/spinner generator, SignalR online user count, and an automatic idle logout for users with administration permissions.
- ASP.NET Core 8.0 - Cookies And Claims
- ASP.NET Core 8.0 - Cookie Authentication
- ASP.NET Core 8.0 - Remember Me Or Not
- ASP.NET Core 8.0 - Authorized Access
- ASP.NET Core 8.0 - Administrator Claim
- ASP.NET Core 8.0 - Admin Idle Logout
- ASP.NET Core 8.0 - Cookie Consent
- ASP.NET Core 8.0 - SignalR Online User Count
- ASP.NET Core 8.0 - AES Cipher
- ASP.NET Core 8.0 - Password Hasher
- ASP.NET Core 8.0 - Message Generator
The CACP implements a group of utilities: AES Cipher, Password Hasher, Password Validation, Message Generator, and Spinner Generator. This article describes the Password Hasher. The Net 5.0 and Net 6.0 Identity V3 specification implemented SHA256 with 10000 iterations. Net 7.0 and above implements an upgraded V3 specification with SHA512 and 100000 iterations. The utility is implemented with the PasswordHasherV3 and PasswordHasherV3VerificationResult classes. The PasswordHasherV3 class implements the Identity V3 new specification password hashing and updates the hash from the old V3 specification if detected.
PasswordHasherV3.cs:
// Copyright © Ken Haggerty (https://kenhaggerty.com) // Licensed under the MIT License. namespace CookiesAndClaims.Services; /// <summary> /// Implements the Identity V3 new specification password hashing and updating the hash from the old specification. /// </summary> /// <remarks> /// Password Hash Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } /// (All UInt32s are stored big-endian.) /// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. /// Net 7.0 and above implements an upgraded V3 specification with SHA512 and 100000 iterations. /// PBKDF2 with HMAC-SHA512, 128-bit salt, 256-bit subkey, 100000 iterations. /// https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs /// https://github.com/dotnet/aspnetcore/pull/40987 /// </remarks> public static class PasswordHasherV3 { /// <summary> /// IdentityV3 Format Marker /// </summary> private const byte _formatMarker = (byte)0x01; /// <summary> /// Pseudorandom Function /// https://en.wikipedia.org/wiki/Pseudorandom_function_family /// </summary> private const KeyDerivationPrf _prf = KeyDerivationPrf.HMACSHA512; private const KeyDerivationPrf _prfOld = KeyDerivationPrf.HMACSHA256; private const string _prfSHA512String = "HMACSHA512"; private const string _prfSHA256String = "HMACSHA256"; private const int _saltByteLength = 16; private const int _subkeyByteLength = 32; private const int _iterations = 100000; private const int _iterationsOld = 10000; private const int _headerByteLength = 13; /// <summary> /// Creates a hashed representation of the supplied <paramref name="password" />. /// </summary> /// <param name="password">The password to hash.</param> /// <returns> /// A <see cref="string" /> with a hashed representation of the supplied <paramref name="password" />. /// </returns> public static string HashPassword(string password) { if (string.IsNullOrEmpty(password)) throw new ArgumentNullException(nameof(password)); // Create a random salt byte[] salt = new byte[_saltByteLength]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(salt); byte[] subkey = new byte[_subkeyByteLength]; subkey = KeyDerivation.Pbkdf2(password, salt, _prf, _iterations, _subkeyByteLength); var outputBytes = new byte[_headerByteLength + _saltByteLength + _subkeyByteLength]; outputBytes[0] = _formatMarker; WriteNetworkByteOrder(outputBytes, 1, (uint)_prf); WriteNetworkByteOrder(outputBytes, 5, (uint)_iterations); WriteNetworkByteOrder(outputBytes, 9, (uint)_saltByteLength); Buffer.BlockCopy(salt, 0, outputBytes, _headerByteLength, _saltByteLength); Buffer.BlockCopy(subkey, 0, outputBytes, _headerByteLength + _saltByteLength, _subkeyByteLength); return Convert.ToBase64String(outputBytes); } /// <summary> /// Compares the <paramref name="enteredPassword" /> against the <paramref name="hashedPassword" />. /// </summary> /// <param name="hashedPassword">The hash value for a user's stored password.</param> /// <param name="enteredPassword">The password supplied for comparison.</param> /// <returns> /// A <see cref="PasswordHasherV3VerificationResult" /> indicating the result of a password hash comparison. /// </returns> public static PasswordHasherV3VerificationResult VerifyPassword(string hashedPassword, string enteredPassword) { if (string.IsNullOrEmpty(hashedPassword)) return PasswordHasherV3VerificationResult.Failed("Hashed password is null or empty."); if (string.IsNullOrEmpty(enteredPassword)) return PasswordHasherV3VerificationResult.Failed("Entered password is null or empty."); byte[] decodedHashedPassword; try { decodedHashedPassword = Convert.FromBase64String(hashedPassword); } catch { return PasswordHasherV3VerificationResult.Failed("Hashed password failed to decode."); } if (decodedHashedPassword.Length == 0) return PasswordHasherV3VerificationResult.Failed("Decoded password length = 0."); // Verify IdentityV3 Format Marker if (decodedHashedPassword[0] != _formatMarker) return PasswordHasherV3VerificationResult.Failed("IdentityV3 Format Marker is invalid."); byte[] salt = new byte[_saltByteLength]; byte[] expectedSubkey = new byte[_subkeyByteLength]; byte[] verifySubkey = new byte[_subkeyByteLength]; try { // Read header information uint prfUIntRead = (uint)ReadNetworkByteOrder(decodedHashedPassword, 1); int iterCountRead = (int)ReadNetworkByteOrder(decodedHashedPassword, 5); int saltSizeRead = (int)ReadNetworkByteOrder(decodedHashedPassword, 9); // Verify IdentityV3 HMACSHA512 if (prfUIntRead != (uint)_prf) { // If KeyDerivationPrf identifier = HMACSHA256, verify with the old specification. if (prfUIntRead == (uint)_prfOld) { // Verify Old iteration count. if (iterCountRead != _iterationsOld) return PasswordHasherV3VerificationResult.Failed("Old iteration count is invalid."); // Verify salt length if (saltSizeRead != _saltByteLength) return PasswordHasherV3VerificationResult.Failed("Old salt length is invalid."); // Read the salt Buffer.BlockCopy(decodedHashedPassword, _headerByteLength, salt, 0, _saltByteLength); // Get the hashed subkey (the rest of the payload) int subkeyLengthRead = decodedHashedPassword.Length - _headerByteLength - _saltByteLength; // Verify subkey length if (subkeyLengthRead != _subkeyByteLength) return PasswordHasherV3VerificationResult.Failed("Old subkey length is invalid."); // Read the hashed subkey Buffer.BlockCopy(decodedHashedPassword, _headerByteLength + _saltByteLength, expectedSubkey, 0, _subkeyByteLength); // Hash the incoming password with original salt verifySubkey = KeyDerivation.Pbkdf2(enteredPassword, salt, _prfOld, _iterationsOld, _subkeyByteLength); // Compare the subkeys if (!ByteArraysEqual(verifySubkey, expectedSubkey)) return PasswordHasherV3VerificationResult.Failed("Old subkeys do not match."); // Return a Succeeded result with an updated password hash return PasswordHasherV3VerificationResult.Rehashed(HashPassword(enteredPassword)); } return PasswordHasherV3VerificationResult.Failed("KeyDerivationPrf identifier is invalid."); } // Verify iteration count if (iterCountRead != _iterations) return PasswordHasherV3VerificationResult.Failed("Iteration count is invalid."); // Verify salt length if (saltSizeRead != _saltByteLength) return PasswordHasherV3VerificationResult.Failed("Salt length is invalid."); // Read the salt Buffer.BlockCopy(decodedHashedPassword, _headerByteLength, salt, 0, _saltByteLength); // Get the hashed subkey (the rest of the payload) int subkeyLength = decodedHashedPassword.Length - _headerByteLength - _saltByteLength; // Verify subkey length if (_subkeyByteLength != subkeyLength) return PasswordHasherV3VerificationResult.Failed("Subkey length is invalid."); // Read hashed the subkey Buffer.BlockCopy(decodedHashedPassword, _headerByteLength + _saltByteLength, expectedSubkey, 0, _subkeyByteLength); // Hash the incoming password with original salt verifySubkey = KeyDerivation.Pbkdf2(enteredPassword, salt, _prf, _iterations, _subkeyByteLength); // Compare subkeys for equal if (!ByteArraysEqual(verifySubkey, expectedSubkey)) return PasswordHasherV3VerificationResult.Failed("Subkeys do not match."); // Return a Succeeded result return PasswordHasherV3VerificationResult.Success; } catch { // This should never occur except in the case of a malformed payload. return PasswordHasherV3VerificationResult.Failed("Hashed password has a malformed payload."); } } /// <summary> /// Extracts the <see cref="KeyDerivationPrf" /> for the <paramref name="hashedPassword" />. /// </summary> /// <param name="hashedPassword">The hash value for a user's stored password.</param> /// <returns> /// A <see cref="string" /> indicating the embedded prf code, if found. /// </returns> public static string GetPasswordHashPRF(string? hashedPassword) { if (string.IsNullOrEmpty(hashedPassword)) return "Hashed password is null or empty."; byte[] decodedHashedPassword; try { decodedHashedPassword = Convert.FromBase64String(hashedPassword); } catch { return "ERROR: Hashed password failed to decode."; } if (decodedHashedPassword.Length == 0) return "ERROR: Decoded password length = 0."; try { // Read header information uint prfUIntRead = (uint)ReadNetworkByteOrder(decodedHashedPassword, 1); // Verify IdentityV3 HMACSHA512 if (prfUIntRead == (uint)_prf) return _prfSHA512String; // If KeyDerivationPrf identifier = HMACSHA256, verify with the old specification. if (prfUIntRead == (uint)_prfOld) return _prfSHA256String; return "ERROR: KeyDerivationPrf identifier is invalid."; } catch { // This should never occur except in the case of a malformed payload. return "ERROR: Hashed password has a malformed payload."; } } // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] private static bool ByteArraysEqual(byte[] a, byte[] b) { if (a == null && b == null) return true; if (a == null || b == null || a.Length != b.Length) return false; var areSame = true; for (var i = 0; i < a.Length; i++) { areSame &= (a[i] == b[i]); }; return areSame; } private static uint ReadNetworkByteOrder(byte[] buffer, int offset) { return ((uint)(buffer[offset + 0]) << 24) | ((uint)(buffer[offset + 1]) << 16) | ((uint)(buffer[offset + 2]) << 8) | ((uint)(buffer[offset + 3])); } private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) { buffer[offset + 0] = (byte)(value >> 24); buffer[offset + 1] = (byte)(value >> 16); buffer[offset + 2] = (byte)(value >> 8); buffer[offset + 3] = (byte)(value >> 0); } }
Notice the GetPasswordHashPRF method. You cannot rehash the password without the original password, but you can extract the embedded Pseudorandom Function identifier.
string passwordHashPRF = PasswordHasherV3.GetPasswordHashPRF(appUser.PasswordHash);
Comments(0)