using System.Security.Cryptography; namespace SpaceGame.Api.Auth.Simulation; public sealed class LocalPasswordHasher { private const int SaltSize = 16; private const int KeySize = 32; private const int IterationCount = 120_000; public string HashPassword(string password) { ArgumentException.ThrowIfNullOrWhiteSpace(password); Span salt = stackalloc byte[SaltSize]; RandomNumberGenerator.Fill(salt); var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, IterationCount, HashAlgorithmName.SHA256, KeySize); return $"pbkdf2-sha256${IterationCount}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}"; } public bool VerifyPassword(string password, string encodedHash) { ArgumentException.ThrowIfNullOrWhiteSpace(password); ArgumentException.ThrowIfNullOrWhiteSpace(encodedHash); var parts = encodedHash.Split('$', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 4 || !string.Equals(parts[0], "pbkdf2-sha256", StringComparison.Ordinal)) { return false; } if (!int.TryParse(parts[1], out var iterations)) { return false; } var salt = Convert.FromBase64String(parts[2]); var expected = Convert.FromBase64String(parts[3]); var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length); return CryptographicOperations.FixedTimeEquals(actual, expected); } }