Skip to content

cipherstash/protectphp

Repository files navigation

CipherStash Logo

Protect.php

Implement robust data security without sacrificing performance or usability

Built by CipherStash License

Protect.php brings field-level encryption to your PHP applications. Store encrypted data in any JSONB-compatible database while maintaining searchability on PostgreSQL. Encryption happens directly in your application using a unique key for each value, managed by CipherStash ZeroKMS and backed by AWS KMS.

Installation

Install Protect.php via Composer:

composer require cipherstash/protectphp

Requirements

  • PHP 8.1 or higher

Configuration

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

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.

Quick Start

Encrypt and decrypt data with just a few lines of code:

use CipherStash\Protect\Protect;

$field = 'users.email';
$value = 'john@example.com';

$encrypted = Protect::encrypt($field, $value);
$decrypted = Protect::decrypt($encrypted); // john@example.com

Integrations

  • Laravel (coming soon)

Database Setup

Protect.php 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 the EQL extension. EQL provides the eql_v2_encrypted type:

CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email eql_v2_encrypted,
    name eql_v2_encrypted,
    balance eql_v2_encrypted,
    notes eql_v2_encrypted,
    contact eql_v2_encrypted,
    CONSTRAINT unique_email UNIQUE ((email->>'hm')) -- Enforce unique emails
);

See the EQL installation instructions to get started.

Encrypting Data

Encrypt values for specific table columns using the Protect::encrypt() method. This method accepts the field name in dot notation, value, and optional configuration:

use CipherStash\Protect\Protect;

$field = 'users.email';
$value = 'john@example.com';

$encrypted = Protect::encrypt($field, $value);

The Protect::encrypt() method automatically handles data type conversion and applies sensible default indexes based on the PHP data type. You can customize the data type and indexing behavior by providing configuration options:

use CipherStash\Protect\Protect;

$field = 'users.email';
$value = 'john@example.com';

$options = [
    'cast_as' => 'string',
    'indexes' => [
        'unique' => [],
        'ore' => [],
        'match' => [],
    ],
];

$encrypted = Protect::encrypt($field, $value, $options);

Decrypting Data

Decrypt an encrypted envelope back to its original value using the Protect::decrypt() method. This method accepts the encrypted envelope array returned from encryption and optional configuration:

use CipherStash\Protect\Protect;

$field = 'users.email';
$value = 'john@example.com';

$encrypted = Protect::encrypt($field, $value);
$decrypted = Protect::decrypt($encrypted); // john@example.com

The Protect::decrypt() method automatically converts the decrypted data back to the original PHP data type. You can customize the data type conversion by providing configuration options:

use CipherStash\Protect\Protect;

$field = 'users.balance';
$value = 1575000;

$options = [
    'cast_as' => 'string',
];

$encrypted = Protect::encrypt($field, $value, $options);
$decrypted = Protect::decrypt($encrypted, $options); // "1575000"

Bulk Operations

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

Bulk Encryption

Encrypt multiple values using the Protect::encryptAttributes() method. This method accepts the table name and an associative array of column names and values:

use CipherStash\Protect\Protect;

$attributes = [
    'email' => 'john@example.com',
    'name' => 'John Doe',
    'balance' => 1575000,
    'notes' => 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.',
    'contact' => [
        'phone' => '15551234567',
        'mailing_address' => [
            'street' => '742 Evergreen Terrace',
            'city' => 'Springfield',
            'state' => 'OR',
            'zip' => '97403',
        ],
    ],
];

$encrypted = Protect::encryptAttributes('users', $attributes);

The Protect::encryptAttributes() method automatically handles data type conversion and applies sensible default indexes based on the PHP data type for each column. You can provide per-column configuration options:

use CipherStash\Protect\Protect;

$attributes = [
    'email' => 'john@example.com',
    'name' => 'John Doe',
    'balance' => 1575000,
    'notes' => 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.',
    'contact' => [
        'phone' => '15551234567',
        'mailing_address' => [
            'street' => '742 Evergreen Terrace',
            'city' => 'Springfield',
            'state' => 'OR',
            'zip' => '97403',
        ],
    ],
];

$options = [
    'email' => [
        'indexes' => [
            'unique' => [],
            'ore' => [],
            'match' => [],
        ],
    ],
    'name' => [
        'indexes' => [
            'unique' => [],
            'ore' => [],
            'match' => [],
        ],
    ],
    'notes' => [
        'indexes' => [
            'match' => [],
        ],
    ],
];

$encrypted = Protect::encryptAttributes('users', $attributes, $options);

Bulk Decryption

Decrypt multiple encrypted envelopes using the Protect::decryptAttributes() method. This method accepts the table name and an associative array of column names and their corresponding encrypted envelopes:

use CipherStash\Protect\Protect;

$attributes = [
    'email' => 'john@example.com',
    'name' => 'John Doe',
    'balance' => 1575000,
    'notes' => 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.',
    'contact' => [
        'phone' => '15551234567',
        'mailing_address' => [
            'street' => '742 Evergreen Terrace',
            'city' => 'Springfield',
            'state' => 'OR',
            'zip' => '97403',
        ],
    ],
];

$encrypted = Protect::encryptAttributes('users', $attributes);
$decrypted = Protect::decryptAttributes('users', $encrypted);

The Protect::decryptAttributes() method automatically converts the decrypted data back to the original PHP data types. You can customize the data type conversion by providing per-column configuration options:

use CipherStash\Protect\Protect;

$attributes = [
    'email' => 'john@example.com',
    'name' => 'John Doe',
    'balance' => 1575000,
    'notes' => 'Account flagged for fraud monitoring after suspicious transaction pattern detected. Customer disputed charges on 2007-07-27. Priority support required for high-value client.',
    'contact' => [
        'phone' => '15551234567',
        'mailing_address' => [
            'street' => '742 Evergreen Terrace',
            'city' => 'Springfield',
            'state' => 'OR',
            'zip' => '97403',
        ],
    ],
];

$options = [
    'balance' => [
        'cast_as' => 'string',
    ],
];

$encrypted = Protect::encryptAttributes('users', $attributes, $options);
$decrypted = Protect::decryptAttributes('users', $encrypted, $options);

Searchable Encryption

Create search terms that enable querying encrypted data without decryption using the Protect::createSearchTerms() method. This method accepts an associative array where keys are field names in dot notation and values are the data to search:

use CipherStash\Protect\Protect;

$fields = [
    'users.email' => 'john@example.com',
    'users.balance' => 1575000,
];

$searchTerms = Protect::createSearchTerms($fields);

The Protect::createSearchTerms() method automatically applies sensible default indexes based on the PHP data types, ensuring search terms match the indexes used for encrypted data with the same defaults. You can provide per-field configuration options:

use CipherStash\Protect\Protect;

$fields = [
    'users.email' => 'john@example.com',
    'users.balance' => 1575000,
];

$options = [
    'users.email' => [
        'indexes' => [
            'unique' => [],
            'ore' => [],
            'match' => [],
        ],
    ],
];

$searchTerms = Protect::createSearchTerms($fields, $options);

This feature integrates with EQL 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:

-- Find user record by email address
-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM users
WHERE email = '{"hm":"f3ca71fd39ae9d3d1d1fc25141bcb6da...","ob":["57e58bb3ebd195a5cdd5b77902732a6a..."],"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:

-- Find users with exact balance amount
-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM users
WHERE balance = '{"hm":null,"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":null,"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":null,"ob":["99f7adadadadadadc68b2822197a849e..."],"bf":null,"i":{"t":"users","c":"balance"}}'::jsonb
  AND '{"hm":null,"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:

-- Find users with notes containing specified terms
-- Using search terms (encrypted ahead of time, plaintext not loggable):
SELECT * FROM users
WHERE notes LIKE '{"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:

-- Find users where contact 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 users where contact 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;

Options

The Protect.php library provides configuration options to customize encryption behavior, indexing strategies, and contextual security. Each method supports the following options:

Option Type Default Description Protect::encrypt() Protect::decrypt() Protect::encryptAttributes() Protect::decryptAttributes() Protect::createSearchTerms()
cast_as string Auto-detected Override auto-detected PHP data type âś“ âś“ âś“ âś“ âś“
indexes array Type-based Configure query capabilities âś“ âś— âś“ âś— âś“
context array|null null Bind encryption to specific context âś“ âś“ âś“ âś“ âś“
skip boolean false Skip processing for specific columns âś— âś— âś“ âś“ âś—

Options not supported by a specific method are silently ignored, allowing you to use the same configuration across different operations.

Data Types

The cast_as parameter determines how data is processed and converted during encryption and decryption. In most cases, you can rely on automatic type detection, but you can specify the PHP data type when needed:

Type Description Example
string Text data supporting exact matching and full-text search capabilities john@example.com
bool Binary state values for exact equality queries only true or false
int Whole numbers enabling range queries and mathematical ordering 2147483647
float Decimal values supporting precise range and ordering operations 25.99
date Temporal data enabling date range queries and chronological sorting new DateTime('2020-11-10')
array Structured data supporting containment and relationship queries ['foo', 'bar']

Basic usage:

$options = [
    'cast_as' => 'string',
];

Index Types

The indexes parameter determines what queries are supported on encrypted data:

Index Type Description Applied by Default Response Parameter Supported Queries
unique Exact equality queries and uniqueness constraints string, bool hm =
ore Equality, range comparisons, range queries, and ordering string, int, float, date ob =, >, <, BETWEEN, ORDER BY
match Full-text search queries - bf LIKE
ste_vec JSONB containment queries - sv @>, <@

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:

$options = [
    'indexes' => [
        'unique' => [], // 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:

$options = [
    'indexes' => [
        'unique' => [
            'token_filters' => [
                ['kind' => 'downcase'],
            ],
        ],
    ],
];

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

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

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:

$options = [
    'indexes' => [
        'ore' => [],
    ],
];

This index type has no configurable parameters.

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:

$options = [
    'indexes' => [
        'match' => [], // 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:

$options = [
    'indexes' => [
        'match' => [
            'tokenizer' => [
                'kind' => 'ngram',
                'token_length' => 3,
            ],
            'token_filters' => [
                ['kind' => 'downcase'],
            ],
            'k' => 8,
            'm' => 1024,
            'include_original' => true,
        ],
    ],
];

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:

$options = [
    'indexes' => [
        'ste_vec' => [
            'prefix' => 'users.contact',
        ],
    ],
];

Configuration parameters:

Parameter Type Required Default Description
prefix string âś“ - Domain separator for cryptographic hashing that must be unique per column (recommended format is table.column)

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.

Context Types

The context parameter determines what contextual authentication is supported:

Context Type Description Supported Index Types
identity_claim Identity-aware encryption using JWT claims (requires CTS authentication) unique, ore, match
tag Label-aware encryption using string tags unique, ore, match
value Attribute-aware encryption using key-value pairs unique, ore, match

Important

Encryption context is not supported with ste_vec indexes and will cause decryption to fail.

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.

Note

While identity claim context is supported by passing JWT claims in the context.identity_claim parameter, automatic CTS authentication is not yet implemented. This means you'll need to manually obtain CTS tokens by calling the CTS API directly. If automatic CTS integration would be valuable for your use case, please open an issue and let us know. Community feedback helps us prioritize new features.

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\Protect;

$field = 'users.email';
$value = 'john@example.com';

$options = [
    'context' => [
        'tag' => ['pii'],
    ],
];

$encrypted = Protect::encrypt($field, $value, $options);
$decrypted = Protect::decrypt($encrypted, $options); // john@example.com

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\Protect;

$field = 'users.email';
$value = 'john@example.com';

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

$encrypted = Protect::encrypt($field, $value, $options);
$decrypted = Protect::decrypt($encrypted, $options); // john@example.com

Warning

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

Skip Processing

The skip parameter enables selective processing control for specific columns in bulk operations. When set to true, the specified column bypasses encryption and/or decryption while other columns are processed normally:

use CipherStash\Protect\Protect;

$attributes = [
    'email' => 'john@example.com',
    'name' => 'John Doe',
    'balance' => 1575000,
];

$options = [
    'balance' => ['skip' => true],
];

$encrypted = Protect::encryptAttributes('users', $attributes, $options);
$decrypted = Protect::decryptAttributes('users', $encrypted, $options);

Reference

Encryption Response

The Protect::encrypt() method returns an encrypted envelope array. 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' => 'mBbKlk}G7QdaGiNj$dL7#+AOrA^}*VJx...',
    'dt' => 'text',
    'hm' => 'f3ca71fd39ae9d3d1d1fc25141bcb6da...',
    'ob' => ['57e58bb3ebd195a5cdd5b77902732a6a...'],
    'bf' => [1124,2134,987,1456,743,2201],
    'i' => [
        't' => 'users',
        '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
dt string Always Data type for casting (from cast_as configuration parameter)
hm string|null unique HMAC index for exact equality queries and uniqueness constraints
ob array|null ore Order-revealing encryption index for equality checks, range comparisons, range queries, and sorting operations
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', 'c' => 'column']
v int Always Schema version for backward compatibility

STE Vec Index Response

For columns configured with the ste_vec index:

[
    'k' => 'sv',
    'c' => 'mBbLQ2^Io|1eh_K2*n^LSCVVQuGhkL>w...',
    'dt' => 'jsonb',
    'sv' => [
        [
            's' => 'dd4659b9c279af040dd05ce21b2a22f7...',
            't' => '22303061363334333330316661653633...',
            'r' => 'mBbLQ2^Io|1eh_K2*n^LSCVVQuGhkL>w...',
            'pa' => false,
        ],
    ],
    'i' => [
        't' => 'users',
        'c' => 'contact',
    ],
    '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
dt string Always Data type for casting (from cast_as configuration parameter)
sv array|null ste_vec Structured text encryption vector for JSONB containment queries
sv[].s string ste_vec Tokenized selector representing the encrypted JSON path to the value
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', 'c' => 'column']
v int Always Schema version for backward compatibility

Search Terms Response

The Protect::createSearchTerms() method returns an associative array where keys are field names and values contain search terms with only the encryption 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:

[
    'users.email' => [
        'hm' => 'f3ca71fd39ae9d3d1d1fc25141bcb6da...',
        'ob' => ['57e58bb3ebd195a5cdd5b77902732a6a...'],
        'bf' => [1124,2134,987,1456,743,2201],
        'i' => [
            't' => 'users',
            '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 equality checks, range comparisons, range queries, and sorting operations
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', 'c' => 'column']

STE Vec Index Response

For columns configured with ste_vec indexes:

[
    'users.contact' => [
        'sv' => [
            [
                's' => 'dd4659b9c279af040dd05ce21b2a22f7...',
                't' => '22303061363334333330316661653633...',
                'r' => 'mBbLkCZcaJ2U|G333rRC>f;r}uFEp7Tg...',
                'pa' => false,
            ],
            [
                's' => 'df08a4c4157bdb5bf6fa9be89cf18d10...',
                't' => '22303063343133306135646334356130...',
                'r' => 'mBbLkCZcaJ2U|G333rRC>f;r}E&d@?`;...',
                'pa' => false,
            ],
        ],
        'i' => [
            't' => 'users',
            'c' => 'contact',
        ],
    ],
]

Response parameters:

Parameter Type Source Description
sv array|null ste_vec Structured text encryption vector for JSONB containment queries
sv[].s string ste_vec Tokenized selector representing the encrypted JSON path to the value
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', 'c' => 'column']

Error Handling

Protect.php operations may throw exceptions when errors occur during library operations. Proper error handling ensures your application can gracefully handle configuration issues, network problems, or invalid data scenarios.

Exception Types

Protect.php defines several specific exception types for different error conditions. For most use cases, you can catch all exceptions using the base Exception or Throwable:

use CipherStash\Protect\Protect;
use Throwable;

try {
    $encrypted = Protect::encrypt('users.email', 'john@example.com');
} catch (Throwable $e) {
    // Handle any errors
    // ...
}

For more granular error handling, you can catch specific exception types:

use CipherStash\Protect\Exceptions\EncryptException;
use CipherStash\Protect\Exceptions\ValidationException;
use CipherStash\Protect\Protect;

try {
    $encrypted = Protect::encrypt('users.email', 'john@example.com');
} catch (ValidationException $e) {
    // Handle validation errors
    // ...
} catch (EncryptException $e) {
    // Handle encryption errors
    // ...
}

Contributing

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

About

Field-level encryption for PHP with searchable encrypted data

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages