All files / src/derive/factors ooba.js

100% Statements 27/27
100% Branches 2/2
100% Functions 4/4
100% Lines 25/25

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 113 114                  1x 1x 1x 1x                                                                                     7x 6x   6x 6x 6x     6x   6x       6x 6x 36x   6x 6x 6x 6x     6x 6x 6x                       6x         6x                 6x         1x  
/**
 * @file MFKDF OOBA Factor Derivation
 * @copyright Multifactor, Inc. 2022–2025
 *
 * @description
 * Derive Out-of-Band Authentication (OOBA) factor for multi-factor key derivation
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */
const crypto = require('crypto')
const { hkdfSync } = require('crypto')
const { randomInt: random } = require('crypto')
const { encrypt, decrypt } = require('../../crypt')
let subtle
/* istanbul ignore next */
if (typeof window !== 'undefined') {
  subtle = window.crypto.subtle
} else {
  subtle = crypto.webcrypto.subtle
}
 
/**
 * Derive 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') // -> 01d0…2516
 * derive.key.toString('hex') // -> 01d0…2516
 *
 * @param {number} code - The one-time code from which to derive an MFKDF factor
 * @returns {function(config:Object): Promise<MFKDFFactor>} Async function to generate MFKDF factor information
 * @author Vivek Nair (https://nair.me) <[email protected]>
 * @since 1.1.0
 * @memberof derive.factors
 */
function ooba (code) {
  if (typeof code !== 'string') throw new TypeError('code must be a string')
  code = code.toUpperCase()
 
  return async (params) => {
    const pad = Buffer.from(params.pad, 'base64')
    const prevKey = Buffer.from(
      hkdfSync('sha256', Buffer.from(code), '', '', 32)
    )
    const target = decrypt(pad, prevKey)
 
    return {
      type: 'ooba',
      data: target,
      params: async ({ key }) => {
        let code = ''
        for (let i = 0; i < params.length; i++) {
          code += (await random(0, 35)).toString(36)
        }
        code = code.toUpperCase()
        const config = JSON.parse(JSON.stringify(params.params))
        config.code = code
        const nextKey = Buffer.from(
          hkdfSync('sha256', Buffer.from(code), '', '', 32)
        )
        const pad = encrypt(target, nextKey)
        const plaintext = Buffer.from(JSON.stringify(config))
        const publicKey = await subtle.importKey(
          'jwk',
          params.key,
          {
            name: 'RSA-OAEP',
            modulusLength: 2048,
            hash: 'SHA-256',
            publicExponent: new Uint8Array([0x01, 0x00, 0x01])
          },
          false,
          ['encrypt']
        )
        const ciphertext = await subtle.encrypt(
          { name: 'RSA-OAEP' },
          publicKey,
          plaintext
        )
        return {
          length: params.length,
          key: params.key,
          params: params.params,
          next: Buffer.from(ciphertext).toString('hex'),
          pad: pad.toString('base64')
        }
      },
      output: async () => {
        return {}
      }
    }
  }
}
module.exports.ooba = ooba