提问人:ieugen 提问时间:11/9/2023 最后编辑:ieugen 更新时间:11/10/2023 访问量:72
在 Java 中使用 Rfc2898DeriveBytes.Pbkdf2
Working Rfc2898DeriveBytes.Pbkdf2 in Java
问:
我有一些使用 C#(较旧的实现)进行哈希处理的密码,并希望将此代码移植到 java。Rfc2898DeriveBytes.Pbkdf2(bytes,src,5000,HashAlgorithmName.SHA1,24)
但是,我似乎没有得到两个哈希值相同。 我不知道为什么以及如何。 我尝试了多种实现,它们在 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# 中的哈希值不同。
UPDATE: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 次迭代和 192 位用于 dkLen 给我下面的哈希:
pR/jfNDZoVqCiXQ7YTGR3g/h7O3J/sMR
答:
编码不同:在 C# 代码中,salt 是 Base64 解码的,在 Java 代码中是 UTF-8 编码的。在 C# 代码中,密码在 .NET 中指定 UTF16-LE 进行编码,在 Java 代码中,密码采用 UTF-8 编码。Unicode
修复:在 Java 代码中使用与 C# 代码中相同的编码。
但是,有一个小问题:PBKDF2 的 JCA/JCE 实现在内部执行 UTF-8 编码,即 getPasswordBytes()。
既不能更改编码,也不能直接处理字节序列而不是 , s。 PBEKeySpec
。解决办法:应用另一个库,例如 BouncyCastle 中的 PKCS5S2ParametersGenerator
。下面是一个示例实现:char[]
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# 代码相同的结果。
关于 UTF-16LE 编码的备注:可以想出这样的想法:转换 C# 代码从中生成 UTF-16LE 字节序列的初始字符串,使 UTF-8 编码生成相同的字节序列:
String passwordStr = new String("<your password>".getBytes(StandardCharsets.UTF_16LE), StandardCharsets.UTF_8);
但是,这在一般情况下不起作用,但仅在某些情况下有效,因为并非每个 UTF-16LE 字节序列都是 UTF-8 可解码的。
例如,Unicode 码位< 128(ASCII 字符)的字符是可能的,因此它适用于 ,但不适用于 (在转换过程中会损坏)。此外,转换后的密码包含空格(这在这里并不重要,因为密码没有打印出来)。mama
mama§
评论