Implementing Blind Indexes in Laravel

What is a blind index?

Blind indexing is an approach to securely search encrypted data with minimal information leakage.

Use case

Let’s say that you are building a platform which is going to store large amounts of personal information. Data such as SSN numbers, Aadhaar card or Pan card etc. You also need the ability to search through them to run validation checks etc.

Implementation

First you need to have your application and database servers running on different machines. You obvious don’t want your SALT keys and database to be on the same machine.

Let’s assume your table schema looks something like this (PostgreSQL):

CREATE TABLE users (    id SERIAL PRIMARY KEY,
first_name TEXT,
last_name TEXT,
ssn TEXT, /* encrypted */
ssn_bidx TEXT /* blind index */
);CREATE INDEX ON users(ssn_bidx);

The ssn column stores the encrypted value and ssn_bidx column stores the blind index value.

Laravel Implementation

The below implementation leverages Laravel’s accessors and mutators to access attributes before saving and after retrieving data from database.

The following set of files will enable Laravel’s QueryBuilder to read an array of encrypted column names in a model. This is basically a mapping between your encrypted column name and blind index column name. That way you can simply add$encrypted property to your models and the implementation will automatically encrypt all new data thats being saved and retrieved.

Let’s start with creating a Blind index trait inside the App\Model\Concerns folder. Below is an example of what a Blind Index trait would look like.

<?php

namespace App\Models\Concerns;


trait BlindIndex
{

/**
*
@return array
*/
public function getEncrypted(): array
{
return $this->encrypted;
}


}

Let’s create another trait for SSN (whatever encrypted field name is), so this can be utilized by multiple models across the application.

<?php

namespace App\Models\Concerns;

use App\Security\BlindIndexEncryption as BI;

trait SocialSecurityNumber
{

/**
*
@param $value
*
@return void
*/
public function setSsnAttribute($value)
{
$value = trim($value);

if (!empty($value)) {
$this->attributes['ssn'] = BI::encrypt($value);
if (!empty($this->encrypted['ssn']))
$this->attributes[$this->encrypted['ssn']] = BI::getBlindIndex($value);
} else {
$this->attributes['ssn'] = $value;
}
}

/**
*
@param $value
*
@return string
*/
public function getSsnAttribute($value)
{
if (!empty($value)) {
return BI::decrypt($value);
}
return '';
}
}

You also need to overwrite Laravel’s QueryBuilder, so it runs your where conditions on ssn_bidx instead of ssn field. Below is the implementation of extending Laravel’s QueryBuilder with a custom query builder.

<?php

namespace App\Models\Eloquent;

use App\Security\BlindIndexEncryption as BI;
use Illuminate\Database\Eloquent\Builder as BaseBuilder;

class Builder extends BaseBuilder
{

/**
*
@param array|\Closure|string $column
*
@param null $operator
*
@param null $value
*
@param string $boolean
*
@return Builder
*/
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if (is_string($column) && method_exists($this->model, 'getEncrypted')) {
$encryptedFields = $this->model->getEncrypted();

if (!empty($encryptedFields[$column])) {
$column = $encryptedFields[$column];

if (!empty($operator) && !$value) {
$operator = BI::getBlindIndex($operator);
} elseif (!empty($value)) {
$value = BI::getBlindIndex($value);
}
}
}

return parent::where($column, $operator, $value, $boolean);
}


}

Your custom QueryBuilder trait.

<?phpnamespace App\Models\Concerns;

use App\Models\Eloquent\Builder;

trait QueryBuilder
{

/**
*
@param $query
*
@return Builder
*/
public function newEloquentBuilder($query)
{
return new Builder($query);
}
}

And now the BlindIndexEncryption file.

<?php

namespace App\Security;

class BlindIndexEncryption
{

/**
*
*/
const PRIVATE_KEY = 'your_super_secret_key';
const INDEX_KEY = 'your_index_key';

/**
*
@param string $secret
*
@return string
*
@throws \SodiumException
*/
public static function encrypt(string $secret): string
{
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$key = base64_decode(self::PRIVATE_KEY);
// encrypt message and combine with nonce
$cipher = base64_encode($nonce . sodium_crypto_secretbox($secret, $nonce, $key));

// cleanup
sodium_memzero($secret);
sodium_memzero($key);
return $cipher;
}

public static function decrypt(string $encrypted): string
{
$decoded = base64_decode($encrypted);
$key = base64_decode(self::PRIVATE_KEY);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');

$plain = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
if (!is_string($plain)) {
throw new Exception('Invalid MAC');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return $plain;
}

public static function getBlindIndex(string $secret): string
{
$index_key = base64_decode(self::INDEX_KEY);
return bin2hex(
sodium_crypto_pwhash(
32,
$secret,
$index_key,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE,
)
);
}
}

Finally, now in your model class you can add the above traits we created.

<?phpnamespace App\Models;use App\Models\Concerns;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
use
Concerns\QueryBuilder,
Concerns\SocialSecurityNumber,
Concerns\BlindIndex;
protected $table = 'users';
protected $guarded = ['id', 'created_at', 'updated_at'];
/**
*
@var string[]
* Blind Indexes columns in DB
*/
protected $encrypted = [
'ssn' => 'ssn_bidx',
];
}

Writing queries

$user = User::where('ssn', '123456789')->first();Or$user = User::where('ssn', $request->ssn)->exists();

That’s it!

Your encrypted data is now searchable!

Notes:

  • The above code was tested with Laravel 5.8 ≤8
  • Works with MySQL, PostgreSQL and MongoDB databases.
  • Should work fine with any database as Eloquent handles it very nicely.
  • Works with PHP 7.x +only
  • Test your code well after implementation.
  • The above implementation does not support Fuzzy search. You can only run a full match query.

Written by

Product Lead at StegoSOC

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store