Crypto recipe

Constant-time comparison

Compare secrets without leaking them through timing side channels.

When you compare a secret — a MAC, an auth token, a password-reset code — against a user-supplied value, an ordinary byte-by-byte comparison returns early on the first mismatch. That timing difference leaks where the two values diverge, and over many attempts an attacker can recover the secret one byte at a time.

The fix is a constant-time comparison that always examines every byte. Every language ships one; reach for it whenever you compare secret material.

Get it right

  • Compare secrets (MACs, tokens) with a constant-time comparator, never ==.
  • These helpers assume equal-length inputs and can still leak length — keep secrets fixed-length where it matters.
  • The surrounding logic should also avoid secret-dependent branches and table lookups.
  • For HMAC and AEAD, prefer the library's own verify function — it is already constant-time.

Implementation

Setup Standard library (crypto/subtle).

Go
package main

import (
	"crypto/subtle"
	"fmt"
)

func main() {
	expected := []byte("expected-token")
	provided := []byte("user-supplied-token")

	// Returns 1 if equal, 0 otherwise — in time independent of where they differ.
	// (It also returns 0 for unequal lengths.)
	equal := subtle.ConstantTimeCompare(expected, provided) == 1
	fmt.Println("equal:", equal)
}

Setup Standard library (hmac.compare_digest, also secrets.compare_digest).

Python
import hmac

expected = b"expected-token"
provided = b"user-supplied-token"

# Constant-time for equal-length inputs; never use == for secrets.
equal = hmac.compare_digest(expected, provided)

Setup Built in (node:crypto). Never use === or == to compare secrets.

Node.js
import { timingSafeEqual } from 'node:crypto';

const expected = Buffer.from('expected-token');
const provided = Buffer.from('user-supplied-token');

// timingSafeEqual is constant-time but REQUIRES equal-length buffers (it throws
// otherwise), so guard the length first.
const equal = expected.length === provided.length && timingSafeEqual(expected, provided);

Setup Built in (System.Security.Cryptography).

.NET
using System.Security.Cryptography;

byte[] expected = Convert.FromHexString("aabbccdd");
byte[] provided = Convert.FromHexString("aabbccee");

// Constant-time; returns false (does not throw) on a length mismatch.
bool equal = CryptographicOperations.FixedTimeEquals(expected, provided);

Setup libsodium (sodium_memcmp). Plain memcmp is NOT constant-time.

C++
#include <sodium.h>

int main() {
    if (sodium_init() < 0) return 1;

    unsigned char expected[32]; // e.g. a recomputed MAC
    unsigned char provided[32]; // the value to check
    // ... fill both buffers ...

    // Returns 0 only if all 32 bytes match; runs in constant time.
    bool equal = sodium_memcmp(expected, provided, 32) == 0;
}

Setup Built in (java.security.MessageDigest, constant-time since Java 6u17).

Java
import java.security.MessageDigest;

byte[] expected = "expected-token".getBytes();
byte[] provided = "user-supplied-token".getBytes();

// Despite the name, MessageDigest.isEqual is the JDK's constant-time comparator.
boolean equal = MessageDigest.isEqual(expected, provided);

Setup Cargo.toml: subtle = "2".

Rust
use subtle::ConstantTimeEq;

fn main() {
    let expected = b"expected-token";
    let provided = b"user-supplied-token";

    // ct_eq returns a Choice; collapse it to a bool only after the comparison.
    let equal: bool = expected.ct_eq(provided).into();
    println!("equal: {}", equal);
}