All files / src/derive/factors totp.js

100% Statements 43/43
100% Branches 15/15
100% Functions 5/5
100% Lines 38/38

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 115 116 117 118 119 120 121 122 123 124 125 126 127                  1x 1x     112x                                                                 43x 42x 42x 1x   41x   40x 40x 40x 40x   40x   40x   39x   39x 18x 18x     39x 39x 39x   39x       39x 39x   39x 39x   39x   39x 31x   31x                     31x   31x 24x 24x     31x     39x                     39x         1x  
/**
 * @file MFKDF TOTP Factor Derivation
 * @copyright Multifactor, Inc. 2022–2025
 *
 * @description
 * Derive TOTP factor for multi-factor key derivation
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */
const speakeasy = require('speakeasy')
const { decrypt } = require('../../crypt')
 
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('abcdefghijklmnopqrst'),
 *     time: 1650430806597
 *   })
 * ])
 *
 * // derive key with totp factor
 * const derive = await mfkdf.derive.key(setup.policy, {
 *   totp: mfkdf.derive.factors.totp(953265, { time: 1650430943604 })
 * })
 *
 * setup.key.toString('hex') // -> 01d0…2516
 * derive.key.toString('hex') // -> 01d0…2516
 *
 * @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()
 * @param {Object} [options.oracle] - Timing oracle offsets to use; none by default
 * @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')
 
    let offset = offsets.readUInt32BE(4 * index)
 
    if (options.oracle) {
      const time = nowCounter * params.step * 1000
      offset = mod(offset - options.oracle[time], 10 ** params.digits)
    }
 
    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 = decrypt(pad, key)
 
        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.subarray(0, 20).toString('hex'),
              encoding: 'hex',
              step: params.step,
              counter,
              algorithm: params.hash,
              digits: params.digits
            })
          )
 
          let offset = mod(target - code, 10 ** params.digits)
 
          if (options.oracle) {
            const time = counter * params.step * 1000
            offset = mod(offset + options.oracle[time], 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