Skip to content

cipherstash/protectphp-ffi

Repository files navigation

Protect.php FFI

Protect.php FFI provides PHP bindings for the CipherStash Client SDK via PHP's Foreign Function Interface (FFI).

Field-level encryption operations happen directly in your application with a unique key for each encrypted value, managed by CipherStash ZeroKMS and backed by AWS KMS. The encrypted data can be stored in any database that supports JSONB.

Important

For most applications, you'll want to use the Protect.php library instead, as it provides a more convenient API built on top of these bindings.

Installation

Install Protect.php FFI via Composer:

composer require cipherstash/protectphp-ffi

Requirements

Protect.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:

  • macOS: Apple Silicon (ARM64) and Intel (x86_64) processors
  • Linux: x86_64 and ARM64 architectures with GNU libc
  • Windows: x86_64 architecture with MSVC runtime

Configuration

Before using Protect.php FFI, you must configure your CipherStash credentials. Set these values in your application's environment variables:

CS_CLIENT_ID=your-client-id
CS_CLIENT_ACCESS_KEY=your-client-access-key
CS_CLIENT_KEY=your-client-key
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 or CipherStash Dashboard.

Basic Usage

Database Setup

Protect.php FFI works with any database that supports JSONB storage. The encrypted data is structured as an Encrypt Query Language (EQL) JSON payload.

For advanced querying capabilities (searching, sorting, filtering), you'll need PostgreSQL with EQL support. EQL provides the eql_v2_encrypted type:

CREATE TABLE patient_records (
    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,
    CONSTRAINT unique_email UNIQUE ((email->>'hm')) -- Enforce unique emails
);

See the EQL installation instructions to get started.

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.

Basic structure:

$config = [
    'v' => 2,
    'tables' => [
        'table_name' => [
            'column_name' => [
                'cast_as' => 'data_type',
                'indexes' => [
                    'unique' => [
                        'token_filters' => [
                            ['kind' => 'downcase'],
                        ],
                    ],
                    'match' => (object) [],
                ],
            ],
        ],
    ],
];

Configuration parameters:

Parameter Type Required Description
v int âś“ Schema version for backward compatibility (must be 2)
tables object âś“ Table definitions containing column configurations
tables.<table_name> object âś“ Column definitions for the specified table
tables.<table_name>.<column_name> object âś“ Configuration for the specified column
tables.<table_name>.<column_name>.cast_as string âś— Data type for processing before encryption (defaults to text)
tables.<table_name>.<column_name>.indexes object âś— Encryption indexes for query patterns
tables.<table_name>.<column_name>.indexes.<index_type> object âś— Configuration parameters for the specified index type (see individual index type documentation)
tables.<table_name>.<column_name>.indexes.<index_type>.<param> 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

The cast_as field determines how plaintext data is processed before encryption:

Type Description Example Input
text String data john@example.com
boolean Boolean values true or false
small_int 16-bit integer numbers 32767
int 32-bit integer numbers 29
big_int 64-bit integer numbers 9223372036854775807
real Single-precision floating point 25.99
double Double-precision floating point 3.141592653589793
date Date strings in ISO format 2020-11-10
jsonb JSON data {"key": "value"}

Index Types

The indexes field determines what operations are supported on encrypted data:

Index Type Use Case Response Field Plaintext Queries Search Terms Queries
unique Exact equality queries and uniqueness constraints hm eql_v2.hmac_256() eql_v2.hmac_256()
ore Equality, range comparisons, range queries, and ordering ob eql_v2.ore_block_u64_8_256() eql_v2.ore_block_u64_8_256()
match Full-text search queries bf eql_v2.bloom_filter() eql_v2.bloom_filter()
ste_vec JSONB containment queries sv cs_ste_vec_v2() @>, <@

Unique Index (unique)

Enables exact equality queries and database uniqueness constraints. Uses the hm response field and works with the eql_v2.hmac_256() EQL function. This index generates HMAC-based hashes for exact equality matching.

Basic usage:

'patient_records' => [
    'email' => [
        'cast_as' => 'text',
        'indexes' => [
            'unique' => (object) [], // Uses defaults
        ],
    ],
],

Configuration parameters:

Parameter Type Required Default Description
token_filters array âś— [] Text processing filters applied before hashing
token_filters[].kind string âś— - Filter type: downcase to convert to lowercase

With custom parameters:

'patient_records' => [
    'email' => [
        'cast_as' => 'text',
        'indexes' => [
            'unique' => [
                'token_filters' => [
                    ['kind' => 'downcase'],
                ],
            ],
        ],
    ],
],

Example SQL queries:

-- Find patient record by email address
-- Using plaintext (encrypted in real-time, plaintext loggable):
SELECT * FROM patient_records 
WHERE eql_v2.hmac_256(email) = eql_v2.hmac_256(
  '{"k":"pt","p":"john@example.com","q":"hmac_256","i":{"t":"patient_records","c":"email"},"v":2}'
);

-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM patient_records 
WHERE eql_v2.hmac_256(email) = eql_v2.hmac_256(
  '{"hm":"0f4f3b99671e74c0f8b5a1d2e3f4...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"}}'
);

For database-level uniqueness constraints, add a unique constraint on the hm response field:

CONSTRAINT unique_email UNIQUE ((email->>'hm'))

Order Revealing Encryption Index (ore)

Enables equality, range operations, and ordering on encrypted data. Uses the ob response field and works with the eql_v2.ore_block_u64_8_256() EQL function. This index creates order-preserving encrypted values for equality checks, range comparisons, range queries, and sorting operations.

Basic usage:

'patient_records' => [
    'systolic_bp' => [
        'cast_as' => 'int',
        'indexes' => [
            'ore' => (object) [],
        ],
    ],
],

Configuration parameters:

This index type has no configurable parameters.

Example SQL queries:

-- Find patients with exact blood pressure value
-- Using plaintext (encrypted in real-time, plaintext loggable):
SELECT * FROM patient_records 
WHERE eql_v2.ore_block_u64_8_256(systolic_bp) = eql_v2.ore_block_u64_8_256(
  '{"k":"pt","p":"120","q":"ore","i":{"t":"patient_records","c":"systolic_bp"},"v":2}'
);

-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM patient_records 
WHERE eql_v2.ore_block_u64_8_256(systolic_bp) = eql_v2.ore_block_u64_8_256(
  '{"hm":null,"ob":["0x1a2b3c..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}'
);

-- Find patients with blood pressure above specified threshold
-- Using plaintext (encrypted in real-time, plaintext loggable):
SELECT * FROM patient_records 
WHERE eql_v2.ore_block_u64_8_256(systolic_bp) >= eql_v2.ore_block_u64_8_256(
  '{"k":"pt","p":"140","q":"ore","i":{"t":"patient_records","c":"systolic_bp"},"v":2}'
);

-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM patient_records 
WHERE eql_v2.ore_block_u64_8_256(systolic_bp) >= eql_v2.ore_block_u64_8_256(
  '{"hm":null,"ob":["0x1a2b3c..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}'
);

-- Find patients with blood pressure in specified range
-- Using plaintext (encrypted in real-time, plaintext loggable):
SELECT * FROM patient_records 
WHERE eql_v2.ore_block_u64_8_256(systolic_bp) BETWEEN 
      eql_v2.ore_block_u64_8_256('{"k":"pt","p":"90","q":"ore","i":{"t":"patient_records","c":"systolic_bp"},"v":2}') 
  AND eql_v2.ore_block_u64_8_256('{"k":"pt","p":"120","q":"ore","i":{"t":"patient_records","c":"systolic_bp"},"v":2}');

-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM patient_records 
WHERE eql_v2.ore_block_u64_8_256(systolic_bp) BETWEEN 
      eql_v2.ore_block_u64_8_256('{"hm":null,"ob":["0x1f5e2d..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}') 
  AND eql_v2.ore_block_u64_8_256('{"hm":null,"ob":["0x9c8b7a..."],"bf":null,"i":{"t":"patient_records","c":"systolic_bp"}}');

-- Order patients by blood pressure from lowest to highest
SELECT * FROM patient_records 
ORDER BY eql_v2.ore_block_u64_8_256(systolic_bp) ASC;

-- Order patients by blood pressure from highest to lowest
SELECT * FROM patient_records 
ORDER BY eql_v2.ore_block_u64_8_256(systolic_bp) DESC;

Match Index (match)

Enables full-text search on encrypted text data using bloom filters. Uses the bf response field and works with the eql_v2.bloom_filter() EQL function. This index creates bloom filter representations of tokenized text for probabilistic matching.

Basic usage:

'patient_records' => [
    'medical_notes' => [
        'cast_as' => 'text',
        'indexes' => [
            'match' => (object) [], // Uses defaults
        ],
    ],
],

Configuration parameters:

Parameter Type Required Default Description
tokenizer object âś— {"kind": "standard"} Text tokenization method
tokenizer.kind string âś— standard Tokenizer type: standard or ngram
tokenizer.token_length integer âś— 3 Token length for ngram tokenizer
token_filters array âś— [] Text processing filters
token_filters[].kind string âś— - Filter type: downcase
k integer âś— 6 Hash function count for bloom filter
m integer âś— 2048 Bloom filter size in bits
include_original boolean âś— false Include original text in search results

With custom parameters:

'patient_records' => [
    'medical_notes' => [
        'cast_as' => 'text',
        'indexes' => [
            'match' => [
                'tokenizer' => [
                    'kind' => 'ngram',
                    'token_length' => 3,
                ],
                'token_filters' => [
                    ['kind' => 'downcase'],
                ],
                'k' => 8,
                'm' => 1024,
                'include_original' => true,
            ],
        ],
    ],
],

Example SQL queries:

-- Find patients with medical notes containing specified terms
-- Using plaintext (encrypted in real-time, plaintext loggable):
SELECT * FROM patient_records 
WHERE eql_v2.bloom_filter(medical_notes) @> eql_v2.bloom_filter(
  '{"k":"pt","p":"diabetes","q":"match","i":{"t":"patient_records","c":"medical_notes"},"v":2}'
);

-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM patient_records 
WHERE eql_v2.bloom_filter(medical_notes) @> eql_v2.bloom_filter(
  '{"hm":null,"ob":null,"bf":[142,891,1337,1847,2001],"i":{"t":"patient_records","c":"medical_notes"}}'
);

Structured Text Encryption Vector Index (ste_vec)

Enables containment queries on encrypted JSONB data. Uses the sv response field and works with the cs_ste_vec_v2() EQL function for plaintext queries and PostgreSQL containment operators (@>, <@) for search terms queries. This index creates structured text encryption vectors that preserve JSON path relationships for encrypted JSONB containment matching.

Basic usage:

'patient_records' => [
    'health_assessment' => [
        'cast_as' => 'jsonb',
        'indexes' => [
            'ste_vec' => [
                'prefix' => 'patient_records/health_assessment',
            ],
        ],
    ],
],

Configuration parameters:

Parameter Type Required Default Description
prefix string âś“ - Unique identifier prefix for the encryption context (recommended format is table_name/column_name)

Example SQL queries:

-- Find records where encrypted data contains specified values
-- Using plaintext (encrypted in real-time, plaintext loggable):
SELECT * FROM patient_records 
WHERE cs_ste_vec_v2(health_assessment, '{"conditions":["diabetes","hypertension"],"severity":"moderate","lab_results":{"cholesterol":{"total":180,"hdl":45},"glucose":95},"visit_date":"2025-02-04"}');

-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM patient_records 
WHERE health_assessment @> '{"sv":[{"tokenized_selector":"dd4659b9c279af040dd05ce21b2a22f7","term":"00a6343301fae638379a8b1f9147eda082","record":"mBbLOIqSF%n4>5ajY+w?-+!*eKqJt(|G8c0rEaxnXm!MLTLGT0tuse(H;lHjz!hWQpW&^_vF3;xdm^M%l{vX7mB05%=#-7DSapsQ$y(uxxWphCxN}>hI__Q00^;tc;bvpcK_`<{cx)595mX{~O#Z^4zy","parent_is_array":false}],"i":{"t":"patient_records","c":"health_assessment"}}';

-- 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":[{"tokenized_selector":"cff40d3394bcb913237661f679280999","term":"022d3a7feb298b2d93b9f3a2cd0c0bebf8c524b8a991f9eedfbfe52477fe7b3817de6ae2fec499e5b3e7b0a5daefc88ea45923e2cc5c6658c18f477f7eb6542106","record":"mBbLOIqSF%n4>5ajY+w?-+!*e9B5XGJ%B#Hphr%zC1ge&;+sJ+zW5p~UC^A%;KU#qxN}>hI__Q00^;tc;bvpcK_`<{cx)595mX{~O#Z^4zy","parent_is_array":false}],"i":{"t":"patient_records","c":"health_assessment"}}';

This index differs from other indexes in its query patterns. Plaintext queries use cs_ste_vec_v2() with JSON data and only support the PostgreSQL @> operator, while search term queries can use both PostgreSQL @> and <@ operators with pre-computed vectors from the sv response field in the search terms response.

Creating a Client

Create a client instance with your encryption configuration to perform encryption and decryption operations:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
            'systolic_bp' => [
                'cast_as' => 'int',
                'indexes' => [
                    'ore' => (object) [],
                ],
            ],
            'medical_notes' => [
                'cast_as' => 'text',
                'indexes' => [
                    'match' => (object) [],
                ],
            ],
            'health_assessment' => [
                'cast_as' => 'jsonb',
                'indexes' => [
                    'ste_vec' => [
                        'prefix' => 'patient_records/health_assessment',
                    ],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    // Your encryption operations go here...
} finally {
    // Always cleanup to prevent memory leaks
    $client->freeClient($clientPtr);
}

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:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $resultJson = $client->encrypt(
        client: $clientPtr,
        plaintext: 'john@example.com',
        columnName: 'email',
        tableName: 'patient_records',
    );

    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);

    $ciphertext = $result['c'];

    echo $ciphertext;
    // mBbM8rvts7^sycKCI!-Y9x2kL8vN...
} finally {
    $client->freeClient($clientPtr);
}

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

The encrypt() method returns a JSON string containing the encrypted data. The response format depends on the configured indexes.

Standard Indexes Response

For columns configured with the unique, ore, and/or match indexes:

{
    "k": "ct",
    "c": "mBbM8rvts7^sycKCI!-Y9x2kL8vN...",
    "hm": "0f4f3b99671e74c0f8b5a1d2e3f4...",
    "ob": null,
    "bf": null,
    "i": {
        "t": "patient_records",
        "c": "email"
    },
    "v": 2
}

Response parameters:

Parameter Type Source Description
k string Always Key type identifier (always ct for ciphertext)
c string Always Base85-encoded ciphertext containing the encrypted data
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"}
v int Always Schema version for backward compatibility

STE Vec Index Response

For columns configured with the ste_vec index:

{
    "k": "sv",
    "c": "mBbKND$(wyS}0*#KjqS!Is$dX...",
    "sv": [
        {
            "tokenized_selector": "dd4659b9c279af040dd05ce21b2a22f7",
            "term": "00a6343301fae638379a8b1f9147eda082",
            "record": "mBbKND$(wyS}0*#KjqS!Is$dX...",
            "parent_is_array": false
        }
    ],
    "i": {
        "t": "patient_records",
        "c": "health_assessment"
    },
    "v": 2
}

Response parameters:

Parameter Type Source Description
k string Always Key type identifier (always sv for structured vector)
c string Always Base85-encoded ciphertext containing the encrypted data
sv array ste_vec Structured text encryption vector for JSONB containment queries
sv[].tokenized_selector string ste_vec Encrypted selector for the JSON path
sv[].term string ste_vec Encrypted term value
sv[].record string ste_vec Base85-encoded encrypted record data
sv[].parent_is_array 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"}
v int Always Schema version for backward compatibility

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:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $resultJson = $client->encrypt(
        client: $clientPtr,
        plaintext: 'john@example.com',
        columnName: 'email',
        tableName: 'patient_records',
    );

    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);

    $ciphertext = $result['c'];

    $plaintext = $client->decrypt($clientPtr, $ciphertext);

    echo $plaintext;
    // john@example.com
} finally {
    $client->freeClient($clientPtr);
}

Returns the decrypted plaintext as a string.

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.

Protect.php FFI supports three types of encryption 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) 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 library for this type of encryption 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:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $context = [
        'tag' => ['pii', 'hipaa'],
    ];

    $contextJson = json_encode($context, JSON_THROW_ON_ERROR);

    $resultJson = $client->encrypt(
        client: $clientPtr,
        plaintext: 'john@example.com',
        columnName: 'email',
        tableName: 'patient_records',
        contextJson: $contextJson,
    );

    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);

    $ciphertext = $result['c'];

    $plaintext = $client->decrypt($clientPtr, $ciphertext, $contextJson);

    echo $plaintext;
    // john@example.com
} finally {
    $client->freeClient($clientPtr);
}

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:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $context = [
        'value' => [
            ['key' => 'tenant_id', 'value' => 'tenant_2ynTJf38e9HvuAO8jaX5kAyVaKI'],
            ['key' => 'role', 'value' => 'admin'],
        ],
    ];

    $contextJson = json_encode($context, JSON_THROW_ON_ERROR);

    $resultJson = $client->encrypt(
        client: $clientPtr,
        plaintext: 'john@example.com',
        columnName: 'email',
        tableName: 'patient_records',
        contextJson: $contextJson,
    );

    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);

    $ciphertext = $result['c'];

    $plaintext = $client->decrypt($clientPtr, $ciphertext, $contextJson);

    echo $plaintext;
    // john@example.com
} finally {
    $client->freeClient($clientPtr);
}

Warning

You must use the same context for both encryption and decryption operations. Wrong contexts will result in decryption failures.

Bulk Operations

For improved performance when handling multiple records, use bulk encryption and decryption operations:

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, and table for encryption:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
            'medical_notes' => [
                'cast_as' => 'text',
                'indexes' => [
                    'match' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $items = [
        [
            'plaintext' => 'john@example.com',
            'column' => 'email',
            'table' => 'patient_records',
        ],
        [
            'plaintext' => 'Patient shows improvement in mobility and pain management.',
            'column' => 'medical_notes',
            'table' => 'patient_records',
        ],
    ];

    $itemsJson = json_encode($items, JSON_THROW_ON_ERROR);
    $resultJson = $client->encryptBulk($clientPtr, $itemsJson);

    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);

    foreach ($result as $encryptedData) {
        $ciphertext = $encryptedData['c'];

        echo $ciphertext;
        // mBbM8rvts7^sycKCI!-Y9x2kL8vN...
    }
} finally {
    $client->freeClient($clientPtr);
}

Returns a JSON array where each element follows the same structure as documented in the Encryption Response section.

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 key with the base85-encoded ciphertext string:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
            'medical_notes' => [
                'cast_as' => 'text',
                'indexes' => [
                    'match' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $items = [
        [
            'plaintext' => 'john@example.com',
            'column' => 'email',
            'table' => 'patient_records',
        ],
        [
            'plaintext' => 'Patient shows improvement in mobility and pain management.',
            'column' => 'medical_notes',
            'table' => 'patient_records',
        ],
    ];

    $itemsJson = json_encode($items, JSON_THROW_ON_ERROR);
    $resultJson = $client->encryptBulk($clientPtr, $itemsJson);
    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);
    $ciphertexts = array_column($result, 'c');

    $ciphertextItems = array_map(function ($ciphertext) {
        return ['ciphertext' => $ciphertext];
    }, $ciphertexts);

    $ciphertextItemsJson = json_encode($ciphertextItems, JSON_THROW_ON_ERROR);

    echo $ciphertextItemsJson;
    // [{"ciphertext":"mBbM8rvts7^sycKCI!-Y9x2kL8vN..."},{"ciphertext":"nCcN9swus8^tzdLDJ!-Z0y3lM9wO..."}]

    $decryptedResultJson = $client->decryptBulk($clientPtr, $ciphertextItemsJson);

    echo $decryptedResultJson;
    // ["john@example.com","Patient shows improvement in mobility and pain management."]

    $plaintexts = json_decode(json: $decryptedResultJson, associative: true, flags: JSON_THROW_ON_ERROR);
} finally {
    $client->freeClient($clientPtr);
}

Returns a JSON array of decrypted plaintext strings in the same order as the input JSON array.

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, and table for generating search terms:

use CipherStash\Protect\FFI\Client;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
            'systolic_bp' => [
                'cast_as' => 'int',
                'indexes' => [
                    'ore' => (object) [],
                ],
            ],
        ],
    ],
];

$configJson = json_encode($config, JSON_THROW_ON_ERROR);
$clientPtr = $client->newClient($configJson);

try {
    $terms = [
        [
            'plaintext' => 'john@example.com',
            'column' => 'email',
            'table' => 'patient_records',
        ],
        [
            'plaintext' => '120',
            'column' => 'systolic_bp',
            'table' => 'patient_records',
        ],
    ];

    $termsJson = json_encode($terms, JSON_THROW_ON_ERROR);
    $resultJson = $client->createSearchTerms($clientPtr, $termsJson);
    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);

    foreach ($result as $searchTerms) {
        echo json_encode($searchTerms);
        // {"hm":"0f4f3b99671e74c0f8b5a1d2e3f4...","ob":null,"bf":null,"i":{"t":"patient_records","c":"email"}}
    }
} finally {
    $client->freeClient($clientPtr);
}

This functionality integrates with EQL and is currently only supported on PostgreSQL databases.

Search Terms Response

The createSearchTerms() method returns a JSON string containing search terms with only the searchable indexes (without the full ciphertext). The response format depends on the configured indexes.

Standard Indexes Response

For columns configured with unique, ore, and/or match indexes:

{
    "hm": "0f4f3b99671e74c0f8b5a1d2e3f4...",
    "ob": null,
    "bf": null,
    "i": {
        "t": "patient_records",
        "c": "email"
    }
}

Response parameters:

Parameter Type Source Description
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"}

STE Vec Index Response

For columns configured with ste_vec indexes:

{
    "sv": [
        {
            "tokenized_selector": "dd4659b9c279af040dd05ce21b2a22f7",
            "term": "00a6343301fae638379a8b1f9147eda082",
            "record": "mBbM0GYe4Wa7OJ<2HG_ZQ42Z5KmmLn7{+K)z~e9h*+$l...",
            "parent_is_array": false
        },
        {
            "tokenized_selector": "cff40d3394bcb913237661f679280999",
            "term": "022d3a7feb298b2d93b9f3a2cd0c0bebf8c524b8a991...",
            "record": "mBbM0GYe4Wa7OJ<2HG_ZQ42Z59Mj-WD;uRkcn7ZHj&a4...",
            "parent_is_array": false
        }
    ],
    "i": {
        "t": "patient_records",
        "c": "health_assessment"
    }
}

Response parameters:

Parameter Type Source Description
sv array ste_vec Structured text encryption vector for JSONB containment queries
sv[].tokenized_selector string ste_vec Encrypted selector for the JSON path
sv[].term string ste_vec Encrypted term value
sv[].record string ste_vec Base85-encoded encrypted record data
sv[].parent_is_array 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

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.

Exception Types

All FFI operations throw FFIException instances that contain descriptive error messages:

use CipherStash\Protect\FFI\Client;
use CipherStash\Protect\FFI\Exceptions\FFIException;

$client = new Client;

$config = [
    'v' => 2,
    'tables' => [
        'patient_records' => [
            'email' => [
                'cast_as' => 'text',
                'indexes' => [
                    'unique' => (object) [],
                ],
            ],
        ],
    ],
];

try {
    $configJson = json_encode($config, JSON_THROW_ON_ERROR);
    $clientPtr = $client->newClient($configJson);

    $resultJson = $client->encrypt(
        client: $clientPtr,
        plaintext: 'john@example.com',
        columnName: 'email',
        tableName: 'patient_records',
    );

    $result = json_decode(json: $resultJson, associative: true, flags: JSON_THROW_ON_ERROR);
    $ciphertext = $result['c'];

    echo $ciphertext;
    // mBbM8rvts7^sycKCI!-Y9x2kL8vN...
} catch (FFIException $e) {
    error_log($e->getMessage());

    throw new \RuntimeException('Failed to process encrypted data.', 0, $e);
} finally {
    $client->freeClient($clientPtr);
}

Contributing

We welcome contributions! Please see our Contributing Guide for details.

About

PHP bindings for the CipherStash Client SDK

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •