Wednesday, April 16, 2014

Secure Password Storage with MongoDB and Node

Summary

In this Tech Tip I'm going to discuss how to store passwords securely in a mongoDB database utilizing the crypto methods included with Node.js.

Background

I'm going to simulate a database of user info that includes the user's ID and password.  The password will be stored as a hash utilizing the 'key strengthening' algorithm PBKDF2.  I'll use the crypto functions included in the node.js distribution to implement the password hashing function.  A really nice discussion of password hashing is here.

Implementation

PBKDF2 utilizes a salt and multiple iterations of a hash function to create a cryptographically strong hash.  Below I set up some variables for a 512-bit salt, 10000 iterations, and a 512-bit resulting key length.

var saltLengthBytes = 64;
var hashIterations = 10000;
var keyLengthBytes = 64;


Below is the main body of the demo code.  Steps that occur:
  1. Initiate the MongoClient connection with a callback that initiates the remainder of the functions.
  2. Create a user record with an id of '1000' and a password of 'userPa$$word'.
  3. Perform a series of 3 authentication tests.  The first test uses a valid username and password.  The second uses an invalid username.  The third uses a valid username, but invalid password.
  4. Delete the user record.
  5. Close the database.


mongodb.MongoClient.connect('mongodb://localhost:27017/configDb', function(err, db){
  if (!err && db)
  {
  userColl = db.collection('users');
  createUser({_id : 1000, name : 'user1000', password :   'userPa$$word'}, function(result1){
  if (!(result1 instanceof Error))
{
console.log('Authenticating valid user name (1000) and password (userPa$$word)')
authenticateUser(1000, 'userPa$$word', function(result2){
if (!(result2 instanceof Error) && result2 == true)
console.log('Results: User authenticated');
else
console.log('Results: User name or password does not match');

console.log('\n'+ 'Authenticating invalid user name (1111)')
authenticateUser(1111, 'userPa$$word', function(result3){
if (!(result3 instanceof Error) && result3 == true)
console.log('Results: User authenticated');
else
console.log('Results: User name or password does not match');

console.log('\n' + 'Authenticating valid user name (1000) but invalid password (userPassword)')
authenticateUser(1000, 'userPassword', function(result4){
  if (!(result4 instanceof Error) && result4 == true)
console.log('Results: User authenticated');
else
console.log('Results: User name or password does not match');
deleteUser(1000, function(result5){
db.close();
});
  });
  });
  });
  }
  });
  }
}); 


Below is a snippet of the createUser function with the password hashing portions depicted.  Of note, I'm using node's built-in strong random number generator for the salt and its PBKDF2 hash function (node uses SHA-1 for the underlying hash).  I also use Mongo's Binary object for storing the resulting salt and password hash into the database.


function createUser(doc, callback)
{

  try
  {
  crypto.randomBytes(saltLengthBytes, function(err1, buf) {  //create the salt for the hash function
  if (err1)
  {
  console.log(err1.message);
  if (callback)
callback(err1);
  }

   else
  {
  doc.salt = new Binary(buf);  //put salt result in a mongodb Binary object

  //Invoke hash function with salt object
  crypto.pbkdf2(doc.password, doc.salt.read(0, doc.salt.length()), hashIterations, keyLengthBytes, function(err2, key){ 



Below is the authenticate function.  It simply applies the hashing steps again to the user-supplied credentials and then compares them to what is in the database.  If the username and hashed password match, the user is authenticated.  Otherwise, authentication fails.


function authenticateUser(id, password, callback)
{
  try

  {
  readUser(id, function(result) {
  if (result instanceof Error)
  {
  console(result.message);
  if (callback)
  callback(result);
  }
  else
  {
  if (result)
  {
crypto.pbkdf2(password, result.salt.read(0, result.salt.length()), hashIterations, keyLengthBytes, function(err, key){ 


Output

Authenticating valid user name (1000) and password (userPa$$word)
Results: User authenticated

Authenticating invalid user name (1111)
Results: User name or password does not match

Authenticating valid user name (1000) but invalid password (userPassword)

Results: User name or password does not match


Full source code here.
Copyright ©1993-2024 Joey E Whelan, All rights reserved.