├── src ├── Exceptions │ ├── SerializationException.php │ ├── UnserializationException.php │ ├── MissingEncryptionKeyException.php │ └── MissingEncryptionCipherException.php ├── Encryptable.php ├── EncryptableServiceProvider.php ├── Encrypter.php ├── Encryption.php ├── Utils │ └── Serializer.php ├── Rules │ ├── ExistsEncrypted.php │ └── UniqueEncrypted.php ├── DBEncrypter.php └── PHPEncrypter.php ├── config └── encryptable.php ├── LICENSE.md ├── CHANGELOG.md ├── composer.json └── README.md /src/Exceptions/SerializationException.php: -------------------------------------------------------------------------------- 1 | decrypt($value); 12 | } 13 | 14 | public function set($model, string $key, $value, array $attributes) 15 | { 16 | return Encryption::php()->encrypt($value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/encryptable.php: -------------------------------------------------------------------------------- 1 | env('ENCRYPTION_KEY'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Encryption cipher 19 | |-------------------------------------------------------------------------- 20 | | 21 | | The cipher used to encrypt data. 22 | | Once defined, never change it or encrypted data cannot be correctly decrypted. 23 | | Default value is the cipher algorithm used by default in MySQL. 24 | | 25 | */ 26 | 27 | 'cipher' => env('ENCRYPTION_CIPHER', 'aes-128-ecb'), 28 | ]; 29 | -------------------------------------------------------------------------------- /src/EncryptableServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-encryptable') 17 | ->hasConfigFile(); 18 | } 19 | 20 | public function bootingPackage() 21 | { 22 | Rule::macro( 23 | 'uniqueEncrypted', 24 | fn (string $table, string $column = 'NULL') => new UniqueEncrypted($table, $column) 25 | ); 26 | 27 | Rule::macro( 28 | 'existsEncrypted', 29 | fn (string $table, string $column = 'NULL') => new ExistsEncrypted($table, $column) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Encrypter.php: -------------------------------------------------------------------------------- 1 | encrypter = $encrypter; 12 | } 13 | 14 | public static function php(): self 15 | { 16 | return new self( 17 | app(PHPEncrypter::class) 18 | ); 19 | } 20 | 21 | public static function db(): self 22 | { 23 | return new self( 24 | app(DBEncrypter::class) 25 | ); 26 | } 27 | 28 | public static function isEncrypted($value): bool 29 | { 30 | return self::php()->encrypter 31 | ->isEncrypted($value); 32 | } 33 | 34 | public function encrypt($value, bool $serialize = true) 35 | { 36 | return $this->encrypter 37 | ->encrypt($value, $serialize); 38 | } 39 | 40 | public function decrypt(?string $payload, bool $unserialize = true) 41 | { 42 | return $this->encrypter 43 | ->decrypt($payload, $unserialize); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 MAIZE SRL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Utils/Serializer.php: -------------------------------------------------------------------------------- 1 | rule = new Exists($table, $column); 21 | } 22 | 23 | public function __call(string $name, array $arguments) 24 | { 25 | $this->forwardCallTo($this->rule, $name, $arguments); 26 | 27 | return $this; 28 | } 29 | 30 | public function passes($attribute, $value): bool 31 | { 32 | $attribute = Str::before($attribute, '.'); 33 | 34 | return ! Validator::make([ 35 | $attribute => Encryption::php()->encrypt($value), 36 | ], [ 37 | $attribute => $this->rule, 38 | ])->fails(); 39 | } 40 | 41 | public function message(): string 42 | { 43 | return __('validation.exists'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rules/UniqueEncrypted.php: -------------------------------------------------------------------------------- 1 | rule = new Unique($table, $column); 21 | } 22 | 23 | public function __call(string $name, array $arguments) 24 | { 25 | $this->forwardCallTo($this->rule, $name, $arguments); 26 | 27 | return $this; 28 | } 29 | 30 | public function passes($attribute, $value): bool 31 | { 32 | $attribute = Str::before($attribute, '.'); 33 | 34 | return ! Validator::make([ 35 | $attribute => Encryption::php()->encrypt($value), 36 | ], [ 37 | $attribute => $this->rule, 38 | ])->fails(); 39 | } 40 | 41 | public function message(): string 42 | { 43 | return __('validation.unique'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-encryptable` will be documented in this file. 4 | 5 | ## 4.0.0 - 2025-10-19 6 | 7 | ### What's Changed 8 | 9 | * ADD Laravel 12 support by @enricodelazzari in https://github.com/maize-tech/laravel-encryptable/pull/37 10 | 11 | **Full Changelog**: https://github.com/maize-tech/laravel-encryptable/compare/3.3.1...4.0.0 12 | 13 | ## 3.3.1 - 2025-09-29 14 | 15 | ### What's Changed 16 | 17 | * FIX validation massages by @enricodelazzari in https://github.com/maize-tech/laravel-encryptable/pull/35 18 | 19 | **Full Changelog**: https://github.com/maize-tech/laravel-encryptable/compare/3.3.0...3.3.1 20 | 21 | ## 3.3.0 - 2024-03-27 22 | 23 | ### What's Changed 24 | 25 | * Laravel 11.x Compatibility by @enricodelazzari in https://github.com/maize-tech/laravel-encryptable/pull/26 26 | 27 | ## 3.2.1 - 2024-01-25 28 | 29 | ### What's Changed 30 | 31 | * FIX php unserializer issues 32 | 33 | ## 3.2.0 - 2023-02-13 34 | 35 | ### What's Changed 36 | 37 | - Add support to Laravel 10.x 38 | 39 | ## 3.1.0 - 2022-11-25 40 | 41 | ### What's Changed 42 | 43 | - Added a new Rule macro for both `UniqueEncrypted` and `ExistsEncrypted` rules 44 | - Fixed `UniqueEncrypted` and `ExistsEncrypted` rules minor issues 45 | 46 | **Full Changelog**: https://github.com/maize-tech/laravel-encryptable/compare/3.0.0...3.1.0 47 | 48 | ## 3.0.0 - 2022-02-15 49 | 50 | - add laravel 9 support 51 | - drop support to older laravel versions 52 | 53 | ## 2.0.0 - 2021-10-12 54 | 55 | - UPDATE package namespace 56 | 57 | ## 1.0.0 - 2021-06-25 58 | 59 | - first release 🚀 60 | -------------------------------------------------------------------------------- /src/DBEncrypter.php: -------------------------------------------------------------------------------- 1 | grammar = DB::query()->getGrammar(); 17 | } 18 | 19 | public function encrypt($value, bool $serialize = true): ?string 20 | { 21 | throw new EncryptException('Operation not supported.'); 22 | } 23 | 24 | public function decrypt(?string $payload, bool $unserialize = true) 25 | { 26 | if (is_null($payload)) { 27 | return null; 28 | } 29 | 30 | if ($this->grammar instanceof PostgresGrammar) { 31 | $grammar = $this->getPostgresGrammarDecrypt(); 32 | } else { 33 | $grammar = $this->getMysqlGrammarDecrypt(); 34 | } 35 | 36 | return sprintf( 37 | $grammar, 38 | $payload, 39 | $this->getEncryptionKey(), 40 | $this->getEncryptionCipherAlgorithm() 41 | ); 42 | } 43 | 44 | protected function getMysqlGrammarDecrypt(): string 45 | { 46 | return "CONVERT( SUBSTRING_INDEX( AES_DECRYPT( FROM_BASE64(%s), '%s' ), ':', -1 ) USING 'UTF8' )"; 47 | } 48 | 49 | protected function getPostgresGrammarDecrypt(): string 50 | { 51 | return "split_part( convert_from( decrypt( decode(%s, 'base64'), '%s', '%s'), 'UTF8' ), ':', 3 )"; 52 | } 53 | 54 | protected function getEncryptionCipherAlgorithm(): string 55 | { 56 | $cipher = $this->getEncryptionCipher(); 57 | 58 | $algorithm = Str::before($cipher, '-'); 59 | $mode = Str::afterLast($cipher, '-'); 60 | 61 | return "{$algorithm}-{$mode}"; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maize-tech/laravel-encryptable", 3 | "description": "Laravel Encryptable", 4 | "keywords": [ 5 | "maize-tech", 6 | "laravel", 7 | "encryptable", 8 | "encrypt", 9 | "anonymize" 10 | ], 11 | "homepage": "https://github.com/maize-tech/laravel-encryptable", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Enrico De Lazzari", 16 | "email": "enrico.delazzari@maize.io", 17 | "role": "Developer" 18 | }, 19 | { 20 | "name": "Riccardo Dalla Via", 21 | "email": "riccardo.dallavia@maize.io", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.2", 27 | "ext-openssl": "*", 28 | "illuminate/contracts": "^11.0|^12.0", 29 | "illuminate/database": "^11.0|^12.0", 30 | "illuminate/support": "^11.0|^12.0", 31 | "illuminate/validation": "^11.0|^12.0", 32 | "spatie/laravel-package-tools": "^1.14.1" 33 | }, 34 | "require-dev": { 35 | "larastan/larastan": "^3.7", 36 | "laravel/pint": "^1.25", 37 | "orchestra/testbench": "^9.0|^10.0", 38 | "pestphp/pest": "^3.8", 39 | "pestphp/pest-plugin-laravel": "^3.2", 40 | "phpunit/phpunit": "^10.5|^11.0" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Maize\\Encryptable\\": "src" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Maize\\Encryptable\\Tests\\": "tests" 50 | } 51 | }, 52 | "scripts": { 53 | "format": "vendor/bin/pint", 54 | "analyse": "vendor/bin/phpstan analyse", 55 | "test": "vendor/bin/pest", 56 | "test-coverage": "vendor/bin/pest --coverage" 57 | }, 58 | "config": { 59 | "sort-packages": true, 60 | "allow-plugins": { 61 | "pestphp/pest-plugin": true 62 | } 63 | }, 64 | "extra": { 65 | "laravel": { 66 | "providers": [ 67 | "Maize\\Encryptable\\EncryptableServiceProvider" 68 | ] 69 | } 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /src/PHPEncrypter.php: -------------------------------------------------------------------------------- 1 | isEncrypted($value)) { 19 | return $value; 20 | } 21 | 22 | if ($serialize) { 23 | $value = Serializer::serialize($value); 24 | } 25 | 26 | $value = $this->addDirtyBit($value); 27 | 28 | $value = $this->openSSLEncrypt($value); 29 | 30 | $value = $this->base64Encode($value); 31 | 32 | return $value; 33 | } 34 | 35 | public function decrypt(?string $payload, bool $unserialize = true) 36 | { 37 | if (is_null($payload)) { 38 | return null; 39 | } 40 | 41 | if (! $this->isEncrypted($payload)) { 42 | return $payload; 43 | } 44 | 45 | $payload = $this->base64Decode($payload); 46 | 47 | $payload = $this->openSSLDecrypt($payload); 48 | 49 | $payload = $this->removeDirtyBit($payload); 50 | 51 | if ($unserialize) { 52 | $payload = Serializer::unserialize($payload); 53 | } 54 | 55 | return $payload; 56 | } 57 | 58 | public function isEncrypted($value): bool 59 | { 60 | try { 61 | $value = $this->base64Decode($value); 62 | 63 | $value = $this->openSSLDecrypt($value); 64 | 65 | return Str::startsWith($value, self::DIRTY_BIT_KEY); 66 | } catch (\Exception $e) { 67 | return false; 68 | } 69 | } 70 | 71 | protected function addDirtyBit(string $value): string 72 | { 73 | return Str::start($value, self::DIRTY_BIT_KEY); 74 | } 75 | 76 | protected function openSSLEncrypt(string $value): string 77 | { 78 | $value = openssl_encrypt( 79 | $value, 80 | $this->getEncryptionCipher(), 81 | $this->getEncryptionKey(), 82 | OPENSSL_RAW_DATA 83 | ); 84 | 85 | if (! $value) { 86 | throw new EncryptException('Could not encrypt the data.'); 87 | } 88 | 89 | return $value; 90 | } 91 | 92 | protected function base64Encode(string $value): string 93 | { 94 | $value = base64_encode($value); 95 | 96 | if (! $value) { 97 | throw new EncryptException('Could not encrypt the data.'); 98 | } 99 | 100 | return $value; 101 | } 102 | 103 | protected function base64Decode(string $payload): string 104 | { 105 | $payload = base64_decode($payload); 106 | 107 | if (! $payload) { 108 | throw new DecryptException('Could not decrypt the data.'); 109 | } 110 | 111 | return $payload; 112 | } 113 | 114 | protected function openSSLDecrypt(string $payload): string 115 | { 116 | $payload = openssl_decrypt( 117 | $payload, 118 | $this->getEncryptionCipher(), 119 | $this->getEncryptionKey(), 120 | OPENSSL_RAW_DATA 121 | ); 122 | 123 | if (! $payload) { 124 | throw new DecryptException('Could not decrypt the data.'); 125 | } 126 | 127 | return $payload; 128 | } 129 | 130 | protected function removeDirtyBit(string $payload): string 131 | { 132 | if (! Str::startsWith($payload, self::DIRTY_BIT_KEY)) { 133 | throw new DecryptException('Could not decrypt the data.'); 134 | } 135 | 136 | return Str::after($payload, self::DIRTY_BIT_KEY); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | 6 | Social Card of Laravel Encryptable 7 | 8 |

9 | 10 | # Laravel Encryptable 11 | 12 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/maize-tech/laravel-encryptable.svg?style=flat-square)](https://packagist.org/packages/maize-tech/laravel-encryptable) 13 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/maize-tech/laravel-encryptable/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/maize-tech/laravel-encryptable/actions?query=workflow%3Arun-tests+branch%3Amain) 14 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/maize-tech/laravel-encryptable/php-cs-fixer.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/maize-tech/laravel-encryptable/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 15 | [![Total Downloads](https://img.shields.io/packagist/dt/maize-tech/laravel-encryptable.svg?style=flat-square)](https://packagist.org/packages/maize-tech/laravel-encryptable) 16 | 17 | 18 | This package allows you to anonymize sensitive data (like the name, surname and email address of a user) similarly to Laravel's Encryption feature, but still have the ability to make direct queries to the database. 19 | An example use case could be the need to make search queries through anonymized attributes. 20 | 21 | This package currently supports `MySQL` and `PostgreSQL` databases. 22 | 23 | ## Installation 24 | 25 | You can install the package via composer: 26 | 27 | ```bash 28 | composer require maize-tech/laravel-encryptable 29 | ``` 30 | 31 | You can publish the config file with: 32 | ```bash 33 | php artisan vendor:publish --provider="Maize\Encryptable\EncryptableServiceProvider" --tag="encryptable-config" 34 | ``` 35 | 36 | This is the content of the published config file: 37 | 38 | ```php 39 | return [ 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Encryption key 43 | |-------------------------------------------------------------------------- 44 | | 45 | | The key used to encrypt data. 46 | | Once defined, never change it or encrypted data cannot be correctly decrypted. 47 | | 48 | */ 49 | 50 | 'key' => env('ENCRYPTION_KEY'), 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Encryption cipher 55 | |-------------------------------------------------------------------------- 56 | | 57 | | The cipher used to encrypt data. 58 | | Once defined, never change it or encrypted data cannot be correctly decrypted. 59 | | Default value is the cipher algorithm used by default in MySQL. 60 | | 61 | */ 62 | 63 | 'cipher' => env('ENCRYPTION_CIPHER', 'aes-128-ecb'), 64 | ]; 65 | ``` 66 | 67 | ## Usage 68 | 69 | ### Basic 70 | 71 | To use the package, just add the `Encryptable` cast to all model attributes you want to anonymize. 72 | 73 | ``` php 74 | Encryptable::class, 90 | 'email' => Encryptable::class, 91 | ]; 92 | } 93 | ``` 94 | 95 | Once done, all values will be encrypted before being stored in the database, and decrypted when querying them via Eloquent. 96 | 97 | ### Manually encrypt via PHP 98 | 99 | ``` php 100 | use Maize\Encryptable\Encryption; 101 | 102 | $value = "your-decrypted-value"; 103 | 104 | $encrypted = Encryption::php()->encrypt($value); // returns the encrypted value 105 | ``` 106 | 107 | ### Manually decrypt via PHP 108 | 109 | ``` php 110 | use Maize\Encryptable\Encryption; 111 | 112 | $encrypted = "your-encrypted-value"; 113 | 114 | $value = Encryption::php()->decrypt($value); // returns the decrypted value 115 | ``` 116 | 117 | ### Manually decrypt via DB 118 | 119 | ``` php 120 | use Maize\Encryptable\Encryption; 121 | 122 | $encrypted = "your-encrypted-value"; 123 | 124 | $encryptedQuery = Encryption::db()->encrypt($value); // returns the query used to find the decrypted value 125 | ``` 126 | 127 | ### Custom validation rules 128 | 129 | You can use one of the two custom rules to check the uniqueness or existence of a given encryptable value. 130 | 131 | `ExistsEncrypted` is an extension of Laravel's `Exists` rule, whereas `UniqueEncrypted` is an extension of Laravel's `Unique` rule. 132 | You can use them in the same way as Laravel's base rules: 133 | ``` php 134 | use Maize\Encryptable\Rules\ExistsEncrypted; 135 | use Illuminate\Support\Facades\Validator; 136 | use Illuminate\Validation\Rule; 137 | 138 | $data = [ 139 | 'email' => 'email@example.com', 140 | ]; 141 | 142 | Validator::make($data, [ 143 | 'email' => [ 144 | 'required', 145 | 'string', 146 | 'email', 147 | new ExistsEncrypted('users'), // checks whether the given email exists in the database 148 | Rule::existsEncrypted('users') // alternative way to invoke the rule 149 | ], 150 | ]); 151 | ``` 152 | 153 | ``` php 154 | use Maize\Encryptable\Rules\UniqueEncrypted; 155 | use Illuminate\Support\Facades\Validator; 156 | 157 | $data = [ 158 | 'email' => 'email@example.com', 159 | ]; 160 | 161 | Validator::make($data, [ 162 | 'email' => [ 163 | 'required', 164 | 'string', 165 | 'email', 166 | new UniqueEncrypted('users'), // checks whether the given email does not already exist in the database 167 | Rule::uniqueEncrypted('users') // alternative way to invoke the rule 168 | ], 169 | ]); 170 | ``` 171 | 172 | ## Testing 173 | 174 | ```bash 175 | composer test 176 | ``` 177 | 178 | ## Changelog 179 | 180 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 181 | 182 | ## Contributing 183 | 184 | Please see [CONTRIBUTING](https://github.com/maize-tech/.github/blob/main/CONTRIBUTING.md) for details. 185 | 186 | ## Security Vulnerabilities 187 | 188 | Please review [our security policy](https://github.com/maize-tech/.github/security/policy) on how to report security vulnerabilities. 189 | 190 | ## Credits 191 | 192 | - [Enrico De Lazzari](https://github.com/enricodelazzari) 193 | - [Riccardo Dalla Via](https://github.com/riccardodallavia) 194 | - [All Contributors](../../contributors) 195 | 196 | ## License 197 | 198 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 199 | --------------------------------------------------------------------------------