cipherstash-client 0.12.5

The official CipherStash SDK
Documentation
use hmac::{Hmac, Mac};
use sha2::Sha256;
use zerokms_protocol::cipherstash_config::column;

use super::text::TokenFilter;

use super::{errors::EncryptionError, plaintext::Plaintext, IndexTerm};

type HmacSha256 = Hmac<Sha256>;

pub struct UniqueIndexer {
    index_key: [u8; 32],
    token_filters: Vec<TokenFilter>,
}

impl UniqueIndexer {
    pub fn new(root_key: [u8; 32], token_filters: Vec<column::TokenFilter>) -> Self {
        // TODO: Later we should derive an index specific key
        Self {
            index_key: root_key,
            token_filters: token_filters.into_iter().map(|v| v.into()).collect(),
        }
    }

    pub(super) fn create_hmac(&self) -> Result<HmacSha256, EncryptionError> {
        Ok(HmacSha256::new_from_slice(&self.index_key)?)
    }

    pub(super) fn encrypt_into_hmac(
        &self,
        mac: &mut HmacSha256,
        plaintext: &Plaintext,
    ) -> Result<(), EncryptionError> {
        let plaintext_bytes = match plaintext {
            Plaintext::Utf8Str(None)
            | Plaintext::BigInt(None)
            | Plaintext::Boolean(None)
            | Plaintext::Decimal(None)
            | Plaintext::Float(None)
            | Plaintext::Int(None)
            | Plaintext::NaiveDate(None)
            | Plaintext::SmallInt(None)
            | Plaintext::Timestamp(None) => return Ok(()),

            Plaintext::Utf8Str(Some(utf_str)) => {
                let filtered_string = self
                    .token_filters
                    .iter()
                    .try_fold(utf_str.to_string(), |s, filter| filter.process_single(s));
                Plaintext::from(filtered_string).to_vec()
            }

            x => x.to_vec(),
        };

        mac.update(&plaintext_bytes);

        Ok(())
    }

    pub fn encrypt(&self, plaintext: &Plaintext) -> Result<IndexTerm, EncryptionError> {
        if plaintext.is_null() {
            Ok(IndexTerm::Null)
        } else {
            let mut mac = self.create_hmac()?;
            self.encrypt_into_hmac(&mut mac, plaintext)?;
            Ok(IndexTerm::Binary(mac.finalize().into_bytes().to_vec()))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use zerokms_protocol::cipherstash_config::column::TokenFilter;

    #[test]
    fn test_should_case_insensitive_compare() {
        let indexer = UniqueIndexer::new([1; 32], vec![TokenFilter::Downcase]);

        let first = indexer
            .encrypt(&"hello WORLD".into())
            .expect("Failed to encrypt");
        let second = indexer
            .encrypt(&"HELLO world".into())
            .expect("Failed to encrypt");

        assert_eq!(first, second);
    }
}