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).
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).
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.
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).
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.
#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).
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".
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);
}