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, ""); } }