All files / src/setup/factors ooba.js

100% Statements 35/35
100% Branches 14/14
100% Functions 3/3
100% Lines 27/27

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                  1x 1x 1x 1x                                                                                               11x 11x 10x 9x 8x 7x 6x 5x   4x   4x           4x 4x 24x   4x 4x 4x 4x 4x 4x 4x 4x                 4x       1x  
/**
 * @file MFKDF OOBA Factor Setup
 * @copyright Multifactor 2022 All Rights Reserved
 *
 * @description
 * Setup an Out-of-Band Authentication (OOBA) factor for multi-factor key derivation
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */
const defaults = require('../../defaults')
const crypto = require('crypto')
const xor = require('buffer-xor')
const random = require('random-number-csprng')
let subtle
/* istanbul ignore next */
if (typeof window !== 'undefined') {
  subtle = window.crypto.subtle
} else {
  subtle = crypto.webcrypto.subtle
}
 
/**
 * Setup an MFKDF Out-of-Band Authentication (OOBA) factor
 *
 * @example
 * // setup RSA key pair (on out-of-band server)
 * const keyPair = await crypto.webcrypto.subtle.generateKey({hash: 'SHA-256', modulusLength: 2048, name: 'RSA-OAEP', publicExponent: new Uint8Array([1, 0, 1])}, true, ['encrypt', 'decrypt'])
 *
 * // setup key with out-of-band authentication factor
 * const setup = await mfkdf.setup.key([
 *   await mfkdf.setup.factors.ooba({
 *     key: keyPair.publicKey, params: { email: '[email protected]' }
 *   })
 * ])
 *
 * // decrypt and send code (on out-of-band server)
 * const next = setup.policy.factors[0].params.next
 * const decrypted = await crypto.webcrypto.subtle.decrypt({name: 'RSA-OAEP'}, keyPair.privateKey, Buffer.from(next, 'hex'))
 * const code = JSON.parse(Buffer.from(decrypted).toString()).code;
 *
 * // derive key with out-of-band factor
 * const derive = await mfkdf.derive.key(setup.policy, {
 *   ooba: mfkdf.derive.factors.ooba(code)
 * })
 *
 * setup.key.toString('hex') // -> 01d0c7236adf2516
 * derive.key.toString('hex') // -> 01d0c7236adf2516
 *
 * @param {Object} [options] - Configuration options
 * @param {string} [options.id='ooba'] - Unique identifier for this factor
 * @param {number} [options.length=6] - Number of characters to use in one-time codes
 * @param {CryptoKey} options.key - Public key of out-of-band channel
 * @param {Object} options.params - Parameters to provide out-of-band channel
 * @returns {MFKDFFactor} MFKDF factor information
 * @author Vivek Nair (https://nair.me) <[email protected]>
 * @since 1.1.0
 * @async
 * @memberof setup.factors
 */
async function ooba (options) {
  options = Object.assign(Object.assign({}, defaults.ooba), options)
  if (typeof options.id !== 'string') throw new TypeError('id must be a string')
  if (options.id.length === 0) throw new RangeError('id cannot be empty')
  if (!Number.isInteger(options.length)) throw new TypeError('length must be an interger')
  if (options.length <= 0) throw new RangeError('length must be positive')
  if (options.length > 32) throw new RangeError('length must be at most 32')
  if (options.key.type !== 'public') throw new TypeError('key must be a public CryptoKey')
  if (typeof options.params !== 'object') throw new TypeError('params must be an object')
 
  const target = crypto.randomBytes(options.length)
 
  return {
    type: 'ooba',
    id: options.id,
    data: target,
    entropy: Math.log2(36 ** options.length),
    params: async ({ key }) => {
      let code = ''
      for (let i = 0; i < options.length; i++) {
        code += (await random(0, 35)).toString(36)
      }
      code = code.toUpperCase()
      const params = JSON.parse(JSON.stringify(options.params))
      params.code = code
      const pad = xor(Buffer.from(code), target)
      const plaintext = Buffer.from(JSON.stringify(params))
      const ciphertext = await subtle.encrypt({ name: 'RSA-OAEP' }, options.key, plaintext)
      const jwk = await subtle.exportKey('jwk', options.key)
      return {
        length: options.length,
        key: jwk,
        params: options.params,
        next: Buffer.from(ciphertext).toString('hex'),
        pad: pad.toString('base64')
      }
    },
    output: async () => {
      return { }
    }
  }
}
module.exports.ooba = ooba