Files
skills/docs/security-stack/security/hibp/src/range/constant-time-compare.md
2026-05-29 08:20:56 +01:00

2.0 KiB

range/constant-time-compare.ts

Purpose

constantTimeEqualsHex compares two hex suffixes without early-exit on the first mismatching character. It is used by the range-API client to verify whether any line in the response matches the input password's 35-char suffix, so a timing-side-channel attacker cannot infer which slot the real suffix lands in. The instrumented variant (constantTimeEqualsHexInstrumented) returns the byte-XOR count for test assertions.

Features

  • Branch-free byte loop using OR-accumulator over XOR pairs.
  • Touches every byte of the input — no length-based shortcut beyond the initial length-mismatch check.
  • ASCII-only (uppercase hex) inputs guarantee charCodeAt is single-byte, keeping per-character cost predictable.
  • Test instrumentation exposed without changing the hot path.

Usage: Basic Example

import { constantTimeEqualsHex } from "@suaveplan/hibp";

if (constantTimeEqualsHex(line.suffix, target)) {
    /* matched */
}

Usage: Advanced Example

import { constantTimeEqualsHexInstrumented } from "@suaveplan/hibp";

const { equal, bytesTouched } = constantTimeEqualsHexInstrumented(a, b);
if (!equal && bytesTouched !== a.length) {
    throw new Error("comparator regressed to early-exit");
}

API Reference

  • constantTimeEqualsHex(a: string, b: string): boolean.
  • constantTimeEqualsHexInstrumented(a, b): { equal, bytesTouched }.

Implementation Notes

JavaScript does not expose a true constant-time string compare, but the loop pattern above is the canonical approximation. The OR-accumulator trick converts every byte XOR into a single boolean test at the end, eliminating the data-dependent branch that an if (a[i] !== b[i]) return false would introduce. The length-mismatch check leaks only the public fact that the two strings differ in length, which is never secret in this protocol. The test suite asserts the wall-clock mean delta between equal and unequal pairs stays within tolerance over 10 000 samples; the more rigorous proof is the bytes-touched invariant.