Last active
October 30, 2024 18:59
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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