laravel

The idea is to write a trait that adds encryption capabilities to the Eloquent model. The trait has an abstract getEncryptKey() method that returns the encryption key and needs to be implemented by the model class. This way the model can use a single encryption key or some logic to return different keys for different entities.
The getEncryptedFields() method returns an array of fields that need to be encrypted.
Finally the trait overrides the model's performInsert() and performUpdate() methods to implement automatic encryption. Encryption happens at the database level, so a database expression will be injected into Eloquents insert/update query. In this example Postgres pgp_sym_encrypt is used for encryption.

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
trait DatabaseEncryption
{
/**
* Initialize global scope for decryption
* @return void
*/
public static function bootDatabaseEncryption()
{
static::addGlobalScope(new DatabaseEncryptionScope());
}
/**
* Returns the encrypted fields
* @return array
*/
public static function getEncryptedFields()
{
return [];
}
/**
* Returns the encrypt key
* @return string
*/
abstract protected function getEncryptKey();
/**
* Perform insert with encryption
* @param Builder $query
* @return bool
*/
protected function performInsert(Builder $query)
{
$encryptedFields = static::getEncryptedFields();
if (count($encryptedFields) && !$this->getEncryptKey()) {
throw new \RuntimeException("No encryption key specified");
}
foreach ($encryptedFields as $encryptedField) {
$quotedText = DB::connection()->getPdo()->quote($this->attributes[$encryptedField]);
$quotedEncryptKey = DB::connection()->getPdo()->quote(static::getEncryptKey());
$this->attributes[$encryptedField] = DB::raw("pgp_sym_encrypt($quotedText, $quotedEncryptKey)");
}
return parent::performInsert($query);
}
/**
* Perform update with encryption
* @param Builder $query
* @return bool
*/
protected function performUpdate(Builder $query)
{
$encryptedFields = static::getEncryptedFields();
if (count($encryptedFields) && !$this->getEncryptKey()) {
throw new \RuntimeException("No encryption key specified");
}
foreach ($encryptedFields as $encryptedField) {
$quotedText = DB::connection()->getPdo()->quote($this->attributes[$encryptedField]);
$quotedEncryptKey = DB::connection()->getPdo()->quote(static::getEncryptKey());
$this->attributes[$encryptedField] = DB::raw("pgp_sym_encrypt($quotedText, $quotedEncryptKey)");
}
return parent::performUpdate($query);
}
}

A Eloquent query scope is used for automatic decryption. The class injects the decryption expression into all built queries.

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Facades\DB;
class DatabaseEncryptionScope implements Scope
{
/**
* All of the extensions to be added to the builder.
*
* @var array
*/
protected $extensions = ['WithDecryptKey'];
/**
* Apply the scope to a given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
foreach ($model::getEncryptedFields() as $encryptedField) {
$builder->addSelect(DB::raw("$encryptedField as {$encryptedField}_encrypted"));
}
}
/**
* Extend the query builder with the needed functions.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function extend(Builder $builder)
{
foreach ($this->extensions as $extension) {
$this->{"add{$extension}"}($builder);
}
}
/**
* Add the with-decrypt-key extension to the builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
protected function addWithDecryptKey(Builder $builder)
{
$builder->macro('withDecryptKey', function (Builder $builder, $decryptKey) {
$model = $builder->getModel();
/** @var DatabaseEncryptionServiceInterface $encryptionService */
$encryptionService = $model::getEncryptionService();
foreach ($model::getEncryptedFields() as $encryptedField) {
$decryptStmt = $encryptionService->getDecryptExpression($encryptedField, $decryptKey);
$builder->addSelect(DB::raw("$decryptStmt as $encryptedField"));
}
return $builder;
});
}
}

An example model class can look like this:

namespace App;
use Anexia\EloquentEncryption\DatabaseEncryption;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable, DatabaseEncryption;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* @return array
*/
protected static function getEncryptedFields()
{
return [
'password'
];
}
/**
* @return string
*/
protected function getEncryptKey()
{
return 'dasisteinlangerencryptkey';
}
}

The user class can then be used like this:

/* Insert a new user like always. The password field will be encrypted automatically */
$user = new User([
'name' => 'name',
'email' => 'email',
'password' => 'Password'
]);
$user->save();
/* Retrieve the user */
$user = User::withDecryptKey('dasisteinlangerencryptkey')->find(1);

Technology:

Model Encryption in Laravel

Sometimes it is desired to store certain model data with encryption. The goal is to have the model class automatically encrypt when data is saved and decrypt when it is queried. It should be possible to have different encryption/decrpytion keys.
Subscribe to laravel