using System.Security.Cryptography; using System.Text; namespace Journal.WebGateway.Security; public static class GatewayPasswordHasher { private const string AlgorithmName = "PBKDF2-SHA256"; private const int SaltSize = 16; private const int KeySize = 32; private const int IterationCount = 600_000; public static string HashPassword(string password) { ArgumentException.ThrowIfNullOrWhiteSpace(password); var salt = RandomNumberGenerator.GetBytes(SaltSize); var key = Rfc2898DeriveBytes.Pbkdf2( Encoding.UTF8.GetBytes(password), salt, IterationCount, HashAlgorithmName.SHA256, KeySize); return string.Join( '$', AlgorithmName, IterationCount.ToString(System.Globalization.CultureInfo.InvariantCulture), Convert.ToBase64String(salt), Convert.ToBase64String(key)); } public static bool VerifyPassword(string password, string passwordHash) { if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(passwordHash)) return false; var parts = passwordHash.Split('$', StringSplitOptions.None); if (parts.Length != 4) return false; if (!string.Equals(parts[0], AlgorithmName, StringComparison.Ordinal)) return false; if (!int.TryParse(parts[1], out var iterations) || iterations <= 0) return false; byte[] salt; byte[] expectedKey; try { salt = Convert.FromBase64String(parts[2]); expectedKey = Convert.FromBase64String(parts[3]); } catch (FormatException) { return false; } var actualKey = Rfc2898DeriveBytes.Pbkdf2( Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, expectedKey.Length); return CryptographicOperations.FixedTimeEquals(actualKey, expectedKey); } }