使用Java创建与Symfony2相同的哈希(SHA-512)

4
我这里有一个基于Symfony2的旧应用程序,现在我正在使用Java的Dropwizard开发替代品。我已经将所有用户记录从旧数据库迁移到新数据模型中。我还添加了新的密码字段,并导入了旧密码和盐字段。
现在我想执行众所周知的过程。让用户登录,尝试使用新密码字段。如果失败,则尝试使用迁移的密码,如果它们有效,则使用新算法对明文密码进行编码,并将新哈希存储在新密码字段中。这样用户就可以将其密码哈希从旧过程转移到新过程。
听起来很简单,通常都能正常工作,但是Symfony和PHP让我发疯。
我卡住的地方是如何使用Java创建与Symfony相同的哈希值。 旧应用程序使用MessageDigestPasswordEncoder,采用默认值"sha512"、base64编码和5000次迭代。
重要的方法包括: MessageDigestPasswordEncoder:
public function encodePassword($raw, $salt) {
  if ($this->isPasswordTooLong($raw)) {
    throw new BadCredentialsException('Invalid password.');
  }

  if (!in_array($this->algorithm, hash_algos(), true)) {
    throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
  }

  $salted = $this->mergePasswordAndSalt($raw, $salt);
  $digest = hash($this->algorithm, $salted, true);

  // "stretch" hash
  for ($i = 1; $i < $this->iterations; ++$i) {
    $digest = hash($this->algorithm, $digest.$salted, true);
  }

  return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
}

还有BasePasswordEncoder:

protected function mergePasswordAndSalt($password, $salt) {
  if (empty($salt)) {
    return $password;
  }

  if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) {
    throw new \InvalidArgumentException('Cannot use { or } in salt.');
  }

  return $password.'{'.$salt.'}';
}

这看起来很简单,但我被卡住了。 按照以下步骤进行操作:

  1. 将盐和明文密码合并为 "password{salt}"
  2. 使用 SHA-512 对此字符串进行散列,并将结果作为二进制字符串存储在 digest 变量中
  3. 迭代 5k 次,并使用 digest 和合并的明文密码重新散列为 digest
  4. 将 digest 编码为 base64

以下是我在 Java 中的尝试:

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public void legacyEncryption(String salt, String clearPassword) throws UnsupportedEncodingException, NoSuchAlgorithmException {
  // Get digester instance for algorithm "SHA-512" using BounceCastle
  MessageDigest digester = MessageDigest.getInstance("SHA-512", new BouncyCastleProvider());

  // Create salted password string
  String mergedPasswordAndSalt = clearPassword + "{" + salt + "}";

  // First time hash the input string by using UTF-8 encoded bytes.
  byte[] hash = digester.digest(mergedPasswordAndSalt.getBytes("UTF-8"));

  // Loop 5k times
  for (int i = 0; i < 5000; i++) {
    // Concatenate the hash bytes with the clearPassword bytes and rehash
    hash = digester.digest(ArrayUtils.addAll(hash, mergedPasswordAndSalt.getBytes("UTF-8")));
  }

  // Log the resulting hash as base64 String
  logger.info("Legace password digest: salt=" + salt + " hash=" + Base64.getEncoder().encodeToString(hash));
}

有人看到问题了吗?我觉得区别在于:

PHP: binary.binary

JAVA: addAll(byte[],byte[])

预先感谢您的帮助。


为什么你使用了BouncyCastleProvider? - Zain Elabidine
1
因为我编写这个程序时,Bouncy Castle 是每个想要具有良好默认安全性的人的事实标准。现在我会使用 OpenSSL 实现。 - Rene M.
1个回答

4

在php端的实现正确地进行了5k次迭代,首先进行一轮哈希,然后循环4999次。

$digest = hash($this->algorithm, $salted, true);

for ($i = 1; $i < $this->iterations; ++$i) {
  $digest = hash($this->algorithm, $digest.$salted, true);
}

在Java实现中,for循环从0开始,导致迭代次数为5k + 1。

如果在Java中将for循环从1开始,则生成的密码哈希值相等。

byte[] hash = digester.digest(mergedPasswordAndSalt.getBytes("UTF-8"));

for (int i = 0; i < 5000; i++) {
  hash = digester.digest(ArrayUtils.addAll(hash, mergedPasswordAndSalt.getBytes("UTF-8")));
}

听起来不错,很有道理的;)明天我会试一下。刚刚才写完今天的内容。 - Rene M.
在PHP代码中,如果$this->iterations5,000并且在for循环中初始化$i1,那么for循环将迭代4,999次,而不是5,000次。而且,由于您说第一轮在PHP代码中在循环外完成,因此PHP和Java代码都应该迭代5,000次。 - Jonny Henly
你说得对,PHP代码确实迭代了5000次(4999 + 1)。然而,Java代码迭代了5001次(5000 + 1),因为它还有循环外的初始轮。我在回答中混淆了两种语言,抱歉。不过我仍然认为这是问题的根源。 - Daniel Hintze
不是最好的答案;) 但它点出了问题所在。我一直忽略了在php代码中循环从1开始而不是0,因为我们想要5k个循环而不是+1 ;) 我根据php代码更改了我的代码以从1开始,并且现在我的结果哈希是正确的。我可以对我的测试密码进行编码。非常感谢。 - Rene M.
1
我对你的帖子进行了编辑,改正了答案。我彻底修改了整个文本。希望你没问题。它仍在审核中,可能尚未被接受。 - Rene M.
似乎你的编辑被拒绝了,我也不知道(如果有的话)我还能否接受它 - 无论如何,我根据你的编辑中的更改调整了答案。 ;) - Daniel Hintze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接