├── .gitignore ├── .phpunit.result.cache ├── .travis.yml ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── Builders │ └── EncryptionEloquentBuilder.php ├── Config │ └── config.php ├── Console │ └── Commands │ │ ├── DecryptModel.php │ │ └── EncryptModel.php ├── Encrypter.php ├── Providers │ └── DBEncryptionServiceProvider.php └── Traits │ └── EncryptedAttribute.php └── tests ├── TestCase.php ├── TestUser.php ├── Unit └── EncryptedTest.php └── database ├── factories └── TestUserFactory.php └── migrations └── 2018_09_29_000001_create_test_users_table.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | .DS_Store 4 | tests/database/factories/.DS_Store 5 | src/Console/.DS_Store 6 | src/Console/Commands/.DS_Store 7 | src/Traits/.DS_Store 8 | src/Providers/.DS_Store 9 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":3640:{a:2:{s:7:"defects";a:16:{s:85:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_if_encryption_decoding_is_working";i:4;s:85:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_if_encryption_encoding_is_working";i:4;s:104:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_encrypt_model_commands_encrypt_existing_records";i:4;s:90:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_where_in_query_builder_is_working";i:4;s:110:"ESolution\DBEncryption\Tests\EncryptedTest::it_assert_that_where_does_not_retrieve_a_user_with_incorrect_email";i:4;s:109:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_exists_when_record_exists_is_working";i:4;s:118:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_exists_when_record_does_not_exists_is_working";i:4;s:109:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_unique_when_record_exists_is_working";i:4;s:118:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_unique_when_record_does_not_exists_is_working";i:4;s:88:"ESolution\DBEncryption\Tests\EncryptedTest::it_tests_that_empty_values_are_not_encrypted";i:3;s:83:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_decrypt_command_is_working";i:4;s:96:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_encrypted_value_is_stored_in_lower_case";i:3;s:105:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_where_query_is_working_with_non_lowercase_values";i:4;s:88:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_convert_to_camelcase_is_working";i:4;s:84:"ESolution\DBEncryption\Tests\EncryptedTest::it_tests_that_empty_values_are_encrypted";i:4;s:95:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_whereencrypted_can_handle_single_quote";i:4;}s:5:"times";a:16:{s:85:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_if_encryption_decoding_is_working";d:0.285;s:85:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_if_encryption_encoding_is_working";d:0.118;s:104:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_encrypt_model_commands_encrypt_existing_records";d:0.197;s:90:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_where_in_query_builder_is_working";d:0.131;s:110:"ESolution\DBEncryption\Tests\EncryptedTest::it_assert_that_where_does_not_retrieve_a_user_with_incorrect_email";d:0.129;s:109:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_exists_when_record_exists_is_working";d:0.137;s:118:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_exists_when_record_does_not_exists_is_working";d:0.11;s:109:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_unique_when_record_exists_is_working";d:0.131;s:118:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_validation_rule_unique_when_record_does_not_exists_is_working";d:0.128;s:88:"ESolution\DBEncryption\Tests\EncryptedTest::it_tests_that_empty_values_are_not_encrypted";d:0.022;s:83:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_decrypt_command_is_working";d:0.259;s:96:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_encrypted_value_is_stored_in_lower_case";d:0.013;s:105:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_where_query_is_working_with_non_lowercase_values";d:0.135;s:88:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_convert_to_camelcase_is_working";d:0.023;s:84:"ESolution\DBEncryption\Tests\EncryptedTest::it_tests_that_empty_values_are_encrypted";d:0.13;s:95:"ESolution\DBEncryption\Tests\EncryptedTest::it_test_that_whereencrypted_can_handle_single_quote";d:0.126;}}} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | 6 | services: 7 | - mysql 8 | 9 | matrix: 10 | fast_finish: true 11 | 12 | before_install: 13 | - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' 14 | 15 | before_script: 16 | - travis_retry composer self-update 17 | - travis_retry composer install --prefer-source --no-interaction 18 | 19 | script: 20 | - phpunit -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Database Encryption Package 2 | 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/elgibor-solution/laravel-database-encryption.svg?style=flat-square)](https://packagist.org/packages/elgibor-solution/laravel-database-encryption) 5 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![Build Status](https://travis-ci.com/elgibor-solution/laravel-database-encryption.svg?branch=main)](https://travis-ci.com/elgibor-solution/laravel-database-encryption) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/elgibor-solution/laravel-database-encryption.svg?style=flat-square)](https://packagist.org/packages/elgibor-solution/laravel-database-encryption) 8 | 9 | 10 | ## Package for encrypting and decrypting model attributes for Laravel using openssl 11 | 12 | ## Key Features 13 | 14 | * Encrypt, Decrypt database fields easily 15 | * Minimal configuration 16 | * Include searching encrypted data using the following: 17 | `whereEncrypted` and `orWhereEncrypted` 18 | * uses openssl for encrypting and decrypting fields 19 | 20 | ## Requirements 21 | 22 | * Laravel: >= 5 23 | * PHP: >= 7.3 24 | 25 | ## Schema Requirements 26 | 27 | Encrypted values are usually longer than plain text values, sometimes much longer. 28 | You may find that the column widths in your database tables need to be altered to 29 | store the encrypted values generated by this package. 30 | 31 | We highly recommend to alter your column types to `TEXT` or `LONGTEXT` 32 | 33 | ## Installation 34 | 35 | ### Step 1: Composer 36 | 37 | Via Composer command line: 38 | 39 | ```bash 40 | $ composer require elgibor-solution/laravel-database-encryption 41 | ``` 42 | 43 | ### Step 2: Add ServiceProvider to your app/config.php file (Laravel 5.4 or below) 44 | Add the service provider to the providers array in the config/app.php config file as follows: 45 | ```php 46 | 'providers' => [ 47 | ... 48 | \ESolution\DBEncryption\Providers\DBEncryptionServiceProvider::class, 49 | ], 50 | ``` 51 | 52 | ## Usage 53 | 54 | Use the `EncryptedAttribute` trait in any Eloquent model that you wish to apply encryption 55 | to and define a `protected $encrypted` array containing a list of the attributes to encrypt. 56 | 57 | For example: 58 | 59 | ```php 60 | 61 | use ESolution\DBEncryption\Traits\EncryptedAttribute; 62 | 63 | class User extends Eloquent { 64 | use EncryptedAttribute; 65 | 66 | /** 67 | * The attributes that should be encrypted on save. 68 | * 69 | * @var array 70 | */ 71 | protected $encryptable = [ 72 | 'first_name', 'last_name' 73 | ]; 74 | } 75 | ``` 76 | 77 | By including the `EncryptedAttribute` trait, the `setAttribute()`, `getAttribute()` and `getAttributeFromArray()` 78 | methods provided by Eloquent are overridden to include an additional step. 79 | 80 | ### Searching Encrypted Fields Example: 81 | Searching encrypted field can be done by calling the `whereEncrypted` and `orWhereEncrypted` functions 82 | similar to laravel eloquent `where` and `orWhere`. 83 | 84 | 85 | ```php 86 | namespace App\Http\Controllers; 87 | 88 | use App\User; 89 | class UsersController extends Controller { 90 | public function index(Request $request) 91 | { 92 | $user = User::whereEncrypted('first_name','john') 93 | ->orWhereEncrypted('last_name','!=','Doe')->firstOrFail(); 94 | 95 | return $user; 96 | } 97 | } 98 | ``` 99 | 100 | ### Encrypt your current data 101 | If you have current data in your database you can encrypt it with the following command. 102 | 103 | ```php 104 | php artisan encryptable:encryptModel 'App\User' 105 | ``` 106 | 107 | Additionally you can decrypt it using the following commmand. 108 | 109 | ```php 110 | php artisan encryptable:decryptModel 'App\User' 111 | ``` 112 | 113 | Note: You must implement first the `Encryptable` trait and set `$encryptable` attributes 114 | 115 | ### Exists and Unique Validation Rules 116 | If you are using exists and unique rules with encrypted values replace it with exists_encrypted and unique_encrypted 117 | ```php 118 | $validator = validator(['email'=>'foo@bar.com'], ['email'=>'exists_encrypted:users,email']); 119 | $validator = validator(['email'=>'foo@bar.com'], ['email'=>'unique_encrypted:users,email']); 120 | ``` 121 | 122 | ## Frequently Asked Question 123 | #### Can I search encrypted data? 124 | YES! You will able to search on attributes which are encrypted by this package because. 125 | If you need to search on data then use the `whereEncrypted` and `orWhereEncrypted` function: 126 | ``` 127 | User::whereEncrypted('email','test@gmail.com')->orWhereEncrypted('email','test2@gmail.com')->firstOrFail(); 128 | ``` 129 | It will automatically added on the eloquent once the model uses `EncryptedAttribute` 130 | 131 | #### Can I encrypt all my `User` model data? 132 | Aside from IDs you can encrypt everything you wan't 133 | 134 | For example: 135 | Logging-in on encrypted email 136 | ``` 137 | $user = User::whereEncrypted('email','test@gmail.com')->filter(function ($item) use ($request) { 138 | return Hash::check($password, $item->password); 139 | })->where('active',1)->first(); 140 | ``` 141 | 142 | ## Credits 143 | This package was inspired from the following: 144 | [austinheap/laravel-database-encryption](https://github.com/austinheap/laravel-database-encryption) 145 | [magros/laravel-model-encryption](https://github.com/magros/laravel-model-encryption) 146 | [DustApplication/laravel-database-model-encryption](https://github.com/DustApplication/laravel-database-model-encryption.git) 147 | 148 | ## License 149 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 150 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elgibor-solution/laravel-database-encryption", 3 | "description": "Auto Encrypt and Decrypt Database through Eloquent", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "E-Solution", 9 | "email": "info@elgibor-solution.com" 10 | } 11 | ], 12 | "require": {}, 13 | "autoload": { 14 | "psr-4": { 15 | "ESolution\\DBEncryption\\": "src" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "ESolution\\DBEncryption\\Tests\\": "tests", 21 | "ESolution\\DBEncryption\\Tests\\Database\\Factories\\": "tests/database/factories" 22 | } 23 | }, 24 | "require-dev": { 25 | "orchestra/testbench": "^6.0", 26 | "phpunit/phpunit": "^9.4" 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "ESolution\\DBEncryption\\Providers\\DBEncryptionServiceProvider" 32 | ] 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit", 37 | "test-f": "vendor/bin/phpunit --filter" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | src/ 19 | 20 | 21 | 22 | 23 | ./tests/Unit 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Builders/EncryptionEloquentBuilder.php: -------------------------------------------------------------------------------- 1 | field = $param1; 15 | $filter->operation = isset($param3) ? $param2 : '='; 16 | $filter->value = isset($param3) ? $param3 : $param2; 17 | 18 | $salt = substr(hash('sha256', config('laravelDatabaseEncryption.encrypt_key')), 0, 16); 19 | 20 | return self::whereRaw("CONVERT(AES_DECRYPT(FROM_BASE64(`{$filter->field}`), '{$salt}') USING utf8mb4) {$filter->operation} ? ", [$filter->value]); 21 | } 22 | 23 | public function orWhereEncrypted($param1, $param2, $param3 = null) 24 | { 25 | $filter = new \stdClass(); 26 | $filter->field = $param1; 27 | $filter->operation = isset($param3) ? $param2 : '='; 28 | $filter->value = isset($param3) ? $param3 : $param2; 29 | 30 | $salt = substr(hash('sha256', config('laravelDatabaseEncryption.encrypt_key')), 0, 16); 31 | 32 | return self::orWhereRaw("CONVERT(AES_DECRYPT(FROM_BASE64(`{$filter->field}`), '{$salt}') USING utf8mb4) {$filter->operation} ? ", [$filter->value]); 33 | } 34 | 35 | public function orderByEncrypted($column, $direction = 'asc') { 36 | $salt = substr(hash('sha256', config('laravelDatabaseEncryption.encrypt_key')), 0, 16); 37 | return self::orderByRaw("CONVERT(AES_DECRYPT(FROM_bASE64(`{$column}`), '{$salt}') USING utf8mb4) {$direction}"); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Config/config.php: -------------------------------------------------------------------------------- 1 | true, 5 | 'encrypt_method' => 'aes-128-ecb', 6 | 'encrypt_key' => env('APP_KEY', null) 7 | ]; -------------------------------------------------------------------------------- /src/Console/Commands/DecryptModel.php: -------------------------------------------------------------------------------- 1 | argument('model'); 42 | $this->model = $this->guardClass($class); 43 | $this->attributes = $this->model->getEncryptableAttributes(); 44 | $table = $this->model->getTable(); 45 | $pk_id = $this->model->getKeyName(); 46 | $total = $this->model->where('encrypted', 1)->count(); 47 | $this->model::$enableEncryption = false; 48 | 49 | if($total > 0){ 50 | $this->comment($total.' records will be decrypted'); 51 | $bar = $this->output->createProgressBar($total); 52 | $bar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'); 53 | 54 | $records = $this->model->orderBy($pk_id, 'asc')->where('encrypted', 1) 55 | ->chunkById(100, function($records) use($table, $bar, $pk_id) { 56 | foreach ($records as $record) { 57 | $record->timestamps = false; 58 | $attributes = $this->getDecryptedAttributes($record); 59 | $update_id = "{$record->{$pk_id}}"; 60 | DB::table($table)->where($pk_id, $update_id)->update($attributes); 61 | $bar->advance(); 62 | $record = null; 63 | $attributes = null; 64 | } 65 | }); 66 | 67 | $bar->finish(); 68 | 69 | } 70 | 71 | $this->comment('Finished Model Decryption'); 72 | } 73 | 74 | private function getDecryptedAttributes($record) 75 | { 76 | $encryptedFields = ['encrypted' => 0 ]; 77 | 78 | foreach ($this->attributes as $attribute) { 79 | $raw = $record->{$attribute}; 80 | 81 | // if (str_contains($raw, $record->encrypter()->getPrefix())) { 82 | 83 | $encryptedFields[$attribute] = $this->model->decryptAttribute($raw); 84 | // } 85 | } 86 | return $encryptedFields; 87 | } 88 | 89 | private function validateHasEncryptedColumn($model) 90 | { 91 | $table = $model->getTable(); 92 | if (! Schema::hasColumn($table, 'encrypted')) { 93 | $this->comment('Creating encrypted column'); 94 | Schema::table($table, function (Blueprint $table) { 95 | $table->tinyInteger('encrypted')->default(0); 96 | }); 97 | } 98 | } 99 | 100 | /** 101 | * @param $class 102 | * @return Model 103 | * @throws \Exception 104 | */ 105 | public function guardClass($class) 106 | { 107 | if (!class_exists($class)) 108 | throw new \Exception("Class {$class} does not exists"); 109 | $model = new $class(); 110 | $this->validateHasEncryptedColumn($model); 111 | return $model; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Console/Commands/EncryptModel.php: -------------------------------------------------------------------------------- 1 | argument('model'); 42 | $this->model = $this->guardClass($class); 43 | $this->attributes = $this->model->getEncryptableAttributes(); 44 | $table = $this->model->getTable(); 45 | $pk_id = $this->model->getKeyName(); 46 | $total = $this->model->where('encrypted', 0)->count(); 47 | $this->model::$enableEncryption = false; 48 | 49 | if($total > 0){ 50 | $this->comment($total.' records will be encrypted'); 51 | $bar = $this->output->createProgressBar($total); 52 | $bar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'); 53 | 54 | $records = $this->model->orderBy($pk_id, 'asc')->where('encrypted', 0) 55 | ->chunkById(100, function($records) use($table, $bar, $pk_id) { 56 | foreach ($records as $record) { 57 | $record->timestamps = false; 58 | $attributes = $this->getEncryptedAttributes($record); 59 | 60 | $update_id = "{$record->{$pk_id}}"; 61 | DB::table($table)->where($pk_id, $update_id)->update($attributes); 62 | $bar->advance(); 63 | $record = null; 64 | $attributes = null; 65 | } 66 | }); 67 | 68 | $bar->finish(); 69 | } 70 | 71 | $this->comment('Finished encryption'); 72 | } 73 | 74 | private function getEncryptedAttributes($record) 75 | { 76 | $encryptedFields = ['encrypted' => 1]; 77 | 78 | foreach ($this->attributes as $attribute) { 79 | $raw = $record->getOriginal($attribute); 80 | // if (!str_contains($raw, $record->encrypter()->getPrefix())) { 81 | $encryptedFields[$attribute] = $this->model->encryptAttribute($raw); 82 | // } 83 | } 84 | return $encryptedFields; 85 | } 86 | 87 | private function validateHasEncryptedColumn($model) 88 | { 89 | $table = $model->getTable(); 90 | if (! Schema::hasColumn($table, 'encrypted')) { 91 | $this->comment('Creating encrypted column'); 92 | Schema::table($table, function (Blueprint $table) { 93 | $table->tinyInteger('encrypted')->default(0); 94 | }); 95 | } 96 | } 97 | 98 | /** 99 | * @param $class 100 | * @return Model 101 | * @throws \Exception 102 | */ 103 | public function guardClass($class) 104 | { 105 | if (!class_exists($class)) 106 | throw new \Exception("Class {$class} does not exists"); 107 | $model = new $class(); 108 | $this->validateHasEncryptedColumn($model); 109 | return $model; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Encrypter.php: -------------------------------------------------------------------------------- 1 | bootValidators(); 29 | 30 | if ($this->app->runningInConsole()) { 31 | 32 | $this->publishes([ 33 | __DIR__.'/../Config/config.php' => config_path('laravelDatabaseEncryption.php'), 34 | ], 'config'); 35 | 36 | $this->commands([ 37 | EncryptModel::class, 38 | DecryptModel::class 39 | ]); 40 | } 41 | } 42 | 43 | /** 44 | * Register the application services. 45 | * 46 | * @return void 47 | */ 48 | public function register() 49 | { 50 | $this->mergeConfigFrom(__DIR__.'/../Config/config.php', 'laravelDatabaseEncryption'); 51 | } 52 | 53 | 54 | private function bootValidators() 55 | { 56 | 57 | Validator::extend('unique_encrypted', function ($attribute, $value, $parameters, $validator) { 58 | 59 | // Initialize 60 | $salt = substr(hash('sha256', config('laravelDatabaseEncryption.encrypt_key')), 0, 16); 61 | 62 | $withFilter = count($parameters) > 3 ? true : false; 63 | 64 | $ignore_id = isset($parameters[2]) ? $parameters[2] : ''; 65 | 66 | // Check using normal checker 67 | $data = DB::table($parameters[0])->whereRaw("CONVERT(AES_DECRYPT(FROM_BASE64(`{$parameters[1]}`), '{$salt}') USING utf8mb4) = '{$value}' "); 68 | $data = $ignore_id != '' ? $data->where('id','!=',$ignore_id) : $data; 69 | 70 | if ($withFilter) { 71 | $data->where($parameters[3], $parameters[4]); 72 | } 73 | 74 | if($data->first()){ 75 | return false; 76 | } 77 | 78 | return true; 79 | }); 80 | 81 | Validator::extend('exists_encrypted', function ($attribute, $value, $parameters, $validator) { 82 | 83 | // Initialize 84 | $salt = substr(hash('sha256', config('laravelDatabaseEncryption.encrypt_key')), 0, 16); 85 | 86 | $withFilter = count($parameters) > 3 ? true : false; 87 | if(!$withFilter){ 88 | $ignore_id = isset($parameters[2]) ? $parameters[2] : ''; 89 | }else{ 90 | $ignore_id = isset($parameters[4]) ? $parameters[4] : ''; 91 | } 92 | 93 | // Check using normal checker 94 | $data = DB::table($parameters[0])->whereRaw("CONVERT(AES_DECRYPT(FROM_BASE64(`{$parameters[1]}`), '{$salt}') USING utf8mb4) = '{$value}' "); 95 | $data = $ignore_id != '' ? $data->where('id','!=',$ignore_id) : $data; 96 | 97 | if ($withFilter) { 98 | $data->where($parameters[2], $parameters[3]); 99 | } 100 | 101 | if ($data->first()) { 102 | return true; 103 | } 104 | 105 | return false; 106 | }); 107 | } 108 | } -------------------------------------------------------------------------------- /src/Traits/EncryptedAttribute.php: -------------------------------------------------------------------------------- 1 | encryptable); 27 | } 28 | 29 | return false; 30 | } 31 | 32 | /** 33 | * @return mixed 34 | */ 35 | public function getEncryptableAttributes() 36 | { 37 | return $this->encryptable; 38 | } 39 | 40 | public function getAttribute($key) 41 | { 42 | $value = parent::getAttribute($key); 43 | if ($this->isEncryptable($key) && (!is_null($value) && $value != '')) 44 | { 45 | try { 46 | $value = Encrypter::decrypt($value); 47 | } catch (\Exception $th) {} 48 | } 49 | return $value; 50 | } 51 | 52 | public function setAttribute($key, $value) 53 | { 54 | if ($this->isEncryptable($key) && (!is_null($value) && $value != '')) 55 | { 56 | try { 57 | $value = Encrypter::encrypt($value); 58 | } catch (\Exception $th) {} 59 | } 60 | return parent::setAttribute($key, $value); 61 | } 62 | 63 | public function attributesToArray() 64 | { 65 | $attributes = parent::attributesToArray(); 66 | if ($attributes) { 67 | foreach ($attributes as $key => $value) 68 | { 69 | if ($this->isEncryptable($key) && (!is_null($value)) && $value != '') 70 | { 71 | $attributes[$key] = $value; 72 | try { 73 | $attributes[$key] = Encrypter::decrypt($value); 74 | } catch (\Exception $th) {} 75 | } 76 | } 77 | } 78 | return $attributes; 79 | } 80 | 81 | // Extend EncryptionEloquentBuilder 82 | public function newEloquentBuilder($query) 83 | { 84 | return new EncryptionEloquentBuilder($query); 85 | } 86 | 87 | /** 88 | * Decrypt Attribute 89 | * 90 | * @param string $value 91 | * 92 | * @return string 93 | */ 94 | public function decryptAttribute($value) 95 | { 96 | return (!is_null($value) && $value != '') ? Encrypter::decrypt($value) : $value; 97 | } 98 | 99 | /** 100 | * Encrypt Attribute 101 | * 102 | * @param string $value 103 | * 104 | * @return string 105 | */ 106 | public function encryptAttribute($value) 107 | { 108 | return (!is_null($value) && $value != '') ? Encrypter::encrypt($value) : $value; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__ . '/database/migrations'); 15 | } 16 | 17 | protected function getPackageProviders($app) 18 | { 19 | return [ 20 | DBEncryptionServiceProvider::class, 21 | ]; 22 | } 23 | 24 | protected function getEnvironmentSetUp($app) 25 | { 26 | // perform environment setup 27 | $app['config']->set('database.default', 'mysql'); 28 | $app['config']->set('database.connections.mysql', [ 29 | 'driver' => 'mysql', 30 | 'host' => '127.0.0.1', 31 | 'port' => '3306', 32 | 'database' => 'test', 33 | 'username' => 'root', 34 | 'password' => '' 35 | ]); 36 | } 37 | 38 | public function createUser($name = 'Jhon Doe', $email = 'jhon@doe.com') : TestUser 39 | { 40 | $user = TestUser::factory()->create(["name" => $name, "email" => $email, "password"=>"abcdef"]); 41 | 42 | return $user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/TestUser.php: -------------------------------------------------------------------------------- 1 | createUser($name, $email); 22 | 23 | $this->assertEquals($user->email, $email); 24 | $this->assertEquals($user->name, $name); 25 | } 26 | 27 | /** 28 | * @test 29 | */ 30 | public function it_test_if_encryption_encoding_is_working() 31 | { 32 | $name = 'Jhon'; 33 | $email = 'foo@bar.com'; 34 | $user = $this->createUser($name, $email); 35 | 36 | $userRaw = DB::table('test_users')->select('*')->first(); 37 | 38 | $this->assertEquals($userRaw->email, $user->encryptAttribute($email)); 39 | $this->assertEquals($userRaw->name, $user->encryptAttribute($name)); 40 | } 41 | 42 | 43 | /** 44 | * @test 45 | */ 46 | public function it_test_that_encrypt_model_commands_encrypt_existing_records() 47 | { 48 | TestUser::$enableEncryption = false; 49 | 50 | $user = $this->createUser(); 51 | 52 | $this->artisan('encryptable:encryptModel', ['model' => TestUser::class]); 53 | $raw = DB::table('test_users')->select('*')->first(); 54 | 55 | $this->assertEquals($raw->email, $user->encryptAttribute($user->email)); 56 | $this->assertEquals($raw->name, $user->encryptAttribute($user->name)); 57 | 58 | TestUser::$enableEncryption = true; 59 | } 60 | 61 | 62 | /** 63 | * @test 64 | */ 65 | public function it_test_that_where_in_query_builder_is_working() 66 | { 67 | $email = 'example@email.com'; 68 | $this->createUser('Jhon Doe', $email); 69 | 70 | $user = TestUser::whereEncrypted('email', '=', $email)->first(); 71 | 72 | $this->assertNotNull($user); 73 | } 74 | 75 | /** 76 | * @test 77 | */ 78 | public function it_assert_that_where_does_not_retrieve_a_user_with_incorrect_email() 79 | { 80 | $this->createUser(); 81 | 82 | $user = TestUser::whereEncrypted('email', '=', 'non_existing@email.com')->first(); 83 | 84 | $this->assertNull($user); 85 | } 86 | 87 | 88 | /** 89 | * @test 90 | */ 91 | public function it_test_that_validation_rule_exists_when_record_exists_is_working() 92 | { 93 | $email = 'example@email.com'; 94 | 95 | $this->createUser('Jhon Doe', $email); 96 | 97 | $validator = validator(compact('email'), ['email' => 'exists_encrypted:test_users,email']); 98 | 99 | $this->assertFalse($validator->fails()); 100 | } 101 | 102 | /** 103 | * @test 104 | */ 105 | public function it_test_that_validation_rule_exists_when_record_does_not_exists_is_working() 106 | { 107 | $this->createUser(); 108 | 109 | $validator = validator( 110 | ['email' => 'non_existing@email.com'], 111 | ['email' => 'exists_encrypted:test_users,email'] 112 | ); 113 | 114 | $this->assertTrue($validator->fails()); 115 | } 116 | 117 | 118 | /** 119 | * @test 120 | */ 121 | public function it_test_that_validation_rule_unique_when_record_exists_is_working() 122 | { 123 | $email = 'example@email.com'; 124 | 125 | $this->createUser('Jhon Doe', $email); 126 | 127 | $validator = validator(compact('email'), ['email' => 'unique_encrypted:test_users,email']); 128 | 129 | $this->assertTrue($validator->fails()); 130 | } 131 | 132 | /** 133 | * @test 134 | */ 135 | public function it_test_that_validation_rule_unique_when_record_does_not_exists_is_working() 136 | { 137 | $this->createUser(); 138 | 139 | $validator = validator( 140 | ['email' => 'non_existing@email.com'], 141 | ['email' => 'unique_encrypted:test_users,email'] 142 | ); 143 | 144 | $this->assertFalse($validator->fails()); 145 | } 146 | 147 | /** 148 | * @test 149 | */ 150 | public function it_tests_that_empty_values_are_not_encrypted() 151 | { 152 | $user = $this->createUser(null, 'example@email.com'); 153 | $raw = DB::table('test_users')->select('*')->first(); 154 | 155 | $this->assertEmpty($raw->name); 156 | $this->assertEmpty($user->name); 157 | } 158 | 159 | 160 | /** 161 | * @test 162 | */ 163 | public function it_test_that_decrypt_command_is_working() 164 | { 165 | TestUser::$enableEncryption = false; 166 | 167 | $user = $this->createUser(); 168 | 169 | $this->artisan('encryptable:encryptModel', ['model' => TestUser::class]); 170 | $this->artisan('encryptable:decryptModel', ['model' => TestUser::class]); 171 | $raw = DB::table('test_users')->select('*')->first(); 172 | 173 | 174 | $this->assertEquals($user->email, $raw->email); 175 | $this->assertEquals($user->name, $raw->name); 176 | 177 | TestUser::$enableEncryption = true; 178 | } 179 | 180 | /** 181 | * @test 182 | */ 183 | public function it_test_that_where_query_is_working_with_non_lowercase_values() 184 | { 185 | $this->createUser(); 186 | $this->assertNotNull(TestUser::whereEncrypted('email', '=', 'JhOn@DoE.cOm')->first()); 187 | } 188 | 189 | /** 190 | * @test 191 | */ 192 | public function it_test_that_whereencrypted_can_handle_single_quote() 193 | { 194 | $email = "JhOn@DoE.cOm'"; 195 | $name = "Single's"; 196 | $this->createUser($name, $email); 197 | $query = TestUser::whereEncrypted('email', $email)->orWhereEncrypted('name', $name)->first(); 198 | 199 | $this->assertNotNull($query); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/database/factories/TestUserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 25 | 'email' => $this->faker->unique()->safeEmail, 26 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/database/migrations/2018_09_29_000001_create_test_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('name')->nullable(); 18 | $table->string('email')->unique(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::drop('test_users'); 33 | } 34 | } --------------------------------------------------------------------------------