#[must_use = "a cryptographic comparison decision must be composed or explicitly declassified"]
pub struct CtDecision {
mask: u8,
}
impl CtDecision {
const TRUE_MASK: u8 = u8::MAX;
#[inline(always)]
const fn from_difference(difference: u64) -> Self {
let nonzero = ((difference | difference.wrapping_neg()) >> 63) as u8;
Self {
mask: nonzero.wrapping_sub(1),
}
}
#[inline(always)]
#[must_use]
pub const fn declassify(self) -> bool {
self.mask == Self::TRUE_MASK
}
#[inline(always)]
#[allow(dead_code)]
pub(crate) const fn into_u8(self) -> u8 {
self.mask & 1
}
#[inline(always)]
#[allow(dead_code)]
pub(crate) const fn into_mask(self) -> u8 {
self.mask
}
}
impl core::ops::BitAnd for CtDecision {
type Output = Self;
#[inline(always)]
fn bitand(self, rhs: Self) -> Self::Output {
Self {
mask: self.mask & rhs.mask,
}
}
}
impl core::ops::BitOr for CtDecision {
type Output = Self;
#[inline(always)]
fn bitor(self, rhs: Self) -> Self::Output {
Self {
mask: self.mask | rhs.mask,
}
}
}
impl core::ops::Not for CtDecision {
type Output = Self;
#[inline(always)]
fn not(self) -> Self::Output {
Self {
mask: self.mask ^ Self::TRUE_MASK,
}
}
}
#[inline(always)]
fn byte_difference(left: &[u8], right: &[u8]) -> u64 {
let mut difference = 0u64;
let mut left_chunks = left.chunks_exact(8);
let mut right_chunks = right.chunks_exact(8);
for (left_chunk, right_chunk) in left_chunks.by_ref().zip(right_chunks.by_ref()) {
let (Ok(left_bytes), Ok(right_bytes)) = (<&[u8; 8]>::try_from(left_chunk), <&[u8; 8]>::try_from(right_chunk))
else {
return u64::MAX;
};
difference |= u64::from_ne_bytes(*left_bytes) ^ u64::from_ne_bytes(*right_bytes);
}
let mut remainder = 0u8;
for (left_byte, right_byte) in left_chunks.remainder().iter().zip(right_chunks.remainder()) {
remainder |= left_byte ^ right_byte;
}
difference | u64::from(remainder)
}
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn fixed_eq<const N: usize>(left: &[u8; N], right: &[u8; N]) -> CtDecision {
CtDecision::from_difference(core::hint::black_box(byte_difference(left, right)))
}
#[inline]
#[allow(dead_code)]
pub(crate) fn public_len_eq(left: &[u8], right: &[u8]) -> CtDecision {
if left.len() != right.len() {
return CtDecision::from_difference(1);
}
CtDecision::from_difference(byte_difference(left, right))
}
#[inline(always)]
pub(crate) fn zeroize_no_fence(buf: &mut [u8]) {
let (prefix, words, suffix) = unsafe { buf.align_to_mut::<u64>() };
for byte in prefix.iter_mut() {
unsafe { core::ptr::write_volatile(byte, 0) };
}
for word in words.iter_mut() {
unsafe { core::ptr::write_volatile(word, 0) };
}
for byte in suffix.iter_mut() {
unsafe { core::ptr::write_volatile(byte, 0) };
}
}
#[inline(always)]
pub fn zeroize(buf: &mut [u8]) {
zeroize_no_fence(buf);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
mod word_zero_sealed {
#[allow(dead_code)]
pub trait WordZero: Copy {
const ZERO: Self;
}
impl WordZero for u8 {
const ZERO: Self = 0;
}
impl WordZero for u16 {
const ZERO: Self = 0;
}
impl WordZero for u32 {
const ZERO: Self = 0;
}
impl WordZero for u64 {
const ZERO: Self = 0;
}
impl WordZero for u128 {
const ZERO: Self = 0;
}
impl WordZero for usize {
const ZERO: Self = 0;
}
}
pub(crate) use word_zero_sealed::WordZero;
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn zeroize_words_no_fence<T: WordZero>(words: &mut [T]) {
for word in words {
unsafe { core::ptr::write_volatile(word, T::ZERO) };
}
}
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn zeroize_words<T: WordZero>(words: &mut [T]) {
zeroize_words_no_fence(words);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_eq_checks_every_position() {
let value = [0x5a; 64];
assert!(fixed_eq(&value, &value).declassify());
for index in [0, value.len() / 2, value.len() - 1] {
let mut different = value;
different[index] ^= 1;
assert!(!fixed_eq(&value, &different).declassify());
}
}
#[test]
fn public_len_eq_exposes_only_length_and_result() {
assert!(public_len_eq(b"abcdef", b"abcdef").declassify());
assert!(!public_len_eq(b"abcdef", b"abcdeg").declassify());
assert!(!public_len_eq(b"abcdef", b"abcde").declassify());
}
#[test]
fn decisions_compose_before_declassification() {
let equal = fixed_eq(b"equal", b"equal");
let different = fixed_eq(b"equal", b"other");
assert!(!(equal & different).declassify());
let equal = fixed_eq(b"equal", b"equal");
let different = fixed_eq(b"equal", b"other");
assert!((equal | different).declassify());
assert!((!fixed_eq(b"equal", b"other")).declassify());
}
#[test]
fn zeroize_clears_buffer() {
let mut buf = [0xFFu8; 37]; zeroize(&mut buf);
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn zeroize_empty_is_noop() {
let mut buf = [];
zeroize(&mut buf); }
}