Home Reference Source

node-user-accounts-boilerplate-nahid/crypt/PBKDF2.js

"use strict";

const Crypt = require('./Crypt');
const crypto = require('crypto');

/**
 * Use crypto.pbkdf2
 * 
 * implementation adapted from https://gist.github.com/skeggse/52672ddee97c8efec269
 */
class PBKDF2 extends Crypt
{

  /**
   * @param {Object} [options={}]
   * @param {String} [options.hashDigest=sha512] see https://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
   * @param {Number} [options.hashBytes=512] see https://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
   * @param {Number} [options.saltBytes=16] see https://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
   * @param {Number} [options.iterations=10000] see https://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
   */
  constructor(options = {})
  {
    super();
    this.hashDigest = options.hashDigest || 'sha512';
    this.hashBytes = options.hashBytes || 512;
    this.saltBytes = options.saltBytes || 16;
    this.iterations = options.iterations || 10000;
  }

  /**
   * @override
   */
  hashImplementation(password)
  {
    return new Promise((resolve, reject) =>
    {
      // generate a salt for pbkdf2
      crypto.randomBytes(this.saltBytes, (err, salt) =>
      {
        if (err)
        {
          return reject(err);
        }
        crypto.pbkdf2(password, salt, this.iterations, this.hashBytes, this.hashDigest, (err, hash) =>
        {
          if (err)
          {
            console.log('XXX', err);

            return reject(err);
          }

          let 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(this.iterations, 4, true);
          salt.copy(combined, 8);
          hash.copy(combined, salt.length + 8);

          resolve(combined.toString('hex'));
        });
      });
    });
  }

  /**
   * @override
   */
  verifyImplementation(password, oldhash)
  {
    return new Promise((resolve, reject) =>
    {
      let combined = new Buffer(oldhash, 'hex');
      // extract the salt and hash from the combined buffer
      const saltBytes = combined.readUInt32BE(0);
      const hashBytes = combined.length - saltBytes - 8;
      const iterations = combined.readUInt32BE(4);
      const salt = combined.slice(8, saltBytes + 8);
      const hash = combined.toString('binary', saltBytes + 8);
      // verify the salt and hash against the password

      crypto.pbkdf2(password, salt, iterations, hashBytes, this.hashDigest, (err, verify) =>
      {
        if (err)
        {
          return reject(err);
        }
        resolve(verify.toString('binary') === hash);
      });
    });
  }

}

module.exports = PBKDF2;