61 lines
2.0 KiB
C#
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}";
|
|
}
|
|
} |