using System.Globalization; using System.Security.Cryptography; using System.Text; namespace SeaHaven.Services.Helpers { /// /// Generation and hashing for emailed password reset codes. The raw code exists /// only in memory and in the email sent to the account holder. Hashes are keyed /// with a server-side key, so a copy of the database alone cannot be used to /// brute-force the six-digit codes offline. /// public static class PasswordResetCodeSecrets { private static readonly byte[] KeyInfo = Encoding.UTF8.GetBytes("password-reset-code-v1"); /// /// Derives the code-hashing key from an existing server secret with HKDF, so no /// new secret is needed and the derived key is useless for anything else. /// public static byte[] DeriveKey(string serverSecret) { ArgumentException.ThrowIfNullOrEmpty(serverSecret); return HKDF.DeriveKey(HashAlgorithmName.SHA256, Encoding.UTF8.GetBytes(serverSecret), 32, Array.Empty(), KeyInfo); } public static string NewCode() { return RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6", CultureInfo.InvariantCulture); } public static string NewSalt() { return Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(); } /// A value shaped like a hash that no code can match. public static string NewUnmatchableHash() { return Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant(); } public static string Hash(byte[] key, string salt, string code) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(salt); ArgumentNullException.ThrowIfNull(code); return Convert.ToHexString(HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(salt + ":" + code))).ToLowerInvariant(); } public static bool Matches(byte[] key, string salt, string candidate, string expectedHash) { if (string.IsNullOrEmpty(salt) || string.IsNullOrEmpty(expectedHash)) return false; var actual = Encoding.ASCII.GetBytes(Hash(key, salt, candidate.Trim())); var expected = Encoding.ASCII.GetBytes(expectedHash); return CryptographicOperations.FixedTimeEquals(actual, expected); } } }