All files / src kdf.js

100% Statements 43/43
100% Branches 24/24
100% Functions 12/12
100% Lines 39/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112                        1x 1x 1x 1x 1x 1x                                                                       462x 462x   462x 116x 116x   116x 116x     346x 13x   13x 13x     13x   13x       13x   13x 13x         333x 6x 6x 6x     327x 319x 319x 319x 318x 319x 319x     8x 7x 7x 7x       1x     1x  
/**
 * @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 { hkdf } = require('@panva/hkdf')
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
 */
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) => {
      hkdf(options.params.digest, input, salt, '', size).then((result) => {
        resolve(Buffer.from(result))
      })
    })
  } else {
    throw new RangeError('kdf should be one of pbkdf2, bcrypt, scrypt, argon2i, argon2d, or argon2id (default)')
  }
}
module.exports.kdf = kdf