using System.Security.Cryptography; namespace GB5Shared.EncryptionHelper { // One-way password hashing (PBKDF2-SHA256) — distinct from PasswordEncryption.cs, which is // reversible AES-GCM encryption for a different purpose. This is the shared, GB5Shared-hosted // equivalent of DXPBLL.Auth.DXPPasswordHasher (GB5Solution/DXP/DXPBLL/Auth/DXPPasswordHasher.cs) // — same algorithm/parameters, promoted here so other modules (starting with Entitlement's // client-facing auth) can use it without taking a dependency on DXP. DXPPasswordHasher itself // is left untouched; it is not modified or rewired to call into this class. // // Uses .NET's built-in Rfc2898DeriveBytes (no external dependency). Self-describing stored // format: "{iterations}.{saltBase64}.{hashBase64}" — the iteration count travels with the hash, // so raising Iterations later does not invalidate passwords hashed under a lower count; they // keep verifying against their own stored iteration count until rehashed. public static class PasswordHasher { private const int SaltSize = 16; private const int HashSize = 32; private const int Iterations = 210_000; // OWASP-recommended minimum for PBKDF2-SHA256 (2023+) public static string Hash(string password) { var salt = RandomNumberGenerator.GetBytes(SaltSize); var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, HashSize); return $"{Iterations}.{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}"; } public static bool Verify(string password, string storedHash) { var parts = storedHash.Split('.', 3); if (parts.Length != 3) return false; if (!int.TryParse(parts[0], out var iterations)) return false; byte[] salt, expected; try { salt = Convert.FromBase64String(parts[1]); expected = Convert.FromBase64String(parts[2]); } catch (FormatException) { return false; } var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length); return CryptographicOperations.FixedTimeEquals(actual, expected); } } }