Files
skills/docs/security-stack/crypto/crypto-utils/src/constant-time/constant-time-equal/constant-time-equal.md
2026-05-29 08:20:56 +01:00

2.4 KiB

constantTimeEqual

(no submodules)

Purpose

Byte-for-byte comparison of two Uint8Array buffers that takes time dependent only on the input length, not on where they first differ. Use it — and never the built-in ===, Array.prototype.every, or Buffer.equals — for comparing authentication tags, MAC digests, password hashes, and any other value where an attacker might use wall-clock time to learn a secret byte-by-byte.

Features

  • No early return on mismatch within the byte loop.
  • Constant-time in buffer contents; length check is the only short-circuit (lengths of MACs/tags are public constants).
  • Zero allocations; pure loop over the inputs.
  • No dependencies.

Usage: Basic Example

import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";

const tagReceived = new Uint8Array(16);
const tagExpected = new Uint8Array(16);
if (!constantTimeEqual(tagReceived, tagExpected)) {
    throw new Error("tag mismatch");
}

Usage: Advanced Example

import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";

// Verify a computed MAC against a header-supplied expected MAC in one step,
// without ever comparing via `===`, `for-of`, or `Buffer.equals`.
function verifyMessageAuth(
    macImpl: (msg: Uint8Array) => Uint8Array,
    message: Uint8Array,
    expected: Uint8Array
): boolean {
    const computed = macImpl(message);
    return constantTimeEqual(computed, expected);
}

API Reference

  • constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean — returns true iff both buffers have the same length and every byte matches. When lengths differ, returns false immediately (length is not secret).

Implementation Notes

The loop computes diff |= a[i] ^ b[i], accumulating a nonzero value iff any pair of bytes differs; the final check diff === 0 is also constant time because it inspects a single integer. JavaScript engines aggressively inline this pattern, so the generated machine code is effectively identical to the hand-written one found in @noble/hashes and libsodium's sodium_memcmp. The length short-circuit exists because returning false on length mismatch does not leak secret information — MAC and tag lengths are public protocol constants — and because continuing the loop with a shorter buffer would require masking that adds complexity with no security benefit.