如何使用 PBKDF2 在 Java 和 Ruby 中生成相同的安全散列
How can I generate the same secure hash in Java and Ruby using PBKDF2
我正在将 Web 应用程序从 Ruby 移植到 Java,并希望允许用户在不重置密码的情况下登录。这是使用 pbkdf2 gem:
生成散列的 Ruby 代码
PBKDF2.new { |p|
p.password = password
p.salt = salt
p.iterations = 10000
}.hex_string
阅读 Ruby gem 的源代码,它使用 OpenSSL::Digest.new("sha256") 作为默认哈希函数并生成 32 字节的值,使用 'unpack("H*")'.
转换为 64 个字符的字符串
因此,在 Java 中,我尝试了以下操作:
public String generatePasswordHash(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException
{
char[] chars = password.toCharArray();
byte[] saltBytes =salt.getBytes();
PBEKeySpec spec = new PBEKeySpec(chars, saltBytes, 1000, 256);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
BigInteger bi = new BigInteger(1, hash);
return bi.toString(16);
}
使用密码 = "apassword" 和盐 = "somesalt" 测试两段代码,我得到以下结果。
Ruby: 3fa1eb7544ca49b1f371eb17d24bf0010c433fa263a84aff7df446741371706b
Java: 77a7c0b1ea9760d0b1ef02e7a2633c40ccd7848ee4fa822ec71b5794e476f354
我测试了 Ruby 和 Java 十六进制字符串编码,它们的工作原理相同,所以问题看起来出在散列算法中,或者可能是字符串转换为字节数组的方式。
问题出在迭代次数上。如果您在 Java 中将其更改为 10,000 而不是您正在使用的 1,000,它将为您提供与在 Ruby:
中获得的结果相同的结果
PBEKeySpec spec = new PBEKeySpec(chars, saltBytes, 10000, 256);
补充说明:
始终最好确保根据已知字符集从字符串中获取字节。没有字符集,它使用默认字符集,它可能会引起意外。
另外,最好不要依赖BigInteger.toString(16)
,因为如果前几个字节是0,就会return一个小于64个字符的字符串。使用 String.format()
代替:
public static String generatePasswordHash(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException
{
char[] chars = password.toCharArray();
byte[] saltBytes =salt.getBytes(StandardCharsets.US_ASCII);
PBEKeySpec spec = new PBEKeySpec(chars, saltBytes, 10000, 256);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
BigInteger bi = new BigInteger(1, hash);
return String.format("%064x", bi);
}
(我假设 salt
是纯 ASCII 文本。您可以更改字符集,但请记住使用 Ruby 使用的相同字符集)。
我正在将 Web 应用程序从 Ruby 移植到 Java,并希望允许用户在不重置密码的情况下登录。这是使用 pbkdf2 gem:
生成散列的 Ruby 代码PBKDF2.new { |p|
p.password = password
p.salt = salt
p.iterations = 10000
}.hex_string
阅读 Ruby gem 的源代码,它使用 OpenSSL::Digest.new("sha256") 作为默认哈希函数并生成 32 字节的值,使用 'unpack("H*")'.
转换为 64 个字符的字符串因此,在 Java 中,我尝试了以下操作:
public String generatePasswordHash(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException
{
char[] chars = password.toCharArray();
byte[] saltBytes =salt.getBytes();
PBEKeySpec spec = new PBEKeySpec(chars, saltBytes, 1000, 256);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
BigInteger bi = new BigInteger(1, hash);
return bi.toString(16);
}
使用密码 = "apassword" 和盐 = "somesalt" 测试两段代码,我得到以下结果。
Ruby: 3fa1eb7544ca49b1f371eb17d24bf0010c433fa263a84aff7df446741371706b
Java: 77a7c0b1ea9760d0b1ef02e7a2633c40ccd7848ee4fa822ec71b5794e476f354
我测试了 Ruby 和 Java 十六进制字符串编码,它们的工作原理相同,所以问题看起来出在散列算法中,或者可能是字符串转换为字节数组的方式。
问题出在迭代次数上。如果您在 Java 中将其更改为 10,000 而不是您正在使用的 1,000,它将为您提供与在 Ruby:
中获得的结果相同的结果 PBEKeySpec spec = new PBEKeySpec(chars, saltBytes, 10000, 256);
补充说明:
始终最好确保根据已知字符集从字符串中获取字节。没有字符集,它使用默认字符集,它可能会引起意外。
另外,最好不要依赖BigInteger.toString(16)
,因为如果前几个字节是0,就会return一个小于64个字符的字符串。使用 String.format()
代替:
public static String generatePasswordHash(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException
{
char[] chars = password.toCharArray();
byte[] saltBytes =salt.getBytes(StandardCharsets.US_ASCII);
PBEKeySpec spec = new PBEKeySpec(chars, saltBytes, 10000, 256);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
BigInteger bi = new BigInteger(1, hash);
return String.format("%064x", bi);
}
(我假设 salt
是纯 ASCII 文本。您可以更改字符集,但请记住使用 Ruby 使用的相同字符集)。