Skip to content

Instantly share code, notes, and snippets.

@skeggse
Last active April 17, 2024 21:04
Show Gist options
  • Star 84 You must be signed in to star a gist
  • Fork 24 You must be signed in to fork a gist
  • Save skeggse/52672ddee97c8efec269 to your computer and use it in GitHub Desktop.
Save skeggse/52672ddee97c8efec269 to your computer and use it in GitHub Desktop.
Example of using crypto.pbkdf2 to hash and verify passwords asynchronously, while storing the hash and salt in a single combined buffer along with the original hash settings
var crypto = require('crypto');
// larger numbers mean better security, less
var config = {
// size of the generated hash
hashBytes: 32,
// larger salt means hashed passwords are more resistant to rainbow table, but
// you get diminishing returns pretty fast
saltBytes: 16,
// more iterations means an attacker has to take longer to brute force an
// individual password, so larger is better. however, larger also means longer
// to hash the password. tune so that hashing the password takes about a
// second
iterations: 872791
};
/**
* Hash a password using Node's asynchronous pbkdf2 (key derivation) function.
*
* Returns a self-contained buffer which can be arbitrarily encoded for storage
* that contains all the data needed to verify a password.
*
* @param {!String} password
* @param {!function(?Error, ?Buffer=)} callback
*/
function hashPassword(password, callback) {
// generate a salt for pbkdf2
crypto.randomBytes(config.saltBytes, function(err, salt) {
if (err) {
return callback(err);
}
crypto.pbkdf2(password, salt, config.iterations, config.hashBytes,
function(err, hash) {
if (err) {
return callback(err);
}
var combined = new Buffer(hash.length + salt.length + 8);
// include the size of the salt so that we can, during verification,
// figure out how much of the hash is salt
combined.writeUInt32BE(salt.length, 0, true);
// similarly, include the iteration count
combined.writeUInt32BE(config.iterations, 4, true);
salt.copy(combined, 8);
hash.copy(combined, salt.length + 8);
callback(null, combined);
});
});
}
/**
* Verify a password using Node's asynchronous pbkdf2 (key derivation) function.
*
* Accepts a hash and salt generated by hashPassword, and returns whether the
* hash matched the password (as a boolean).
*
* @param {!String} password
* @param {!Buffer} combined Buffer containing hash and salt as generated by
* hashPassword.
* @param {!function(?Error, !boolean)}
*/
function verifyPassword(password, combined, callback) {
// extract the salt and hash from the combined buffer
var saltBytes = combined.readUInt32BE(0);
var hashBytes = combined.length - saltBytes - 8;
var iterations = combined.readUInt32BE(4);
var salt = combined.slice(8, saltBytes + 8);
var hash = combined.toString('binary', saltBytes + 8);
// verify the salt and hash against the password
crypto.pbkdf2(password, salt, iterations, hashBytes, function(err, verify) {
if (err) {
return callback(err, false);
}
callback(null, verify.toString('binary') === hash);
});
}
exports.hashPassword = hashPassword;
exports.verifyPassword = verifyPassword;
@nawlbergs
Copy link

How do you get a combined hash in the database (string) back into a readable buffer for the verify function... im having some issues and don't really understand buffers very well.

@owencjones
Copy link

@nawlbergs The use of a buffer is to return an object from which the salt and/or hash can be retrieved, in virtually any encoding - which is useful for DB storage.

For instance, you may want to store the output hash as Base64 in your database to avoid any characters having meaning to your DB engine (for instance, if you were using SQL, then characters like ; have meaning in the statements), or you may want to output them as UTF-8 for file storage, or one of the ISO formats.

Worth nothing - I'm not condoning any of those methods, they are just some possible reasons for need the flexibility of a buffer.

Buffers are a very low-level way of working. They have set lengths, and represents allocated areas of memory. In many ways they have similarity to programming in C or a similar language. You can read from them, and specify which octets to start and end at.

The method shown for encoding PBKDF2 returns a buffer including the length of the salt, in the first 4 octets (4 x 8 = 32 : a 32-bit unsigned integer), the number of iterations in the nexf 4 iterations, for the same reason, and then octets 9 to (9 + salt length) are the salt, and the remainder are the hash.

This might seem OTT, but it allows the salt and hash to be returned in a single flexible object that can be extracted in any octet range, and any encoding, from a relatively low-level source. One other possibility is to return the salt and hash seperately, which could work, but you may end up using buffers again to achieve whatever encoding you need.

I hope that all made sense?

@CalebEverett
Copy link

Thanks for putting this up. It was really useful to see an implementation of authenticating passwords without saving them. Also really useful to see the implementation of storing the decryption info in the binary along with the encrypted info. I used your example to develop a system for encrypting files stored in a database using the AWS Key Management System data keys. You get a data key from the service that includes an encrypted key and plaintext key. The encrypted key gets stored with the encrypted data, in my case along with the initialization vector and authentication tag for AES-GCM. Then when you want to decrypt, you unpack the binary and send the encrypted key back to KMS, along with the tag and additional context, and if that is authenticated, the service sends you back a plain text key for decrypting. It is pretty amazing how fast the whole thing happens - more or less completely transparent when you click on a link to view a document. You can set up different master keys and report on api usage.

I had zero experience in this before this implementation, so it took a good amount of time to figure out what was what. I got a lot of mileage out of your gist, so thought I would try to pay it forward. This is complete overkill for my project, but I wanted to learn how to implement a high quality solution. I started with pgcrypto, but decided the encryption needed to happen outside of the database in order to not risk revealing keys in the logs. I had a cool system set up where the key got brought in via secure connection with instance permissions from an encrypted S3 object using openpgpjs, and I thought about setting up a lambda function (also looked at Vault which also seemed like a great option), but the AWS system seemed state of the art and easy to integrate with the sdk. I also referred a bunch to the node crypto and buffer docs.

Here is a link to the decrypt functions I implemented. The encryption ones need to be cleaned up a bit, but happy to share if useful for anyone.

@gordysc
Copy link

gordysc commented May 4, 2016

I absolutely loved this gist! I was banging my head against the wall for a while and this made sense. Thank you so much for the example! Note, in Node 6.X.Y new Buffer has been deprecated: nodejs/node#4682

@mba7
Copy link

mba7 commented Aug 5, 2016

Thank you a lot for this gist.
I found it very useful and i made a fork that return promises (from bluebird ).
https://gist.github.com/mba7/979e6c3fe715fc618549fae4d09019ef
It could be useful for someone ...

@Globik
Copy link

Globik commented Dec 27, 2016

Scmp.js is any need for this sample?
https://www.npmjs.com/package/scmp
Why? To minimize vulnerability against timing attacks.

@si-harps
Copy link

si-harps commented Mar 15, 2017

Thanks for the snippet, very useful! One question around iterations though...I have been reading a fair amount about this subject recently and more often than not, i see programmers using around 3000-10000 iterations when generating the hash. This seems very low compared to the 872791 iterations you are using. Please could you offer some insight? I have chosen the following settings for the application i am working on...

const config = {
    iterations: 200000,
    hashBytes: 32,
    digest: 'sha512'
};

// Generate PBKDF2 hash
function pbkdf2(value, salt) {

    return new Promise( (resolve, reject) => {

        const {
            iterations,
            hashBytes,
            digest
        } = config;

        crypto.pbkdf2( value, salt, iterations, hashBytes, digest, (err, key) => {

            if (err)
                return reject(err)

            resolve(key.toString('base64'));

        });
    })
}

Can you see any issues with this setup?

@Hammster
Copy link

I've updated the code in a fork, feel free to check it out ;)

@noosxe
Copy link

noosxe commented Oct 29, 2017

@si-harps more iterations is better for security as it takes more time for attackers to guess the right hash with brute force, but also increases time needed for legitimate hash generations, also I guess it uses more resource to calculate that iterations, so it is just about finding the right balance related to hardware you have

@spimou
Copy link

spimou commented May 26, 2018

Hi there, just using your example in node v8.11.1, express v4.16.3. You have to update your code to include
digest : 'crypt'
in the config and also include it as an argument in the pbkdf2
crypto.pbkdf2(password, salt, config.iterations, config.hashBytes, config.digest,(err, hash) =>{
otherwise you get TypeError: The "digest" argument is required and must not be undefined

Thanks

@afkhalid
Copy link

Hi there, just using your example in node v8.11.1, express v4.16.3. You have to update your code to include
digest : 'crypt'
in the config and also include it as an argument in the pbkdf2
crypto.pbkdf2(password, salt, config.iterations, config.hashBytes, config.digest,(err, hash) =>{
otherwise you get TypeError: The "digest" argument is required and must not be undefined

Thanks

Thanks but it's 'crypto' not 'crypt'

@petazeta
Copy link

I like this way for storing the salt and other config data.
*** 2021: new Buffer() as constructor is deprecated, you can use Buffer.alloc ***

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment