Skip to content
Open
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
337 changes: 163 additions & 174 deletions Cargo.lock

Large diffs are not rendered by default.

18 changes: 3 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
[workspace]
resolver = "2"
members = ["packages/*"]
# Vendored crate is consumed only via [patch.crates-io] below, not as a member.
exclude = ["vendor/stack-auth"]

[workspace.package]
version = "2.2.4"
Expand Down Expand Up @@ -45,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.38.0" }
cipherstash-config = { version = "0.38.0" }
cts-common = { version = "0.38.0" }
Comment on lines +46 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm published versions and that no stale patch/exclude remains
for c in cipherstash-client cipherstash-config cts-common; do
  echo "== $c =="
  curl -s "https://crates.io/api/v1/crates/$c" | jq -r '.versions[].num' | rg -n '0\.38\.0' || echo "0.38.0 NOT found"
done
echo "== residual stack-auth references =="
rg -n 'stack-auth|vendor/stack-auth' Cargo.toml Cargo.lock 2>/dev/null

Repository: cipherstash/proxy

Length of output: 550


Critical Build Failure: Versions 0.38.0 are unpublished.

The versions 0.38.0 for cipherstash-client, cipherstash-config, and cts-common are not available on crates.io. Removing the [patch.crates-io] override with these versions will cause the build to fail immediately as the resolver cannot find these artifacts.

Required Action:

  • Revert the Cargo.toml changes to use a published version or restore the [patch.crates-io] entry with a valid local or git source for stack-auth until the crates are published. Do not merge this PR in its current state.
    [dangerous_changes]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` around lines 46 - 48, The Cargo.toml dependency update points
cipherstash-client, cipherstash-config, and cts-common at unpublished 0.38.0
versions, which will break dependency resolution. Revert these entries to a
published crates.io version or restore the [patch.crates-io] override to a valid
local/git source for stack-auth until those crates are available. Keep the
existing dependency names and patch configuration aligned so Cargo can resolve
them successfully.


thiserror = "2.0.9"
tokio = { version = "1.44.2", features = ["full"] }
Expand All @@ -58,13 +56,3 @@ tracing-subscriber = { version = "^0.3.20", features = [
"env-filter",
"std",
] }

# HOTFIX (CIP-3159): backport the stack-auth token-refresh CancelGuard fix onto
# the 0.34.1-alpha.4 source that cipherstash-client 0.34.1-alpha.4 pins. Without
# this, a cancelled get_token() future could strand `refresh_in_progress = true`,
# wedging all later refreshes and causing ZeroKMS "Request not authorized" exactly
# ~15 min (token TTL) after startup. The patch keeps version 0.34.1-alpha.4 so it
# satisfies cipherstash-client's exact pin while replacing the registry source.
# Remove once Proxy moves to a cipherstash-client built against stack-auth >= 0.36.0.
[patch.crates-io]
stack-auth = { path = "vendor/stack-auth" }
2 changes: 1 addition & 1 deletion mise.local.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ CS_CLIENT_KEY = "client-key"
CS_CLIENT_ID = "client-id"

# The release of EQL that the proxy tests will use and releases will be built with
CS_EQL_VERSION = "eql-2.3.0-pre.3"
CS_EQL_VERSION = "eql-2.3.1"

# TLS variables are required for providing TLS to Proxy's clients.
# CS_TLS__TYPE can be either "Path" or "Pem" (case-sensitive).
Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ CS_PROXY__HOST = "host.docker.internal"
# Misc
DOCKER_CLI_HINTS = "false" # Please don't show us What's Next.

CS_EQL_VERSION = "eql-2.3.0-pre.3"
CS_EQL_VERSION = "eql-2.3.1"


[tools]
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()
);
}

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()
);
}

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()
);
}

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
1 change: 0 additions & 1 deletion vendor/stack-auth/.gitignore

This file was deleted.

Loading
Loading