我有一些使用
Rfc2898DeriveBytes.Pbkdf2(bytes,src,5000,HashAlgorithmName.SHA1,24)
(较旧的实现)通过 C# 进行哈希处理的密码,并且希望将此代码移植到 java。
但是我似乎没有让两个哈希值相同。 我不知道为什么以及如何。 我尝试了多种实现,它们在 Java 中产生相同的结果,与 C# 不同。
我还发布在 https://github.com/skradel/Zetetic.Security/issues/1 上,因为原始代码使用
Zetetic.Security
包。
C#代码:
using System.Security.Cryptography;
using System;
using System.Text;
namespace MyApp
{
class Program
{
private const string _ALGORITHM = "pbkdf2";
public static string PrintBytes(byte[] byteArray)
{
var sb = new StringBuilder("new byte[] { ");
for (var i = 0; i < byteArray.Length; i++)
{
var b = byteArray[i];
sb.Append(b);
if (i < byteArray.Length - 1)
{
sb.Append(", ");
}
}
sb.Append(" }");
return sb.ToString();
}
static Program()
{
CryptoConfig.AddAlgorithm(typeof(Zetetic.Security.Pbkdf2Hash), _ALGORITHM);
}
public static string EncodePassword(string pass, int passwordFormat, string salt)
{
var bytes = Encoding.Unicode.GetBytes(pass);
var src = Convert.FromBase64String(salt);
byte[] inArray;
var hashAlgorithm = HashAlgorithm.Create(_ALGORITHM);
Console.WriteLine("hashAlgorithm is KeyedHashAlgorithm");
var algorithm2 = (KeyedHashAlgorithm)hashAlgorithm;
Console.WriteLine("algorithm2.Key.Length == src.Length " + src.Length);
algorithm2.Key = src;
Console.WriteLine("Compute hash" + algorithm2.HashSize);
inArray = algorithm2.ComputeHash(bytes);
Console.WriteLine("Bytes " + inArray.Length + PrintBytes(inArray));
Console.WriteLine("Salt " + PrintBytes(src));
var pass2 = Rfc2898DeriveBytes.Pbkdf2(bytes,src,5000,HashAlgorithmName.SHA1,24);
Console.WriteLine(Convert.ToBase64String(pass2));
return Convert.ToBase64String(inArray);
}
static void Main(string[] args)
{
var password = args[0];
var salt = args[1];
var format = Int32.Parse(args[2]);
Console.WriteLine("==Convert password '" + password + "' using salt '" + salt + "' and format '" + format + "' ==");
Console.WriteLine(EncodePassword(password, format, salt));
}
}
}
Java 代码:
byte[] saltBytes = saltStr.getBytes(UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
PBEKeySpec pbeKeySpec = new PBEKeySpec("my-secret".toCharArray(), saltBytes, 5000, 192);
Key secretKey = factory.generateSecret(pbeKeySpec);
java.util.Base64.getEncoder().encodeToString(key.getEncoded())
除了官方的 Java 实现之外,我还尝试了另外 2 个:https://rtner.de/software/PBKDF2.html 和来自 https://stackoverflow.com/a/1360237/638331 的一个。 我在 Java 中得到相同的哈希值。
我尝试过 Zetetic 实现和 C# 标准库中的实现,我在那里得到了相同的结果,但与 Java 中不同。
更新: Java 版本与 https://8gwifi.org/pbkdf.jsp 兼容。 我可以使用相同的参数在 Java 中生成与上述 JS 应用程序相同的哈希值:5000 次迭代、PBKDF2WithHmacSHA1、192 位和我的盐。
(顺便说一句,网站将数据发送到服务器,所以不要使用任何真实的密码/数据)
哈希值与 C# 中的哈希值不同。
更新:C# 运行示例
$ ./bin/Debug/net7.0/pw-encoder mama dNuhxK7K5SaJk2M5HqwyNA== 1
==Convert password 'mama' using salt 'dNuhxK7K5SaJk2M5HqwyNA==' and format '1' ==
hashAlgorithm is KeyedHashAlgorithm
algorithm2.Key.Length == src.Length 16
Compute hash192
Bytes 24new byte[] { 25, 111, 18, 46, 51, 211, 183, 94, 103, 93, 54, 65, 64, 46, 0, 105, 76, 70, 33, 212, 150, 24, 185, 168 }
Salt new byte[] { 116, 219, 161, 196, 174, 202, 229, 38, 137, 147, 99, 57, 30, 172, 50, 52 }
GW8SLjPTt15nXTZBQC4AaUxGIdSWGLmo
GW8SLjPTt15nXTZBQC4AaUxGIdSWGLmo
使用与 Java 本地代码相同的密码和值,以及 https://8gwifi.org/pbkdf.jsp 使用参数:PBKDF2WithHmacSHA1,16 位初始向量 [ dNuhxK7K5SaJk2M5HqwyNA==] ,5000 次迭代和 dkLen 的 192 位 给我下面的哈希:
pR/jfNDZoVqCiXQ7YTGR3g/h7O3J/sMR
编码不同:在 C# 代码中,盐是 Base64 解码的,在 Java 代码中,盐是 UTF-8 编码的。在 C# 代码中,密码采用
Unicode
编码,在 .NET 中指定 UTF16-LE,在 Java 代码中,密码采用 UTF-8 编码。
修复:在 Java 代码中使用与 C# 代码中相同的编码。
但是,有一个小问题:PBKDF2 的 JCA/JCE 实现在内部执行 UTF-8 编码。既不可能更改编码,也无法直接处理字节序列而不是字符串 s。
PBEKeySpec
。解决方法:应用另一个库,例如来自 BouncyCastle 的 PKCS5S2ParametersGenerator
。这是一个示例实现:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
...
String saltStr = "dNuhxK7K5SaJk2M5HqwyNA==";
String passwordStr = "mama";
byte[] salt = Base64.getDecoder().decode(saltStr); // Fix: Base64 decode salt
byte[] password = passwordStr.getBytes(StandardCharsets.UTF_16LE); // Fix: UTF-16LE encode password
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA1Digest()); // Fix: Apply a PBKDF2 implementation that can handle byte sequences
generator.init(password, salt, 5000);
byte[] key = ((KeyParameter)generator.generateDerivedParameters(192)).getKey();
System.out.println(Base64.getEncoder().encodeToString(key)); // GW8SLjPTt15nXTZBQC4AaUxGIdSWGLmo
此代码提供与 C# 代码相同的结果。