using System.Security.Cryptography; using System.Text; namespace GameList.Infrastructure; public static class PasswordHasher { public const int LegacyVersion = 1; public const int CurrentVersion = 2; private const int SaltSize = 16; private const int KeySize = 32; private const int IterationsV1 = 210_000; private const int IterationsV2 = 350_000; public static (byte[] Hash, byte[] Salt) HashPassword(string password) => HashPassword(password, CurrentVersion); public static (byte[] Hash, byte[] Salt) HashPassword(string password, int version) { if (string.IsNullOrEmpty(password)) throw new ArgumentException("Password required", nameof(password)); var normalizedVersion = NormalizeHashVersion(version); var salt = RandomNumberGenerator.GetBytes(SaltSize); var hash = PBKDF2(password, salt, normalizedVersion); return (hash, salt); } public static bool Verify(string password, byte[] hash, byte[] salt) => Verify(password, hash, salt, LegacyVersion, out _); public static bool Verify(string password, byte[] hash, byte[] salt, int version, out bool needsRehash) { needsRehash = false; if (hash.Length == 0 || salt.Length == 0) return false; var normalizedVersion = NormalizeVerifyVersion(version); if (normalizedVersion == 0) return false; var computed = PBKDF2(password, salt, normalizedVersion); var verified = CryptographicOperations.FixedTimeEquals(computed, hash); needsRehash = verified && normalizedVersion < CurrentVersion; return verified; } private static int NormalizeHashVersion(int version) { return version switch { <= LegacyVersion => LegacyVersion, CurrentVersion => CurrentVersion, _ => throw new ArgumentOutOfRangeException(nameof(version), "Unsupported password hash version.") }; } private static int NormalizeVerifyVersion(int version) { return version switch { <= LegacyVersion => LegacyVersion, CurrentVersion => CurrentVersion, _ => 0 }; } private static int ResolveIterations(int version) { return version switch { LegacyVersion => IterationsV1, CurrentVersion => IterationsV2, _ => IterationsV1 }; } private static byte[] PBKDF2(string password, byte[] salt, int version) { var iterations = ResolveIterations(version); return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, KeySize); } }