kdf.js

/**
 * @file Key Derivation Function (KDF)
 * @copyright Multifactor 2021 All Rights Reserved
 *
 * @description
 * Implements several key derivation functions (KDFs) that can underly the MFKDF
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */

// const argon2 = require('argon2-browser')

const crypto = require('crypto')
const pbkdf2 = require('pbkdf2')
const bcrypt = require('bcryptjs')
const scrypt = require('scrypt-js')
const { hkdfSync } = require('crypto')
const hash = require('hash-wasm')

/**
 * Single-factor (traditional) key derivation function; produces a derived a key from a single input.
 * Supports a number of underlying KDFs: pbkdf2, scrypt, bcrypt, and argon2 (recommended).
 *
 * @example
 * // setup kdf configuration
 * const config = await mfkdf.setup.kdf({
 *   kdf: 'pbkdf2',
 *   pbkdf2rounds: 100000,
 *   pbkdf2digest: 'sha256'
 * }); // -> { type: 'pbkdf2', params: { rounds: 100000, digest: 'sha256' } }
 *
 * // derive key
 * const key = await mfkdf.kdf('password', 'salt', 8, config);
 * key.toString('hex') // -> 0394a2ede332c9a1
 *
 * @param {Buffer|string} input - KDF input string
 * @param {Buffer|string} salt - KDF salt string
 * @param {number} size - Size of derived key to return, in bytes
 * @param {Object} options - KDF configuration options
 * @param {string} options.type - KDF algorithm to use; hkdf, pbkdf2, bcrypt, scrypt, argon2i, argon2d, or argon2id
 * @param {Object} options.params - Specify parameters of chosen kdf
 * @param {number} options.params.rounds - Number of rounds to use
 * @param {number} [options.params.digest] - Hash function to use (if using pbkdf2 or hdkf)
 * @param {number} [options.params.blocksize] - Block size to use (if using scrypt)
 * @param {number} [options.params.parallelism] - Parallelism to use (if using scrypt or argon2)
 * @param {number} [options.params.memory] - Memory to use (if using argon2)
 * @returns A derived key as a Buffer
 * @author Vivek Nair (https://nair.me) <[email protected]>
 * @since 0.0.3
 * @async
 * @memberOf kdfs
 * @deprecated
 */
async function kdf (input, salt, size, options) {
  if (typeof input === 'string') input = Buffer.from(input)
  if (typeof salt === 'string') salt = Buffer.from(salt)

  if (options.type === 'pbkdf2') {
    // PBKDF2
    return new Promise((resolve, reject) => {
      pbkdf2.pbkdf2(
        input,
        salt,
        options.params.rounds,
        size,
        options.params.digest,
        (err, derivedKey) => {
          /* istanbul ignore if */
          if (err) reject(err)
          else resolve(derivedKey)
        }
      )
    })
  } else if (options.type === 'bcrypt') {
    // bcrypt
    return new Promise((resolve, reject) => {
      // pre-hash to maximize entropy; safe when using base64 encoding
      const inputhash = crypto
        .createHash('sha256')
        .update(input)
        .digest('base64')
      const salthash = crypto
        .createHash('sha256')
        .update(salt)
        .digest('base64')
        .replace(/\+/g, '.')

      // bcrypt with fixed salt
      bcrypt.hash(
        inputhash,
        '$2a$' + options.params.rounds + '$' + salthash,
        function (err, hash) {
          /* istanbul ignore if */
          if (err) {
            reject(err)
          } else {
            // use pbkdf2/sha256 for stretching
            pbkdf2.pbkdf2(
              hash,
              salthash,
              1,
              size,
              'sha256',
              (err, derivedKey) => {
                /* istanbul ignore if */
                if (err) reject(err)
                else resolve(derivedKey)
              }
            )
          }
        }
      )
    })
  } else if (options.type === 'scrypt') {
    return new Promise((resolve, reject) => {
      scrypt
        .scrypt(
          input,
          salt,
          options.params.rounds,
          options.params.blocksize,
          options.params.parallelism,
          size
        )
        .then((result) => {
          resolve(Buffer.from(result))
        })
    })
  } else if (
    options.type === 'argon2i' ||
    options.type === 'argon2d' ||
    options.type === 'argon2id'
  ) {
    return new Promise((resolve, reject) => {
      let argon2 = hash.argon2id
      if (options.type === 'argon2i') argon2 = hash.argon2i
      else if (options.type === 'argon2d') argon2 = hash.argon2d
      argon2({
        password: input.toString(),
        salt: salt.toString(),
        iterations: options.params.rounds,
        memorySize: options.params.memory,
        hashLength: size,
        parallelism: options.params.parallelism,
        outputType: 'hex'
      }).then((result) => {
        resolve(Buffer.from(result, 'hex'))
      })
    })
  }
  if (options.type === 'hkdf') {
    return new Promise((resolve, reject) => {
      return resolve(
        Buffer.from(hkdfSync(options.params.digest, input, salt, '', size))
      )
    })
  } else {
    throw new RangeError(
      'kdf should be one of pbkdf2, bcrypt, scrypt, argon2i, argon2d, or argon2id (default)'
    )
  }
}
module.exports.kdf = kdf