using System.Security.Cryptography; using Microsoft.AspNetCore.Cryptography.KeyDerivation; namespace EnotaryoPH.Web.Common.Services { public class PasswordService : IPasswordService { private readonly int _iterCount = 100_000; private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); public string HashPassword(string password) => Convert.ToBase64String(HashPasswordV3(password, _rng)); public bool VerifyHashedPassword(string hashedPassword, string providedPassword) { var decodedHashedPassword = Convert.FromBase64String(hashedPassword); return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out var embeddedIterCount, out var prf); } private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) { // Produce a version 3 (see comment above) text hash. var salt = new byte[saltSize]; rng.GetBytes(salt); var subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); var outputBytes = new byte[13 + salt.Length + subkey.Length]; outputBytes[0] = 0x01; // format marker WriteNetworkByteOrder(outputBytes, 1, (uint)prf); WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); return outputBytes; } private static uint ReadNetworkByteOrder(byte[] buffer, int offset) => ((uint)(buffer[offset + 0]) << 24) | ((uint)(buffer[offset + 1]) << 16) | ((uint)(buffer[offset + 2]) << 8) | buffer[offset + 3]; private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount, out KeyDerivationPrf prf) { iterCount = default; prf = default; try { // Read header information prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); var saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); // Read the salt: must be >= 128 bits if (saltLength < 128 / 8) { return false; } var salt = new byte[saltLength]; Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); // Read the subkey (the rest of the payload): must be >= 128 bits var subkeyLength = hashedPassword.Length - 13 - salt.Length; if (subkeyLength < 128 / 8) { return false; } var expectedSubkey = new byte[subkeyLength]; Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); // Hash the incoming password and verify it var actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey); } catch { // This should never occur except in the case of a malformed payload, where // we might go off the end of the array. Regardless, a malformed payload // implies verification failed. return false; } } 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); } private byte[] HashPasswordV3(string password, RandomNumberGenerator rng) => HashPasswordV3(password, rng, prf: KeyDerivationPrf.HMACSHA512, iterCount: _iterCount, saltSize: 128 / 8, numBytesRequested: 256 / 8); } }