{"id":28894912,"url":"https://github.com/cipherstash/protectphp-ffi","last_synced_at":"2026-01-31T08:01:53.437Z","repository":{"id":299396290,"uuid":"999070201","full_name":"cipherstash/protectphp-ffi","owner":"cipherstash","description":"PHP bindings for the CipherStash Client SDK","archived":false,"fork":false,"pushed_at":"2025-07-25T14:45:51.000Z","size":35705,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-10T17:16:15.058Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://cipherstash.com","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cipherstash.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-09T17:34:28.000Z","updated_at":"2025-07-25T14:45:52.000Z","dependencies_parsed_at":null,"dependency_job_id":"066e3645-b398-45ab-b6c7-2d0f17cc1cee","html_url":"https://github.com/cipherstash/protectphp-ffi","commit_stats":null,"previous_names":["cipherstash/protectphp-ffi"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/cipherstash/protectphp-ffi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cipherstash%2Fprotectphp-ffi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cipherstash%2Fprotectphp-ffi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cipherstash%2Fprotectphp-ffi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cipherstash%2Fprotectphp-ffi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cipherstash","download_url":"https://codeload.github.com/cipherstash/protectphp-ffi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cipherstash%2Fprotectphp-ffi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28934613,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-31T07:49:44.436Z","status":"ssl_error","status_checked_at":"2026-01-31T07:49:34.274Z","response_time":128,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-21T04:15:17.402Z","updated_at":"2026-01-31T08:01:53.425Z","avatar_url":"https://github.com/cipherstash.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Protect.php FFI\n\nProtect.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.\n\nThis 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.\n\n\u003e [!IMPORTANT]\n\u003e 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.\n\n## Installation\n\nInstall Protect.php FFI via Composer:\n\n```bash\ncomposer require cipherstash/protectphp-ffi\n```\n\n## Requirements\n\nProtect.php FFI requires PHP 8.1 or higher with the FFI extension (included in most distributions). This library includes prebuilt native libraries for the following platforms:\n\n- macOS: Apple Silicon (ARM64) and Intel (x86_64) processors\n- Linux: x86_64 and ARM64 architectures with GNU libc\n- Windows: x86_64 architecture with MSVC runtime\n\n## Configuration\n\nBefore using Protect.php FFI, you must configure your CipherStash credentials. Set these environment variables in your application:\n\n```bash\nCS_CLIENT_ID=your-client-id\nCS_CLIENT_ACCESS_KEY=your-client-access-key\nCS_CLIENT_KEY=your-client-key\nCS_WORKSPACE_CRN=your-workspace-crn\n```\n\nCredentials 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/).\n\n## Database Setup\n\nProtect.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.\n\nFor advanced querying capabilities (searching, sorting, filtering), you'll need PostgreSQL with the EQL extension. EQL provides the `eql_v2_encrypted` type:\n\n```sql\nCREATE TABLE users (\n    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n    email eql_v2_encrypted,\n    name eql_v2_encrypted,\n    balance eql_v2_encrypted,\n    contact eql_v2_encrypted,\n    notes eql_v2_encrypted,\n    CONSTRAINT unique_email UNIQUE ((email-\u003e\u003e'hm')) -- Enforce unique emails\n);\n```\n\nSee the [EQL installation instructions](https://github.com/cipherstash/encrypt-query-language#installation) to get started.\n\n## Encryption Configuration\n\nThe 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.\n\nBasic structure:\n\n```php\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n            'balance' =\u003e [\n                'cast_as' =\u003e 'int',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'ore' =\u003e (object) [],\n                ],\n            ],\n            'notes' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'match' =\u003e (object) [],\n                ],\n            ],\n            'contact' =\u003e [\n                'cast_as' =\u003e 'jsonb',\n                'indexes' =\u003e [\n                    'ste_vec' =\u003e [\n                        'prefix' =\u003e 'users.contact',\n                    ],\n                ],\n            ],\n        ],\n    ],\n];\n```\n\nConfiguration parameters:\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `v` | `int` | ✓ | Schema version for backward compatibility (must be `2`) |\n| `tables` | `object` | ✓ | Table definitions containing column configurations |\n| `tables.\u003ctable\u003e` | `object` | ✓ | Column definitions for the specified table |\n| `tables.\u003ctable\u003e.\u003ccolumn\u003e` | `object` | ✓ | Configuration for the specified column |\n| `tables.\u003ctable\u003e.\u003ccolumn\u003e.cast_as` | `string` | ✗ | Data type for processing before encryption (defaults to `text`) |\n| `tables.\u003ctable\u003e.\u003ccolumn\u003e.indexes` | `object` | ✗ | Encryption indexes for query patterns |\n| `tables.\u003ctable\u003e.\u003ccolumn\u003e.indexes.\u003cindex_type\u003e` | `object` | ✗ | Configuration parameters for the specified index type (see individual index type documentation) |\n| `tables.\u003ctable\u003e.\u003ccolumn\u003e.indexes.\u003cindex_type\u003e.\u003cparam\u003e` | `mixed` | ✗ | Index-specific configuration parameter |\n\n\u003e [!IMPORTANT]\n\u003e 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.\n\n### Data Types\n\nThe `cast_as` parameter determines how plaintext data is processed before encryption:\n\n| Type | Description | Example Input |\n|------|-------------|---------------|\n| `text` | String data | `john@example.com` |\n| `boolean` | Boolean values | `true` or `false` |\n| `small_int` | 16-bit integer numbers | `32767` |\n| `int` | 32-bit integer numbers | `2147483647` |\n| `big_int` | 64-bit integer numbers | `9223372036854775807` |\n| `real` | Single-precision floating point | `25.99` |\n| `double` | Double-precision floating point | `3.141592653589793` |\n| `date` | Date strings in ISO format | `2020-11-10` |\n| `jsonb` | JSON data | `{\"key\": \"value\"}` |\n\n### Index Types\n\nThe `indexes` parameter determines what queries are supported on encrypted data:\n\n| Index Type | Description | Response Parameter | Supported Queries |\n|------------|-------------|-------------------|------------------|\n| `unique` | Exact equality queries and uniqueness constraints | `hm` | `=` |\n| `ore` | Equality, range comparisons, range queries, and ordering | `ob` | `=`, `\u003e`, `\u003c`, `BETWEEN`, `ORDER BY` |\n| `match` | Full-text search queries | `bf` | `~~` |\n| `ste_vec` | JSONB containment queries | `sv` | `@\u003e`, `\u003c@` |\n\n#### Unique Index (`unique`)\n\nEnables exact equality queries and database uniqueness constraints. Uses the `hm` response parameter to generate HMAC-based hashes for exact equality matching.\n\nBasic usage:\n\n```php\n'users' =\u003e [\n    'email' =\u003e [\n        'cast_as' =\u003e 'text',\n        'indexes' =\u003e [\n            'unique' =\u003e (object) [], // Uses defaults\n        ],\n    ],\n],\n```\n\nConfiguration parameters:\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `token_filters` | `array` | ✗ | `[]` | Text processing filters applied before hashing |\n| `token_filters[].kind` | `string` | ✗ | - | Filter type: `downcase` to convert to lowercase |\n\nWith custom parameters:\n\n```php\n'users' =\u003e [\n    'email' =\u003e [\n        'cast_as' =\u003e 'text',\n        'indexes' =\u003e [\n            'unique' =\u003e [\n                'token_filters' =\u003e [\n                    ['kind' =\u003e 'downcase'],\n                ],\n            ],\n        ],\n    ],\n],\n```\n\nFor database-level uniqueness constraints, add a unique constraint on the `hm` response parameter:\n\n```sql\nCONSTRAINT unique_email UNIQUE ((email-\u003e\u003e'hm'))\n```\n\n#### Order Revealing Encryption Index (`ore`)\n\nEnables 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.\n\nBasic usage:\n\n```php\n'users' =\u003e [\n    'balance' =\u003e [\n        'cast_as' =\u003e 'int',\n        'indexes' =\u003e [\n            'ore' =\u003e (object) [],\n        ],\n    ],\n],\n```\n\nConfiguration parameters:\n\n*This index type has no configurable parameters.*\n\n#### Match Index (`match`)\n\nEnables 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.\n\nBasic usage:\n\n```php\n'users' =\u003e [\n    'notes' =\u003e [\n        'cast_as' =\u003e 'text',\n        'indexes' =\u003e [\n            'match' =\u003e (object) [], // Uses defaults\n        ],\n    ],\n],\n```\n\nConfiguration parameters:\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `tokenizer` | `object` | ✗ | `{\"kind\": \"standard\"}` | Text tokenization method |\n| `tokenizer.kind` | `string` | ✗ | `standard` | Tokenizer type: `standard` or `ngram` |\n| `tokenizer.token_length` | `integer` | ✗ | `3` | Token length for ngram tokenizer |\n| `token_filters` | `array` | ✗ | `[]` | Text processing filters |\n| `token_filters[].kind` | `string` | ✗ | - | Filter type: `downcase` |\n| `k` | `integer` | ✗ | `6` | Hash function count for bloom filter |\n| `m` | `integer` | ✗ | `2048` | Bloom filter size in bits |\n| `include_original` | `boolean` | ✗ | `false` | Include original text in search results |\n\nWith custom parameters:\n\n```php\n'users' =\u003e [\n    'notes' =\u003e [\n        'cast_as' =\u003e 'text',\n        'indexes' =\u003e [\n            'match' =\u003e [\n                'tokenizer' =\u003e [\n                    'kind' =\u003e 'ngram',\n                    'token_length' =\u003e 3,\n                ],\n                'token_filters' =\u003e [\n                    ['kind' =\u003e 'downcase'],\n                ],\n                'k' =\u003e 8,\n                'm' =\u003e 1024,\n                'include_original' =\u003e true,\n            ],\n        ],\n    ],\n],\n```\n\n#### Structured Text Encryption Vector Index (`ste_vec`)\n\nEnables 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.\n\nBasic usage:\n\n```php\n'users' =\u003e [\n    'contact' =\u003e [\n        'cast_as' =\u003e 'jsonb',\n        'indexes' =\u003e [\n            'ste_vec' =\u003e [\n                'prefix' =\u003e 'users.contact',\n            ],\n        ],\n    ],\n],\n```\n\nConfiguration parameters:\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `prefix` | `string` | ✓ | - | Domain separator for cryptographic hashing that must be unique per column (recommended format is `table.column`) |\n\n## Creating a Client\n\nCreate a client instance with your encryption configuration to perform encryption and decryption operations:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                ],\n            ],\n            'balance' =\u003e [\n                'cast_as' =\u003e 'int',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'ore' =\u003e (object) [],\n                ],\n            ],\n            'notes' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'match' =\u003e (object) [],\n                ],\n            ],\n            'contact' =\u003e [\n                'cast_as' =\u003e 'jsonb',\n                'indexes' =\u003e [\n                    'ste_vec' =\u003e [\n                        'prefix' =\u003e 'users.contact',\n                    ],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    // ...\n} finally {\n    // Always cleanup to prevent memory leaks\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\n## Encrypting Data\n\nEncrypt 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $encryptResultJson = $client-\u003eencrypt(\n        client: $clientPtr,\n        plaintext: 'john@example.com',\n        column: 'email',\n        table: 'users',\n    );\n\n    // {\"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}\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\n\u003e [!IMPORTANT]\n\u003e 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.\n\n### Encryption Response\n\nThe `encrypt()` method returns a JSON string containing the encrypted envelope. The response format depends on the configured indexes.\n\n#### Standard Indexes Response\n\nFor columns configured with the `unique`, `ore`, and/or `match` indexes:\n\n```json\n{\n    \"k\": \"ct\",\n    \"c\": \"mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx...\",\n    \"dt\": \"text\",\n    \"hm\": \"f3ca71fd39ae9d3d1d1fc25141bcb6da...\",\n    \"ob\": null,\n    \"bf\": [1124,2134,987,1456,743,2201],\n    \"i\": {\n        \"t\": \"users\",\n        \"c\": \"email\"\n    },\n    \"v\": 2\n}\n```\n\nResponse parameters:\n\n| Parameter | Type | Source | Description |\n|-----------|-----------|--------|-------------|\n| `k` | `string` | Always | Key type identifier (always `ct` for ciphertext) |\n| `c` | `string` | Always | Base85-encoded ciphertext containing the encrypted data |\n| `dt` | `string` | Always | Data type for casting (from `cast_as` configuration parameter) |\n| `hm` | `string\\|null` | `unique` | HMAC index for exact equality queries and uniqueness constraints |\n| `ob` | `array\\|null` | `ore` | Order-revealing encryption index for range queries |\n| `bf` | `array\\|null` | `match` | Bloom filter index for full-text search queries |\n| `i` | `object` | Always | Table and column identifier for this encrypted value: `{\"t\":\"table\",\"c\":\"column\"}` |\n| `v` | `int` | Always | Schema version for backward compatibility |\n\n#### STE Vec Index Response\n\nFor columns configured with the `ste_vec` index:\n\n```json\n{\n    \"k\": \"sv\",\n    \"c\": \"mBbLQ2^Io|1eh_K2*n^LSCVVQuGhkL\u003ew...\",\n    \"dt\": \"jsonb\",\n    \"sv\": [\n        {\n            \"s\": \"dd4659b9c279af040dd05ce21b2a22f7...\",\n            \"t\": \"22303061363334333330316661653633...\",\n            \"r\": \"mBbLQ2^Io|1eh_K2*n^LSCVVQuGhkL\u003ew...\",\n            \"pa\": false\n        }\n    ],\n    \"i\": {\n        \"t\": \"users\",\n        \"c\": \"contact\"\n    },\n    \"v\": 2\n}\n```\n\nResponse parameters:\n\n| Parameter | Type | Source | Description |\n|-----------|------|--------|-------------|\n| `k` | `string` | Always | Key type identifier (always `sv` for structured vector) |\n| `c` | `string` | Always | Base85-encoded ciphertext containing the encrypted data |\n| `dt` | `string` | Always | Data type for casting (from `cast_as` configuration parameter) |\n| `sv` | `array\\|null` | `ste_vec` | Structured text encryption vector for JSONB containment queries |\n| `sv[].s` | `string` | `ste_vec` | Tokenized selector representing the encrypted JSON path to the value |\n| `sv[].t` | `string` | `ste_vec` | Encrypted term value for equality and order-preserving queries |\n| `sv[].r` | `string` | `ste_vec` | Base85-encoded ciphertext containing the encrypted record data |\n| `sv[].pa` | `boolean` | `ste_vec` | Whether the parent JSON element is an array |\n| `i` | `object` | Always | Table and column identifier for this encrypted value: `{\"t\":\"table\",\"c\":\"column\"}` |\n| `v` | `int` | Always | Schema version for backward compatibility |\n\n## Decrypting Data\n\nDecrypt 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $encryptResultJson = $client-\u003eencrypt(\n        client: $clientPtr,\n        plaintext: 'john@example.com',\n        column: 'email',\n        table: 'users',\n    );\n\n    $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR);\n\n    $ciphertext = $encryptResult['c'];\n\n    $decryptResult = $client-\u003edecrypt($clientPtr, $ciphertext); // john@example.com\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\nReturns the decrypted plaintext as a string.\n\n## Encryption Context\n\nProvide 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.\n\n### Context Types\n\nThe `context` parameter determines what contextual authentication is supported:\n\n| Context Type | Supported Index Types | Description |\n|--------------|----------------------|-------------|\n| `identity_claim` | `unique`, `ore`, `match` | Identity-aware encryption using JWT claims (requires CTS authentication) |\n| `tag` | `unique`, `ore`, `match` | Label-aware encryption using string tags |\n| `value` | `unique`, `ore`, `match` | Attribute-aware encryption using key-value pairs |\n\n\u003e [!IMPORTANT]\n\u003e Encryption context is not supported with `ste_vec` indexes and will cause decryption to fail.\n\n### Identity Claim Context\n\nIdentity 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.\n\nIdentity 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.\n\n### Tag Context\n\nTag 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $context = [\n        'tag' =\u003e ['pii', 'hipaa'],\n    ];\n\n    $contextJson = json_encode($context, JSON_THROW_ON_ERROR);\n\n    $encryptResultJson = $client-\u003eencrypt(\n        client: $clientPtr,\n        plaintext: 'john@example.com',\n        column: 'email',\n        table: 'users',\n        contextJson: $contextJson,\n    );\n\n    $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR);\n\n    $ciphertext = $encryptResult['c'];\n\n    $decryptResult = $client-\u003edecrypt($clientPtr, $ciphertext, $contextJson); // john@example.com\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\n### Value Context\n\nValue 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $context = [\n        'value' =\u003e [\n            ['key' =\u003e 'tenant_id', 'value' =\u003e 'tenant_2ynTJf38e9HvuAO8jaX5kAyVaKI'],\n            ['key' =\u003e 'role', 'value' =\u003e 'admin'],\n        ],\n    ];\n\n    $contextJson = json_encode($context, JSON_THROW_ON_ERROR);\n\n    $encryptResultJson = $client-\u003eencrypt(\n        client: $clientPtr,\n        plaintext: 'john@example.com',\n        column: 'email',\n        table: 'users',\n        contextJson: $contextJson,\n    );\n\n    $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR);\n\n    $ciphertext = $encryptResult['c'];\n\n    $decryptResult = $client-\u003edecrypt($clientPtr, $ciphertext, $contextJson); // john@example.com\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\n\u003e [!WARNING]\n\u003e You must use the same context for both encryption and decryption operations. Wrong contexts will result in decryption failures.\n\n## Bulk Operations\n\nFor improved performance when handling multiple records, use bulk encryption and decryption operations:\n\n### Bulk Encryption\n\nEncrypt 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n            'notes' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $items = [\n        [\n            'plaintext' =\u003e 'john@example.com',\n            'column' =\u003e 'email',\n            'table' =\u003e 'users',\n        ],\n        [\n            'plaintext' =\u003e 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.',\n            'column' =\u003e 'notes',\n            'table' =\u003e 'users',\n        ],\n    ];\n\n    $itemsJson = json_encode($items, JSON_THROW_ON_ERROR);\n    $encryptResultsJson = $client-\u003eencryptBulk($clientPtr, $itemsJson);\n    // [{\"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\u003c8tOEI+Z`KFUV`q\u0026kmdWtO#DKxW|...\",\"dt\":\"text\",\"hm\":null,\"ob\":null,\"bf\":[1397,378,1463,1673,1474,1226],\"i\":{\"t\":\"users\",\"c\":\"notes\"},\"v\":2}]\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\nReturns a JSON array of encrypted envelopes where each element follows the same structure as documented in the [Encryption Response](#encryption-response) section.\n\n### Bulk Decryption\n\nDecrypt 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n            'notes' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $items = [\n        [\n            'plaintext' =\u003e 'john@example.com',\n            'column' =\u003e 'email',\n            'table' =\u003e 'users',\n            'context' =\u003e [\n                'tag' =\u003e ['pii', 'hipaa'],\n            ],\n        ],\n        [\n            'plaintext' =\u003e 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.',\n            'column' =\u003e 'notes',\n            'table' =\u003e 'users',\n        ],\n    ];\n\n    $itemsJson = json_encode($items, JSON_THROW_ON_ERROR);\n    $encryptResultsJson = $client-\u003eencryptBulk($clientPtr, $itemsJson);\n    $encryptResults = json_decode(json: $encryptResultsJson, associative: true, flags: JSON_THROW_ON_ERROR);\n\n    $decryptItems = array_map(function ($item, $encryptResult) {\n        $decryptItem = ['ciphertext' =\u003e $encryptResult['c']];\n\n        if (isset($item['context'])) {\n            $decryptItem['context'] = $item['context'];\n        }\n\n        return $decryptItem;\n    }, $items, $encryptResults);\n\n    $decryptItemsJson = json_encode($decryptItems, JSON_THROW_ON_ERROR);\n    // [{\"ciphertext\":\"mBbK\u003eBcAYctW$Gy)vK2)Y$\u0026nBBKz{oL1...\",\"context\":{\"tag\":[\"pii\",\"hipaa\"]}},{\"ciphertext\":\"mBbJ\u003c8tOEI+Z`KFUV`q\u0026kmdWtO#DKxW|...\"}]\n\n    $decryptResultsJson = $client-\u003edecryptBulk($clientPtr, $decryptItemsJson);\n    // [\"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.\"]\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\nReturns a JSON array of decrypted plaintext strings in the same order as the input JSON array.\n\n## Searchable Encryption\n\nCreate 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:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n            'balance' =\u003e [\n                'cast_as' =\u003e 'int',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'ore' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $items = [\n        [\n            'plaintext' =\u003e 'john@example.com',\n            'column' =\u003e 'email',\n            'table' =\u003e 'users',\n            'context' =\u003e [\n                'tag' =\u003e ['pii', 'hipaa'],\n            ],\n        ],\n        [\n            'plaintext' =\u003e '1575000',\n            'column' =\u003e 'balance',\n            'table' =\u003e 'users',\n        ],\n    ];\n\n    $itemsJson = json_encode($items, JSON_THROW_ON_ERROR);\n    $searchTermResultsJson = $client-\u003ecreateSearchTerms($clientPtr, $itemsJson);\n    // [{\"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\"}}]\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\nThis feature integrates with [EQL](https://github.com/cipherstash/encrypt-query-language) and is currently only supported on PostgreSQL databases.\n\n### Querying with Search Terms\n\nThese 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.\n\n#### Exact Equality Queries\n\nFor exact equality queries, EQL uses the `unique` index (`hm` response parameter) from your search terms:\n\n```sql\n-- Find user by email address\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE email = '{\"hm\":\"f3ca71fd39ae9d3d1d1fc25141bcb6da...\",\"ob\":null,\"bf\":[1124,2134,987,1456,743,2201],\"i\":{\"t\":\"users\",\"c\":\"email\"}}'::jsonb;\n```\n\n#### Equality, Range, and Sorting Queries\n\nFor equality, range comparisons, and sorting, EQL uses the `ore` index (`ob` response parameter) from your search terms:\n\n```sql\n-- Find users with exact balance amount\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE balance = '{\"hm\":\"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...\",\"ob\":[\"99f7adadadadadadc68b2822197a849e...\"],\"bf\":null,\"i\":{\"t\":\"users\",\"c\":\"balance\"}}'::jsonb;\n\n-- Find users above specified balance\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE balance \u003e= '{\"hm\":\"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...\",\"ob\":[\"99f7adadadadadadc68b2822197a849e...\"],\"bf\":null,\"i\":{\"t\":\"users\",\"c\":\"balance\"}}'::jsonb;\n\n-- Find users with balance in specified range\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE balance BETWEEN\n      '{\"hm\":\"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...\",\"ob\":[\"99f7adadadadadadc68b2822197a849e...\"],\"bf\":null,\"i\":{\"t\":\"users\",\"c\":\"balance\"}}'::jsonb\n  AND '{\"hm\":\"a8d5f2e9c4b7a1f3e8d2c5b9f6a3e7d1...\",\"ob\":[\"99f7adadadadadadc68b2822197a849e...\"],\"bf\":null,\"i\":{\"t\":\"users\",\"c\":\"balance\"}}'::jsonb;\n\n-- Order users by balance from lowest to highest\nSELECT * FROM users\nORDER BY balance ASC;\n\n-- Order users by balance from highest to lowest\nSELECT * FROM users\nORDER BY balance DESC;\n```\n\n#### Full-Text Search Queries\n\nFor searching within text content, EQL uses the `match` index (`bf` response parameter) from your search terms:\n\n```sql\n-- Find users with notes containing specified terms\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE notes ~~ '{\"hm\":null,\"ob\":null,\"bf\":[1397,378,1463,1673,1474,1226],\"i\":{\"t\":\"users\",\"c\":\"notes\"}}'::jsonb;\n```\n\n#### JSONB Containment Queries\n\nFor structured data queries, EQL uses the `ste_vec` index (`sv` response parameter) from your search terms:\n\n```sql\n-- Find records where encrypted data contains specified values\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE contact @\u003e '{\"sv\":[{\"s\":\"dd4659b9c279af040dd05ce21b2a22f7...\",\"t\":\"22303061363334333330316661653633...\",\"r\":\"mBbL}QHJ\u0026a(@rwS5n)u^G+Fb+t}Soo-h...\",\"pa\":false}],\"i\":{\"t\":\"users\",\"c\":\"contact\"}}'::jsonb;\n\n-- Find records where encrypted data is contained by specified values\n-- Using search terms (encrypted ahead of time, plaintext not loggable):\nSELECT * FROM users\nWHERE contact \u003c@ '{\"sv\":[{\"s\":\"df08a4c4157bdb5bf6fa9be89cf18d10...\",\"t\":\"22303063343133306135646334356130...\",\"r\":\"mBbL}QHJ\u0026a(@rwS5n)u^G+Fb+Ex8ofB!...\",\"pa\":false}],\"i\":{\"t\":\"users\",\"c\":\"contact\"}}'::jsonb;\n```\n\n### Search Terms Response\n\nThe `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.\n\n#### Standard Indexes Response\n\nFor columns configured with `unique`, `ore`, and/or `match` indexes:\n\n```json\n{\n    \"hm\": \"f3ca71fd39ae9d3d1d1fc25141bcb6da...\",\n    \"ob\": null,\n    \"bf\": [1124,2134,987,1456,743,2201],\n    \"i\": {\n        \"t\": \"users\",\n        \"c\": \"email\"\n    }\n}\n```\n\nResponse parameters:\n\n| Parameter | Type | Source | Description |\n|-----------|------|--------|-------------|\n| `hm` | `string\\|null` | `unique` | HMAC index for exact equality queries and uniqueness constraints |\n| `ob` | `array\\|null` | `ore` | Order-revealing encryption index for range queries |\n| `bf` | `array\\|null` | `match` | Bloom filter index for full-text search queries |\n| `i` | `object` | Always | Table and column identifier for this encrypted value: `{\"t\":\"table\",\"c\":\"column\"}` |\n\n#### STE Vec Index Response\n\nFor columns configured with `ste_vec` indexes:\n\n```json\n{\n    \"sv\": [\n        {\n            \"s\": \"dd4659b9c279af040dd05ce21b2a22f7...\",\n            \"t\": \"22303061363334333330316661653633...\",\n            \"r\": \"mBbLkCZcaJ2U|G333rRC\u003ef;r}uFEp7Tg...\",\n            \"pa\": false\n        },\n        {\n            \"s\": \"df08a4c4157bdb5bf6fa9be89cf18d10...\",\n            \"t\": \"22303063343133306135646334356130...\",\n            \"r\": \"mBbLkCZcaJ2U|G333rRC\u003ef;r}E\u0026d@?`;...\",\n            \"pa\": false\n        }\n    ],\n    \"i\": {\n        \"t\": \"users\",\n        \"c\": \"contact\"\n    }\n}\n```\n\nResponse parameters:\n\n| Parameter | Type | Source | Description |\n|-----------|------|--------|-------------|\n| `sv` | `array\\|null` | `ste_vec` | Structured text encryption vector for JSONB containment queries |\n| `sv[].s` | `string` | `ste_vec` | Tokenized selector representing the encrypted JSON path to the value |\n| `sv[].t` | `string` | `ste_vec` | Encrypted term value for equality and order-preserving queries |\n| `sv[].r` | `string` | `ste_vec` | Base85-encoded ciphertext containing the encrypted record data |\n| `sv[].pa` | `boolean` | `ste_vec` | Whether the parent JSON element is an array |\n| `i` | `object` | Always | Table and column identifier for this encrypted value: `{\"t\":\"table\",\"c\":\"column\"}` |\n\n## Error Handling\n\nProtect.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.\n\n### Exception Types\n\nAll FFI operations throw `FFIException` exceptions that contain descriptive error messages:\n\n```php\nuse CipherStash\\Protect\\FFI\\Client;\nuse CipherStash\\Protect\\FFI\\Exceptions\\FFIException;\n\n$client = new Client;\n\n$config = [\n    'v' =\u003e 2,\n    'tables' =\u003e [\n        'users' =\u003e [\n            'email' =\u003e [\n                'cast_as' =\u003e 'text',\n                'indexes' =\u003e [\n                    'unique' =\u003e (object) [],\n                    'match' =\u003e (object) [],\n                ],\n            ],\n        ],\n    ],\n];\n\n$clientPtr = null;\n\ntry {\n    $configJson = json_encode($config, JSON_THROW_ON_ERROR);\n    $clientPtr = $client-\u003enewClient($configJson);\n\n    $encryptResultJson = $client-\u003eencrypt(\n        client: $clientPtr,\n        plaintext: 'john@example.com',\n        column: 'email',\n        table: 'users',\n    );\n\n    $encryptResult = json_decode(json: $encryptResultJson, associative: true, flags: JSON_THROW_ON_ERROR);\n    $ciphertext = $encryptResult['c']; // mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx...\n} catch (FFIException $e) {\n    // Handle FFI errors\n    // ...\n} finally {\n    if ($clientPtr !== null) {\n        $client-\u003efreeClient($clientPtr);\n    }\n}\n```\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcipherstash%2Fprotectphp-ffi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcipherstash%2Fprotectphp-ffi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcipherstash%2Fprotectphp-ffi/lists"}