Skip to content

Instantly share code, notes, and snippets.

@pierreis
Last active September 24, 2018 21:09
Show Gist options
  • Save pierreis/2768346 to your computer and use it in GitHub Desktop.
Save pierreis/2768346 to your computer and use it in GitHub Desktop.
How to generate and check strong binary hashes for passwords with Node.JS
/*!
* Password hashing
*
* In essence, passwords are hashed with a global salt (the same for every password), which is
* to remain secret, and a local salt (only specific to one password). If you don't provide
* local salt to `hashPassword`, it will generate one for you.
* The result is a 48-byte long buffer which includes your hashed password along with the local
* salt in clear, that you can store in your DB. You may call buffer.toString('hex') in case
* you want to store it as hex and waste space.
*
* The global hash makes lookup table assisted dictionary attack difficult.
* The local hash makes creating rainbow tables difficult.
*/
var crypto = require('crypto');
/**
* Hash `password` with local `globalSalt` and `localSalt`, and invoke
* `callback(err, buffer)` if provided, or return resulting buffer.
* If no `localSalt` is provided, random bytes will be generated instead.
*
* @param {String} password
* @param {Buffer|String} globalSalt
* @param {Buffer} [localSalt]
* @param {Function} [callback]
* @return {Buffer} if no `callback` provided
* @api public
*/
function hashPassword(password, globalSalt, localSalt, callback) {
// Support callback as 3rd argument
if(typeof localSalt === 'function') {
callback = localSalt;
localSalt = null;
}
// Create the main hash
var hash = crypto.createHash('sha256').update(password);
// Add the global salt
hash.update(globalSalt);
// Prepare for the rest of the job
function finish(err, localSalt) {
if(err) {
if(callback) return callback(err);
throw err;
}
// Add the local salt
hash.update(localSalt);
// Return result
var acc = new Buffer(48);
new Buffer(hash.digest('hex'), 'hex').copy(acc);
localSalt.copy(acc, 32);
if(callback) return callback(null, acc);
return acc;
}
// Do it
if(!localSalt) {
localSalt = crypto.randomBytes(16, callback ? finish : null);
if(!callback) return finish(null, localSalt);
} else {
if(localSalt.length !== 16 || !(localSalt instanceof Buffer)) {
var err = new Error('Invalid `localSalt` provided');
if(callback) return callback(err);
throw err;
}
return finish(null, localSalt);
}
}
/**
* Check whether `password` matches `hash` with `globalSalt`, and invoke
* `callback(err, result)` if provided, or return result.
*
* @param {String} password
* @param {Buffer} hash
* @param {Buffer|String} globalSalt
* @param {Function} [callback]
* @return {Boolean} if no `callback` provided
* @api public
*/
function checkPasswordHash(password, hash, globalSalt, callback) {
if(!(hash instanceof Buffer) || hash.length !== 48) {
return callback(new Error('Invalid `hash` provided'));
}
var localSalt = hash.slice(32, 48);
// Prepare callback function
function onHashed(err, computed) {
if(err) {
if(callback) return callback(err);
throw err;
}
for(var i = 0; i < 32; i++) {
if(hash[i] !== computed[i]) {
if(callback) return callback(err, false);
return false;
}
}
if(callback) return callback(null, true);
return true;
}
// Start computation
var computed = hashPassword(password, globalSalt, localSalt,
callback ? onHashed : null);
// Return if sync
if(!callback) return onHashed(null, computed);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment