using System.Security.Cryptography; using System.Text; using Konscious.Security.Cryptography; namespace GameList.Infrastructure; public static class PasswordHasher { public const int LegacyVersion = 1; public const int Pbkdf2Version = 2; public const int CurrentVersion = 3; private const int SaltSize = 16; private const int KeySize = 32; private const int IterationsV1 = 210_000; private const int IterationsV2 = 350_000; private const int Argon2Iterations = 2; private const int Argon2MemoryKiB = 19_456; private const int Argon2DegreeOfParallelism = 1; 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 = Derive(password, salt, normalizedVersion); return (hash, salt); } public static bool Verify(string password, byte[] hash, byte[] salt) => Verify(password, hash, salt, CurrentVersion, 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 = Derive(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, Pbkdf2Version => Pbkdf2Version, CurrentVersion => CurrentVersion, _ => throw new ArgumentOutOfRangeException(nameof(version), "Unsupported password hash version.") }; } private static int NormalizeVerifyVersion(int version) { return version switch { <= LegacyVersion => LegacyVersion, Pbkdf2Version => Pbkdf2Version, CurrentVersion => CurrentVersion, _ => 0 }; } private static byte[] Derive(string password, byte[] salt, int version) { return version switch { LegacyVersion => PBKDF2(password, salt, IterationsV1), Pbkdf2Version => PBKDF2(password, salt, IterationsV2), CurrentVersion => Argon2id(password, salt), _ => throw new ArgumentOutOfRangeException(nameof(version), "Unsupported password hash version.") }; } private static byte[] PBKDF2(string password, byte[] salt, int iterations) { return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, KeySize); } private static byte[] Argon2id(string password, byte[] salt) { using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) { Salt = salt, Iterations = Argon2Iterations, MemorySize = Argon2MemoryKiB, DegreeOfParallelism = Argon2DegreeOfParallelism }; return argon2.GetBytes(KeySize); } }