C#实现PBKDF2-SHA256:参数不同却获相同哈希结果的问题
我按照AVM的Session ID文档实现密码哈希流程:
- 通过HTTP请求获取Challenge,拆分得到:iter1=60000、salt1=f56cbe583d35a155cfbdf24c8d6bfa45,iter2=6000、salt2=99a2a02c9bd6989e52307a6e83bab646
- 先用PBKDF2-SHA256对密码和salt1进行iter1次哈希得到hash1
- 再用hash1和salt2进行iter2次哈希得到hash2
但运行C#代码后,hash1和hash2结果完全相同,输出如下:
Salt 1: f56cbe583d35a155cfbdf24c8d6bfa45 ~> 60000
Salt 2: 99a2a02c9bd6989e52307a6e83bab646 ~> 6000
Hashed PW: 53797374656D2E53656375726974792E43727970746F6772617068792E526663323839384465726976654279746573
Hashed Request: 53797374656D2E53656375726974792E43727970746F6772617068792E526663323839384465726976654279746573
Result: 99a2a02c9bd6989e52307a6e83bab646$53797374656d2e53656375726974792e43727970746f6772617068792e526663323839384465726976654279746573
我的C#代码如下:
private void CreateChallenge(String challenge, LoginData data) { // PBKDF2 (ab FRITZ!OS 7.24) if(challenge.Contains("$")) { String[] parts = challenge.Split('$'); String type = parts[0]; // 2 int iter1 = int.Parse(parts[1]); // 600000 byte[] salt1 = Encoding.UTF8.GetBytes(parts[2]); // HEX int iter2 = int.Parse(parts[3]); // 600000 byte[] salt2 = Encoding.UTF8.GetBytes(parts[4]); // HEX System.Diagnostics.Debug.Print("Salt 1: " + parts[2] + " ~> " + iter1); System.Diagnostics.Debug.Print("Salt 2: " + parts[4] + " ~> " + iter2); // Calculate byte[] hash1 = HMAC(Encoding.UTF8.GetBytes(data.Password), salt1, iter1); System.Diagnostics.Debug.Print("Hashed PW: " + BitConverter.ToString(hash1).Replace("-", "")); byte[] hash2 = HMAC(hash1, salt2, iter2); System.Diagnostics.Debug.Print("Hashed Request: " + BitConverter.ToString(hash2).Replace("-", "")); this.CallSignin(parts[4] + "$" + BitConverter.ToString(hash2).Replace("-", "").ToLower(), data); // MD5-Fallback } else { this.CallSignin(challenge + "-" + MD5(challenge + "-" + data.Password), data); } } private byte[] HMAC(byte[] input, byte[] salt, int iterations) { using (var pbkdf2 = new Rfc2898DeriveBytes(input, salt, iterations, HashAlgorithmName.SHA256)) { return Encoding.UTF8.GetBytes(pbkdf2.ToString()); //return pbkdf2.GetBytes(length); } } private String MD5(String input) { MD5 hasher = System.Security.Cryptography.MD5.Create(); byte[] data = hasher.ComputeHash(Encoding.UTF8.GetBytes(input)); StringBuilder output = new StringBuilder(); for(int index = 0; index < data.Length; index++) { output.Append(data[index].ToString("x2")); } return output.ToString().ToLower(); }
我已经尝试修改HMAC方法及Rfc2898DeriveBytes的使用方式,但问题仍存在。
核心问题分析
代码存在两个致命错误:
- 错误调用
Rfc2898DeriveBytes.ToString():该方法返回的是对象的类型名称(System.Security.Cryptography.Rfc2898DeriveBytes),并非PBKDF2哈希计算结果。因此无论参数如何变化,返回的字节数组始终相同。 - 盐值处理逻辑错误:从Challenge拆分得到的salt1、salt2是十六进制字符串,直接用
Encoding.UTF8.GetBytes()转换会破坏原始盐值的字节结构,必须将十六进制字符串解码为字节数组。
修正后的代码
1. 修复PBKDF2哈希获取逻辑
将pbkdf2.ToString()替换为pbkdf2.GetBytes(32)(SHA256哈希长度为32字节):
private byte[] HMAC(byte[] input, byte[] salt, int iterations) { using (var pbkdf2 = new Rfc2898DeriveBytes(input, salt, iterations, HashAlgorithmName.SHA256)) { // SHA256对应哈希长度为32字节 return pbkdf2.GetBytes(32); } }
2. 添加十六进制字符串转字节数组方法
替换原有的盐值转换逻辑:
private byte[] HexStringToByteArray(string hex) { if (hex.Length % 2 != 0) throw new ArgumentException("十六进制字符串长度必须为偶数"); byte[] bytes = new byte[hex.Length / 2]; for (int i = 0; i < hex.Length; i += 2) { bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); } return bytes; }
修改CreateChallenge中的盐值转换代码:
byte[] salt1 = HexStringToByteArray(parts[2]); byte[] salt2 = HexStringToByteArray(parts[4]);
3. 同步迭代次数注释(可选)
修正注释与实际值不符的问题:
int iter1 = int.Parse(parts[1]); // 60000 int iter2 = int.Parse(parts[3]); // 6000
验证效果
修正后,两次PBKDF2计算会基于正确的盐值和哈希结果生成不同的hash1和hash2,最终输出符合AVM文档要求。
内容的提问来源于stack exchange,提问作者Adrian Preuss

