-
-
Save skeggse/52672ddee97c8efec269 to your computer and use it in GitHub Desktop.
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 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?
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.
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
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 ...
Scmp.js is any need for this sample?
https://www.npmjs.com/package/scmp
Why? To minimize vulnerability against timing attacks.
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?
I've updated the code in a fork, feel free to check it out ;)
@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
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
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 theconfig
and also include it as an argument in thepbkdf2
crypto.pbkdf2(password, salt, config.iterations, config.hashBytes, config.digest,(err, hash) =>{
otherwise you getTypeError: The "digest" argument is required and must not be undefined
Thanks
Thanks but it's 'crypto' not 'crypt'
I like this way for storing the salt and other config data.
*** 2021: new Buffer() as constructor is deprecated, you can use Buffer.alloc ***
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.