using System.Security.Cryptography;
using System.Text;
namespace Rmsys.Api.Auth;
///
/// 口令校验。
/// 存储格式(与 db/seed_all.py 写入的一致):`迭代次数$盐hex$哈希hex`
/// 算法:PBKDF2-HMAC-SHA256,默认 60000 次迭代,每个账号独立随机盐。
/// 说明:政务系统不建议用 MD5/SHA1;此处不引第三方库,用 BCL 自带实现。
///
public static class PasswordHasher
{
public const int DefaultIterations = 60000;
public static (string hash, string salt) Hash(string password, int iterations = DefaultIterations)
{
var salt = RandomNumberGenerator.GetBytes(16);
var saltHex = Convert.ToHexString(salt).ToLowerInvariant();
var h = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, 32);
return ($"{iterations}${saltHex}${Convert.ToHexString(h).ToLowerInvariant()}", saltHex);
}
public static bool Verify(string password, string? stored)
{
if (string.IsNullOrWhiteSpace(stored)) return false;
var parts = stored.Split('$');
if (parts.Length != 3) return false;
if (!int.TryParse(parts[0], out var iters) || iters <= 0) return false;
byte[] salt;
byte[] expected;
try
{
salt = Convert.FromHexString(parts[1]);
expected = Convert.FromHexString(parts[2]);
}
catch
{
return false;
}
var actual = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(password), salt, iters, HashAlgorithmName.SHA256, expected.Length);
// 定长比较,避免时序侧信道
return CryptographicallyEquals(actual, expected);
}
private static bool CryptographicallyEquals(byte[] a, byte[] b)
{
if (a.Length != b.Length) return false;
var diff = 0;
for (var i = 0; i < a.Length; i++) diff |= a[i] ^ b[i];
return diff == 0;
}
/// 口令复杂度:长度≥8,含大小写字母 + 数字 + 符号
public static (bool ok, string reason) CheckStrength(string pwd)
{
if (string.IsNullOrEmpty(pwd) || pwd.Length < 8) return (false, "口令长度不得少于 8 位");
if (!pwd.Any(char.IsUpper)) return (false, "口令需包含大写字母");
if (!pwd.Any(char.IsLower)) return (false, "口令需包含小写字母");
if (!pwd.Any(char.IsDigit)) return (false, "口令需包含数字");
if (!pwd.Any(c => !char.IsLetterOrDigit(c))) return (false, "口令需包含符号");
return (true, "");
}
}