All files / src/derive/factors totp.js

100% Statements 37/37
100% Branches 11/11
100% Functions 5/5
100% Lines 31/31

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                  1x 1x     25x                                                               12x 11x 11x 10x   9x 9x 9x 9x   9x   9x   8x   8x 8x 8x   8x       8x 8x   8x 8x   8x   8x 17x   17x                 17x   17x     8x                     8x         1x  
/**
 * @file MFKDF TOTP Factor Derivation
 * @copyright Multifactor 2022 All Rights Reserved
 *
 * @description
 * Derive TOTP factor for multi-factor key derivation
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */
const xor = require('buffer-xor')
const speakeasy = require('speakeasy')
 
function mod (n, m) {
  return ((n % m) + m) % m
}
 
/**
 * Derive an MFKDF TOTP factor
 *
 * @example
 * // setup key with totp factor
 * const setup = await mfkdf.setup.key([
 *   await mfkdf.setup.factors.totp({
 *     secret: Buffer.from('hello world'),
 *     time: 1650430806597
 *   })
 * ], {size: 8})
 *
 * // derive key with totp factor
 * const derive = await mfkdf.derive.key(setup.policy, {
 *   totp: mfkdf.derive.factors.totp(528258, { time: 1650430943604 })
 * })
 *
 * setup.key.toString('hex') // -> 01d0c7236adf2516
 * derive.key.toString('hex') // -> 01d0c7236adf2516
 *
 * @param {number} code - The TOTP code from which to derive an MFKDF factor
 * @param {Object} [options] - Additional options for deriving the TOTP factor
 * @param {number} [options.time] - Current time for TOTP; defaults to Date.now()
 * @returns {function(config:Object): Promise<MFKDFFactor>} Async function to generate MFKDF factor information
 * @author Vivek Nair (https://nair.me) <[email protected]>
 * @since 0.13.0
 * @memberof derive.factors
 */
function totp (code, options = {}) {
  if (!Number.isInteger(code)) throw new TypeError('code must be an integer')
  if (typeof options.time === 'undefined') options.time = Date.now()
  if (!Number.isInteger(options.time)) throw new TypeError('time must be an integer')
  if (options.time <= 0) throw new RangeError('time must be positive')
 
  return async (params) => {
    const offsets = Buffer.from(params.offsets, 'base64')
    const startCounter = Math.floor(params.start / (params.step * 1000))
    const nowCounter = Math.floor(options.time / (params.step * 1000))
 
    const index = nowCounter - startCounter
 
    if (index >= params.window) throw new RangeError('TOTP window exceeded')
 
    const offset = offsets.readUInt32BE(4 * index)
 
    const target = mod(offset + code, 10 ** params.digits)
    const buffer = Buffer.allocUnsafe(4)
    buffer.writeUInt32BE(target, 0)
 
    return {
      type: 'totp',
      data: buffer,
      params: async ({ key }) => {
        const pad = Buffer.from(params.pad, 'base64')
        const secret = xor(pad, key.slice(0, Buffer.byteLength(pad)))
 
        const time = options.time
        const newOffsets = Buffer.allocUnsafe(4 * params.window)
 
        offsets.copy(newOffsets, 0, 4 * index)
 
        for (let i = params.window - index; i < params.window; i++) {
          const counter = Math.floor(time / (params.step * 1000)) + i
 
          const code = parseInt(speakeasy.totp({
            secret: secret.toString('hex'),
            encoding: 'hex',
            step: params.step,
            counter,
            algorithm: params.hash,
            digits: params.digits
          }))
 
          const offset = mod(target - code, 10 ** params.digits)
 
          newOffsets.writeUInt32BE(offset, 4 * i)
        }
 
        return {
          start: time,
          hash: params.hash,
          digits: params.digits,
          step: params.step,
          window: params.window,
          pad: params.pad,
          offsets: newOffsets.toString('base64')
        }
      },
      output: async () => {
        return { }
      }
    }
  }
}
module.exports.totp = totp