diff --git a/Cargo.lock b/Cargo.lock index d8232f55..77467a9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -779,9 +779,9 @@ dependencies = [ [[package]] name = "cipherstash-client" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8289d3e99f43e1224a9fec77da6683a26662defeab001757c5bd82f767e9b639" +checksum = "904462ba39c48d8bb3dc1c33cffc678b1f4e3ebeee22b3f8d480a8ed4536a65f" dependencies = [ "aes-gcm-siv", "anyhow", @@ -4711,9 +4711,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" -version = "0.1.0-pre3" +version = "0.1.0-pre4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565a7733c4d683c67ca8e9815d81a132d7bd3e83f3b9f42e8b28e79bbd4bb1ce" +checksum = "3f800bb9d02311571a8de172d63a4c9c10e07869089e3ddd84a8b05f3bef2d93" dependencies = [ "vitaminc-encrypt", "vitaminc-protected", @@ -4723,9 +4723,9 @@ dependencies = [ [[package]] name = "vitaminc-aead" -version = "0.1.0-pre3" +version = "0.1.0-pre4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049ed06ef3e9e66356a063c7110263fc810ffa6837f6a5e1b83448ce32d3b5d" +checksum = "4e2b9f64cfbcc0c8d781e2a7d33beb183fd8f58735bed8c662286548042de820" dependencies = [ "bytes", "serde", @@ -4736,9 +4736,9 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" -version = "0.1.0-pre3" +version = "0.1.0-pre4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded1d063f6c5421f0c25901407c436047eefa3e9dd9f0e12d3f3771a234fa624" +checksum = "dc871c8b8c61e5d2e4e097526e9cf7bc99913a45c23f31c2f6d5f1f2d0b8eec7" dependencies = [ "aws-lc-rs", "vitaminc-aead", @@ -4776,9 +4776,9 @@ dependencies = [ [[package]] name = "vitaminc-random" -version = "0.1.0-pre3" +version = "0.1.0-pre4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b9a9c7c91719dc780486f2e026f92f31650584b7dcf5b175a597cd0e01325e" +checksum = "30ed841b58f676dc55a49d65284c91a2da2b71e57508c646a77c91f4dd31eb96" dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", @@ -4790,9 +4790,9 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" -version = "0.1.0-pre3" +version = "0.1.0-pre4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7258fae4c7ca793a984edca016bcf03c0d64c5f8803fd57c62caed8fe16aeda" +checksum = "172cdde4c52be52584990097655de39d835e9bcca5593c2b3517c8978ac87e89" dependencies = [ "proc-macro2", "quote", @@ -4801,9 +4801,9 @@ dependencies = [ [[package]] name = "vitaminc-traits" -version = "0.1.0-pre3" +version = "0.1.0-pre4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10dcb9cabed5726f1c2c997d60850309302938654b621f6b3028a5499be74821" +checksum = "fb63364ce6b2a33176d2a1aba75d6b77d982518654e84192419d18b8c97f36b5" dependencies = [ "anyhow", "bytes", diff --git a/Cargo.toml b/Cargo.toml index e0697e91..692acdaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ debug = true [workspace.dependencies] sqltk = { version = "0.10.0" } -cipherstash-client = "0.30.0" +cipherstash-client = { version = "0.31.0" } cts-common = { version = "0.4.0" } thiserror = "2.0.9" diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs index 8a1f0a8a..8faa0c58 100644 --- a/packages/cipherstash-proxy/src/error.rs +++ b/packages/cipherstash-proxy/src/error.rs @@ -268,7 +268,7 @@ pub enum EncryptError { /// This should in practice be unreachable #[error("Missing encrypt configuration for column type `{plaintext_type}`. For help visit {}#encrypt-missing-encrypt-configuration", ERROR_DOC_BASE_URL)] - MissingEncryptConfiguration { plaintext_type: String }, + MissingEncryptConfiguration { plaintext_type: &'static str }, #[error("Decrypted column could not be encoded as the expected type. For help visit {}#encrypt-plaintext-could-not-be-encoded", ERROR_DOC_BASE_URL)] PlaintextCouldNotBeEncoded, @@ -308,6 +308,78 @@ pub enum EncryptError { #[error("Unknown Index Term for column '{}' in table '{}'. For help visit {}#encrypt-unknown-index-term", _0.column(), _0.table(), ERROR_DOC_BASE_URL)] UnknownIndexTerm(Identifier), + + #[error("ZeroKMS error: `{}`", _0)] + ZeroKMS(String), +} + +// This impl is very boilerplatey but we can't simply re-export the `cipherstash-client` version of the error +// because Proxy currently manages the documentation links. +impl From for EncryptError { + fn from(value: cipherstash_client::eql::EncryptError) -> Self { + match value { + cipherstash_client::eql::EncryptError::CiphertextCouldNotBeSerialised(error) => { + Self::CiphertextCouldNotBeSerialised(error) + } + cipherstash_client::eql::EncryptError::ColumnCouldNotBeParsed => { + Self::ColumnCouldNotBeParsed + } + cipherstash_client::eql::EncryptError::ColumnIsNull => Self::ColumnIsNull, + cipherstash_client::eql::EncryptError::ColumnCouldNotBeDeserialised { + table, + column, + } => Self::ColumnCouldNotBeDeserialised { table, column }, + cipherstash_client::eql::EncryptError::ColumnCouldNotBeEncrypted { table, column } => { + Self::ColumnCouldNotBeEncrypted { table, column } + } + cipherstash_client::eql::EncryptError::ColumnConfigurationMismatch { + table, + column, + } => Self::ColumnConfigurationMismatch { table, column }, + cipherstash_client::eql::EncryptError::CouldNotDecryptDataForKeyset { keyset_id } => { + Self::CouldNotDecryptDataForKeyset { keyset_id } + } + cipherstash_client::eql::EncryptError::InvalidIndexTerm => Self::InvalidIndexTerm, + cipherstash_client::eql::EncryptError::KeysetIdCouldNotBeParsed { id } => { + Self::KeysetIdCouldNotBeParsed { id } + } + cipherstash_client::eql::EncryptError::KeysetIdCouldNotBeSet => { + Self::KeysetIdCouldNotBeSet + } + cipherstash_client::eql::EncryptError::KeysetNameCouldNotBeSet => { + Self::KeysetNameCouldNotBeSet + } + cipherstash_client::eql::EncryptError::MissingEncryptConfiguration { + plaintext_type, + } => Self::MissingEncryptConfiguration { plaintext_type }, + cipherstash_client::eql::EncryptError::PlaintextCouldNotBeEncoded => { + Self::PlaintextCouldNotBeEncoded + } + cipherstash_client::eql::EncryptError::Pipeline(encryption_error) => { + Self::Pipeline(encryption_error) + } + cipherstash_client::eql::EncryptError::PlaintextCouldNotBeDecoded(type_parse_error) => { + Self::PlaintextCouldNotBeDecoded(type_parse_error) + } + cipherstash_client::eql::EncryptError::MissingKeysetIdentifier => { + Self::MissingKeysetIdentifier + } + cipherstash_client::eql::EncryptError::UnexpectedSetKeyset => Self::UnexpectedSetKeyset, + cipherstash_client::eql::EncryptError::UnknownColumn { table, column } => { + Self::UnknownColumn { table, column } + } + cipherstash_client::eql::EncryptError::UnknownKeysetIdentifier { keyset } => { + Self::UnknownKeysetIdentifier { keyset } + } + cipherstash_client::eql::EncryptError::UnknownTable { table } => { + Self::UnknownTable { table } + } + cipherstash_client::eql::EncryptError::UnknownIndexTerm(identifier) => { + Self::UnknownIndexTerm(identifier) + } + cipherstash_client::eql::EncryptError::ZeroKMS(message) => Self::ZeroKMS(message), + } + } } #[derive(Error, Debug)] diff --git a/packages/cipherstash-proxy/src/proxy/zerokms/mod.rs b/packages/cipherstash-proxy/src/proxy/zerokms/mod.rs index 21cf4429..fb9a4092 100644 --- a/packages/cipherstash-proxy/src/proxy/zerokms/mod.rs +++ b/packages/cipherstash-proxy/src/proxy/zerokms/mod.rs @@ -3,17 +3,11 @@ mod zerokms; pub use zerokms::ZeroKms; -use crate::{ - config::TandemConfig, - error::{EncryptError, Error}, - Identifier, EQL_SCHEMA_VERSION, -}; +use crate::config::TandemConfig; use cipherstash_client::config::{ConfigError, ZeroKMSConfigWithClientKey}; use cipherstash_client::{ config::EnvSource, credentials::{auto_refresh::AutoRefresh, ServiceCredentials}, - encryption::{Encrypted, EncryptedEntry, EncryptedSteVecTerm, IndexTerm, Plaintext}, - eql::{self, EqlEncryptedBody, EqlEncryptedIndexes}, zerokms::ClientKey, ConsoleConfig, CtsConfig, ZeroKMS, ZeroKMSConfig, }; @@ -63,204 +57,3 @@ pub fn build_zerokms_config( builder.build_with_client_key() } - -pub(crate) fn to_eql_encrypted_from_index_term( - index_term: IndexTerm, - identifier: &Identifier, -) -> Result { - let selector = match index_term { - IndexTerm::SteVecSelector(s) => Some(hex::encode(s.as_bytes())), - _ => return Err(EncryptError::InvalidIndexTerm.into()), - }; - - Ok(eql::EqlEncrypted { - identifier: identifier.to_owned(), - version: EQL_SCHEMA_VERSION, - body: EqlEncryptedBody { - ciphertext: None, - indexes: EqlEncryptedIndexes { - bloom_filter: None, - ore_block_u64_8_256: None, - hmac_256: None, - blake3: None, - ore_cllw_u64_8: None, - ore_cllw_var_8: None, - selector, - ste_vec_index: None, - }, - is_array_item: None, - }, - }) -} - -pub(crate) fn to_eql_encrypted( - encrypted: Encrypted, - identifier: &Identifier, -) -> Result { - match encrypted { - Encrypted::Record(ciphertext, terms) => { - let mut match_index: Option> = None; - let mut ore_index: Option> = None; - let mut unique_index: Option = None; - let mut blake3_index: Option = None; - let mut ore_cclw_fixed_index: Option = None; - let mut ore_cclw_var_index: Option = None; - let mut selector: Option = None; - - for index_term in terms { - match index_term { - IndexTerm::Binary(bytes) => { - unique_index = Some(format_index_term_binary(&bytes)) - } - IndexTerm::BitMap(inner) => match_index = Some(inner), - IndexTerm::OreArray(bytes) => { - ore_index = Some(format_index_term_ore_array(&bytes)); - } - IndexTerm::OreFull(bytes) => { - ore_index = Some(format_index_term_ore(&bytes)); - } - IndexTerm::OreLeft(bytes) => { - ore_index = Some(format_index_term_ore(&bytes)); - } - IndexTerm::BinaryVec(_) => todo!(), - IndexTerm::SteVecSelector(s) => { - selector = Some(hex::encode(s.as_bytes())); - } - IndexTerm::SteVecTerm(ste_vec_term) => match ste_vec_term { - EncryptedSteVecTerm::Mac(bytes) => blake3_index = Some(hex::encode(bytes)), - EncryptedSteVecTerm::OreFixed(ore) => { - ore_cclw_fixed_index = Some(hex::encode(&ore)) - } - EncryptedSteVecTerm::OreVariable(ore) => { - ore_cclw_var_index = Some(hex::encode(&ore)) - } - _ => ore_cclw_var_index = None, - }, - IndexTerm::SteQueryVec(_query) => {} // TODO: what do we do here? - IndexTerm::Null => {} - }; - } - - Ok(eql::EqlEncrypted { - identifier: identifier.to_owned(), - version: EQL_SCHEMA_VERSION, - body: EqlEncryptedBody { - ciphertext: Some(ciphertext), - indexes: EqlEncryptedIndexes { - bloom_filter: match_index, - ore_block_u64_8_256: ore_index, - hmac_256: unique_index, - blake3: blake3_index, - ore_cllw_u64_8: ore_cclw_fixed_index, - ore_cllw_var_8: ore_cclw_var_index, - selector, - ste_vec_index: None, - }, - is_array_item: None, - }, - }) - } - Encrypted::SteVec(ste_vec) => { - let ciphertext = ste_vec.root_ciphertext()?.clone(); - - let ste_vec_index: Vec = ste_vec - .into_iter() - .map( - |EncryptedEntry { - tokenized_selector, - term, - record, - parent_is_array, - }| { - let indexes = match term { - EncryptedSteVecTerm::Mac(bytes) => EqlEncryptedIndexes { - selector: Some(hex::encode(tokenized_selector.as_bytes())), - blake3: Some(hex::encode(bytes)), - ..Default::default() - }, - EncryptedSteVecTerm::OreFixed(ore) => EqlEncryptedIndexes { - selector: Some(hex::encode(tokenized_selector.as_bytes())), - ore_cllw_u64_8: Some(hex::encode(&ore)), - ..Default::default() - }, - EncryptedSteVecTerm::OreVariable(ore) => EqlEncryptedIndexes { - selector: Some(hex::encode(tokenized_selector.as_bytes())), - ore_cllw_var_8: Some(hex::encode(&ore)), - ..Default::default() - }, - _ => EqlEncryptedIndexes::default(), - }; - - eql::EqlEncryptedBody { - ciphertext: Some(record), - indexes, - is_array_item: Some(parent_is_array), - } - }, - ) - .collect(); - - // FIXME: I'm unsure if I've handled the root ciphertext correctly - // The way it's implemented right now is that it will be repeated one in the ste_vec_index. - Ok(eql::EqlEncrypted { - identifier: identifier.to_owned(), - version: EQL_SCHEMA_VERSION, - body: EqlEncryptedBody { - ciphertext: Some(ciphertext.clone()), - indexes: EqlEncryptedIndexes { - bloom_filter: None, - ore_block_u64_8_256: None, - hmac_256: None, - blake3: None, - ore_cllw_u64_8: None, - ore_cllw_var_8: None, - selector: None, - ste_vec_index: Some(ste_vec_index), - }, - is_array_item: None, - }, - }) - } - } -} - -fn format_index_term_binary(bytes: &Vec) -> String { - hex::encode(bytes) -} - -fn format_index_term_ore_bytea(bytes: &Vec) -> String { - hex::encode(bytes) -} - -/// -/// Formats a Vec> into a Vec -/// -fn format_index_term_ore_array(vec_of_bytes: &[Vec]) -> Vec { - vec_of_bytes - .iter() - .map(format_index_term_ore_bytea) - .collect() -} - -/// -/// Formats a Vec> into a single elenent Vec -/// -fn format_index_term_ore(bytes: &Vec) -> Vec { - vec![format_index_term_ore_bytea(bytes)] -} - -pub(crate) fn plaintext_type_name(pt: Plaintext) -> String { - match pt { - Plaintext::BigInt(_) => "BigInt".to_string(), - Plaintext::BigUInt(_) => "BigUInt".to_string(), - Plaintext::Boolean(_) => "Boolean".to_string(), - Plaintext::Decimal(_) => "Decimal".to_string(), - Plaintext::Float(_) => "Float".to_string(), - Plaintext::Int(_) => "Int".to_string(), - Plaintext::NaiveDate(_) => "NaiveDate".to_string(), - Plaintext::SmallInt(_) => "SmallInt".to_string(), - Plaintext::Timestamp(_) => "Timestamp".to_string(), - Plaintext::Utf8Str(_) => "Utf8Str".to_string(), - Plaintext::JsonB(_) => "JsonB".to_string(), - } -} diff --git a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs index 4c682d4e..6961fa94 100644 --- a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs +++ b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs @@ -7,9 +7,8 @@ use crate::{ proxy::EncryptionService, }; use cipherstash_client::{ - encryption::QueryOp, - encryption::{Plaintext, PlaintextTarget, Queryable, ReferencedPendingPipeline}, - eql, + encryption::{Plaintext, ReferencedPendingPipeline}, + eql::{self, decrypt_eql, encrypt_eql, EqlEncryptionSpec}, }; use metrics::counter; use moka::future::Cache; @@ -17,10 +16,7 @@ use std::{sync::Arc, time::Duration}; use tracing::{debug, info, warn}; use uuid::Uuid; -use super::{ - init_zerokms_client, plaintext_type_name, to_eql_encrypted, to_eql_encrypted_from_index_term, - ScopedCipher, ZerokmsClient, -}; +use super::{init_zerokms_client, ScopedCipher, ZerokmsClient}; /// Memory size of a single ScopedCipher instance for cache weighing const SCOPED_CIPHER_SIZE: usize = std::mem::size_of::(); @@ -145,62 +141,24 @@ impl EncryptionService for ZeroKms { } let cipher = self.init_cipher(keyset_id).await?; - - let mut pipeline = ReferencedPendingPipeline::new(cipher.clone()); - let mut index_term_plaintexts = vec![None; columns.len()]; - - for (idx, item) in plaintexts.into_iter().zip(columns.iter()).enumerate() { - match item { - (Some(plaintext), Some(column)) => { - if column.is_encryptable() { - let encryptable = PlaintextTarget::new(plaintext, column.config.clone()); - pipeline.add_with_ref::(encryptable, idx)?; + let pipeline = ReferencedPendingPipeline::new(cipher.clone()); + + let encryption_specs: Vec> = columns + .iter() + .map(|col| { + col.as_ref().map(|col| { + if col.is_encryptable() { + EqlEncryptionSpec::Full(col.identifier.clone(), col.config.clone()) } else { - index_term_plaintexts[idx] = Some(plaintext); + EqlEncryptionSpec::SearchOnly(col.identifier.clone(), col.config.clone()) } - } - (None, Some(column)) => { - // Parameter is NULL - debug!(target: ENCRYPT, msg = "Null parameter", ?column); - } - (Some(plaintext), None) => { - // Should be unreachable - let plaintext_type = plaintext_type_name(plaintext); - return Err(EncryptError::MissingEncryptConfiguration { plaintext_type }.into()); - } - (None, None) => { - // Parameter is not encryptable - } - } - } - - let mut encrypted_eql = vec![]; - - let mut result = pipeline.encrypt(None, None).await?; - - for (idx, opt) in columns.iter().enumerate() { - let mut encrypted = None; - - if let Some(column) = opt { - if let Some(e) = result.remove(idx) { - encrypted = Some(to_eql_encrypted(e, &column.identifier)?); - } else if let Some(plaintext) = index_term_plaintexts[idx].clone() { - let index = column.config.clone().into_ste_vec_index().unwrap(); - let op = QueryOp::SteVecSelector; - - let index_term = (index, plaintext).build_queryable(cipher.clone(), op)?; - - encrypted = Some(to_eql_encrypted_from_index_term( - index_term, - &column.identifier, - )?); - } - } - - encrypted_eql.push(encrypted); - } + }) + }) + .collect(); - Ok(encrypted_eql) + Ok(encrypt_eql(cipher, pipeline, plaintexts, &encryption_specs) + .await + .map_err(EncryptError::from)?) } /// @@ -222,41 +180,10 @@ impl EncryptionService for ZeroKms { let cipher = self.init_cipher(keyset_id.clone()).await?; - // Create a mutable vector to hold the decrypted results - let mut results = vec![None; ciphertexts.len()]; - - // Collect the index and ciphertext details for every Some(ciphertext) - let (indices, encrypted): (Vec<_>, Vec<_>) = ciphertexts - .into_iter() - .enumerate() - .filter_map(|(idx, eql)| Some((idx, eql?.body.ciphertext.unwrap()))) - .collect::<_>(); - - let decrypted = cipher.decrypt(encrypted).await.map_err(|err| -> Error { - match &err { - cipherstash_client::zerokms::Error::Decrypt(_) => { - let error_msg = err.to_string(); - if error_msg.contains("Failed to retrieve key") { - EncryptError::CouldNotDecryptDataForKeyset { - keyset_id: keyset_id - .map(|id| id.to_string()) - .unwrap_or("default_keyset".to_string()), - } - .into() - } else { - Error::ZeroKMS(err.into()) - } - } - _ => Error::ZeroKMS(err.into()), - } - })?; - - // Merge the decrypted values as plaintext into their original indexed positions - for (idx, decrypted) in indices.into_iter().zip(decrypted) { - let plaintext = Plaintext::from_slice(&decrypted)?; - results[idx] = Some(plaintext); - } - - Ok(results) + Ok( + decrypt_eql(keyset_id.map(|keyset_id| keyset_id.0), cipher, ciphertexts) + .await + .map_err(EncryptError::from)?, + ) } }