entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
//! Constant-time byte comparison utilities.
//!
//! This module provides comparison functions that execute in constant time
//! relative to the input length, preventing timing side-channel attacks. A
//! naive byte-by-byte comparison short-circuits on the first mismatch, leaking
//! information about *where* two values diverge. An attacker can exploit that
//! timing variance to iteratively guess secret material one byte at a time.
//!
//! # Design Decisions
//!
//! * **XOR + OR accumulator** -- Each byte pair is XOR-ed and the result is
//!   folded into a single accumulator with bitwise OR. This avoids any
//!   data-dependent branch.
//! * **`std::hint::black_box`** -- Wraps the final accumulator before the
//!   equality check so the compiler cannot observe that we only care about
//!   zero-vs-nonzero and reintroduce an early exit.
//! * **Length check is *not* constant-time** -- The lengths of the values we
//!   compare (e.g. HMAC digests) are public and fixed by the algorithm, so
//!   leaking length via an early return is acceptable. This is explicitly
//!   documented on the function.
//!
//! # Security Considerations
//!
//! This implementation targets best-effort constant-time behaviour in safe
//! Rust. Hardware-level guarantees (e.g. constant-time XOR on all
//! micro-architectures) are outside our control; however the approach is
//! consistent with widely accepted practice (OpenSSL, ring, subtle).

use std::hint::black_box;

/// Compares two byte slices in constant time (relative to their length).
///
/// Returns `true` if and only if `a` and `b` have the same length and every
/// byte is identical.
///
/// # Security
///
/// * The comparison iterates over *all* byte positions regardless of where a
///   mismatch occurs, and `std::hint::black_box` prevents the optimiser from
///   short-circuiting the final check.
/// * **Length is not treated as secret.** If the slices differ in length the
///   function returns `false` immediately. This is safe for our use cases
///   (fixed-size HMAC/hash digests) and is documented here so callers are
///   aware of the trade-off.
///
/// # Examples
///
/// ```
/// use entropy_auth::crypto::constant_time::constant_time_eq;
///
/// assert!(constant_time_eq(b"secret", b"secret"));
/// assert!(!constant_time_eq(b"secret", b"differ"));
/// ```
#[must_use]
#[inline]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    // SECURITY: Length is public in our protocol (fixed-size digests), so an
    // early return here does not leak secret information.
    if a.len() != b.len() {
        return false;
    }

    let mut acc: u8 = 0;

    for (x, y) in a.iter().zip(b.iter()) {
        // SECURITY: XOR produces zero only when bytes are equal; OR ensures
        // any single mismatch sets bits in the accumulator permanently.
        acc |= x ^ y;
    }

    // SECURITY: black_box prevents the compiler from observing that we only
    // test for zero, which could otherwise let it reintroduce an early exit.
    black_box(acc) == 0
}

#[cfg(test)]
mod tests {
    use super::*;

    // --- Equal Slices ---

    #[test]
    fn equal_slices() {
        let a = b"the quick brown fox";
        let b = b"the quick brown fox";
        assert!(constant_time_eq(a, b));
    }

    // --- Unequal Slices ---

    #[test]
    fn unequal_same_length() {
        let a = b"hello world";
        let b = b"hello_world";
        assert!(!constant_time_eq(a, b));
    }

    #[test]
    fn unequal_different_length() {
        let a = b"short";
        let b = b"much longer";
        assert!(!constant_time_eq(a, b));
    }

    // --- Edge Cases ---

    #[test]
    fn empty_slices_equal() {
        assert!(constant_time_eq(b"", b""));
    }

    #[test]
    fn one_empty_one_not() {
        assert!(!constant_time_eq(b"", b"x"));
        assert!(!constant_time_eq(b"x", b""));
    }

    #[test]
    fn single_byte_equal() {
        assert!(constant_time_eq(&[0x42], &[0x42]));
    }

    #[test]
    fn single_byte_differ() {
        assert!(!constant_time_eq(&[0x42], &[0x43]));
    }

    #[test]
    fn differ_at_last_byte() {
        let a = b"identical_prefix\x00";
        let b = b"identical_prefix\x01";
        assert!(!constant_time_eq(a, b));
    }

    #[test]
    fn all_zeros_vs_all_ones() {
        let a = [0x00u8; 64];
        let b = [0xFFu8; 64];
        assert!(!constant_time_eq(&a, &b));
    }
}