From 0a0c2a608673e26cb335fd4f5310f1402b8fc0c4 Mon Sep 17 00:00:00 2001 From: coreyhn Date: Tue, 22 Jul 2025 16:28:50 -0600 Subject: [PATCH 1/4] docs(readme): improve language, formatting, and API examples --- README.md | 169 +++++++++++++++++++++++++++--------------------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 69b76a2..cd6f22b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Protect.php FFI provides PHP bindings for the [CipherStash Client SDK](https://crates.io/crates/cipherstash-client) via PHP's [Foreign Function Interface (FFI)](https://www.php.net/manual/en/book.ffi.php). -Field-level encryption operations happen directly in your application with a unique key for each encrypted value, managed by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) and backed by [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). The encrypted data can be stored in any database that supports JSONB. +Field-level encryption operations happen directly in your application using a unique key for each encrypted value, managed by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) and backed by [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). The encrypted data can be stored in any database that supports JSONB. > [!IMPORTANT] > For most applications, you'll want to use the [Protect.php](https://github.com/cipherstash/protectphp) library instead, as it provides a more convenient API built on top of these bindings. @@ -25,7 +25,7 @@ Protect.php FFI requires PHP 8.1 or higher with the FFI extension (included in m ## Configuration -Before using Protect.php FFI, you must configure your CipherStash credentials. Set these values in your application's environment variables: +Before using Protect.php FFI, you must configure your CipherStash credentials. Set these environment variables in your application: ```bash CS_CLIENT_ID=your-client-id @@ -42,7 +42,7 @@ Credentials can be generated by logging in or signing up for CipherStash and set Protect.php FFI works with any database that supports JSONB storage. The encrypted data is structured as an [Encrypt Query Language (EQL)](https://github.com/cipherstash/encrypt-query-language) JSON payload. -For advanced querying capabilities (searching, sorting, filtering), you'll need PostgreSQL with EQL support. EQL provides the `eql_v2_encrypted` type: +For advanced querying capabilities (searching, sorting, filtering), you'll need PostgreSQL with the EQL extension. EQL provides the `eql_v2_encrypted` type: ```sql CREATE TABLE patient_records ( @@ -118,7 +118,7 @@ The `cast_as` parameter determines how plaintext data is processed before encryp **Index Types** -The `indexes` parameter determines what operations are supported on encrypted data: +The `indexes` parameter determines what queries are supported on encrypted data: | Index Type | Description | Response Parameter | Supported Queries | |------------|-------------|-------------------|------------------| @@ -378,14 +378,18 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { - // Your encryption operations go here... + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + + // ... } finally { // Always cleanup to prevent memory leaks - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -412,10 +416,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $encryptResultJson = $client->encrypt( client: $clientPtr, plaintext: 'john@example.com', @@ -423,14 +429,11 @@ try { tableName: 'patient_records', ); - $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); - - $ciphertext = $encryptResult['c']; - - echo $ciphertext; - // mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx... + // {"k":"ct","c":"mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx...","dt":"text","hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"},"v":2} } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -537,10 +540,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $encryptResultJson = $client->encrypt( client: $clientPtr, plaintext: 'john@example.com', @@ -552,12 +557,11 @@ try { $ciphertext = $encryptResult['c']; - $decryptResult = $client->decrypt($clientPtr, $ciphertext); - - echo $decryptResult; - // john@example.com + $decryptResult = $client->decrypt($clientPtr, $ciphertext); // john@example.com } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -569,7 +573,7 @@ Provide additional encryption context for an additional layer of security by bin **Context Types** -The `context` parameter determines what contextual authentication is supported on encrypted data: +The `context` parameter determines what contextual authentication is supported: | Context Type | Supported Index Types | Description | |--------------|----------------------|-------------| @@ -609,10 +613,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $context = [ 'tag' => ['pii', 'hipaa'], ]; @@ -631,12 +637,12 @@ try { $ciphertext = $encryptResult['c']; - $decryptResult = $client->decrypt($clientPtr, $ciphertext, $contextJson); + $decryptResult = $client->decrypt($clientPtr, $ciphertext, $contextJson); // john@example.com - echo $decryptResult; - // john@example.com } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -663,10 +669,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $context = [ 'value' => [ ['key' => 'tenant_id', 'value' => 'tenant_2ynTJf38e9HvuAO8jaX5kAyVaKI'], @@ -688,12 +696,12 @@ try { $ciphertext = $encryptResult['c']; - $decryptResult = $client->decrypt($clientPtr, $ciphertext, $contextJson); + $decryptResult = $client->decrypt($clientPtr, $ciphertext, $contextJson); // john@example.com - echo $decryptResult; - // john@example.com } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -733,10 +741,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $items = [ [ 'plaintext' => 'john@example.com', @@ -751,18 +761,12 @@ try { ]; $itemsJson = json_encode($items, JSON_THROW_ON_ERROR); - $encryptResultJson = $client->encryptBulk($clientPtr, $itemsJson); - - $encryptResults = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); - - foreach ($encryptResults as $encryptResult) { - $ciphertext = $encryptResult['c']; - - echo $ciphertext; - // mBbKuXT|+vBh~K2WV-!n5_W3DBFd4`Mp... - } + $encryptResultsJson = $client->encryptBulk($clientPtr, $itemsJson); + // [{"k":"ct","c":"mBbKuXT|+vBh~K2WV-!n5_W3DBFd4`Mp...","dt":"text","hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"},"v":2},{"k":"ct","c":"mBbJ<8tOEI+Z`KFUV`q&kmdWtO#DKxW|...","dt":"text","hm":null,"ob":null,"bf":[1397,378,1463,1673,1474,1226],"i":{"t":"patient_records","c":"medical_notes"},"v":2}] } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -797,10 +801,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $items = [ [ 'plaintext' => 'john@example.com', @@ -818,8 +824,8 @@ try { ]; $itemsJson = json_encode($items, JSON_THROW_ON_ERROR); - $encryptResultJson = $client->encryptBulk($clientPtr, $itemsJson); - $encryptResults = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); + $encryptResultsJson = $client->encryptBulk($clientPtr, $itemsJson); + $encryptResults = json_decode(json: $encryptResultsJson, associative: true, flags: JSON_THROW_ON_ERROR); $decryptItems = array_map(function ($item, $encryptResult) { $decryptItem = ['ciphertext' => $encryptResult['c']]; @@ -832,18 +838,14 @@ try { }, $items, $encryptResults); $decryptItemsJson = json_encode($decryptItems, JSON_THROW_ON_ERROR); - - echo $decryptItemsJson; // [{"ciphertext":"mBbK>BcAYctW$Gy)vK2)Y$&nBBKz{oL1...","context":{"tag":["pii","hipaa"]}},{"ciphertext":"mBbJ<8tOEI+Z`KFUV`q&kmdWtO#DKxW|..."}] - $decryptResultJson = $client->decryptBulk($clientPtr, $decryptItemsJson); - - echo $decryptResultJson; - // ["john@example.com","Patient shows improvement in mobility and pain management."] - - $decryptResults = json_decode(json: $decryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); + $decryptResultsJson = $client->decryptBulk($clientPtr, $decryptItemsJson); + // ["john@example.com", "Patient shows improvement in mobility and pain management."] } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` @@ -878,10 +880,12 @@ $config = [ ], ]; -$configJson = json_encode($config, JSON_THROW_ON_ERROR); -$clientPtr = $client->newClient($configJson); +$clientPtr = null; try { + $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = $client->newClient($configJson); + $searchTerms = [ [ 'plaintext' => 'john@example.com', @@ -899,19 +903,16 @@ try { ]; $searchTermsJson = json_encode($searchTerms, JSON_THROW_ON_ERROR); - $searchTermsResultJson = $client->createSearchTerms($clientPtr, $searchTermsJson); - $searchTermsResult = json_decode(json: $searchTermsResultJson, associative: true, flags: JSON_THROW_ON_ERROR); - - foreach ($searchTermsResult as $searchTerms) { - echo json_encode($searchTerms); - // {"hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"}} - } + $searchTermResultsJson = $client->createSearchTerms($clientPtr, $searchTermsJson); + // [{"hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"}}, ...] } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` -This functionality integrates with [EQL](https://github.com/cipherstash/encrypt-query-language) and is currently only supported on PostgreSQL databases. +This feature integrates with [EQL](https://github.com/cipherstash/encrypt-query-language) and is currently only supported on PostgreSQL databases. #### Search Terms Response @@ -982,11 +983,11 @@ Response parameters: ### Error Handling -Protect.php FFI operations may throw `FFIException` instances when errors occur during encryption, decryption, or client operations. Proper error handling ensures your application can gracefully handle configuration issues, network problems, or invalid data scenarios. +Protect.php FFI operations may throw `FFIException` exceptions when errors occur during encryption, decryption, or client operations. Proper error handling ensures your application can gracefully handle configuration issues, network problems, or invalid data scenarios. #### Exception Types -All FFI operations throw `FFIException` instances that contain descriptive error messages: +All FFI operations throw `FFIException` exceptions that contain descriptive error messages: ```php use CipherStash\Protect\FFI\Client; @@ -1010,6 +1011,8 @@ $config = [ try { $configJson = json_encode($config, JSON_THROW_ON_ERROR); + $clientPtr = null; + $clientPtr = $client->newClient($configJson); $encryptResultJson = $client->encrypt( @@ -1020,16 +1023,14 @@ try { ); $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); - $ciphertext = $encryptResult['c']; - - echo $ciphertext; - // mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx... + $ciphertext = $encryptResult['c']; // mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx... } catch (FFIException $e) { - error_log($e->getMessage()); - - throw new \RuntimeException('Failed to process encrypted data.', 0, $e); + // Handle FFI errors + // ... } finally { - $client->freeClient($clientPtr); + if ($clientPtr !== null) { + $client->freeClient($clientPtr); + } } ``` From b3634e9ac9a8812c05211f5ec84b9ab5db8c8ff3 Mon Sep 17 00:00:00 2001 From: coreyhn Date: Tue, 22 Jul 2025 16:59:14 -0600 Subject: [PATCH 2/4] docs(readme): restructure README heading hierarchy --- README.md | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index cd6f22b..361b6ed 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,7 @@ CS_WORKSPACE_CRN=your-workspace-crn Credentials can be generated by logging in or signing up for CipherStash and setting up a new workspace via the [CipherStash CLI](https://cipherstash.com/docs/sdk/how-to/cli) or [CipherStash Dashboard](https://dashboard.cipherstash.com/). -## Basic Usage - -### Database Setup +## Database Setup Protect.php FFI works with any database that supports JSONB storage. The encrypted data is structured as an [Encrypt Query Language (EQL)](https://github.com/cipherstash/encrypt-query-language) JSON payload. @@ -57,7 +55,7 @@ CREATE TABLE patient_records ( See the [EQL installation instructions](https://github.com/cipherstash/encrypt-query-language#installation) to get started. -### Encryption Configuration +## Encryption Configuration The encryption configuration defines your schema and determines what types of operations are supported on encrypted data. It consists of a JSON structure that specifies tables, columns, data types, and encryption indexes. @@ -335,7 +333,7 @@ SELECT * FROM patient_records WHERE health_assessment <@ '{"sv":[{"s":"df08a4c4157bdb5bf6fa9be89cf18d10...","t":"22303063343133306135646334356130...","r":"mBbL}QHJ&a(@rwS5n)u^G+Fb+Ex8ofB!...","pa":false}],"i":{"t":"patient_records","c":"health_assessment"}}'::jsonb; ``` -### Creating a Client +## Creating a Client Create a client instance with your encryption configuration to perform encryption and decryption operations: @@ -393,7 +391,7 @@ try { } ``` -### Encrypting Data +## Encrypting Data Encrypt plaintext data for specific table columns using the `encrypt()` method. This method accepts a client pointer and individual parameters for the plaintext string, column name, and table name. The encryption configuration defines how each column should be encrypted and what data type it represents: @@ -440,7 +438,7 @@ try { > [!IMPORTANT] > The `plaintext` parameter must always be a string. The `cast_as` configuration parameter determines how the string is processed by the native library before encryption, not the input format, and indicates the intended data type for parsing decrypted strings. Convert all values to strings before calling this method. -#### Encryption Response +### Encryption Response The `encrypt()` method returns a JSON string containing the encrypted data. The response format depends on the configured indexes. @@ -517,7 +515,7 @@ Response parameters: | `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | | `v` | `int` | Always | Schema version for backward compatibility | -### Decrypting Data +## Decrypting Data Decrypt ciphertext back to its original plaintext using the `decrypt()` method. This method accepts a client pointer and the base85-encoded ciphertext string from the encryption response: @@ -567,7 +565,7 @@ try { Returns the decrypted plaintext as a string. -### Encryption Context +## Encryption Context Provide additional encryption context for an additional layer of security by binding encrypted data to specific contextual information of your choosing. This prevents data encrypted with one context from being decrypted with a different context, even when using the same encryption keys. @@ -584,13 +582,13 @@ The `context` parameter determines what contextual authentication is supported: > [!IMPORTANT] > Encryption context is not supported with `ste_vec` indexes and will cause decryption to fail. -#### Identity Claim Context +### Identity Claim Context Identity claim context binds encrypted data to specific user identities using JWT claims. This enables identity-aware encryption where data can only be decrypted by authenticated users who match the identity criteria. Identity claim context requires [CipherStash Token Service (CTS)](https://cipherstash.com/docs/cts/about) authentication for both encryption and decryption operations. The FFI layer supports identity claim parsing but cannot perform cryptographic operations with identity claims without valid CTS tokens. Use the [Protect.php](https://github.com/cipherstash/protectphp) library for this type of encryption context. -#### Tag Context +### Tag Context Tag context binds encrypted data to specific string labels. This enables label-aware encryption where data can only be decrypted when the same tag context is provided: @@ -646,7 +644,7 @@ try { } ``` -#### Value Context +### Value Context Value context binds encrypted data to specific key-value pairs. This enables attribute-aware encryption where data can only be decrypted when the same value context is provided: @@ -708,11 +706,11 @@ try { > [!WARNING] > You must use the same context for both encryption and decryption operations. Wrong contexts will result in decryption failures. -### Bulk Operations +## Bulk Operations For improved performance when handling multiple records, use bulk encryption and decryption operations: -#### Bulk Encryption +### Bulk Encryption Encrypt multiple plaintext strings using the `encryptBulk()` method. This method accepts a client pointer and a JSON array of objects, where each object specifies the `plaintext`, `column`, `table`, and optional `context` for encryption: @@ -772,7 +770,7 @@ try { Returns a JSON array where each element follows the same structure as documented in the [Encryption Response](#encryption-response) section. -#### Bulk Decryption +### Bulk Decryption Decrypt multiple ciphertext strings using the `decryptBulk()` method. This method accepts a client pointer and a JSON array of objects, where each object contains a `ciphertext` with the base85-encoded ciphertext string and an optional `context` for decryption: @@ -851,7 +849,7 @@ try { Returns a JSON array of decrypted plaintext strings in the same order as the input JSON array. -### Searchable Encryption +## Searchable Encryption Create search terms that enable querying encrypted data without decryption using the `createSearchTerms()` method. This method accepts a client pointer and a JSON array of objects, where each object specifies the `plaintext`, `column`, `table`, and optional `context` for generating search terms: @@ -914,11 +912,11 @@ try { This feature integrates with [EQL](https://github.com/cipherstash/encrypt-query-language) and is currently only supported on PostgreSQL databases. -#### Search Terms Response +### Search Terms Response The `createSearchTerms()` method returns a JSON string containing search terms with only the encryption indexes (without the full ciphertext). The response format depends on the configured indexes. -**Standard Indexes Response** +#### Standard Indexes Response For columns configured with `unique`, `ore`, and/or `match` indexes: @@ -943,7 +941,7 @@ Response parameters: | `bf` | `array\|null` | `match` | Bloom filter index for full-text search queries | | `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | -**STE Vec Index Response** +#### STE Vec Index Response For columns configured with `ste_vec` indexes: @@ -981,11 +979,11 @@ Response parameters: | `sv[].pa` | `boolean` | `ste_vec` | Whether the parent JSON element is an array | | `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | -### Error Handling +## Error Handling Protect.php FFI operations may throw `FFIException` exceptions when errors occur during encryption, decryption, or client operations. Proper error handling ensures your application can gracefully handle configuration issues, network problems, or invalid data scenarios. -#### Exception Types +### Exception Types All FFI operations throw `FFIException` exceptions that contain descriptive error messages: From ef34bbe80210308e5aa6648282f1a3634d1294d0 Mon Sep 17 00:00:00 2001 From: coreyhn Date: Tue, 22 Jul 2025 17:13:54 -0600 Subject: [PATCH 3/4] docs(readme): improve ordering, formatting, and API examples --- README.md | 354 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 196 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 361b6ed..df81b6a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Protect.php FFI -Protect.php FFI provides PHP bindings for the [CipherStash Client SDK](https://crates.io/crates/cipherstash-client) via PHP's [Foreign Function Interface (FFI)](https://www.php.net/manual/en/book.ffi.php). +Protect.php FFI provides PHP bindings for the [CipherStash Client SDK](https://crates.io/crates/cipherstash-client) via PHP's [Foreign Function Interface (FFI)](https://www.php.net/manual/en/book.ffi.php). Field-level encryption operations happen directly in your application using a unique key for each encrypted value, managed by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) and backed by [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). The encrypted data can be stored in any JSONB-compatible database while maintaining searchability on PostgreSQL. -Field-level encryption operations happen directly in your application using a unique key for each encrypted value, managed by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) and backed by [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). The encrypted data can be stored in any database that supports JSONB. +This library operates at a low level, providing direct access to the native cryptographic operations. It requires manual memory management and detailed encryption configuration, designed for advanced use cases where you need fine-grained control over the encryption process. > [!IMPORTANT] > For most applications, you'll want to use the [Protect.php](https://github.com/cipherstash/protectphp) library instead, as it provides a more convenient API built on top of these bindings. @@ -43,12 +43,13 @@ Protect.php FFI works with any database that supports JSONB storage. The encrypt For advanced querying capabilities (searching, sorting, filtering), you'll need PostgreSQL with the EQL extension. EQL provides the `eql_v2_encrypted` type: ```sql -CREATE TABLE patient_records ( +CREATE TABLE users ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email eql_v2_encrypted, - systolic_bp eql_v2_encrypted, - medical_notes eql_v2_encrypted, - health_assessment eql_v2_encrypted, + name eql_v2_encrypted, + balance eql_v2_encrypted, + contact eql_v2_encrypted, + notes eql_v2_encrypted, CONSTRAINT unique_email UNIQUE ((email->>'hm')) -- Enforce unique emails ); ``` @@ -65,18 +66,35 @@ Basic structure: $config = [ 'v' => 2, 'tables' => [ - 'table_name' => [ - 'column_name' => [ - 'cast_as' => 'data_type', + 'users' => [ + 'email' => [ + 'cast_as' => 'text', 'indexes' => [ - 'unique' => [ - 'token_filters' => [ - ['kind' => 'downcase'], - ], - ], + 'unique' => (object) [], 'match' => (object) [], ], ], + 'balance' => [ + 'cast_as' => 'int', + 'indexes' => [ + 'unique' => (object) [], + 'ore' => (object) [], + ], + ], + 'notes' => [ + 'cast_as' => 'text', + 'indexes' => [ + 'match' => (object) [], + ], + ], + 'contact' => [ + 'cast_as' => 'jsonb', + 'indexes' => [ + 'ste_vec' => [ + 'prefix' => 'users.contact', + ], + ], + ], ], ], ]; @@ -88,17 +106,17 @@ Configuration parameters: |-----------|------|----------|-------------| | `v` | `int` | ✓ | Schema version for backward compatibility (must be `2`) | | `tables` | `object` | ✓ | Table definitions containing column configurations | -| `tables.` | `object` | ✓ | Column definitions for the specified table | -| `tables..` | `object` | ✓ | Configuration for the specified column | -| `tables...cast_as` | `string` | ✗ | Data type for processing before encryption (defaults to `text`) | -| `tables...indexes` | `object` | ✗ | Encryption indexes for query patterns | -| `tables...indexes.` | `object` | ✗ | Configuration parameters for the specified index type (see individual index type documentation) | -| `tables...indexes..` | `mixed` | ✗ | Index-specific configuration parameter | +| `tables.` | `object` | ✓ | Column definitions for the specified table | +| `tables.
.` | `object` | ✓ | Configuration for the specified column | +| `tables.
..cast_as` | `string` | ✗ | Data type for processing before encryption (defaults to `text`) | +| `tables.
..indexes` | `object` | ✗ | Encryption indexes for query patterns | +| `tables.
..indexes.` | `object` | ✗ | Configuration parameters for the specified index type (see individual index type documentation) | +| `tables.
..indexes..` | `mixed` | ✗ | Index-specific configuration parameter | > [!IMPORTANT] > When configuring indexes without parameters, you must use `(object) []` instead of an empty array `[]`. This ensures PHP's `json_encode()` produces a JSON object (`{}`) rather than a JSON array (`[]`), which is required by the native library's configuration parser. -**Data Types** +### Data Types The `cast_as` parameter determines how plaintext data is processed before encryption: @@ -114,7 +132,7 @@ The `cast_as` parameter determines how plaintext data is processed before encryp | `date` | Date strings in ISO format | `2020-11-10` | | `jsonb` | JSON data | `{"key": "value"}` | -**Index Types** +### Index Types The `indexes` parameter determines what queries are supported on encrypted data: @@ -125,14 +143,14 @@ The `indexes` parameter determines what queries are supported on encrypted data: | `match` | Full-text search queries | `bf` | `~~` | | `ste_vec` | JSONB containment queries | `sv` | `@>`, `<@` | -**Unique Index (`unique`)** +#### Unique Index (`unique`) Enables exact equality queries and database uniqueness constraints. Uses the `hm` response parameter to generate HMAC-based hashes for exact equality matching. Basic usage: ```php -'patient_records' => [ +'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ @@ -152,7 +170,7 @@ Configuration parameters: With custom parameters: ```php -'patient_records' => [ +'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ @@ -166,30 +184,21 @@ With custom parameters: ], ``` -Example SQL query: - -```sql --- Find patient record by email address --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE email = '{"hm":"0f4f3b99671e74c0f8b5a1d2e3f4a5b6c7d8...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"}}'::jsonb; -``` - For database-level uniqueness constraints, add a unique constraint on the `hm` response parameter: ```sql CONSTRAINT unique_email UNIQUE ((email->>'hm')) ``` -**Order Revealing Encryption Index (`ore`)** +#### Order Revealing Encryption Index (`ore`) Enables equality, range operations, and ordering on encrypted data. Uses the `ob` response parameter to create order-preserving encrypted values for equality checks, range comparisons, and sorting operations. Basic usage: ```php -'patient_records' => [ - 'systolic_bp' => [ +'users' => [ + 'balance' => [ 'cast_as' => 'int', 'indexes' => [ 'ore' => (object) [], @@ -202,44 +211,15 @@ Configuration parameters: *This index type has no configurable parameters.* -Example SQL queries: - -```sql --- Find patients with exact blood pressure value --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE systolic_bp = '{"hm":null,"ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}'::jsonb; - --- Find patients with blood pressure above specified threshold --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE systolic_bp >= '{"hm":null,"ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}'::jsonb; - --- Find patients with blood pressure in specified range --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE systolic_bp BETWEEN - '{"hm":null,"ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}'::jsonb - AND '{"hm":null,"ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}'::jsonb; - --- Order patients by blood pressure from lowest to highest -SELECT * FROM patient_records -ORDER BY systolic_bp ASC; - --- Order patients by blood pressure from highest to lowest -SELECT * FROM patient_records -ORDER BY systolic_bp DESC; -``` - -**Match Index (`match`)** +#### Match Index (`match`) Enables full-text search on encrypted text data using bloom filters. Uses the `bf` response parameter to create bloom filter representations of tokenized text for probabilistic matching. Basic usage: ```php -'patient_records' => [ - 'medical_notes' => [ +'users' => [ + 'notes' => [ 'cast_as' => 'text', 'indexes' => [ 'match' => (object) [], // Uses defaults @@ -264,8 +244,8 @@ Configuration parameters: With custom parameters: ```php -'patient_records' => [ - 'medical_notes' => [ +'users' => [ + 'notes' => [ 'cast_as' => 'text', 'indexes' => [ 'match' => [ @@ -285,28 +265,19 @@ With custom parameters: ], ``` -Example SQL query: - -```sql --- Find patients with medical notes containing specified terms --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE medical_notes ~~ '{"hm":null,"ob":null,"bf":[1397,378,1463,1673,1474,1226],"i":{"t":"patient_records","c":"medical_notes"}}'::jsonb; -``` - -**Structured Text Encryption Vector Index (`ste_vec`)** +#### Structured Text Encryption Vector Index (`ste_vec`) Enables containment queries on encrypted JSONB data. Uses the `sv` response parameter to create structured text encryption vectors that preserve JSON path relationships for encrypted JSONB containment matching. Basic usage: ```php -'patient_records' => [ - 'health_assessment' => [ +'users' => [ + 'contact' => [ 'cast_as' => 'jsonb', 'indexes' => [ 'ste_vec' => [ - 'prefix' => 'patient_records.health_assessment', + 'prefix' => 'users.contact', ], ], ], @@ -317,21 +288,7 @@ Configuration parameters: | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| -| `prefix` | `string` | ✓ | - | Domain separator for cryptographic hashing that must be unique per column (recommended format is `table_name.column_name`) | - -Example SQL queries: - -```sql --- Find records where encrypted data contains specified values --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE health_assessment @> '{"sv":[{"s":"dd4659b9c279af040dd05ce21b2a22f7...","t":"22303061363334333330316661653633...","r":"mBbL}QHJ&a(@rwS5n)u^G+Fb+t}Soo-h...","pa":false}],"i":{"t":"patient_records","c":"health_assessment"}}'::jsonb; - --- Find records where encrypted data is contained by specified values --- Using search terms (encrypted ahead of time, plaintext not loggable): -SELECT * FROM patient_records -WHERE health_assessment <@ '{"sv":[{"s":"df08a4c4157bdb5bf6fa9be89cf18d10...","t":"22303063343133306135646334356130...","r":"mBbL}QHJ&a(@rwS5n)u^G+Fb+Ex8ofB!...","pa":false}],"i":{"t":"patient_records","c":"health_assessment"}}'::jsonb; -``` +| `prefix` | `string` | ✓ | - | Domain separator for cryptographic hashing that must be unique per column (recommended format is `table.column`) | ## Creating a Client @@ -345,30 +302,31 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], ], ], - 'systolic_bp' => [ + 'balance' => [ 'cast_as' => 'int', 'indexes' => [ + 'unique' => (object) [], 'ore' => (object) [], ], ], - 'medical_notes' => [ + 'notes' => [ 'cast_as' => 'text', 'indexes' => [ 'match' => (object) [], ], ], - 'health_assessment' => [ + 'contact' => [ 'cast_as' => 'jsonb', 'indexes' => [ 'ste_vec' => [ - 'prefix' => 'patient_records.health_assessment', + 'prefix' => 'users.contact', ], ], ], @@ -403,11 +361,12 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], ], @@ -424,10 +383,10 @@ try { client: $clientPtr, plaintext: 'john@example.com', columnName: 'email', - tableName: 'patient_records', + tableName: 'users', ); - // {"k":"ct","c":"mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx...","dt":"text","hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"},"v":2} + // {"k":"ct","c":"mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx...","dt":"text","hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":[1124,2134,987,1456,743,2201],"i":{"t":"users","c":"email"},"v":2} } finally { if ($clientPtr !== null) { $client->freeClient($clientPtr); @@ -440,9 +399,9 @@ try { ### Encryption Response -The `encrypt()` method returns a JSON string containing the encrypted data. The response format depends on the configured indexes. +The `encrypt()` method returns a JSON string containing the encrypted envelope. The response format depends on the configured indexes. -**Standard Indexes Response** +#### Standard Indexes Response For columns configured with the `unique`, `ore`, and/or `match` indexes: @@ -453,9 +412,9 @@ For columns configured with the `unique`, `ore`, and/or `match` indexes: "dt": "text", "hm": "f3ca71fd39ae9d3d1d1fc25141bcb6da...", "ob": null, - "bf": null, + "bf": [1124,2134,987,1456,743,2201], "i": { - "t": "patient_records", + "t": "users", "c": "email" }, "v": 2 @@ -472,10 +431,10 @@ Response parameters: | `hm` | `string\|null` | `unique` | HMAC index for exact equality queries and uniqueness constraints | | `ob` | `array\|null` | `ore` | Order-revealing encryption index for range queries | | `bf` | `array\|null` | `match` | Bloom filter index for full-text search queries | -| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | +| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table","c":"column"}` | | `v` | `int` | Always | Schema version for backward compatibility | -**STE Vec Index Response** +#### STE Vec Index Response For columns configured with the `ste_vec` index: @@ -493,8 +452,8 @@ For columns configured with the `ste_vec` index: } ], "i": { - "t": "patient_records", - "c": "health_assessment" + "t": "users", + "c": "contact" }, "v": 2 } @@ -512,7 +471,7 @@ Response parameters: | `sv[].t` | `string` | `ste_vec` | Encrypted term value for equality and order-preserving queries | | `sv[].r` | `string` | `ste_vec` | Base85-encoded ciphertext containing the encrypted record data | | `sv[].pa` | `boolean` | `ste_vec` | Whether the parent JSON element is an array | -| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | +| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table","c":"column"}` | | `v` | `int` | Always | Schema version for backward compatibility | ## Decrypting Data @@ -527,11 +486,12 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], ], @@ -548,7 +508,7 @@ try { client: $clientPtr, plaintext: 'john@example.com', columnName: 'email', - tableName: 'patient_records', + tableName: 'users', ); $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); @@ -569,7 +529,7 @@ Returns the decrypted plaintext as a string. Provide additional encryption context for an additional layer of security by binding encrypted data to specific contextual information of your choosing. This prevents data encrypted with one context from being decrypted with a different context, even when using the same encryption keys. -**Context Types** +### Context Types The `context` parameter determines what contextual authentication is supported: @@ -600,11 +560,12 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], ], @@ -627,7 +588,7 @@ try { client: $clientPtr, plaintext: 'john@example.com', columnName: 'email', - tableName: 'patient_records', + tableName: 'users', contextJson: $contextJson, ); @@ -636,7 +597,6 @@ try { $ciphertext = $encryptResult['c']; $decryptResult = $client->decrypt($clientPtr, $ciphertext, $contextJson); // john@example.com - } finally { if ($clientPtr !== null) { $client->freeClient($clientPtr); @@ -656,11 +616,12 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], ], @@ -686,7 +647,7 @@ try { client: $clientPtr, plaintext: 'john@example.com', columnName: 'email', - tableName: 'patient_records', + tableName: 'users', contextJson: $contextJson, ); @@ -695,7 +656,6 @@ try { $ciphertext = $encryptResult['c']; $decryptResult = $client->decrypt($clientPtr, $ciphertext, $contextJson); // john@example.com - } finally { if ($clientPtr !== null) { $client->freeClient($clientPtr); @@ -722,14 +682,15 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], - 'medical_notes' => [ + 'notes' => [ 'cast_as' => 'text', 'indexes' => [ 'match' => (object) [], @@ -749,18 +710,18 @@ try { [ 'plaintext' => 'john@example.com', 'column' => 'email', - 'table' => 'patient_records', + 'table' => 'users', ], [ - 'plaintext' => 'Patient shows improvement in mobility and pain management.', - 'column' => 'medical_notes', - 'table' => 'patient_records', + 'plaintext' => 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.', + 'column' => 'notes', + 'table' => 'users', ], ]; $itemsJson = json_encode($items, JSON_THROW_ON_ERROR); $encryptResultsJson = $client->encryptBulk($clientPtr, $itemsJson); - // [{"k":"ct","c":"mBbKuXT|+vBh~K2WV-!n5_W3DBFd4`Mp...","dt":"text","hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"},"v":2},{"k":"ct","c":"mBbJ<8tOEI+Z`KFUV`q&kmdWtO#DKxW|...","dt":"text","hm":null,"ob":null,"bf":[1397,378,1463,1673,1474,1226],"i":{"t":"patient_records","c":"medical_notes"},"v":2}] + // [{"k":"ct","c":"mBbKuXT|+vBh~K2WV-!n5_W3DBFd4`Mp...","dt":"text","hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":[1124,2134,987,1456,743,2201],"i":{"t":"users","c":"email"},"v":2},{"k":"ct","c":"mBbJ<8tOEI+Z`KFUV`q&kmdWtO#DKxW|...","dt":"text","hm":null,"ob":null,"bf":[1397,378,1463,1673,1474,1226],"i":{"t":"users","c":"notes"},"v":2}] } finally { if ($clientPtr !== null) { $client->freeClient($clientPtr); @@ -768,7 +729,7 @@ try { } ``` -Returns a JSON array where each element follows the same structure as documented in the [Encryption Response](#encryption-response) section. +Returns a JSON array of encrypted envelopes where each element follows the same structure as documented in the [Encryption Response](#encryption-response) section. ### Bulk Decryption @@ -782,14 +743,15 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], - 'medical_notes' => [ + 'notes' => [ 'cast_as' => 'text', 'indexes' => [ 'match' => (object) [], @@ -809,15 +771,15 @@ try { [ 'plaintext' => 'john@example.com', 'column' => 'email', - 'table' => 'patient_records', + 'table' => 'users', 'context' => [ 'tag' => ['pii', 'hipaa'], ], ], [ - 'plaintext' => 'Patient shows improvement in mobility and pain management.', - 'column' => 'medical_notes', - 'table' => 'patient_records', + 'plaintext' => 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.', + 'column' => 'notes', + 'table' => 'users', ], ]; @@ -839,7 +801,7 @@ try { // [{"ciphertext":"mBbK>BcAYctW$Gy)vK2)Y$&nBBKz{oL1...","context":{"tag":["pii","hipaa"]}},{"ciphertext":"mBbJ<8tOEI+Z`KFUV`q&kmdWtO#DKxW|..."}] $decryptResultsJson = $client->decryptBulk($clientPtr, $decryptItemsJson); - // ["john@example.com", "Patient shows improvement in mobility and pain management."] + // ["john@example.com", "Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client."] } finally { if ($clientPtr !== null) { $client->freeClient($clientPtr); @@ -861,16 +823,18 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], - 'systolic_bp' => [ + 'balance' => [ 'cast_as' => 'int', 'indexes' => [ + 'unique' => (object) [], 'ore' => (object) [], ], ], @@ -888,21 +852,21 @@ try { [ 'plaintext' => 'john@example.com', 'column' => 'email', - 'table' => 'patient_records', + 'table' => 'users', 'context' => [ 'tag' => ['pii', 'hipaa'], ], ], [ - 'plaintext' => '120', - 'column' => 'systolic_bp', - 'table' => 'patient_records', + 'plaintext' => '1575000', + 'column' => 'balance', + 'table' => 'users', ], ]; $searchTermsJson = json_encode($searchTerms, JSON_THROW_ON_ERROR); $searchTermResultsJson = $client->createSearchTerms($clientPtr, $searchTermsJson); - // [{"hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"}}, ...] + // [{"hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":[1124,2134,987,1456,743,2201],"i":{"t":"users","c":"email"}},{"hm":"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...","ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"users","c":"balance"}}] } finally { if ($clientPtr !== null) { $client->freeClient($clientPtr); @@ -912,6 +876,79 @@ try { This feature integrates with [EQL](https://github.com/cipherstash/encrypt-query-language) and is currently only supported on PostgreSQL databases. +### Querying with Search Terms + +These examples demonstrate how to use search terms with PostgreSQL and EQL for querying encrypted data without decryption. Each query uses the complete search terms object, and EQL automatically selects the appropriate index for the query operation. + +#### Exact Equality Queries + +For exact equality queries, EQL uses the `unique` index (`hm` response parameter) from your search terms: + +```sql +-- Find user by email address +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE email = '{"hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":null,"bf":[1124,2134,987,1456,743,2201],"i":{"t":"users","c":"email"}}'::jsonb; +``` + +#### Equality, Range, and Sorting Queries + +For equality, range comparisons, and sorting, EQL uses the `ore` index (`ob` response parameter) from your search terms: + +```sql +-- Find users with exact balance amount +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE balance = '{"hm":"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...","ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"users","c":"balance"}}'::jsonb; + +-- Find users above specified balance +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE balance >= '{"hm":"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...","ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"users","c":"balance"}}'::jsonb; + +-- Find users with balance in specified range +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE balance BETWEEN + '{"hm":"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...","ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"users","c":"balance"}}'::jsonb + AND '{"hm":"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...","ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"users","c":"balance"}}'::jsonb; + +-- Order users by balance from lowest to highest +SELECT * FROM users +ORDER BY balance ASC; + +-- Order users by balance from highest to lowest +SELECT * FROM users +ORDER BY balance DESC; +``` + +#### Full-Text Search Queries + +For searching within text content, EQL uses the `match` index (`bf` response parameter) from your search terms: + +```sql +-- Find users with notes containing specified terms +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE notes ~~ '{"hm":null,"ob":null,"bf":[1397,378,1463,1673,1474,1226],"i":{"t":"users","c":"notes"}}'::jsonb; +``` + +#### JSONB Containment Queries + +For structured data queries, EQL uses the `ste_vec` index (`sv` response parameter) from your search terms: + +```sql +-- Find records where encrypted data contains specified values +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE contact @> '{"sv":[{"s":"dd4659b9c279af040dd05ce21b2a22f7...","t":"22303061363334333330316661653633...","r":"mBbL}QHJ&a(@rwS5n)u^G+Fb+t}Soo-h...","pa":false}],"i":{"t":"users","c":"contact"}}'::jsonb; + +-- Find records where encrypted data is contained by specified values +-- Using search terms (encrypted ahead of time, plaintext not loggable): +SELECT * FROM users +WHERE contact <@ '{"sv":[{"s":"df08a4c4157bdb5bf6fa9be89cf18d10...","t":"22303063343133306135646334356130...","r":"mBbL}QHJ&a(@rwS5n)u^G+Fb+Ex8ofB!...","pa":false}],"i":{"t":"users","c":"contact"}}'::jsonb; +``` + ### Search Terms Response The `createSearchTerms()` method returns a JSON string containing search terms with only the encryption indexes (without the full ciphertext). The response format depends on the configured indexes. @@ -924,9 +961,9 @@ For columns configured with `unique`, `ore`, and/or `match` indexes: { "hm": "f3ca71fd39ae9d3d1d1fc25141bcb6da...", "ob": null, - "bf": null, + "bf": [1124,2134,987,1456,743,2201], "i": { - "t": "patient_records", + "t": "users", "c": "email" } } @@ -939,7 +976,7 @@ Response parameters: | `hm` | `string\|null` | `unique` | HMAC index for exact equality queries and uniqueness constraints | | `ob` | `array\|null` | `ore` | Order-revealing encryption index for range queries | | `bf` | `array\|null` | `match` | Bloom filter index for full-text search queries | -| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | +| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table","c":"column"}` | #### STE Vec Index Response @@ -962,8 +999,8 @@ For columns configured with `ste_vec` indexes: } ], "i": { - "t": "patient_records", - "c": "health_assessment" + "t": "users", + "c": "contact" } } ``` @@ -977,11 +1014,11 @@ Response parameters: | `sv[].t` | `string` | `ste_vec` | Encrypted term value for equality and order-preserving queries | | `sv[].r` | `string` | `ste_vec` | Base85-encoded ciphertext containing the encrypted record data | | `sv[].pa` | `boolean` | `ste_vec` | Whether the parent JSON element is an array | -| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table_name","c":"column_name"}` | +| `i` | `object` | Always | Table and column identifier for this encrypted value: `{"t":"table","c":"column"}` | ## Error Handling -Protect.php FFI operations may throw `FFIException` exceptions when errors occur during encryption, decryption, or client operations. Proper error handling ensures your application can gracefully handle configuration issues, network problems, or invalid data scenarios. +Protect.php FFI operations may throw `FFIException` exceptions when errors occur during client, encryption, or decryption operations. Proper error handling ensures your application can gracefully handle configuration issues, network problems, or invalid data scenarios. ### Exception Types @@ -996,28 +1033,29 @@ $client = new Client; $config = [ 'v' => 2, 'tables' => [ - 'patient_records' => [ + 'users' => [ 'email' => [ 'cast_as' => 'text', 'indexes' => [ 'unique' => (object) [], + 'match' => (object) [], ], ], ], ], ]; +$clientPtr = null; + try { $configJson = json_encode($config, JSON_THROW_ON_ERROR); - $clientPtr = null; - $clientPtr = $client->newClient($configJson); $encryptResultJson = $client->encrypt( client: $clientPtr, plaintext: 'john@example.com', columnName: 'email', - tableName: 'patient_records', + tableName: 'users', ); $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR); From 939f4f2c12c8c98922338c0f8f31bad985955b02 Mon Sep 17 00:00:00 2001 From: coreyhn Date: Tue, 22 Jul 2025 20:19:38 -0600 Subject: [PATCH 4/4] docs(conduct): remove beginning newline --- CODE_OF_CONDUCT.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d0bce95..838f537 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,3 @@ - # Contributor Covenant Code of Conduct ## Our Pledge