You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 09:53:31