Files
amrez-nova-eop-services-api/AMREZ.EOP.Infrastructures/Security/BcryptPasswordHasher.cs
Thanakarn Klangkasame 92e614674c Init Git
2025-09-30 11:01:02 +07:00

61 lines
2.0 KiB
C#

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<byte> 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}";
}
}