using System.Security.Cryptography; using System.Text; using Konscious.Security.Cryptography; namespace Infrastructure.PasswordHashing; /// /// Hashes and verifies passwords using the Argon2id algorithm via Konscious.Security.Cryptography. /// public class Argon2Infrastructure : IPasswordInfrastructure { private const int SaltSize = 16; // 128-bit private const int HashSize = 32; // 256-bit private const int ArgonIterations = 4; private const int ArgonMemoryKb = 65536; // 64MB /// /// Hashes a plaintext password using Argon2id with a fresh random salt. /// /// A string of the form "{base64Salt}:{base64Hash}", suitable for storage. public string Hash(string password) { byte[] salt = RandomNumberGenerator.GetBytes(SaltSize); Argon2id argon2 = new(Encoding.UTF8.GetBytes(password)) { Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = ArgonMemoryKb, Iterations = ArgonIterations, }; byte[] hash = argon2.GetBytes(HashSize); return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; } /// /// Verifies a plaintext password against a stored salt/hash string, using a fixed-time comparison /// to mitigate timing attacks. /// /// /// false if the password doesn't match, if is malformed, or if any /// other error occurs during verification. /// public bool Verify(string password, string stored) { try { string[] parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return false; byte[] salt = Convert.FromBase64String(parts[0]); byte[] expected = Convert.FromBase64String(parts[1]); Argon2id argon2 = new(Encoding.UTF8.GetBytes(password)) { Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = ArgonMemoryKb, Iterations = ArgonIterations, }; byte[] actual = argon2.GetBytes(expected.Length); return CryptographicOperations.FixedTimeEquals(actual, expected); } catch { return false; } } }