All files / src/security timingSafeEqual.ts

100% Statements 12/12
100% Branches 2/2
100% Functions 1/1
100% Lines 10/10

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                                                                            9x 10x 10x 10x   10x 5x     5x   5x 23x     5x    
/**
 * @function
 *
 * Compares two strings without leaking, through how long the comparison took,
 * how much of the expected value the caller already got right.
 *
 * @remarks
 * JavaScript's `===` on strings returns at the first differing byte. Over many
 * requests that difference is measurable, and it turns guessing a secret from
 * an exponential search into a linear one: an attacker who can time responses
 * recovers the value one byte at a time. This walks the whole buffer regardless
 * and accumulates the difference with a bitwise OR, so the work done is the
 * same for a value that matches on no bytes as for one that matches on all but
 * the last.
 *
 * Length is the exception and cannot be hidden — a differing length exits
 * immediately. That leaks only the size of the secret, which is not the part
 * worth protecting.
 *
 * Use this for any value a caller could otherwise guess a byte at a time: API
 * keys, webhook secrets, signatures. Comparing a hash of a value is not a
 * substitute, because the hashes are compared the same way.
 *
 * @param a First value to compare.
 * @param b Second value to compare.
 * @returns True only when both values are byte-for-byte identical.
 *
 * @example
 * ```ts
 * if (!timingSafeEqual(provided, expected)) {
 *   return ctx.json({ message: 'Unauthorized' }, 401)
 * }
 * ```
 *
 * @author Bayu Dwiyan Satria
 * @version 1.0.0
 * @since 1.0.0
 */
export const timingSafeEqual = (a: string, b: string): boolean => {
  const encoder = new TextEncoder()
  const bufferA = encoder.encode(a)
  const bufferB = encoder.encode(b)
 
  if (bufferA.byteLength !== bufferB.byteLength) {
    return false
  }
 
  let mismatch = 0
 
  for (let i = 0; i < bufferA.byteLength; i++) {
    mismatch |= bufferA[i] ^ bufferB[i]
  }
 
  return mismatch === 0
}