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
charCodeAtis 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.