secureCompare()
Constant-time equality. The trusted value goes first, and anything odd from the wire is a mismatch rather than an exception.
import { secureCompare } from "unsecure/compare";
secureCompare(storedToken, submittedToken); // boolean#Signature
function secureCompare(
expected: string | BytesSource | undefined,
received: string | BytesSource | null | undefined,
options?: { strict?: boolean },
): boolean;#Why it exists
=== on strings stops at the first differing character. The time that takes is measurable, and an attacker who can measure it recovers a secret one character at a time. secureCompare reads the same number of bytes whatever the input, so the duration carries no information about where the values diverged.
#The argument order is the contract
The first argument determines the loop length.
// Leaks the length of the attacker's input
secureCompare(userInput, serverToken);
// The trusted value first
secureCompare(serverToken, userInput);#What each argument accepts
expected is yours, so a wrong type is your bug and throws:
secureCompare(12_345 as any, "x");
// UnsecureError INVALID_TYPE: secureCompare: expected a string, ArrayBuffer or ArrayBuffer view, got number.received is untrusted, so anything that is not text or bytes is simply a mismatch:
secureCompare("expected", undefined); // false — a missing database column
secureCompare("expected", null); // false — a missing header
secureCompare("expected", 12_345 as any); // false — a number out of a JSON body
secureCompare("expected", [1, 2, 3] as any); // false — an array out of a JSON bodyBoth sides take a string or any BytesSource, and mixing them is fine — a string is compared as its UTF-8 bytes.
secureCompare("match", "match"); // true
secureCompare("hello", new TextEncoder().encode("hello")); // true
secureCompare(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])); // true#Empty and missing expected
By default an empty or undefined expected returns false, the same answer a mismatch gives:
secureCompare("", "something"); // false
secureCompare(undefined, undefined); // false — never "empty matches empty"{ strict: true } throws instead:
secureCompare(undefined, "x", { strict: true });
// UnsecureError OUT_OF_RANGE#Options
| Option | Type | Default | What it does |
|---|---|---|---|
strict | boolean | false | Throw OUT_OF_RANGE when expected is empty or undefined, instead of returning false. |
#Pitfall: strict: true in a request handler
strict: true turns a missing server-side value into a throw, which your framework turns into a 500 while a wrong signature stays a 403. An attacker can tell those apart, which makes the option a side channel in any code path they can reach.
// Distinguishable by status code
const valid = secureCompare(serverSecret, userInput, { strict: true });
// Default mode, plus an explicit check at boot
if (!serverSecret) throw new Error("BOOT: serverSecret is not configured");
const valid = secureCompare(serverSecret, userInput);Keep strict for start-up assertions and tests.
#Note
hmacVerify, hotpVerify, totpVerify and argon2Verify all use secureCompare internally. You do not need to call it yourself when you are using those.