using System.Security.Cryptography; using System.Text; using AMREZ.EOP.Abstractions.Security; namespace AMREZ.EOP.Infrastructures.Security; public sealed class BcryptPasswordHasher : IPasswordHasher { private const int SaltSizeBytes = 16; // 128-bit salt private const int KeySizeBytes = 32; // 256-bit key private const int Iterations = 200_000; // 2e5 (แนะนำ >= 150k บน .NET 8/9) public bool Verify(string plain, string hash) { if (plain is null || hash is null) return false; var parts = hash.Split('$', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 4 || !parts[0].Equals("pbkdf2-sha256", StringComparison.OrdinalIgnoreCase)) return false; if (!int.TryParse(parts[1], out var iters) || iters <= 0) return false; byte[] salt, expected; try { salt = Convert.FromBase64String(parts[2]); expected = Convert.FromBase64String(parts[3]); } catch { return false; } byte[] actual = Rfc2898DeriveBytes.Pbkdf2( password: Encoding.UTF8.GetBytes(plain), salt: salt, iterations: iters, hashAlgorithm: HashAlgorithmName.SHA256, outputLength: expected.Length ); return CryptographicOperations.FixedTimeEquals(actual, expected); } public string Hash(string plain) { if (plain is null) throw new ArgumentNullException(nameof(plain)); Span salt = stackalloc byte[SaltSizeBytes]; RandomNumberGenerator.Fill(salt); byte[] key = Rfc2898DeriveBytes.Pbkdf2( password: Encoding.UTF8.GetBytes(plain), salt: salt.ToArray(), iterations: Iterations, hashAlgorithm: HashAlgorithmName.SHA256, outputLength: KeySizeBytes ); var saltB64 = Convert.ToBase64String(salt); var keyB64 = Convert.ToBase64String(key); return $"pbkdf2-sha256${Iterations}${saltB64}${keyB64}"; } }