Rene M. Rene M. - 3 months ago 15
Java Question

Creating same hashes like symfony2 does with java (SHA-512)

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

Answer

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