denoland/deno · error · RangeError

ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH

ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH

Error message

Input buffers must have the same byte length

What it means

Constant-time comparison works by XOR-ing bytes, which inherently reveals length, so both inputs must have the same byte length. Deno's timingSafeEqual (like Node's) throws ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH before comparing when byteLengthOf(a) !== byteLengthOf(b). This is a safety guard, not a size limit.

Source

Thrown at ext/node/polyfills/internal_binding/_timingSafeEqual.ts:80

      );
    }
    return new DataView(
      TypedArrayPrototypeGetBuffer(ab),
      TypedArrayPrototypeGetByteOffset(ab),
      TypedArrayPrototypeGetByteLength(ab),
    );
  }
  return new DataView(ab);
}

/** Compare to array buffers or data views in a way that timing based attacks
 * cannot gain information about the platform. */
function stdTimingSafeEqual(
  a: ArrayBufferView | ArrayBufferLike | DataView,
  b: ArrayBufferView | ArrayBufferLike | DataView,
): boolean {
  if (byteLengthOf(a) !== byteLengthOf(b)) {
    throw new ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH();
  }
  if (!isDataView(a)) {
    a = toDataView(a);
  }
  if (!isDataView(b)) {
    b = toDataView(b);
  }
  const length = DataViewPrototypeGetByteLength(a);
  let out = 0;
  let i = -1;
  while (++i < length) {
    out |= DataViewPrototypeGetUint8(a, i) ^ DataViewPrototypeGetUint8(b, i);
  }
  return out === 0;
}

const timingSafeEqual = (
  buf1: Buffer | DataView | ArrayBuffer,

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Compare fixed-length digests instead of raw inputs: hash both sides with SHA-256 first, then timingSafeEqual(hash(a), hash(b))
  2. Or length-check first and fail closed — length is not secret: if (a.length !== b.length) return false
  3. Make sure both sides are decoded to raw bytes with the same encoding before comparing

Example fix

// before
const ok = crypto.timingSafeEqual(
  Buffer.from(userMacHex, 'utf8'),   // 64 bytes of hex text
  computedMac,                        // 32 raw bytes -> throws
);

// after
const ok = Buffer.from(userMacHex, 'hex').length === computedMac.length &&
  crypto.timingSafeEqual(Buffer.from(userMacHex, 'hex'), computedMac);
Defensive patterns

Strategy: validation

Validate before calling

function safeTimingSafeEqual(a: Buffer, b: Buffer): boolean {
  if (a.length !== b.length) return false; // length is not secret
  return crypto.timingSafeEqual(a, b);
}
// or compare digests to eliminate length dependence:
const sha256 = (v: Buffer) => crypto.createHash('sha256').update(v).digest();
const ok = crypto.timingSafeEqual(sha256(a), sha256(b));

Try / catch

try {
  ok = crypto.timingSafeEqual(a, b);
} catch (e: any) {
  if (e?.code === 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH') ok = false;
  else throw e;
}

Prevention

When it happens

Trigger: Comparing a hex-encoded digest (64 chars/bytes) with the raw 32-byte digest; user-supplied token of different length than the stored one; one side still containing a prefix ('Bearer ', 'sha256='); hex vs base64 encodings of the same digest producing different lengths.

Common situations: Webhook signature verification (Stripe/GitHub style), session-token comparison, HMAC checks where one side is encoded and the other raw; attacker-controlled input of arbitrary length hitting the compare.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/47b6ffbf1335bbe8. Report an issue: GitHub.