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:
- Merge salt and clear text password to: "password{salt}"
- Hash this string with SHA-512 and return a binary string into digest variable
- iterate 5k times and use digest concatenated with merged cleartext password to rehash into digest
- 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