Skip to content

Instantly share code, notes, and snippets.

@skeggse
Last active September 22, 2024 06:41
Show Gist options
  • 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;
@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