lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0
//
// Security Hardening Utilities
// Constant-time operations and secure memory handling.

//! # Security Hardening
//!
//! This module provides security-critical utilities for LCPFS:
//!
//! - **Constant-time comparisons**: Prevent timing attacks on checksums/MACs
//! - **Secure memory zeroization**: Automatic key material cleanup
//! - **Side-channel resistance**: Timing-attack-resistant operations
//!
//! ## Threat Model
//!
//! ### Timing Attacks
//!
//! Standard equality comparison (`==`) short-circuits on the first mismatch:
//! ```text
//! Time to compare checksums:
//! - Match at byte 0: 1ns (immediate return)
//! - Match at byte 31: 32ns (all bytes compared)
//! ```
//!
//! An attacker measuring execution time can determine which bytes match,
//! enabling progressive checksum/MAC forgery.
//!
//! ### Constant-Time Solution
//!
//! Uses `subtle::ConstantTimeEq` which always compares all bytes:
//! ```text
//! Time to compare checksums:
//! - Match at byte 0: 32ns (all bytes compared)
//! - Match at byte 31: 32ns (all bytes compared)
//! - No match: 32ns (all bytes compared)
//! ```
//!
//! Execution time is independent of input data, preventing timing attacks.

use subtle::ConstantTimeEq;

// ═══════════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════════

/// Convert 4 u64 values to a 32-byte array (compile-time size guarantee).
///
/// This avoids runtime `.try_into().expect()` by directly constructing the array.
#[inline]
fn u64x4_to_bytes(a: u64, b: u64, c: u64, d: u64) -> [u8; 32] {
    let a_bytes = a.to_le_bytes();
    let b_bytes = b.to_le_bytes();
    let c_bytes = c.to_le_bytes();
    let d_bytes = d.to_le_bytes();

    [
        a_bytes[0], a_bytes[1], a_bytes[2], a_bytes[3], a_bytes[4], a_bytes[5], a_bytes[6],
        a_bytes[7], b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3], b_bytes[4], b_bytes[5],
        b_bytes[6], b_bytes[7], c_bytes[0], c_bytes[1], c_bytes[2], c_bytes[3], c_bytes[4],
        c_bytes[5], c_bytes[6], c_bytes[7], d_bytes[0], d_bytes[1], d_bytes[2], d_bytes[3],
        d_bytes[4], d_bytes[5], d_bytes[6], d_bytes[7],
    ]
}

// ═══════════════════════════════════════════════════════════════════════════════
// CONSTANT-TIME CHECKSUM COMPARISON
// ═══════════════════════════════════════════════════════════════════════════════

/// Compare two BLAKE3 checksums in constant time
///
/// # Security
///
/// This function is timing-attack-resistant. Execution time is independent
/// of where the first mismatch occurs, preventing side-channel attacks.
///
/// # Arguments
///
/// * `computed` - Checksum computed from data
/// * `stored` - Checksum stored in metadata
///
/// # Returns
///
/// `true` if checksums match, `false` otherwise
///
/// # Example
///
/// ```rust,ignore
/// use lcpfs::lcpfs_security::constant_time_checksum_eq;
/// use lcpfs::lcpfs_checksum::Checksum;
///
/// let data = b"sensitive data";
/// let computed = Checksum::calculate(data);
/// let stored = Checksum::new([0x12, 0x34, 0x56, 0x78]);
///
/// // Timing-attack-resistant comparison
/// if constant_time_checksum_eq(&computed, &stored) {
///     // Valid checksum
/// } else {
///     // Checksum mismatch - possible corruption or tampering
/// }
/// ```
pub fn constant_time_checksum_eq(
    computed: &crate::integrity::checksum::Checksum,
    stored: &crate::integrity::checksum::Checksum,
) -> bool {
    // Convert 4 u64 values to 32 bytes (4 × 8 = 32, compile-time guarantee)
    let computed_bytes: [u8; 32] = u64x4_to_bytes(
        computed.first(),
        computed.second(),
        computed.third(),
        computed.fourth(),
    );

    let stored_bytes: [u8; 32] = u64x4_to_bytes(
        stored.first(),
        stored.second(),
        stored.third(),
        stored.fourth(),
    );

    // Constant-time comparison prevents timing attacks
    computed_bytes.ct_eq(&stored_bytes).into()
}

/// Compare two u64 checksum arrays in constant time
///
/// # Security
///
/// Timing-attack-resistant comparison for 4x u64 checksum arrays (BLAKE3).
///
/// # Arguments
///
/// * `computed` - Computed checksum array `[u64; 4]`
/// * `stored` - Stored checksum array `[u64; 4]`
///
/// # Returns
///
/// `true` if all 4 u64 values match, `false` otherwise
///
/// # Example
///
/// ```rust,ignore
/// use lcpfs::lcpfs_security::constant_time_u64_array_eq;
///
/// let computed = [0x1234567890abcdef, 0xfedcba0987654321, 0x1111222233334444, 0x5555666677778888];
/// let stored = [0x1234567890abcdef, 0xfedcba0987654321, 0x1111222233334444, 0x5555666677778888];
///
/// if constant_time_u64_array_eq(&computed, &stored) {
///     // Checksums match
/// }
/// ```
pub fn constant_time_u64_array_eq(computed: &[u64; 4], stored: &[u64; 4]) -> bool {
    // Convert 4 u64 values to 32 bytes (4 × 8 = 32, compile-time guarantee)
    let computed_bytes: [u8; 32] =
        u64x4_to_bytes(computed[0], computed[1], computed[2], computed[3]);

    let stored_bytes: [u8; 32] = u64x4_to_bytes(stored[0], stored[1], stored[2], stored[3]);

    computed_bytes.ct_eq(&stored_bytes).into()
}

/// Compare two byte slices in constant time
///
/// # Security
///
/// Generic constant-time comparison for arbitrary byte slices.
/// Useful for MACs, authentication tags, and sensitive data.
///
/// # Panics
///
/// Panics if slices have different lengths (constant-time comparison
/// requires equal-length inputs).
///
/// # Arguments
///
/// * `a` - First byte slice
/// * `b` - Second byte slice
///
/// # Returns
///
/// `true` if slices are equal, `false` otherwise
///
/// # Example
///
/// ```rust,ignore
/// use lcpfs::lcpfs_security::constant_time_bytes_eq;
///
/// let mac1 = b"authentication_tag_1";
/// let mac2 = b"authentication_tag_2";
///
/// if constant_time_bytes_eq(mac1, mac2) {
///     // MACs match - authenticated
/// } else {
///     // MACs differ - reject
/// }
/// ```
pub fn constant_time_bytes_eq(a: &[u8], b: &[u8]) -> bool {
    assert_eq!(
        a.len(),
        b.len(),
        "constant_time_bytes_eq: slices must have equal length"
    );

    a.ct_eq(b).into()
}

// ═══════════════════════════════════════════════════════════════════════════════
// TESTING
// ═══════════════════════════════════════════════════════════════════════════════

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

    #[test]
    fn test_constant_time_u64_array_eq_matching() {
        let a = [
            0x1234567890abcdef,
            0xfedcba0987654321,
            0x1111222233334444,
            0x5555666677778888,
        ];
        let b = [
            0x1234567890abcdef,
            0xfedcba0987654321,
            0x1111222233334444,
            0x5555666677778888,
        ];

        assert!(constant_time_u64_array_eq(&a, &b));
    }

    #[test]
    fn test_constant_time_u64_array_eq_mismatch_first() {
        let a = [
            0xFFFFFFFFFFFFFFFF,
            0xfedcba0987654321,
            0x1111222233334444,
            0x5555666677778888,
        ];
        let b = [
            0x1234567890abcdef,
            0xfedcba0987654321,
            0x1111222233334444,
            0x5555666677778888,
        ];

        assert!(!constant_time_u64_array_eq(&a, &b));
    }

    #[test]
    fn test_constant_time_u64_array_eq_mismatch_last() {
        let a = [
            0x1234567890abcdef,
            0xfedcba0987654321,
            0x1111222233334444,
            0x5555666677778888,
        ];
        let b = [
            0x1234567890abcdef,
            0xfedcba0987654321,
            0x1111222233334444,
            0xFFFFFFFFFFFFFFFF,
        ];

        assert!(!constant_time_u64_array_eq(&a, &b));
    }

    #[test]
    fn test_constant_time_bytes_eq_matching() {
        let a = b"constant_time_comparison_test";
        let b = b"constant_time_comparison_test";

        assert!(constant_time_bytes_eq(a, b));
    }

    #[test]
    fn test_constant_time_bytes_eq_mismatch() {
        // Both slices must be same length for constant-time comparison
        let a = b"constant_time_compare_test_1";
        let b = b"different_compare_test_data2";

        assert!(!constant_time_bytes_eq(a, b));
    }

    #[test]
    #[should_panic(expected = "slices must have equal length")]
    fn test_constant_time_bytes_eq_length_mismatch() {
        let a = b"short";
        let b = b"much_longer_string";

        constant_time_bytes_eq(a, b);
    }

    #[test]
    fn test_constant_time_comparison_is_not_short_circuit() {
        // This test verifies the property we care about (constant-time),
        // though we can't actually measure timing in a unit test.
        //
        // The important property: ct_eq() always compares all bytes,
        // regardless of where the mismatch occurs.

        let a = [0u8; 32];
        let mut b = [0u8; 32];

        // Mismatch at first byte
        b[0] = 0xFF;
        let result: bool = a.ct_eq(&b).into();
        assert!(!result);

        // Mismatch at last byte
        b[0] = 0;
        b[31] = 0xFF;
        let result: bool = a.ct_eq(&b).into();
        assert!(!result);

        // Both should take the same time (verified by subtle crate design)
    }
}