setup/kdf.js

  1. /**
  2. * @file Key Derivation Function (KDF) Setup
  3. * @copyright Multifactor 2022 All Rights Reserved
  4. *
  5. * @description
  6. * Validate and setup a KDF configuration for a multi-factor derived key
  7. *
  8. * @author Vivek Nair (https://nair.me) <vivek@nair.me>
  9. */
  10. const defaults = require('../defaults')
  11. /**
  12. * Validate and setup a KDF configuration for a multi-factor derived key
  13. *
  14. * @example
  15. * // setup kdf configuration
  16. * const config = await mfkdf.setup.kdf({
  17. * kdf: 'pbkdf2',
  18. * pbkdf2rounds: 100000,
  19. * pbkdf2digest: 'sha256'
  20. * }); // -> { type: 'pbkdf2', params: { rounds: 100000, digest: 'sha256' } }
  21. *
  22. * // derive key
  23. * const key = await mfkdf.kdf('password', 'salt', 8, config);
  24. * key.toString('hex') // -> 0394a2ede332c9a1
  25. *
  26. * @param {Object} [options] - KDF configuration options
  27. * @param {string} [options.kdf='argon2id'] - KDF algorithm to use; hkdf, pbkdf2, bcrypt, scrypt, argon2i, argon2d, or argon2id
  28. * @param {string} [options.hkdfdigest='sha256'] - Hash function to use if using hkdf; sha1, sha256, sha384, or sha512
  29. * @param {number} [options.pbkdf2rounds=310000] - Number of rounds to use if using pbkdf2
  30. * @param {string} [options.pbkdf2digest='sha256'] - Hash function to use if using pbkdf2; sha1, sha256, sha384, or sha512
  31. * @param {number} [options.bcryptrounds=10] - Number of rounds to use if using bcrypt
  32. * @param {number} [options.scryptcost=16384] - Iterations count (N) to use if using scrypt
  33. * @param {number} [options.scryptblocksize=8] - Block size (r) to use if using scrypt
  34. * @param {number} [options.scryptparallelism=1] - Parallelism factor (p) to use if using scrypt
  35. * @param {number} [options.argon2time=2] - Iterations to use if using argon2
  36. * @param {number} [options.argon2mem=24576] - Memory to use if using argon2
  37. * @param {number} [options.argon2parallelism=1] - Parallelism to use if using argon2
  38. * @returns {object} A KDF configuration as a JSON object
  39. * @author Vivek Nair (https://nair.me) <vivek@nair.me>
  40. * @since 0.7.0
  41. * @memberOf setup
  42. */
  43. function kdf (options) {
  44. options = Object.assign(Object.assign({}, defaults.kdf), options)
  45. if (typeof options.kdf !== 'string') throw new TypeError('kdf must be a string')
  46. const config = {
  47. type: options.kdf,
  48. params: {}
  49. }
  50. if (options.kdf === 'hkdf') {
  51. // hdkf digest
  52. if (typeof options.hkdfdigest !== 'string') throw new TypeError('hkdfdigest must be a string')
  53. if (!['sha1', 'sha256', 'sha384', 'sha512'].includes(options.hkdfdigest)) throw new RangeError('hkdfdigest must be one of sha1, sha256, sha384, or sha512')
  54. config.params.digest = options.hkdfdigest
  55. } else if (options.kdf === 'pbkdf2') {
  56. // pbkdf2 rounds
  57. if (!(Number.isInteger(options.pbkdf2rounds))) throw new TypeError('pbkdf2rounds must be an integer')
  58. if (!(options.pbkdf2rounds > 0)) throw new RangeError('pbkdf2rounds must be positive')
  59. config.params.rounds = options.pbkdf2rounds
  60. // pbkdf2 digest
  61. if (typeof options.pbkdf2digest !== 'string') throw new TypeError('pbkdf2digest must be a string')
  62. if (!['sha1', 'sha256', 'sha384', 'sha512'].includes(options.pbkdf2digest)) throw new RangeError('pbkdf2digest must be one of sha1, sha256, sha384, or sha512')
  63. config.params.digest = options.pbkdf2digest
  64. } else if (options.kdf === 'bcrypt') {
  65. // bcrypt rounds
  66. if (!(Number.isInteger(options.bcryptrounds))) throw new TypeError('bcryptrounds must be an integer')
  67. if (!(options.bcryptrounds > 0)) throw new RangeError('bcryptrounds must be positive')
  68. config.params.rounds = options.bcryptrounds
  69. } else if (options.kdf === 'scrypt') {
  70. // scrypt rounds
  71. if (!(Number.isInteger(options.scryptcost))) throw new TypeError('scryptcost must be a positive integer')
  72. if (!(options.scryptcost > 0)) throw new RangeError('scryptcost must be positive')
  73. config.params.rounds = options.scryptcost
  74. // scrypt block size
  75. if (!(Number.isInteger(options.scryptblocksize))) throw new TypeError('scryptblocksize must be an integer')
  76. if (!(options.scryptblocksize > 0)) throw new RangeError('scryptblocksize must be positive')
  77. config.params.blocksize = options.scryptblocksize
  78. // scrypt parallelism
  79. if (!(Number.isInteger(options.scryptparallelism))) throw new TypeError('scryptparallelism must be an integer')
  80. if (!(options.scryptparallelism > 0)) throw new RangeError('scryptparallelism must be positive')
  81. config.params.parallelism = options.scryptparallelism
  82. } else if (options.kdf === 'argon2i' || options.kdf === 'argon2d' || options.kdf === 'argon2id') {
  83. // argon2 rounds
  84. if (!(Number.isInteger(options.argon2time))) throw new TypeError('argon2time must be an integer')
  85. if (!(options.argon2time > 0)) throw new RangeError('argon2time must be positive')
  86. config.params.rounds = options.argon2time
  87. // argon2 memory
  88. if (!(Number.isInteger(options.argon2mem))) throw new TypeError('argon2mem must be an integer')
  89. if (!(options.argon2mem > 0)) throw new RangeError('argon2mem must be positive')
  90. config.params.memory = options.argon2mem
  91. // argon2 parallelism
  92. if (!(Number.isInteger(options.argon2parallelism))) throw new TypeError('argon2parallelism must be an integer')
  93. if (!(options.argon2parallelism > 0)) throw new RangeError('argon2parallelism must be positive')
  94. config.params.parallelism = options.argon2parallelism
  95. } else {
  96. throw new RangeError('kdf must be one of pbkdf2, bcrypt, scrypt, argon2i, argon2d, or argon2id')
  97. }
  98. return config
  99. }
  100. module.exports.kdf = kdf