4
votes

I have an old Symfony2 based application here and I am developing a replacement with Dropwizard in Java. I migrated all User records from old DB into my new Datamodel. I also added new fields for passwords and imported the old password and salt fields, too.

Now I want to make the well known procedure. Let the user login, try against new password field. If it fails try the migrated ones, if they work, encode the cleartext password with the new algorithm and store the new hash in the new pasword field. So that the users are porting there password hashes from old procedure to the new one.

Sounds simple and normaly it's work as usual, but this Symfony and PHP drives me crazy.

Where I stuck is to create the same hash with java as symfony does. The old application uses the MessageDigestPasswordEncoder with "sha512", base64 encoding and 5000 iterations, all defaults ;)

The important methods are:

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);
}

And 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.'}';
}

It's seems straight forward but I stuck with it. As I read this it does:

  1. Merge salt and clear text password to: "password{salt}"
  2. Hash this string with SHA-512 and return a binary string into digest variable
  3. iterate 5k times and use digest concatenated with merged cleartext password to rehash into digest
  4. encode digest to base64

So here are my try in 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));
}

Does anybody see the problem? I think the difference is in the result of the: PHP: binary.binary and the JAVA: addAll(byte[],byte[])

Thanks in advance

1
why you used BouncyCastleProvider pleaseZain Elabidine
Because the time i've written this bounce castle was the defacto standard for everyone who wanted a security provider with good defaults. Nova days I would use openssl impl.Rene M.

1 Answers

4
votes

The implementation on php side is correctly doing 5k iterations by doing first round of hashing and then looping 4999 times.

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

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

In the the java implementation the for loop starts at 0 which results in 5k + 1 iteration.

By starting the for loop at 1 in java too, the resulting password hashes are then equals.

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")));
}