derive/key.js

  1. /**
  2. * @file Multi-factor Key Derivation
  3. * @copyright Multifactor 2022 All Rights Reserved
  4. *
  5. * @description
  6. * Derive a multi-factor derived key
  7. *
  8. * @author Vivek Nair (https://nair.me) <vivek@nair.me>
  9. */
  10. const Ajv = require('ajv')
  11. const policySchema = require('./policy.json')
  12. const combine = require('../secrets/combine').combine
  13. const recover = require('../secrets/recover').recover
  14. const kdf = require('../kdf').kdf
  15. const { hkdf } = require('@panva/hkdf')
  16. const xor = require('buffer-xor')
  17. const MFKDFDerivedKey = require('../classes/MFKDFDerivedKey')
  18. /**
  19. * Derive a key from multiple factors of input
  20. *
  21. * @example
  22. * // setup 16 byte 2-of-3-factor multi-factor derived key with a password, HOTP code, and UUID recovery code
  23. * const setup = await mfkdf.setup.key([
  24. * await mfkdf.setup.factors.password('password'),
  25. * await mfkdf.setup.factors.hotp({ secret: Buffer.from('hello world') }),
  26. * await mfkdf.setup.factors.uuid({ id: 'recovery', uuid: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' })
  27. * ], {threshold: 2, size: 16})
  28. *
  29. * // derive key using 2 of the 3 factors
  30. * const derive = await mfkdf.derive.key(setup.policy, {
  31. * password: mfkdf.derive.factors.password('password'),
  32. * hotp: mfkdf.derive.factors.hotp(365287)
  33. * })
  34. *
  35. * setup.key.toString('hex') // -> 34d20ced439ec2f871c96ca377f25771
  36. * derive.key.toString('hex') // -> 34d20ced439ec2f871c96ca377f25771
  37. *
  38. * @param {Object} policy - The key policy for the key being derived
  39. * @param {Object.<string, MFKDFFactor>} factors - Factors used to derive this key
  40. * @returns {MFKDFDerivedKey} A multi-factor derived key object
  41. * @author Vivek Nair (https://nair.me) <vivek@nair.me>
  42. * @since 0.9.0
  43. * @async
  44. * @memberOf derive
  45. */
  46. async function key (policy, factors) {
  47. const ajv = new Ajv()
  48. const valid = ajv.validate(policySchema, policy)
  49. if (!valid) throw new TypeError('invalid key policy', ajv.errors)
  50. if (Object.keys(factors).length < policy.threshold) throw new RangeError('insufficient factors provided to derive key')
  51. const shares = []
  52. const newFactors = []
  53. const outputs = {}
  54. for (const factor of policy.factors) {
  55. if (factors[factor.id] && typeof factors[factor.id] === 'function') {
  56. const material = await factors[factor.id](factor.params)
  57. let share
  58. if (material.type === 'persisted') {
  59. share = material.data
  60. } else {
  61. if (material.type !== factor.type) throw new TypeError('wrong factor material function used for this factor type')
  62. const pad = Buffer.from(factor.pad, 'base64')
  63. let stretched = Buffer.from(await hkdf('sha512', material.data, '', '', policy.size))
  64. if (Buffer.byteLength(pad) > policy.size) stretched = Buffer.concat([Buffer.alloc(Buffer.byteLength(pad) - policy.size), stretched])
  65. share = xor(pad, stretched)
  66. }
  67. shares.push(share)
  68. if (material.output) outputs[factor.id] = await material.output()
  69. newFactors.push(material.params)
  70. } else {
  71. shares.push(null)
  72. newFactors.push(null)
  73. }
  74. }
  75. if (shares.filter(x => Buffer.isBuffer(x)).length < policy.threshold) throw new RangeError('insufficient factors provided to derive key')
  76. const secret = combine(shares, policy.threshold, policy.factors.length)
  77. const key = await kdf(secret, Buffer.from(policy.salt, 'base64'), policy.size, policy.kdf)
  78. const newPolicy = JSON.parse(JSON.stringify(policy))
  79. for (const [index, factor] of newFactors.entries()) {
  80. if (typeof factor === 'function') {
  81. newPolicy.factors[index].params = await factor({ key })
  82. }
  83. }
  84. const originalShares = recover(shares, policy.threshold, policy.factors.length)
  85. return new MFKDFDerivedKey(newPolicy, key, secret, originalShares, outputs)
  86. }
  87. module.exports.key = key