Skip to content
Merged
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
332 changes: 222 additions & 110 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/protect-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
cipherstash-client = "0.22.2"
cipherstash-client = "0.23.0"
hex = { version = "0.4.3", default-features = false }
libc = "0.2"
once_cell = { version = "1.20.2", default-features = false }
Expand Down
39 changes: 34 additions & 5 deletions crates/protect-ffi/src/encrypt_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ const SUPPORTED_SCHEMA_VERSIONS: &[u32] = &[2];
/// Table and column identifier for encryption configuration lookup.
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Identifier {
/// The table name.
#[serde(rename = "t")]
pub table: String,
/// The column name.
#[serde(rename = "c")]
pub column: String,
}
Expand Down Expand Up @@ -61,16 +63,20 @@ impl IntoIterator for Table {
/// Root encryption configuration structure parsed from JSON.
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct EncryptConfig {
/// The schema version.
#[serde(rename = "v")]
pub version: u32,
/// The set of table configurations.
pub tables: Tables,
}

/// Column configuration with encryption casting and index options.
/// Column configuration with casting and encryption indexes.
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
pub struct Column {
/// Data type casting for this column.
#[serde(default)]
cast_as: CastAs,
/// Collection of encryption indexes for this column.
#[serde(default)]
indexes: Indexes,
}
Expand All @@ -79,28 +85,42 @@ pub struct Column {
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CastAs {
/// Treat as UTF-8 text (default).
#[default]
Text,
/// Treat as a boolean value.
Boolean,
/// Treat as a 16-bit integer.
SmallInt,
/// Treat as a 32-bit integer.
Int,
/// Treat as a 64-bit integer.
BigInt,
/// Treat as a single-precision float.
Real,
/// Treat as a double-precision float.
Double,
/// Treat as a date.
Date,
/// Treat as a JSONB value.
#[serde(rename = "jsonb")]
JsonB,
}

/// Collection of index configurations for searchable encryption.
/// Collection of indexes for searchable encryption and uniqueness constraints.
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq)]
pub struct Indexes {
/// Unique index for exact equality queries and enforcing database uniqueness constraints.
#[serde(rename = "unique")]
unique_index: Option<UniqueIndexOpts>,
/// Order-revealing encryption index for equality checks, range comparisons, range queries,
/// and sorting operations.
#[serde(rename = "ore")]
ore_index: Option<OreIndexOpts>,
/// Full-text search index using bloom filters for probabilistic text matching.
#[serde(rename = "match")]
match_index: Option<MatchIndexOpts>,
/// Structured text encryption vector index for JSONB containment queries.
#[serde(rename = "ste_vec")]
ste_vec_index: Option<SteVecIndexOpts>,
}
Expand All @@ -112,21 +132,27 @@ pub struct OreIndexOpts {}
/// Configuration options for full-text search indexes using bloom filters.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct MatchIndexOpts {
/// The tokenizer to use for splitting text.
#[serde(default = "default_tokenizer")]
tokenizer: Tokenizer,
/// Token filters to apply to tokens.
#[serde(default)]
token_filters: Vec<TokenFilter>,
/// Number of hash functions for the bloom filter.
#[serde(default = "default_k")]
k: usize,
/// Bloom filter size in bits.
#[serde(default = "default_m")]
m: usize,
/// Whether to include the original value in the index.
#[serde(default)]
include_original: bool,
}

/// Configuration options for structured text encryption vectors.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct SteVecIndexOpts {
/// The prefix for the structured text encryption vector.
prefix: String,
}

Expand All @@ -145,9 +171,11 @@ fn default_m() -> usize {
2048
}

/// Configuration options for HMAC-based unique constraint indexes.
/// Configuration options for HMAC unique indexes that enable exact equality queries and
/// database uniqueness constraints.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct UniqueIndexOpts {
/// Token filters to apply to unique index tokens.
#[serde(default)]
token_filters: Vec<TokenFilter>,
}
Expand Down Expand Up @@ -182,7 +210,8 @@ impl FromStr for EncryptConfig {
}

impl EncryptConfig {
/// Convert the encryption configuration into a HashMap for fast column lookups.
/// Convert the encryption configuration into a [`HashMap`] mapping [`Identifier`] to
/// [`ColumnConfig`] for fast column lookups.
pub fn into_config_map(self) -> HashMap<Identifier, ColumnConfig> {
let mut map = HashMap::new();
for (table_name, columns) in self.tables.into_iter() {
Expand All @@ -197,7 +226,7 @@ impl EncryptConfig {
}

impl Column {
/// Convert this column configuration into a CipherStash ColumnConfig.
/// Convert this column configuration into a [`ColumnConfig`].
pub fn into_column_config(self, name: &String) -> ColumnConfig {
let mut config = ColumnConfig::build(name.to_string()).casts_as(self.cast_as.into());

Expand Down
64 changes: 41 additions & 23 deletions crates/protect-ffi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! PHP bindings for the CipherStash Client SDK.
//! FFI bindings for the CipherStash Client SDK.
//!
//! Provides C-compatible functions for PHP FFI integration with proper
//! This crate provides C-compatible functions for PHP FFI integration with proper
//! error handling and memory management.
//!
//! The main entry point is the [`Client`] type, which manages encryption and decryption
//! operations. All FFI functions operate on or return a pointer to a [`Client`] instance.

use cipherstash_client::{
config::{
Expand Down Expand Up @@ -47,23 +50,24 @@ pub struct Client {
encrypt_config: Arc<HashMap<Identifier, ColumnConfig>>,
}

/// An encrypted value that can contain either ciphertext or searchable vector data.
/// An encrypted value with associated encryption indexes or structured text encryption vectors.
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "k")]
pub enum Encrypted {
/// Standard encrypted ciphertext with optional search indexes.
/// Encrypted ciphertext with encryption indexes based on column configuration.
#[serde(rename = "ct")]
Ciphertext {
/// Base85-encoded ciphertext containing the encrypted data.
#[serde(rename = "c")]
ciphertext: String,
/// HMAC index for exact equality queries and uniqueness constraints (optional).
/// HMAC index for exact equality queries and uniqueness constraints.
#[serde(rename = "hm")]
unique_index: Option<String>,
/// Order-revealing encryption index for range queries (optional).
/// Order-revealing encryption index for equality checks, range comparisons, range queries, and
/// sorting operations.
#[serde(rename = "ob")]
ore_index: Option<Vec<String>>,
/// Bloom filter index for full-text search queries (optional).
/// Bloom filter index for full-text search queries.
#[serde(rename = "bf")]
match_index: Option<Vec<u16>>,
/// Table and column identifier for this encrypted value.
Expand All @@ -73,7 +77,7 @@ pub enum Encrypted {
#[serde(rename = "v")]
version: u16,
},
/// Structured text encryption vector for JSONB containment queries.
/// Encrypted ciphertext with structured text encryption vector for JSONB containment queries.
#[serde(rename = "sv")]
SteVec {
/// Base85-encoded ciphertext containing the encrypted data.
Expand Down Expand Up @@ -153,13 +157,13 @@ struct ClientConfig {
///
/// # Errors
///
/// Returns an error if the configuration string is invalid JSON, contains unsupported
/// encryption options, or if the CipherStash client cannot be initialized.
/// Returns an error if the `config_str` is invalid JSON, contains unsupported
/// encryption options, or if the client cannot be initialized.
///
/// # Safety
///
/// The caller must ensure `config_str` points to a valid null-terminated C string.
/// The returned pointer must be freed using `free_client()`.
/// The returned pointer must be freed using [`free_client()`].
#[no_mangle]
pub extern "C" fn new_client(
config_str: *const c_char,
Expand Down Expand Up @@ -199,7 +203,7 @@ async fn new_client_inner(encrypt_config: EncryptConfig) -> Result<Client, Error

/// Encrypts plaintext for a specific table column.
///
/// Returns a JSON string containing the encrypted result and search indexes.
/// Returns a JSON string containing the encrypted result and encryption indexes.
///
/// # Errors
///
Expand All @@ -209,7 +213,7 @@ async fn new_client_inner(encrypt_config: EncryptConfig) -> Result<Client, Error
/// # Safety
///
/// All pointer parameters must be valid null-terminated C strings.
/// The returned pointer must be freed using `free_string()`.
/// The returned pointer must be freed using [`free_string()`].
#[no_mangle]
pub extern "C" fn encrypt(
client: *const Client,
Expand Down Expand Up @@ -321,13 +325,13 @@ fn parse_encryption_context(context_json: &str) -> Result<Vec<zerokms::Context>,
///
/// # Errors
///
/// Returns an error if the ciphertext is invalid, the encryption context JSON is malformed,
/// Returns an error if the `ciphertext` is invalid, the encryption context JSON is malformed,
/// or decryption fails due to key or permission issues.
///
/// # Safety
///
/// All pointer parameters must be valid null-terminated C strings.
/// The returned pointer must be freed using `free_string()`.
/// The returned pointer must be freed using [`free_string()`].
#[no_mangle]
pub extern "C" fn decrypt(
client: *const Client,
Expand Down Expand Up @@ -410,7 +414,7 @@ fn to_eql_encrypted(
) -> Result<Encrypted, Error> {
match encrypted {
encryption::Encrypted::Record(ciphertext, terms) => {
// Collect search indexes from encryption terms
// Collect encryption indexes from encryption terms
struct Indexes {
unique_index: Option<String>,
ore_index: Option<Vec<String>>,
Expand Down Expand Up @@ -502,27 +506,40 @@ fn format_index_term_ore(bytes: &Vec<u8>) -> Vec<String> {
vec![format_index_term_ore_bytea(bytes)]
}

/// Bulk encryption request item containing plaintext data and metadata.
#[derive(Deserialize)]
struct BulkEncryptItem {
/// The plaintext data to encrypt.
plaintext: String,
/// The target column name.
column: String,
/// The target table name.
table: String,
/// Optional encryption context (defaults to empty if not provided).
#[serde(default)]
context: Option<serde_json::Value>,
}

/// Bulk decryption request item containing ciphertext and optional context.
#[derive(Deserialize)]
struct BulkDecryptItem {
/// The ciphertext to decrypt.
ciphertext: String,
/// Optional encryption context (defaults to empty if not provided).
#[serde(default)]
context: Option<serde_json::Value>,
}

/// Search term creation request item containing plaintext and target metadata.
#[derive(Deserialize)]
struct SearchTermItem {
/// The plaintext data to create search terms for.
plaintext: String,
/// The target column name.
column: String,
/// The target table name.
table: String,
/// Optional encryption context (defaults to empty if not provided).
#[serde(default)]
context: Option<serde_json::Value>,
}
Expand All @@ -537,7 +554,7 @@ struct SearchTermItem {
/// # Safety
///
/// All pointer parameters must be valid null-terminated C strings.
/// The returned pointer must be freed using `free_string()`.
/// The returned pointer must be freed using [`free_string()`].
#[no_mangle]
pub extern "C" fn encrypt_bulk(
client: *const Client,
Expand Down Expand Up @@ -626,13 +643,13 @@ async fn encrypt_bulk_inner(
///
/// # Errors
///
/// Returns an error if the JSON input is malformed, contains invalid ciphertext,
/// Returns an error if the JSON input is malformed, contains invalid `ciphertext`,
/// has malformed encryption context, or if decryption fails.
///
/// # Safety
///
/// All pointer parameters must be valid null-terminated C strings.
/// The returned pointer must be freed using `free_string()`.
/// The returned pointer must be freed using [`free_string()`].
#[no_mangle]
pub extern "C" fn decrypt_bulk(
client: *const Client,
Expand Down Expand Up @@ -698,7 +715,8 @@ async fn decrypt_bulk_inner(
/// Creates encrypted search terms for querying encrypted data.
///
/// Returns a JSON array of encrypted search terms that can be used in database queries.
/// Each search term contains the search indexes (ore, match, unique, ste_vec) but not the full ciphertext.
/// Each search term contains the encryption indexes (`unique`, `ore`, `match`, `ste_vec`)
/// but not the full ciphertext.
///
/// # Errors
///
Expand All @@ -708,7 +726,7 @@ async fn decrypt_bulk_inner(
/// # Safety
///
/// All pointer parameters must be valid null-terminated C strings.
/// The returned pointer must be freed using `free_string()`.
/// The returned pointer must be freed using [`free_string()`].
#[no_mangle]
pub extern "C" fn create_search_terms(
client: *const Client,
Expand Down Expand Up @@ -796,7 +814,7 @@ pub extern "C" fn create_search_terms(
///
/// # Safety
///
/// The client pointer must have been returned by `new_client()` and not previously freed.
/// The `client` pointer must have been returned by [`new_client()`] and not previously freed.
#[no_mangle]
pub extern "C" fn free_client(client: *mut Client) {
safe_ffi::free_boxed_client(client);
Expand All @@ -806,7 +824,7 @@ pub extern "C" fn free_client(client: *mut Client) {
///
/// # Safety
///
/// The string pointer must have been returned by this library and not previously freed.
/// The `s` pointer must have been returned by this library and not previously freed.
#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
safe_ffi::free_c_string(s);
Expand Down
Loading