Python密码哈希函数迁移至.NET 8后哈希结果不一致问题排查
.NET 8迁移Python密码哈希代码结果不一致的问题分析与修复
问题背景
我们有一套基于Python开发的旧软件系统,使用以下密码哈希函数:
import hashlib, binascii, os def hash_password(password): """Hash a password for storing.""" salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii') pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt, 100000) pwdhash = binascii.hexlify(pwdhash) return (salt + pwdhash).decode('ascii') def verify_password(stored_password, provided_password): """Verify a stored password against one provided by user""" salt = stored_password[:64] stored_password = stored_password[64:] pwdhash = hashlib.pbkdf2_hmac('sha512', provided_password.encode('utf-8'), salt.encode('ascii'), 100000) pwdhash = binascii.hexlify(pwdhash).decode('ascii') return pwdhash == stored_password
尝试将该代码迁移至.NET 8时,生成的哈希结果与Python版本不一致,无法得到正确结果。以下是我们尝试的.NET 8代码:
public string HashPassword(string password) { var salt = Encoding.ASCII.GetBytes(GenerateRandomString(60)); using var pbkdf2 = new Rfc2898DeriveBytes( Encoding.UTF8.GetBytes(password), salt, 100000, HashAlgorithmName.SHA512); var hashedPassword = pbkdf2.GetBytes(64); return Encoding.ASCII.GetString(salt.Concat(hashedPassword).ToArray()); } public bool VerifyPassword(string password, string hash) { var salt = hash[..64]; var passwordHash = Encoding.UTF8.GetBytes(hash[64..]); using var pbkdf2 = new Rfc2898DeriveBytes( Encoding.UTF8.GetBytes(password), Encoding.ASCII.GetBytes(salt), 100000, HashAlgorithmName.SHA512); var hashedPassword = pbkdf2.GetBytes(64); return hashedPassword.SequenceEqual(passwordHash); } private string GenerateRandomString(int length) { var sb = new StringBuilder(); var random = new Random(); for (int i = 0; i < length; i++) { double flt = random.NextDouble(); int shift = Convert.ToInt32(Math.Floor(25 * flt)); char letter = Convert.ToChar(shift + 65); sb.Append(letter); } return sb.ToString(); }
请问我们的迁移方法可能存在什么问题?
问题分析与修复
你的.NET代码和Python代码存在三个核心差异,导致哈希结果不一致:
1. 盐值生成逻辑完全不匹配
Python代码的盐值生成流程是:
- 生成60字节密码学安全的随机二进制数据
- 对该数据做SHA256哈希,得到32字节二进制结果
- 将结果转为64字符的小写16进制字符串,再转成ASCII字节数组作为PBKDF2的盐输入
而你的.NET代码用GenerateRandomString生成60个大写字母的字符串,再转成ASCII字节数组,和Python的盐值逻辑完全不同,这是核心问题。
2. PBKDF2结果的处理方式不同
Python中,PBKDF2生成的二进制哈希值会被转为16进制字符串,再和盐值字符串拼接;而你的.NET代码直接将PBKDF2的二进制结果和盐值字节数组拼接后转成ASCII字符串,相当于直接解析二进制哈希的ASCII字符,和Python的16进制拼接逻辑完全不符。
3. 验证环节的哈希解析错误
在VerifyPassword中,你把存储的哈希部分用Encoding.UTF8.GetBytes()转成字节数组,但Python存储的是哈希值的16进制字符串,正确做法应该是将这个16进制字符串解析为二进制字节数组,而非直接转码。
修正后的.NET代码
using System.Security.Cryptography; using System.Text; public string HashPassword(string password) { // 生成60字节密码学安全随机数据,对齐Python的os.urandom(60) byte[] randomBytes = new byte[60]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomBytes); } // 对随机数据做SHA256哈希,转成64字符小写16进制字符串,对齐Python的sha256(...).hexdigest() string saltHex; using (var sha256 = SHA256.Create()) { byte[] saltHash = sha256.ComputeHash(randomBytes); saltHex = BitConverter.ToString(saltHash).Replace("-", "").ToLowerInvariant(); } // 将盐值的16进制字符串转成ASCII字节数组,作为PBKDF2的盐输入 byte[] saltBytes = Encoding.ASCII.GetBytes(saltHex); // 执行PBKDF2-HMAC-SHA512,迭代次数100000 using var pbkdf2 = new Rfc2898DeriveBytes( Encoding.UTF8.GetBytes(password), saltBytes, 100000, HashAlgorithmName.SHA512); byte[] pwdHashBytes = pbkdf2.GetBytes(64); // 将PBKDF2结果转成16进制字符串,和盐值字符串拼接后返回 string pwdHashHex = BitConverter.ToString(pwdHashBytes).Replace("-", "").ToLowerInvariant(); return saltHex + pwdHashHex; } public bool VerifyPassword(string password, string storedHash) { // 提取盐值的16进制字符串(前64字符) string saltHex = storedHash.Substring(0, 64); // 提取存储的哈希16进制字符串 string storedPwdHashHex = storedHash.Substring(64); // 将盐值转成ASCII字节数组 byte[] saltBytes = Encoding.ASCII.GetBytes(saltHex); // 重新计算PBKDF2哈希 using var pbkdf2 = new Rfc2898DeriveBytes( Encoding.UTF8.GetBytes(password), saltBytes, 100000, HashAlgorithmName.SHA512); byte[] pwdHashBytes = pbkdf2.GetBytes(64); string computedPwdHashHex = BitConverter.ToString(pwdHashBytes).Replace("-", "").ToLowerInvariant(); // 比较16进制字符串是否一致 return computedPwdHashHex.Equals(storedPwdHashHex, StringComparison.OrdinalIgnoreCase); }
额外注意点
- 生成随机数必须用
RandomNumberGenerator而非Random,前者是密码学安全随机数,和Python的os.urandom对齐,后者安全性不足。 - 16进制字符串要统一为小写,Python的
hexdigest()返回小写,避免大小写不匹配导致验证失败。
内容的提问来源于stack exchange,提问作者kroissm
相关产品推荐
相关产品推荐

