use crate::{biterator::Biterator, CllwOpeEncrypt, CllwOreDecrypt, CllwOreEncrypt, Error, Key};
use hex::FromHex;
#[cfg(feature = "postgres-types")]
use postgres_types::ToSql;
use std::cmp::Ordering;
use subtle::ConstantTimeEq;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "postgres-types", derive(ToSql))]
#[cfg_attr(feature = "postgres-types", postgres(name = "ore_cllw_8_v1"))]
pub struct OreCllw8V1<const N: usize> {
bytes: [u8; N],
}
impl<const N: usize> AsRef<[u8]> for OreCllw8V1<N> {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
impl<const N: usize> FromHex for OreCllw8V1<N> {
type Error = Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, <Self as FromHex>::Error> {
let mut out = [0u8; N];
hex::decode_to_slice(hex, &mut out as &mut [u8]).map_err(|_| Error)?;
Ok(Self { bytes: out })
}
}
impl<const N: usize> TryFrom<&[u8]> for OreCllw8V1<N> {
type Error = Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() != N {
return Err(Error);
}
let mut bytes: [u8; N] = [0; N];
bytes.copy_from_slice(value);
Ok(OreCllw8V1 { bytes })
}
}
macro_rules! impl_cllw_ore_uint {
($uint_type:ty, $array_size:expr) => {
impl CllwOreEncrypt for $uint_type {
type Output = OreCllw8V1<$array_size>;
fn encrypt_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let mut hasher = blake3::Hasher::new_keyed(&key.0);
if let Some(salt) = salt {
let _ = hasher.update(salt);
}
let mut bytes: [u8; $array_size] = [0; $array_size];
Biterator::new(self).enumerate().for_each(|(i, bit)| {
let mut buf: [u8; 16] = [0; 16];
hasher.finalize_xof().fill(&mut buf);
let byte = u128::from_be_bytes(buf).wrapping_add(bit as u128) & 0xFF;
let _ = hasher.update(&[bit]);
bytes[i] = byte as u8;
});
Ok(OreCllw8V1 { bytes })
}
}
impl CllwOreDecrypt for OreCllw8V1<$array_size> {
type Output = $uint_type;
fn decrypt_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let mut hasher = blake3::Hasher::new_keyed(&key.0);
if let Some(salt) = salt {
let _ = hasher.update(salt);
}
let mut result: $uint_type = 0;
for (i, &ciphertext_byte) in self.bytes.iter().enumerate() {
let mut buf: [u8; 16] = [0; 16];
hasher.finalize_xof().fill(&mut buf);
let prf_block = u128::from_be_bytes(buf) & 0xFF;
let bit = ciphertext_byte.wrapping_sub(prf_block as u8) & 0x01;
let _ = hasher.update(&[bit]);
result |= (bit as $uint_type) << ((<$uint_type>::BITS - 1) - i as u32);
}
Ok(result)
}
}
};
}
impl_cllw_ore_uint!(u8, 8);
impl_cllw_ore_uint!(u16, 16);
impl_cllw_ore_uint!(u32, 32);
impl_cllw_ore_uint!(u64, 64);
impl_cllw_ore_uint!(u128, 128);
impl<const N: usize> Ord for OreCllw8V1<N> {
fn cmp(&self, other: &Self) -> Ordering {
super::compare_slice(self.bytes.as_slice(), other.bytes.as_slice())
}
}
impl<const N: usize> PartialOrd for OreCllw8V1<N> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<const N: usize> PartialEq for OreCllw8V1<N> {
fn eq(&self, other: &Self) -> bool {
self.bytes.ct_eq(&other.bytes).into()
}
}
impl<const N: usize> Eq for OreCllw8V1<N> {}
macro_rules! impl_cllw_ore_iint {
($iint_type:ty, $uint_type:ty, $array_size:expr) => {
impl CllwOreEncrypt for $iint_type {
type Output = OreCllw8V1<$array_size>;
fn encrypt_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let biased = (self as $uint_type) ^ (1 << (<$uint_type>::BITS - 1));
biased.encrypt_with_salt(key, salt)
}
}
};
}
impl_cllw_ore_iint!(i8, u8, 8);
impl_cllw_ore_iint!(i16, u16, 16);
impl_cllw_ore_iint!(i32, u32, 32);
impl_cllw_ore_iint!(i64, u64, 64);
impl_cllw_ore_iint!(i128, u128, 128);
impl CllwOreEncrypt for bool {
type Output = OreCllw8V1<8>;
fn encrypt_with_salt(self, key: &Key, salt: Option<&[u8]>) -> Result<Self::Output, Error> {
(self as u8).encrypt_with_salt(key, salt)
}
}
macro_rules! impl_cllw_ore_float {
($float_type:ty, $uint_type:ty, $array_size:expr) => {
impl CllwOreEncrypt for $float_type {
type Output = OreCllw8V1<$array_size>;
fn encrypt_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let bits = self.to_bits();
let mask = (bits >> (<$uint_type>::BITS - 1)).wrapping_neg()
| (1 << (<$uint_type>::BITS - 1));
(bits ^ mask).encrypt_with_salt(key, salt)
}
}
};
}
impl_cllw_ore_float!(f32, u32, 32);
impl_cllw_ore_float!(f64, u64, 64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "postgres-types", derive(ToSql))]
#[cfg_attr(feature = "postgres-types", postgres(name = "ore_cllw_8_ope_v1"))]
pub struct OpeCllw8V1<const N: usize> {
bytes: [u8; N],
}
impl<const N: usize> AsRef<[u8]> for OpeCllw8V1<N> {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
impl<const N: usize> FromHex for OpeCllw8V1<N> {
type Error = Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, <Self as FromHex>::Error> {
let mut out = [0u8; N];
hex::decode_to_slice(hex, &mut out as &mut [u8]).map_err(|_| Error)?;
Ok(Self { bytes: out })
}
}
impl<const N: usize> TryFrom<&[u8]> for OpeCllw8V1<N> {
type Error = Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() != N {
return Err(Error);
}
let mut bytes: [u8; N] = [0; N];
bytes.copy_from_slice(value);
Ok(OpeCllw8V1 { bytes })
}
}
macro_rules! impl_cllw_ope_uint {
($uint_type:ty) => {
impl CllwOpeEncrypt for $uint_type {
type Output = OpeCllw8V1<{ <$uint_type>::BITS as usize + 1 }>;
fn encrypt_ope_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let n = <$uint_type>::BITS as usize;
let vec = super::encrypt_ope_bits(Biterator::new(self), n, key, salt);
let mut bytes = [0u8; { <$uint_type>::BITS as usize + 1 }];
bytes.copy_from_slice(&vec);
Ok(OpeCllw8V1 { bytes })
}
}
};
}
impl_cllw_ope_uint!(u8);
impl_cllw_ope_uint!(u16);
impl_cllw_ope_uint!(u32);
impl_cllw_ope_uint!(u64);
impl_cllw_ope_uint!(u128);
macro_rules! impl_cllw_ope_iint {
($iint_type:ty, $uint_type:ty) => {
impl CllwOpeEncrypt for $iint_type {
type Output = OpeCllw8V1<{ <$uint_type>::BITS as usize + 1 }>;
fn encrypt_ope_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let biased = (self as $uint_type) ^ (1 << (<$uint_type>::BITS - 1));
biased.encrypt_ope_with_salt(key, salt)
}
}
};
}
impl_cllw_ope_iint!(i8, u8);
impl_cllw_ope_iint!(i16, u16);
impl_cllw_ope_iint!(i32, u32);
impl_cllw_ope_iint!(i64, u64);
impl_cllw_ope_iint!(i128, u128);
impl CllwOpeEncrypt for bool {
type Output = OpeCllw8V1<{ u8::BITS as usize + 1 }>;
fn encrypt_ope_with_salt(self, key: &Key, salt: Option<&[u8]>) -> Result<Self::Output, Error> {
(self as u8).encrypt_ope_with_salt(key, salt)
}
}
macro_rules! impl_cllw_ope_float {
($float_type:ty, $uint_type:ty) => {
impl CllwOpeEncrypt for $float_type {
type Output = OpeCllw8V1<{ <$uint_type>::BITS as usize + 1 }>;
fn encrypt_ope_with_salt(
self,
key: &Key,
salt: Option<&[u8]>,
) -> Result<Self::Output, Error> {
let bits = self.to_bits();
let mask = (bits >> (<$uint_type>::BITS - 1)).wrapping_neg()
| (1 << (<$uint_type>::BITS - 1));
(bits ^ mask).encrypt_ope_with_salt(key, salt)
}
}
};
}
impl_cllw_ope_float!(f32, u32);
impl_cllw_ope_float!(f64, u64);
impl<const N: usize> OpeCllw8V1<N> {
pub(crate) fn from_bytes(bytes: [u8; N]) -> Self {
Self { bytes }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Key;
use paste::paste;
use quickcheck::{quickcheck, TestResult};
macro_rules! quickcheck_type {
($ty:ident) => {
paste! {
quickcheck! {
fn [<prop_eq_ $ty>](key: Key, x: $ty) -> bool {
let a = key.encrypt(x).unwrap();
let b = key.encrypt(x).unwrap();
a == b
}
fn [<prop_cmp_ $ty>](key: Key, x: $ty, y: $ty) -> bool {
let a = key.encrypt(x).unwrap();
let b = key.encrypt(y).unwrap();
a.cmp(&b) == x.cmp(&y)
}
fn [<prop_low_order_bit_greater_ $ty>](key: Key) -> bool {
let x1 = key.encrypt(1 as $ty).unwrap();
let x2 = key.encrypt(0 as $ty).unwrap();
x1.cmp(&x2) == Ordering::Greater
}
fn [<prop_high_order_bit_greater_ $ty>](key: Key) -> bool {
let x1 = key.encrypt::<$ty>(1 << ($ty::BITS - 1)).unwrap();
let x2 = key.encrypt::<$ty>(0).unwrap();
x1.cmp(&x2) == Ordering::Greater
}
fn [<prop_high_order_bit_less_ $ty>](key: Key) -> bool {
let x1 = key.encrypt::<$ty>(0).unwrap();
let x2 = key.encrypt::<$ty>(1 << ($ty::BITS - 1)).unwrap();
x1.cmp(&x2) == Ordering::Less
}
fn [<prop_high_order_bit_eq_ $ty>](key: Key) -> bool {
let x1 = key.encrypt::<$ty>(1 << ($ty::BITS - 1)).unwrap();
let x2 = key.encrypt::<$ty>(1 << ($ty::BITS - 1)).unwrap();
x1.cmp(&x2) == Ordering::Equal
}
fn [<prop_low_order_bit_less_ $ty>](key: Key) -> bool {
let x1 = key.encrypt::<$ty>(0).unwrap();
let x2 = key.encrypt::<$ty>(1).unwrap();
x1.cmp(&x2) == Ordering::Less
}
fn [<prop_low_order_bit_eq_ $ty>](key: Key) -> bool {
let x1 = key.encrypt::<$ty>(1).unwrap();
let x2 = key.encrypt::<$ty>(1).unwrap();
x1.cmp(&x2) == Ordering::Equal
}
fn [<prop_zero_ $ty>](key: Key) -> bool {
let x1 = key.encrypt::<$ty>(0).unwrap();
let x2 = key.encrypt::<$ty>(0).unwrap();
x1.cmp(&x2) == Ordering::Equal
}
fn [<prop_max_ $ty>](key: Key) -> bool {
let x1 = key.encrypt($ty::MAX).unwrap();
let x2 = key.encrypt($ty::MAX).unwrap();
x1.cmp(&x2) == Ordering::Equal
}
fn [<prop_less_than_max_right_ $ty>](key: Key, x: $ty) -> TestResult {
if x == $ty::MAX {
return TestResult::discard();
}
let x1 = key.encrypt(x).unwrap();
let x2 = key.encrypt($ty::MAX).unwrap();
TestResult::from_bool(x1.cmp(&x2) == Ordering::Less)
}
fn [<prop_more_than_max_left_ $ty>](key: Key, x: $ty) -> TestResult {
if x == $ty::MAX {
return TestResult::discard();
}
let x1 = key.encrypt($ty::MAX).unwrap();
let x2 = key.encrypt::<$ty>(0).unwrap();
TestResult::from_bool(x1.cmp(&x2) == Ordering::Greater)
}
fn [<prop_more_than_zero_left_ $ty>](key: Key, x: $ty) -> TestResult {
if x == 0 {
return TestResult::discard();
}
let x1 = key.encrypt::<$ty>(0).unwrap();
let x2 = key.encrypt(x).unwrap();
TestResult::from_bool(x1.cmp(&x2) == Ordering::Less)
}
fn [<prop_more_than_zero_right_ $ty>](key: Key, x: $ty) -> TestResult {
if x == 0 {
return TestResult::discard();
}
let x1 = key.encrypt(x).unwrap();
let x2 = key.encrypt::<$ty>(0).unwrap();
TestResult::from_bool(x1.cmp(&x2) == Ordering::Greater)
}
fn [<prop_one_less_than_ $ty>](key: Key, x: $ty) -> TestResult {
if x <= 1 {
return TestResult::discard();
}
let x1 = key.encrypt(x - 1).unwrap();
let x2 = key.encrypt(x).unwrap();
TestResult::from_bool(x1.cmp(&x2) == Ordering::Less)
}
fn [<prop_decrypt_ $ty>](key: Key, x: $ty) -> bool {
let ciphertext = key.encrypt(x).unwrap();
let decrypted = key.decrypt(ciphertext).unwrap();
x == decrypted
}
}
}
};
}
quickcheck_type!(u16);
quickcheck_type!(u32);
quickcheck_type!(u64);
quickcheck_type!(u128);
#[test]
fn test_u32_different_salts_produce_different_ciphertexts() {
use crate::CllwOreEncrypt;
let key = Key::from([0; 32]);
let value: u32 = 42;
let salt1 = b"domain1";
let salt2 = b"domain2";
let ct1 = value.encrypt_with_salt(&key, Some(salt1)).unwrap();
let ct2 = value.encrypt_with_salt(&key, Some(salt2)).unwrap();
assert_ne!(ct1, ct2);
}
#[test]
fn test_u64_wrong_salt_produces_wrong_plaintext() {
use crate::{CllwOreDecrypt, CllwOreEncrypt};
let key = Key::from([0; 32]);
let value: u64 = 12345;
let salt1 = b"domain1";
let salt2 = b"domain2";
let ciphertext = value.encrypt_with_salt(&key, Some(salt1)).unwrap();
let decrypted = ciphertext.decrypt_with_salt(&key, Some(salt2)).unwrap();
assert_ne!(value, decrypted);
}
#[test]
fn test_u128_correct_salt_decrypts_correctly() {
use crate::{CllwOreDecrypt, CllwOreEncrypt};
let key = Key::from([0; 32]);
let value: u128 = 999999;
let salt = b"my-domain";
let ciphertext = value.encrypt_with_salt(&key, Some(salt)).unwrap();
let decrypted = ciphertext.decrypt_with_salt(&key, Some(salt)).unwrap();
assert_eq!(value, decrypted);
}
#[test]
fn test_u16_no_salt_and_none_salt_are_equivalent() {
use crate::CllwOreEncrypt;
let key = Key::from([0; 32]);
let value: u16 = 100;
let ct1 = value.encrypt(&key).unwrap();
let ct2 = value.encrypt_with_salt(&key, None).unwrap();
assert_eq!(ct1, ct2);
}
#[test]
fn test_u32_empty_salt_same_as_no_salt() {
use crate::CllwOreEncrypt;
let key = Key::from([0; 32]);
let value: u32 = 42;
let ct_no_salt = value.encrypt_with_salt(&key, None).unwrap();
let ct_empty_salt = value.encrypt_with_salt(&key, Some(b"")).unwrap();
assert_eq!(ct_no_salt, ct_empty_salt);
}
#[test]
fn test_u64_salt_preserves_ordering() {
use crate::CllwOreEncrypt;
let key = Key::from([0; 32]);
let salt = b"domain";
let ct1 = 100u64.encrypt_with_salt(&key, Some(salt)).unwrap();
let ct2 = 200u64.encrypt_with_salt(&key, Some(salt)).unwrap();
assert!(ct1 < ct2);
}
#[test]
fn test_u128_salt_ordering_with_max_values() {
use crate::CllwOreEncrypt;
let key = Key::from([0; 32]);
let salt = b"test-domain";
let ct_zero = 0u128.encrypt_with_salt(&key, Some(salt)).unwrap();
let ct_max = u128::MAX.encrypt_with_salt(&key, Some(salt)).unwrap();
assert!(ct_zero < ct_max);
}
#[test]
fn test_ope_try_from_wrong_length_fails() {
let bytes = [0u8; 16];
let result = OpeCllw8V1::<33>::try_from(bytes.as_slice());
assert!(result.is_err());
}
macro_rules! ope_fixed_matches_variable_be_bytes {
($anchor:ident, $prop:ident, $ty:ty, $values:expr, $canonical_be:expr) => {
#[test]
fn $anchor() {
let key = Key::from([0u8; 32]);
let canonical_be: fn($ty) -> Vec<u8> = $canonical_be;
for x in $values {
let fixed = key.encrypt_ope(x).unwrap();
let variable = key.encrypt_ope(canonical_be(x).as_slice()).unwrap();
assert_eq!(
fixed.as_ref(),
variable.as_ref(),
"fixed vs variable OPE wire mismatch for {x:?}",
);
}
}
quickcheck! {
fn $prop(key: Key, x: $ty) -> bool {
let canonical_be: fn($ty) -> Vec<u8> = $canonical_be;
let fixed = key.encrypt_ope(x).unwrap();
let variable = key.encrypt_ope(canonical_be(x).as_slice()).unwrap();
fixed.as_ref() == variable.as_ref()
}
}
};
}
ope_fixed_matches_variable_be_bytes!(
ope_u8_fixed_matches_variable_be_bytes,
prop_ope_u8_fixed_matches_variable_be_bytes,
u8,
[0u8, 1, 42, 0x7F, 0x80, u8::MAX],
|x| x.to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_u16_fixed_matches_variable_be_bytes,
prop_ope_u16_fixed_matches_variable_be_bytes,
u16,
[0u16, 1, 42, 0x0123, 0xDEAD, u16::MAX],
|x| x.to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_u32_fixed_matches_variable_be_bytes,
prop_ope_u32_fixed_matches_variable_be_bytes,
u32,
[0u32, 1, 42, 0x0123_4567, 0xDEAD_BEEF, u32::MAX],
|x| x.to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_u64_fixed_matches_variable_be_bytes,
prop_ope_u64_fixed_matches_variable_be_bytes,
u64,
[
0u64,
1,
42,
0x0123_4567_89AB_CDEF,
0xDEAD_BEEF_DEAD_BEEF,
u64::MAX
],
|x| x.to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_u128_fixed_matches_variable_be_bytes,
prop_ope_u128_fixed_matches_variable_be_bytes,
u128,
[
0u128,
1,
42,
0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF,
u128::MAX
],
|x| x.to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_i8_fixed_matches_variable_be_bytes,
prop_ope_i8_fixed_matches_variable_be_bytes,
i8,
[i8::MIN, -42, -1, 0, 1, 42, i8::MAX],
|x| ((x as u8) ^ (1 << (u8::BITS - 1))).to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_i16_fixed_matches_variable_be_bytes,
prop_ope_i16_fixed_matches_variable_be_bytes,
i16,
[i16::MIN, -42, -1, 0, 1, 42, i16::MAX],
|x| ((x as u16) ^ (1 << (u16::BITS - 1))).to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_i32_fixed_matches_variable_be_bytes,
prop_ope_i32_fixed_matches_variable_be_bytes,
i32,
[i32::MIN, -42, -1, 0, 1, 42, i32::MAX],
|x| ((x as u32) ^ (1 << (u32::BITS - 1))).to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_i64_fixed_matches_variable_be_bytes,
prop_ope_i64_fixed_matches_variable_be_bytes,
i64,
[i64::MIN, -42, -1, 0, 1, 42, i64::MAX],
|x| ((x as u64) ^ (1 << (u64::BITS - 1))).to_be_bytes().to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_i128_fixed_matches_variable_be_bytes,
prop_ope_i128_fixed_matches_variable_be_bytes,
i128,
[i128::MIN, -42, -1, 0, 1, 42, i128::MAX],
|x| ((x as u128) ^ (1 << (u128::BITS - 1)))
.to_be_bytes()
.to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_bool_fixed_matches_variable_be_bytes,
prop_ope_bool_fixed_matches_variable_be_bytes,
bool,
[false, true],
|b| [b as u8].to_vec()
);
ope_fixed_matches_variable_be_bytes!(
ope_f32_fixed_matches_variable_be_bytes,
prop_ope_f32_fixed_matches_variable_be_bytes,
f32,
[
0.0f32,
-0.0,
1.0,
-1.0,
f32::MIN,
f32::MAX,
f32::INFINITY,
f32::NEG_INFINITY
],
|x| {
let bits = x.to_bits();
let mask = (bits >> (u32::BITS - 1)).wrapping_neg() | (1 << (u32::BITS - 1));
(bits ^ mask).to_be_bytes().to_vec()
}
);
ope_fixed_matches_variable_be_bytes!(
ope_f64_fixed_matches_variable_be_bytes,
prop_ope_f64_fixed_matches_variable_be_bytes,
f64,
[
0.0f64,
-0.0,
1.0,
-1.0,
f64::MIN,
f64::MAX,
f64::INFINITY,
f64::NEG_INFINITY
],
|x| {
let bits = x.to_bits();
let mask = (bits >> (u64::BITS - 1)).wrapping_neg() | (1 << (u64::BITS - 1));
(bits ^ mask).to_be_bytes().to_vec()
}
);
macro_rules! ope_quickcheck_type {
($ty:ident) => {
paste! {
quickcheck! {
fn [<prop_ope_eq_ $ty>](key: Key, x: $ty) -> bool {
let a = key.encrypt_ope(x).unwrap();
let b = key.encrypt_ope(x).unwrap();
a == b
}
fn [<prop_ope_cmp_ $ty>](key: Key, x: $ty, y: $ty) -> bool {
let a = key.encrypt_ope(x).unwrap();
let b = key.encrypt_ope(y).unwrap();
a.cmp(&b) == x.cmp(&y)
}
fn [<prop_ope_zero_less_than_one_ $ty>](key: Key) -> bool {
let c0 = key.encrypt_ope(0 as $ty).unwrap();
let c1 = key.encrypt_ope(1 as $ty).unwrap();
c0 < c1
}
fn [<prop_ope_length_ $ty>](key: Key, x: $ty) -> bool {
let ct = key.encrypt_ope(x).unwrap();
ct.as_ref().len() == <$ty>::BITS as usize + 1
}
fn [<prop_ope_hex_round_trip_ $ty>](key: Key, x: $ty) -> bool {
let ct = key.encrypt_ope(x).unwrap();
let hex_str = hex::encode(ct.as_ref());
let ct2 = OpeCllw8V1::<{ <$ty>::BITS as usize + 1 }>::from_hex(&hex_str).unwrap();
ct == ct2
}
fn [<prop_ope_try_from_bytes_ $ty>](key: Key, x: $ty) -> bool {
let ct = key.encrypt_ope(x).unwrap();
let ct2 = OpeCllw8V1::<{ <$ty>::BITS as usize + 1 }>::try_from(ct.as_ref()).unwrap();
ct == ct2
}
fn [<prop_ope_different_salts_differ_ $ty>](key: Key, x: $ty) -> bool {
let ct1 = x.encrypt_ope_with_salt(&key, Some(b"domain1")).unwrap();
let ct2 = x.encrypt_ope_with_salt(&key, Some(b"domain2")).unwrap();
ct1 != ct2
}
}
}
};
}
ope_quickcheck_type!(u16);
ope_quickcheck_type!(u32);
ope_quickcheck_type!(u64);
ope_quickcheck_type!(u128);
macro_rules! quickcheck_iint_type {
($ty:ident, $uty:ident) => {
paste! {
quickcheck! {
fn [<prop_eq_ $ty>](key: Key, x: $ty) -> bool {
let a = key.encrypt(x).unwrap();
let b = key.encrypt(x).unwrap();
a == b
}
fn [<prop_cmp_ $ty>](key: Key, x: $ty, y: $ty) -> bool {
let a = key.encrypt(x).unwrap();
let b = key.encrypt(y).unwrap();
a.cmp(&b) == x.cmp(&y)
}
fn [<prop_sign_class_ $ty>](key: Key, x: $ty) -> TestResult {
let zero_ct = key.encrypt::<$ty>(0).unwrap();
let ct = key.encrypt(x).unwrap();
TestResult::from_bool(match x.cmp(&0) {
Ordering::Less => ct < zero_ct,
Ordering::Equal => ct == zero_ct,
Ordering::Greater => ct > zero_ct,
})
}
fn [<prop_min_less_than_zero_ $ty>](key: Key) -> bool {
let min_ct = key.encrypt($ty::MIN).unwrap();
let zero_ct = key.encrypt::<$ty>(0).unwrap();
min_ct < zero_ct
}
fn [<prop_max_greater_than_zero_ $ty>](key: Key) -> bool {
let max_ct = key.encrypt($ty::MAX).unwrap();
let zero_ct = key.encrypt::<$ty>(0).unwrap();
max_ct > zero_ct
}
fn [<prop_min_ $ty>](key: Key) -> bool {
let a = key.encrypt($ty::MIN).unwrap();
let b = key.encrypt($ty::MIN).unwrap();
a == b
}
fn [<prop_max_ $ty>](key: Key) -> bool {
let a = key.encrypt($ty::MAX).unwrap();
let b = key.encrypt($ty::MAX).unwrap();
a == b
}
fn [<prop_one_less_than_ $ty>](key: Key, x: $ty) -> TestResult {
if x == $ty::MIN {
return TestResult::discard();
}
let a = key.encrypt(x - 1).unwrap();
let b = key.encrypt(x).unwrap();
TestResult::from_bool(a.cmp(&b) == Ordering::Less)
}
fn [<prop_decrypt_ $ty>](key: Key, x: $ty) -> bool {
let ciphertext = key.encrypt(x).unwrap();
let biased: $uty = key.decrypt(ciphertext).unwrap();
let recovered = (biased ^ (1 << (<$uty>::BITS - 1))) as $ty;
x == recovered
}
}
}
};
}
quickcheck_iint_type!(i16, u16);
quickcheck_iint_type!(i32, u32);
quickcheck_iint_type!(i64, u64);
quickcheck_iint_type!(i128, u128);
macro_rules! ope_quickcheck_iint_type {
($ty:ident, $bits:expr) => {
paste! {
quickcheck! {
fn [<prop_ope_eq_ $ty>](key: Key, x: $ty) -> bool {
let a = key.encrypt_ope(x).unwrap();
let b = key.encrypt_ope(x).unwrap();
a == b
}
fn [<prop_ope_cmp_ $ty>](key: Key, x: $ty, y: $ty) -> bool {
let a = key.encrypt_ope(x).unwrap();
let b = key.encrypt_ope(y).unwrap();
a.cmp(&b) == x.cmp(&y)
}
fn [<prop_ope_sign_class_ $ty>](key: Key, x: $ty) -> TestResult {
let zero_ct = key.encrypt_ope::<$ty>(0).unwrap();
let ct = key.encrypt_ope(x).unwrap();
TestResult::from_bool(match x.cmp(&0) {
Ordering::Less => ct < zero_ct,
Ordering::Equal => ct == zero_ct,
Ordering::Greater => ct > zero_ct,
})
}
fn [<prop_ope_length_ $ty>](key: Key, x: $ty) -> bool {
let ct = key.encrypt_ope(x).unwrap();
ct.as_ref().len() == $bits + 1
}
fn [<prop_ope_hex_round_trip_ $ty>](key: Key, x: $ty) -> bool {
let ct = key.encrypt_ope(x).unwrap();
let hex_str = hex::encode(ct.as_ref());
let ct2 = OpeCllw8V1::<{ $bits + 1 }>::from_hex(&hex_str).unwrap();
ct == ct2
}
fn [<prop_ope_different_salts_differ_ $ty>](key: Key, x: $ty) -> bool {
let ct1 = x.encrypt_ope_with_salt(&key, Some(b"domain1")).unwrap();
let ct2 = x.encrypt_ope_with_salt(&key, Some(b"domain2")).unwrap();
ct1 != ct2
}
}
}
};
}
ope_quickcheck_iint_type!(i16, 16);
ope_quickcheck_iint_type!(i32, 32);
ope_quickcheck_iint_type!(i64, 64);
ope_quickcheck_iint_type!(i128, 128);
#[test]
fn signed_ore_anchors_i32() {
let key = Key::from([0u8; 32]);
let min = key.encrypt(i32::MIN).unwrap();
let neg_one = key.encrypt(-1i32).unwrap();
let zero = key.encrypt(0i32).unwrap();
let one = key.encrypt(1i32).unwrap();
let max = key.encrypt(i32::MAX).unwrap();
assert!(min < neg_one);
assert!(neg_one < zero);
assert!(zero < one);
assert!(one < max);
}
#[test]
fn signed_ore_decrypt_i64_anchors() {
let key = Key::from([7u8; 32]);
for x in [i64::MIN, -1_000_000_000, -1, 0, 1, 1_000_000_000, i64::MAX] {
let ct = key.encrypt(x).unwrap();
let biased: u64 = key.decrypt(ct).unwrap();
let recovered = (biased ^ (1u64 << 63)) as i64;
assert_eq!(recovered, x);
}
}
#[test]
fn signed_ope_anchors_i32() {
let key = Key::from([0u8; 32]);
let min = key.encrypt_ope(i32::MIN).unwrap();
let neg_one = key.encrypt_ope(-1i32).unwrap();
let zero = key.encrypt_ope(0i32).unwrap();
let one = key.encrypt_ope(1i32).unwrap();
let max = key.encrypt_ope(i32::MAX).unwrap();
assert!(min < neg_one);
assert!(neg_one < zero);
assert!(zero < one);
assert!(one < max);
}
#[test]
fn bool_ore_anchors() {
let key = Key::from([0u8; 32]);
let f = key.encrypt(false).unwrap();
let t = key.encrypt(true).unwrap();
assert!(f < t);
assert_eq!(f, key.encrypt(false).unwrap());
assert_eq!(t, key.encrypt(true).unwrap());
}
#[test]
fn bool_ope_anchors() {
let key = Key::from([0u8; 32]);
let f = key.encrypt_ope(false).unwrap();
let t = key.encrypt_ope(true).unwrap();
assert!(f < t);
assert_eq!(f, key.encrypt_ope(false).unwrap());
assert_eq!(t, key.encrypt_ope(true).unwrap());
}
macro_rules! quickcheck_float_type {
($ty:ident) => {
paste! {
quickcheck! {
fn [<prop_eq_ $ty>](key: Key, x: $ty) -> TestResult {
if x.is_nan() {
return TestResult::discard();
}
let a = key.encrypt(x).unwrap();
let b = key.encrypt(x).unwrap();
TestResult::from_bool(a == b)
}
fn [<prop_cmp_ $ty>](key: Key, x: $ty, y: $ty) -> TestResult {
if x.is_nan() || y.is_nan() {
return TestResult::discard();
}
let a = key.encrypt(x).unwrap();
let b = key.encrypt(y).unwrap();
TestResult::from_bool(a.cmp(&b) == x.total_cmp(&y))
}
fn [<prop_ope_eq_ $ty>](key: Key, x: $ty) -> TestResult {
if x.is_nan() {
return TestResult::discard();
}
let a = key.encrypt_ope(x).unwrap();
let b = key.encrypt_ope(x).unwrap();
TestResult::from_bool(a == b)
}
fn [<prop_ope_cmp_ $ty>](key: Key, x: $ty, y: $ty) -> TestResult {
if x.is_nan() || y.is_nan() {
return TestResult::discard();
}
let a = key.encrypt_ope(x).unwrap();
let b = key.encrypt_ope(y).unwrap();
TestResult::from_bool(a.cmp(&b) == x.total_cmp(&y))
}
}
}
};
}
quickcheck_float_type!(f32);
quickcheck_float_type!(f64);
#[test]
fn float_ore_anchors_f32() {
let key = Key::from([0u8; 32]);
let neg_inf = key.encrypt(f32::NEG_INFINITY).unwrap();
let neg_one = key.encrypt(-1f32).unwrap();
let neg_zero = key.encrypt(-0f32).unwrap();
let pos_zero = key.encrypt(0f32).unwrap();
let one = key.encrypt(1f32).unwrap();
let inf = key.encrypt(f32::INFINITY).unwrap();
assert!(neg_inf < neg_one);
assert!(neg_one < neg_zero);
assert!(neg_zero < pos_zero);
assert!(pos_zero < one);
assert!(one < inf);
}
#[test]
fn float_ope_anchors_f64() {
let key = Key::from([0u8; 32]);
let neg_inf = key.encrypt_ope(f64::NEG_INFINITY).unwrap();
let neg_one = key.encrypt_ope(-1f64).unwrap();
let neg_zero = key.encrypt_ope(-0f64).unwrap();
let pos_zero = key.encrypt_ope(0f64).unwrap();
let one = key.encrypt_ope(1f64).unwrap();
let inf = key.encrypt_ope(f64::INFINITY).unwrap();
assert!(neg_inf < neg_one);
assert!(neg_one < neg_zero);
assert!(neg_zero < pos_zero);
assert!(pos_zero < one);
assert!(one < inf);
}
#[test]
#[ignore = "exhaustive exactness check — slow; run with `cargo test -- --ignored`"]
fn ope_exactness_adjacent_u16() {
let num_keys = 20usize;
let mut total = 0u64;
let mut errors = 0u64;
for key_seed in 0..num_keys {
let mut key_bytes = [0u8; 32];
key_bytes[..8].copy_from_slice(&(key_seed as u64).to_le_bytes());
let key = Key::from(key_bytes);
for x in 0u16..u16::MAX {
let ct_x = key.encrypt_ope(x).unwrap();
let ct_y = key.encrypt_ope(x + 1).unwrap();
total += 1;
if ct_x.cmp(&ct_y) != Ordering::Less {
errors += 1;
}
}
}
assert_eq!(
errors, 0,
"exact OPE violated: {errors} ordering errors out of {total} adjacent pairs",
);
}
}