using System.Security.Cryptography;
namespace XFEExtension.NetCore.ServerInteractive.Models.UserModels;
/// 版本化 PBKDF2 密码凭据。
public sealed class PasswordCredential
{
public string Algorithm { get; set; } = "PBKDF2-HMAC-SHA256";
public int Version { get; set; } = 1;
public string Salt { get; set; } = string.Empty;
public int Iterations { get; set; } = PasswordHasher.DefaultIterations;
public string Hash { get; set; } = string.Empty;
}
/// 使用 .NET 标准密码学 API 的密码哈希器。
public static class PasswordHasher
{
public const int DefaultIterations = 600_000;
private const int SaltSize = 16;
private const int HashSize = 32;
private const int MaximumIterations = 5_000_000;
public static PasswordCredential Hash(string password, int iterations = DefaultIterations)
{
ArgumentException.ThrowIfNullOrEmpty(password);
if (iterations is < 100_000 or > MaximumIterations) throw new ArgumentOutOfRangeException(nameof(iterations));
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, HashSize);
return new() { Salt = Convert.ToBase64String(salt), Iterations = iterations, Hash = Convert.ToBase64String(hash) };
}
public static bool Verify(string password, PasswordCredential credential)
{
ArgumentNullException.ThrowIfNull(credential);
if (credential.Algorithm != "PBKDF2-HMAC-SHA256" || credential.Version != 1 || credential.Iterations is < 100_000 or > MaximumIterations)
return false;
try
{
var salt = Convert.FromBase64String(credential.Salt);
var expected = Convert.FromBase64String(credential.Hash);
if (salt.Length != SaltSize || expected.Length != HashSize) return false;
var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, credential.Iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}
catch (FormatException) { return false; }
}
internal static bool FixedTimePlainTextEquals(string supplied, string stored)
{
var suppliedHash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(supplied));
var storedHash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(stored));
return CryptographicOperations.FixedTimeEquals(suppliedHash, storedHash);
}
}