Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 161 additions & 174 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ debug = true

[workspace.dependencies]
sqltk = { version = "0.10.0" }
cipherstash-client = { version = "=0.34.1-alpha.4" }
cipherstash-config = { version = "=0.34.1-alpha.4" }
cts-common = { version = "=0.34.1-alpha.4" }
cipherstash-client = { version = "0.37.0" }
cipherstash-config = { version = "0.37.0" }
cts-common = { version = "0.37.0" }

thiserror = "2.0.9"
tokio = { version = "1.44.2", features = ["full"] }
Expand Down
6 changes: 3 additions & 3 deletions packages/cipherstash-proxy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,9 @@ impl From<cipherstash_client::eql::EqlError> for EncryptError {
cipherstash_client::eql::EqlError::ColumnConfigurationMismatch { table, column } => {
Self::ColumnConfigurationMismatch { table, column }
}
cipherstash_client::eql::EqlError::CouldNotDecryptDataForKeyset { keyset_id } => {
Self::CouldNotDecryptDataForKeyset { keyset_id }
}
cipherstash_client::eql::EqlError::CouldNotDecryptDataForKeyset {
keyset_id, ..
} => Self::CouldNotDecryptDataForKeyset { keyset_id },
cipherstash_client::eql::EqlError::InvalidIndexTerm => Self::InvalidIndexTerm,
cipherstash_client::eql::EqlError::MissingCiphertext(identifier) => {
Self::ColumnCouldNotBeDeserialised {
Expand Down
2 changes: 1 addition & 1 deletion packages/cipherstash-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub use crate::config::{DatabaseConfig, ServerConfig, TandemConfig, TlsConfig};
pub use crate::log::init;
pub use crate::proxy::Proxy;
pub use cipherstash_client::encryption::Plaintext;
pub use cipherstash_client::eql::{EqlCiphertext, Identifier};
pub use cipherstash_client::eql::{EqlCiphertext, EqlOutput, Identifier};

use std::mem;

Expand Down
8 changes: 4 additions & 4 deletions packages/cipherstash-proxy/src/postgresql/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ where
for (col, ct) in projection_columns.iter().zip(ciphertexts) {
match (col, ct) {
(Some(col), Some(ct)) => {
if col.identifier != ct.identifier {
if &col.identifier != ct.identifier() {
return Err(EncryptError::ColumnConfigurationMismatch {
table: col.identifier.table.to_owned(),
column: col.identifier.column.to_owned(),
Expand All @@ -553,8 +553,8 @@ where
// ciphertext with no column configuration is bad
(None, Some(ct)) => {
return Err(EncryptError::ColumnConfigurationMismatch {
table: ct.identifier.table.to_owned(),
column: ct.identifier.column.to_owned(),
table: ct.identifier().table.to_owned(),
column: ct.identifier().column.to_owned(),
}
.into());
}
Expand Down Expand Up @@ -749,7 +749,7 @@ mod tests {
_keyset_id: Option<KeysetIdentifier>,
_plaintexts: Vec<Option<cipherstash_client::encryption::Plaintext>>,
_columns: &[Option<Column>],
) -> Result<Vec<Option<crate::EqlCiphertext>>, Error> {
) -> Result<Vec<Option<crate::EqlOutput>>, Error> {
Ok(vec![])
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cipherstash-proxy/src/postgresql/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ where
&self,
plaintexts: Vec<Option<cipherstash_client::encryption::Plaintext>>,
columns: &[Option<Column>],
) -> Result<Vec<Option<crate::EqlCiphertext>>, Error> {
) -> Result<Vec<Option<crate::EqlOutput>>, Error> {
let keyset_id = self.keyset_identifier();

self.encryption
Expand Down Expand Up @@ -1077,7 +1077,7 @@ mod tests {
_keyset_id: Option<KeysetIdentifier>,
_plaintexts: Vec<Option<cipherstash_client::encryption::Plaintext>>,
_columns: &[Option<Column>],
) -> Result<Vec<Option<crate::EqlCiphertext>>, Error> {
) -> Result<Vec<Option<crate::EqlOutput>>, Error> {
Ok(vec![])
}

Expand Down
10 changes: 5 additions & 5 deletions packages/cipherstash-proxy/src/postgresql/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::prometheus::{
STATEMENTS_PASSTHROUGH_TOTAL, STATEMENTS_UNMAPPABLE_TOTAL,
};
use crate::proxy::EncryptionService;
use crate::EqlCiphertext;
use crate::EqlOutput;
use bytes::BytesMut;
use cipherstash_client::encryption::Plaintext;
use eql_mapper::{self, EqlMapperError, EqlTerm, TypeCheckedStatement};
Expand Down Expand Up @@ -582,13 +582,13 @@ where
/// # Returns
///
/// Vector of encrypted values corresponding to each literal, with `None` for
/// literals that don't require encryption and `Some(EqlCiphertext)` for encrypted values.
/// literals that don't require encryption and `Some(EqlOutput)` for encrypted values.
async fn encrypt_literals(
&mut self,
session_id: SessionId,
typed_statement: &TypeCheckedStatement<'_>,
literal_columns: &Vec<Option<Column>>,
) -> Result<Vec<Option<EqlCiphertext>>, Error> {
) -> Result<Vec<Option<EqlOutput>>, Error> {
let literal_values = typed_statement.literal_values();
if literal_values.is_empty() {
debug!(target: MAPPER,
Expand Down Expand Up @@ -643,7 +643,7 @@ where
async fn transform_statement(
&mut self,
typed_statement: &TypeCheckedStatement<'_>,
encrypted_literals: &Vec<Option<EqlCiphertext>>,
encrypted_literals: &Vec<Option<EqlOutput>>,
) -> Result<Option<ast::Statement>, Error> {
// Convert literals to ast Expr
let mut encrypted_expressions = vec![];
Expand Down Expand Up @@ -1042,7 +1042,7 @@ where
session_id: Option<SessionId>,
bind: &Bind,
statement: &Statement,
) -> Result<Vec<Option<crate::EqlCiphertext>>, Error> {
) -> Result<Vec<Option<crate::EqlOutput>>, Error> {
let plaintexts =
bind.to_plaintext(&statement.param_columns, &statement.postgres_param_types)?;

Expand Down
4 changes: 2 additions & 2 deletions packages/cipherstash-proxy/src/postgresql/messages/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::postgresql::protocol::BytesMutReadString;
use crate::{SIZE_I16, SIZE_I32};
use bytes::{Buf, BufMut, BytesMut};
use cipherstash_client::encryption::Plaintext;
use cipherstash_client::eql::EqlCiphertext;
use cipherstash_client::eql::EqlOutput;
use postgres_types::Type;
use std::fmt::{self, Display, Formatter};
use std::io::Cursor;
Expand Down Expand Up @@ -81,7 +81,7 @@ impl Bind {
Ok(plaintexts)
}

pub fn rewrite(&mut self, encrypted: Vec<Option<EqlCiphertext>>) -> Result<(), Error> {
pub fn rewrite(&mut self, encrypted: Vec<Option<EqlOutput>>) -> Result<(), Error> {
for (idx, ct) in encrypted.iter().enumerate() {
if let Some(ct) = ct {
let json = serde_json::to_value(ct)?;
Expand Down
70 changes: 64 additions & 6 deletions packages/cipherstash-proxy/src/postgresql/messages/data_row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
postgresql::Column,
};
use bytes::{Buf, BufMut, BytesMut};
use cipherstash_client::eql::EqlCiphertext;
use cipherstash_client::eql::{EqlCiphertext, EQL_SCHEMA_VERSION};
use std::io::Cursor;
use tracing::{debug, error};

Expand Down Expand Up @@ -191,7 +191,7 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext {
let input = String::from_utf8_lossy(sliced).to_string();
let input = input.replace("\"\"", "\"");

match serde_json::from_str(&input) {
match eql_ciphertext_from_json(input.as_bytes()) {
Ok(e) => return Ok(e),
Err(err) => {
debug!(target: DECRYPT, error = err.to_string());
Expand Down Expand Up @@ -221,7 +221,7 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext {
let start = 12 + 1;
let sliced = &bytes[start..];

match serde_json::from_slice(sliced) {
match eql_ciphertext_from_json(sliced) {
Ok(e) => {
return Ok(e);
}
Expand All @@ -237,6 +237,64 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext {
}
}

/// Deserialize an EQL ciphertext payload read from the database.
///
/// Supports both the current EQL v2.x storage format (a tagged object
/// discriminated by `"k"`, e.g. `{"k":"ct",...}`) and the legacy pre-v2.x flat
/// format that predates the `cipherstash-client` 0.37 upgrade. Existing customer
/// databases may still hold values written in the legacy format, so the proxy
/// must continue to read them transparently.
fn eql_ciphertext_from_json(input: &[u8]) -> Result<EqlCiphertext, serde_json::Error> {
let value: serde_json::Value = serde_json::from_slice(input)?;

// The current format always carries the `k` discriminator. Anything without
// it is a legacy payload and is remapped onto the current schema.
if value.get("k").is_some() {
serde_json::from_value(value)
} else {
serde_json::from_value(legacy_to_current(value))
}
}

/// Remap a legacy (pre-v2.x) EQL payload onto the current scalar storage shape.
///
/// The legacy format stored the encrypted record under `c`, the identifier under
/// `i`, and index terms under `m` (bloom filter), `o` (block ORE), and `u`
/// (HMAC). The current scalar payload (`k = "ct"`) renames these to `bf`, `ob`,
/// and `hm` respectively. Decryption only requires the root ciphertext (`c`) and
/// identifier (`i`); index terms are carried over best-effort. Legacy structured
/// (JSON / STE-vec) payloads also retained a root `c`, so they decrypt correctly
/// through the same scalar mapping.
fn legacy_to_current(old: serde_json::Value) -> serde_json::Value {
use serde_json::Value;

let mut new = serde_json::Map::new();
new.insert("k".to_string(), Value::String("ct".to_string()));
new.insert(
"v".to_string(),
old.get("v")
.filter(|v| !v.is_null())
.cloned()
.unwrap_or_else(|| Value::from(EQL_SCHEMA_VERSION)),
);

// Carry over a field from the legacy payload under a (possibly renamed) key,
// skipping nulls so optional terms stay absent rather than `null`.
let mut carry = |old_key: &str, new_key: &str| {
if let Some(v) = old.get(old_key).filter(|v| !v.is_null()) {
new.insert(new_key.to_string(), v.clone());
}
};

carry("i", "i"); // identifier
carry("c", "c"); // encrypted record
carry("u", "hm"); // HMAC (exact match)
carry("m", "bf"); // bloom filter (LIKE / ILIKE)
carry("o", "ob"); // block ORE (ordering)

Value::Object(new)
}

#[cfg(test)]
mod tests {
use super::DataRow;
Expand Down Expand Up @@ -284,7 +342,7 @@ mod tests {

assert_eq!(
column_config[1].as_ref().unwrap().identifier,
encrypted[1].as_ref().unwrap().identifier
*encrypted[1].as_ref().unwrap().identifier()
);
Comment on lines 343 to 346
}

Expand Down Expand Up @@ -333,7 +391,7 @@ mod tests {

assert_eq!(
column_config[0].as_ref().unwrap().identifier,
encrypted[0].as_ref().unwrap().identifier
*encrypted[0].as_ref().unwrap().identifier()
);
Comment on lines 392 to 395
}

Expand Down Expand Up @@ -374,7 +432,7 @@ mod tests {

assert_eq!(
column_config[2].as_ref().unwrap().identifier,
encrypted[2].as_ref().unwrap().identifier
*encrypted[2].as_ref().unwrap().identifier()
);
Comment on lines 433 to 436
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ fn canonical_to_map(canonical: CanonicalEncryptionConfig) -> Result<EncryptConfi
mod tests {
use super::*;
use cipherstash_client::eql::Identifier;
use cipherstash_config::column::{ArrayIndexMode, IndexType, TokenFilter, Tokenizer};
use cipherstash_config::column::{
ArrayIndexMode, IndexType, SteVecMode, TokenFilter, Tokenizer,
};
use cipherstash_config::ColumnType;
use serde_json::json;

Expand Down Expand Up @@ -518,6 +520,7 @@ mod tests {
prefix: "event-data".into(),
term_filters: vec![],
array_index_mode: ArrayIndexMode::ALL,
mode: SteVecMode::default(),
},
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cipherstash-proxy/src/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub trait EncryptionService: Send + Sync {
keyset_id: Option<KeysetIdentifier>,
plaintexts: Vec<Option<Plaintext>>,
columns: &[Option<Column>],
) -> Result<Vec<Option<crate::EqlCiphertext>>, Error>;
) -> Result<Vec<Option<crate::EqlOutput>>, Error>;

/// Decrypt values retrieved from the database
async fn decrypt(
Expand Down
12 changes: 6 additions & 6 deletions packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use cipherstash_client::{
encryption::{Plaintext, QueryOp},
eql::{
decrypt_eql, encrypt_eql, EqlCiphertext, EqlDecryptOpts, EqlEncryptOpts, EqlOperation,
PreparedPlaintext,
EqlOutput, PreparedPlaintext,
},
schema::column::IndexType,
};
Expand Down Expand Up @@ -157,7 +157,7 @@ impl EncryptionService for ZeroKms {
keyset_id: Option<KeysetIdentifier>,
plaintexts: Vec<Option<Plaintext>>,
columns: &[Option<Column>],
) -> Result<Vec<Option<EqlCiphertext>>, Error> {
) -> Result<Vec<Option<EqlOutput>>, Error> {
debug!(target: ENCRYPT, msg="Encrypt", ?keyset_id, default_keyset_id = ?self.default_keyset_id);

// A keyset is required if no default keyset has been configured
Expand Down Expand Up @@ -216,7 +216,7 @@ impl EncryptionService for ZeroKms {

// If no plaintexts to encrypt, return all None
if prepared_plaintexts.is_empty() {
return Ok(vec![None; plaintexts.len()]);
return Ok((0..plaintexts.len()).map(|_| None).collect());
}

// Use default opts since cipher is already initialized with the correct keyset
Expand All @@ -231,9 +231,9 @@ impl EncryptionService for ZeroKms {
debug!(target: ENCRYPT, msg="encrypt_eql completed", count = encrypted.len(), duration_ms = encrypt_duration.as_millis());

// Reconstruct the result vector with None values in the right places
let mut result: Vec<Option<EqlCiphertext>> = vec![None; plaintexts.len()];
for (idx, ciphertext) in indices.into_iter().zip(encrypted.into_iter()) {
result[idx] = Some(ciphertext);
let mut result: Vec<Option<EqlOutput>> = (0..plaintexts.len()).map(|_| None).collect();
for (idx, output) in indices.into_iter().zip(encrypted.into_iter()) {
result[idx] = Some(output);
}

Ok(result)
Expand Down
Loading