use crate::zerokms::IndexKey;
use cllw_ore::{CllwOpeEncrypt, Key as OpeKey};
use zerokms_protocol::cipherstash_config::column;
use super::{
errors::EncryptionError,
indexer::{IndexerInit, Indexes, IndexesForQuery, QueryOp},
IndexTerm, Plaintext, QueryBuilder, StorageBuilder,
};
pub struct OpeIndexer;
#[derive(Debug, Default)]
pub struct OpeIndexerOptions;
impl IndexerInit for OpeIndexer {
type Args = OpeIndexerOptions;
type Error = EncryptionError;
fn try_init<A>(_opts: A) -> Result<Self, Self::Error>
where
Self::Args: TryFrom<A, Error = Self::Error>,
{
Ok(Self)
}
}
impl<'k> Indexes<'k, Plaintext> for OpeIndexer {
fn index(
&self,
mut builder: StorageBuilder<'k, Plaintext>,
) -> Result<StorageBuilder<'k, Plaintext>, EncryptionError> {
let index_term = self.encrypt(builder.plaintext(), builder.index_key())?;
builder.add_index_term(index_term);
Ok(builder)
}
}
impl<C> IndexesForQuery<Plaintext, C> for OpeIndexer {
fn query_index(
&self,
builder: QueryBuilder<Plaintext, C>,
_op: QueryOp,
) -> Result<IndexTerm, EncryptionError> {
let index_term = self.encrypt(builder.plaintext(), builder.index_key())?;
Ok(index_term)
}
}
impl Default for OpeIndexer {
fn default() -> Self {
Self
}
}
impl TryFrom<&column::IndexType> for OpeIndexerOptions {
type Error = EncryptionError;
fn try_from(value: &column::IndexType) -> Result<Self, Self::Error> {
match value {
column::IndexType::Ope => Ok(Default::default()),
_ => Err(EncryptionError::IndexingError(
"OpeIndexerOptions can only be created from an Ope index configuration".to_string(),
)),
}
}
}
impl OpeIndexer {
pub fn encrypt(
&self,
value: &Plaintext,
index_key: &IndexKey,
) -> Result<IndexTerm, EncryptionError> {
let key = derive_ope_key(index_key);
match value {
Plaintext::Text(Some(s)) => {
let ciphertext = s.as_str().encrypt_ope(&key)?;
Ok(IndexTerm::OpeVariable(ciphertext.as_ref().to_vec()))
}
Plaintext::NaiveDate(Some(d)) => {
let ciphertext = d.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::Timestamp(Some(ts)) => {
let ciphertext = ts.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::Decimal(Some(d)) => {
let ciphertext = d.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::BigInt(Some(b)) => {
let ciphertext = b.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::Boolean(Some(b)) => {
let ciphertext = b.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::Float(Some(b)) => {
let ciphertext = b.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::Int(Some(i)) => {
let ciphertext = i.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::SmallInt(Some(i)) => {
let ciphertext = i.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::BigUInt(Some(u)) => {
let ciphertext = u.encrypt_ope(&key)?;
Ok(IndexTerm::OpeFixed(ciphertext.as_ref().to_vec()))
}
Plaintext::Json(_) => Err(EncryptionError::IndexingError(
"OPE indexing not supported for JSON documents".into(),
)),
Plaintext::Text(None)
| Plaintext::BigInt(None)
| Plaintext::BigUInt(None)
| Plaintext::Boolean(None)
| Plaintext::Decimal(None)
| Plaintext::Float(None)
| Plaintext::Int(None)
| Plaintext::NaiveDate(None)
| Plaintext::SmallInt(None)
| Plaintext::Timestamp(None) => Ok(IndexTerm::Null),
}
}
}
fn derive_ope_key(index_key: &IndexKey) -> OpeKey {
let mut hasher = blake3::Hasher::new();
hasher.update(b"CIPHERSTASH-OPE-INDEX-V1");
hasher.update(index_key.key());
let mut derived = [0u8; 32];
hasher.finalize_xof().fill(&mut derived);
OpeKey::from(derived)
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> IndexKey {
IndexKey::from([7u8; 32])
}
#[test]
fn encrypts_bigint_to_opefixed_with_65_bytes() {
let ct = OpeIndexer
.encrypt(&Plaintext::BigInt(Some(42)), &key())
.unwrap();
match ct {
IndexTerm::OpeFixed(bytes) => assert_eq!(bytes.len(), 65),
other => panic!("expected OpeFixed, got {other:?}"),
}
}
#[test]
fn encrypts_text_to_opevariable() {
let ct = OpeIndexer
.encrypt(&Plaintext::Text(Some("hello".into())), &key())
.unwrap();
assert!(matches!(ct, IndexTerm::OpeVariable(_)));
}
#[test]
fn null_plaintext_yields_null_term() {
let ct = OpeIndexer
.encrypt(&Plaintext::BigInt(None), &key())
.unwrap();
assert_eq!(ct, IndexTerm::Null);
}
#[test]
fn preserves_order_for_bigint() {
let a = match OpeIndexer
.encrypt(&Plaintext::BigInt(Some(10)), &key())
.unwrap()
{
IndexTerm::OpeFixed(b) => b,
_ => unreachable!(),
};
let b = match OpeIndexer
.encrypt(&Plaintext::BigInt(Some(20)), &key())
.unwrap()
{
IndexTerm::OpeFixed(b) => b,
_ => unreachable!(),
};
assert!(a < b);
}
#[test]
fn preserves_order_across_signed_range() {
let neg = match OpeIndexer
.encrypt(&Plaintext::BigInt(Some(-1)), &key())
.unwrap()
{
IndexTerm::OpeFixed(b) => b,
_ => unreachable!(),
};
let pos = match OpeIndexer
.encrypt(&Plaintext::BigInt(Some(1)), &key())
.unwrap()
{
IndexTerm::OpeFixed(b) => b,
_ => unreachable!(),
};
assert!(neg < pos);
}
#[test]
fn deterministic_for_same_plaintext_and_key() {
let a = OpeIndexer
.encrypt(&Plaintext::BigInt(Some(12345)), &key())
.unwrap();
let b = OpeIndexer
.encrypt(&Plaintext::BigInt(Some(12345)), &key())
.unwrap();
assert_eq!(a, b);
}
#[test]
fn try_from_rejects_non_ope_index_type() {
let err = OpeIndexerOptions::try_from(&column::IndexType::Ore);
assert!(err.is_err());
}
}