All files / src/derive key.js

100% Statements 47/47
100% Branches 20/20
100% Functions 2/2
100% Lines 40/40

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                    1x 1x 1x 1x 1x 1x 1x 1x                                                             164x 164x 164x 163x   161x 161x 161x   161x 324x 253x     252x 3x   249x   248x 248x 248x   248x     251x 251x 251x   71x 71x       322x   156x 156x   156x   156x 317x 250x       156x   156x   1x  
/**
 * @file Multi-factor Key Derivation
 * @copyright Multifactor 2022 All Rights Reserved
 *
 * @description
 * Derive a multi-factor derived key
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */
 
const Ajv = require('ajv')
const policySchema = require('./policy.json')
const combine = require('../secrets/combine').combine
const recover = require('../secrets/recover').recover
const kdf = require('../kdf').kdf
const { hkdf } = require('@panva/hkdf')
const xor = require('buffer-xor')
const MFKDFDerivedKey = require('../classes/MFKDFDerivedKey')
 
/**
 * Derive a key from multiple factors of input
 *
 * @example
 * // setup 16 byte 2-of-3-factor multi-factor derived key with a password, HOTP code, and UUID recovery code
 * const setup = await mfkdf.setup.key([
 *   await mfkdf.setup.factors.password('password'),
 *   await mfkdf.setup.factors.hotp({ secret: Buffer.from('hello world') }),
 *   await mfkdf.setup.factors.uuid({ id: 'recovery', uuid: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' })
 * ], {threshold: 2, size: 16})
 *
 * // derive key using 2 of the 3 factors
 * const derive = await mfkdf.derive.key(setup.policy, {
 *   password: mfkdf.derive.factors.password('password'),
 *   hotp: mfkdf.derive.factors.hotp(365287)
 * })
 *
 * setup.key.toString('hex') // -> 34d20ced439ec2f871c96ca377f25771
 * derive.key.toString('hex') // -> 34d20ced439ec2f871c96ca377f25771
 *
 * @param {Object} policy - The key policy for the key being derived
 * @param {Object.<string, MFKDFFactor>} factors - Factors used to derive this key
 * @returns {MFKDFDerivedKey} A multi-factor derived key object
 * @author Vivek Nair (https://nair.me) <[email protected]>
 * @since 0.9.0
 * @async
 * @memberOf derive
 */
async function key (policy, factors) {
  const ajv = new Ajv()
  const valid = ajv.validate(policySchema, policy)
  if (!valid) throw new TypeError('invalid key policy', ajv.errors)
  if (Object.keys(factors).length < policy.threshold) throw new RangeError('insufficient factors provided to derive key')
 
  const shares = []
  const newFactors = []
  const outputs = {}
 
  for (const factor of policy.factors) {
    if (factors[factor.id] && typeof factors[factor.id] === 'function') {
      const material = await factors[factor.id](factor.params)
      let share
 
      if (material.type === 'persisted') {
        share = material.data
      } else {
        if (material.type !== factor.type) throw new TypeError('wrong factor material function used for this factor type')
 
        const pad = Buffer.from(factor.pad, 'base64')
        let stretched = Buffer.from(await hkdf('sha512', material.data, '', '', policy.size))
        if (Buffer.byteLength(pad) > policy.size) stretched = Buffer.concat([Buffer.alloc(Buffer.byteLength(pad) - policy.size), stretched])
 
        share = xor(pad, stretched)
      }
 
      shares.push(share)
      if (material.output) outputs[factor.id] = await material.output()
      newFactors.push(material.params)
    } else {
      shares.push(null)
      newFactors.push(null)
    }
  }
 
  if (shares.filter(x => Buffer.isBuffer(x)).length < policy.threshold) throw new RangeError('insufficient factors provided to derive key')
 
  const secret = combine(shares, policy.threshold, policy.factors.length)
  const key = await kdf(secret, Buffer.from(policy.salt, 'base64'), policy.size, policy.kdf)
 
  const newPolicy = JSON.parse(JSON.stringify(policy))
 
  for (const [index, factor] of newFactors.entries()) {
    if (typeof factor === 'function') {
      newPolicy.factors[index].params = await factor({ key })
    }
  }
 
  const originalShares = recover(shares, policy.threshold, policy.factors.length)
 
  return new MFKDFDerivedKey(newPolicy, key, secret, originalShares, outputs)
}
module.exports.key = key