Skip to content

Encryption

Ciphersweet

We use the Ciphersweet engine for database field-level encryption of personal identifyable information (PII) and sensitive data.

Our definition of sensitive data is higher than most standards and is anything that would cause interest or intrigue if it was leaked during a security breach. We believe it’s a matter of when, not if, so we want all database data to be benign or encrypted. For example, a Directory Dimension is not encrypted (ex. Department) but all of the Attribute names are (ex. Small Business Sales, Enterprise Sales, Strategic Marketing).

This allow us more flexibility with rulesets, rules, conditions, and using string and value matching since all of the values are encrypted and we don’t have to worry about what is PII and what isn’t, since everything is encrypted.

What may take 15-30ms with unencrypted data may now take 1,500ms to encrypt that data before saving it to the database. We have consciously accepted the time cost of encrypting this data and have moved most operations to background jobs to minimize the user experience loading time.

Access Control is a security service and data safety is more important than spinning wheel performance.

Configuring Models

All encryption is configured in the configureCipherSweet method in the respective model class. All encrypted fields are TEXT types in the database to support the length of the encrypted string.

/**
* Encrypted Fields
*
* Each column that should be encrypted should be added below. Each column
* in the migration should be a `text` type to store the encrypted value.
*
* ```
* ->addField('column_name')
* ->addBooleanField('column_name')
* ->addIntegerField('column_name')
* ->addTextField('column_name')
* ```
*
* A JSON array can be encrypted as long as the key structure is defined in
* a field map. See the docs for details on defining field maps.
*
* ```
* ->addJsonField('column_name', $fieldMap)
* ```
*
* Each field that should be searchable using an exact match needs to be
* added as a blind index. Partial search is not supported. See the docs
* for details on bit sizes and how to use compound indexes.
*
* ```
* ->addBlindIndex('column_name', new BlindIndex('column_name_index'))
* ```
*
* @link https://github.com/spatie/laravel-ciphersweet
* @link https://ciphersweet.paragonie.com/
* @link https://ciphersweet.paragonie.com/php/blind-index-planning
* @link https://github.com/paragonie/ciphersweet/blob/master/src/EncryptedRow.php
*
* @param EncryptedRow $encryptedRow
*/
public static function configureCipherSweet(EncryptedRow $encryptedRow): void
{
$encryptedRow
->addOptionalTextField('source_value')
->addTextField('name')
->addTextField('slug')
->addBlindIndex('source_value', new BlindIndex('source_value_index'))
->addBlindIndex('name', new BlindIndex('name_index'))
->addBlindIndex('slug', new BlindIndex('slug_index'));
}

Querying Encrypted Records

You do not need to perform any encryption or decrypting steps in an action’s handle method, however for encrypted fields, you need to use use a blind index search when querying columns in the database that are encrypted.

Blind indexes use a methodology similar to one way password hashing so the value that you’re searching for will be encrypted and will look for the same encrypted value in the blind indexes table.

You must use exact (full string) matches and cannot use wildcard or partial searches. If you need to partial search, you need to get all of the data and then use a Laravel Collection to filter the full dataset.

$search = 'strategic-mktg';
// This won't work
DirectoryAttribute::where('slug', $search)->firstOrFail();
// Use whereBlind instead
// {Model}::whereBlind('{column}', '{column}_index', $search)->firstOrFail();
DirectoryAttribute::whereBlind('slug', 'slug_index', $search)->firstOrFail();