Replicating Java password hashing code in Node.js

2019-06-25 11:29发布

问题:

Edit: my question has been updated, check the bottom of this post for the latest issue. I left the rest up for people who want to read the whole story :)

I've been working on translating a small Java application into Node.js, which for the most part has been going very well. I've had to look up a lot of Java functions to figure out what they do and how to replicate their behaviour in Node (since I have pretty much no experience whatsoever with Java), but I got most of the functionality working by now.

Unfortunately there is one bit that I just can't seem to get working. It is a method used to generate a password hash, using a set of high level Java-specific functions that don't seem to exist in Node. I've been trying for two days to get this working but I just can't get the results I want.

This is the original Java code:

public static String hashPassword(final String password, final String salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
    final char[] passwordChars = password.toCharArray();
    final byte[] saltBytes = salt.getBytes();
    final PBEKeySpec spec = new PBEKeySpec(passwordChars, saltBytes, 1000, 192);
    final SecretKeyFactory key = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    final byte[] hashedPassword = key.generateSecret(spec).getEncoded();
    return String.format("%x", new BigInteger(hashedPassword));
}

Note: the salt is a fixed value, it is not random. I know this is not how it should be, but this is how the application is set up. So, since the Java code always gets the same result, it should be possible to get the same result in Node as well.

I've tried using crypto.pbkdf2, using various ciphers that seemed similar, but it all gave me a different outcome than the Java code. So I figured I'd ask here, to see if anyone knows how to do this, or has any suggestions on how to approach this.

Note that (as I said) I don't know a thing about Java, so my difficulties getting this to work probably come from the fact that I have a hard time grasping just what is happening within this method, and googling the various functions used gives conflicting answers and mostly just shows other people having a hard time with them as well.

So there are actually three questions I'm asking:

  1. Is this possible to replicate in Node.js or does Java use functionality that just doesn't exist within Node?
  2. Could someone who has more Java experience explain the various lines in this code, and what each one does? Preferably in a way that someone with a decent level of Node.js experience (and some PHP) but who never worked with Java would understand :)
  3. If anyone knows, which Node functions am I looking at to get this working? Can I do it with the built-in crypto module or will I need additional modules?

Lastly, before you say "just implement a Node-specific hashing algorithm" (which would be the easier option), I can't do that since this is to be used on existing databases that already contain these hashed passwords and that are used by other existing Java applications as well. Changing the other applications or the database is currently not an option.

UPDATE: I've gotten an answer that was very helpful, and now I got this in my Node.js code:

hashPassword = function(password, salt){
    crypto.pbkdf2(password, new Buffer(salt), 1000, 24, 'sha1', function(err, key){

    }
}

That's where I'm stuck again. I can't get the string value I need from the key. I googled a bit and found out that the String.format line in the Java code turns the BigInteger into a hexadecimal integer, but I can't seem to get the correct value.

  • I tried simply key.toString('hex') but that didn't work.
  • I found this node-biginteger module, and tried BigInteger.fromBuffer(1, key).toString(24) and some variations thereupon, but it still gives me a very different outcome than the Java application.

Any help on how to get the correct string value from the buffer would be very much appreciated.

Update2: I finally got my application working, as it turns out it was an external module that outputted bad hashes. Implementing the crypto module properly fixed it.

回答1:

These parameters generate the same buffer:

crypto.pbkdf2('test', 'salt', 1000, 24, 'sha1', function(err, key) {});

What is left is to format a string the same way. It can be a bit problematic, since BigInteger is signed, so you should take sign into account as well.

You can do this as following using bn.js:

function format(key) {
  if (key[0] >>> 7 === 0) {
    return key.toString('hex');
  }

  return '-' + new BN(key.toString('hex'), 16).notn(192).add(new BN(1)).toString(16);
}

bn.js doesn't interpret leading bit as a sign, so you have to check it first, and then convert to string according to two's complement representation.



回答2:

I needed to get this to work with node 6, and it turns out, things have gotten simpler (not needing bigints). I never would have figured this out without vkurchatkin's original answer, though:

console.log(hashPassword('password', 'salt').toString('hex'));

function hashPassword(password, salt) {
    return crypto.pbkdf2Sync(password, Buffer.from(salt, 'hex'), 1000, 24, 'sha1');
}