├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── app-key-rotator.php └── src ├── Actions ├── ActionsCollection.php ├── BackupEnvAction.php ├── BeforeActionsCollection.php ├── ReEncryptModels.php └── RotateKeyAction.php ├── AppKeyRotator.php ├── AppKeyRotatorServiceProvider.php ├── Commands └── RotateAppKeyCommand.php ├── Contracts ├── BeforeRotatorAction.php ├── ReEncryptsData.php └── RotatorAction.php ├── Exceptions └── AppKeyNotSetException.php └── Support └── Encrypter.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-app-key-rotator` will be documented in this file 4 | 5 | ## v3.1.1 - 2023-03-17 6 | 7 | ### What's Changed 8 | 9 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/8 10 | - Bump aglipanci/laravel-pint-action from 1.0.0 to 2.1.0 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/7 11 | - Bump creyD/prettier_action from 4.2 to 4.3 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/10 12 | - Laravel 10.x Compatibility by @rawilk in https://github.com/rawilk/laravel-app-key-rotator/pull/9 13 | - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/11 14 | - Add support for php 8.2 15 | 16 | **Full Changelog**: https://github.com/rawilk/laravel-app-key-rotator/compare/v3.1.0...v3.1.1 17 | 18 | ## v3.1.0 - 2022-11-08 19 | 20 | ### What's Changed 21 | 22 | - Fix install issue when resolving the app instance in our service provider 23 | - Add ability to define actions to be run prior to anything else 24 | - Add `BackupEnvAction` that can be run in the new before actions 25 | - Only register bindings necessary for key rotation when the app is running in the console 26 | 27 | **Full Changelog**: https://github.com/rawilk/laravel-app-key-rotator/compare/v3.0.0...v3.1.0 28 | 29 | ## v3.0.0 - 2022-11-07 30 | 31 | ### What's Changed 32 | 33 | - Bump dependabot/fetch-metadata from 1.3.4 to 1.3.5 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/3 34 | - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/2 35 | - Bump creyD/prettier_action from 3.0 to 4.2 by @dependabot in https://github.com/rawilk/laravel-app-key-rotator/pull/4 36 | - App key rotation now performed from dedicated action class instead of directly in the command 37 | - Custom actions now support custom arguments to be passed into their constructors 38 | - `cursor()` is now used instead of `chunk()` in default model re-encryption action 39 | 40 | ### Breaking Changes 41 | 42 | - Drop PHP 7.4 support 43 | - Drop Laravel 8.0 support 44 | - Package config and `AppKeyRotator` instance are now passed into `handle()` instead of `__construct` on each action 45 | - `models` config key now expects a single dimensional array of model classes 46 | - Models requiring re-encryption must implement the `\Rawilk\AppKeyRotator\Contracts\ReEncryptsData` contract and implement the `encryptedProperties` method 47 | 48 | **Full Changelog**: https://github.com/rawilk/laravel-app-key-rotator/compare/v2.0.1...v3.0.0 49 | 50 | ## 2.0.1 - 2022-02-23 51 | 52 | ### Updated 53 | 54 | - Add support for Laravel 9.* 55 | - Add support for PHP 8.0 56 | - Add support for PHP 8.1 57 | 58 | ## 2.0.0 - 2020-09-09 59 | 60 | ### Added 61 | 62 | - Add support for Laravel 8 63 | 64 | ### Removed 65 | 66 | - Drop support for Laravel 7 67 | 68 | ## 1.0.0 - 2020-07-23 69 | 70 | - initial release 71 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Randall Wilk 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App Key Rotator for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/rawilk/laravel-app-key-rotator.svg?style=flat-square)](https://packagist.org/packages/rawilk/laravel-app-key-rotator) 4 | ![Tests](https://github.com/rawilk/laravel-app-key-rotator/workflows/Tests/badge.svg) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/rawilk/laravel-app-key-rotator.svg?style=flat-square)](https://packagist.org/packages/rawilk/laravel-app-key-rotator) 6 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/rawilk/laravel-app-key-rotator?style=flat-square)](https://packagist.org/packages/rawilk/laravel-app-key-rotator) 7 | [![License](https://img.shields.io/github/license/rawilk/laravel-app-key-rotator?style=flat-square)](https://github.com/rawilk/laravel-app-key-rotator/blob/main/LICENSE.md) 8 | 9 | ![Social image](https://banners.beyondco.de/laravel-app-key-rotator.png?theme=light&packageManager=composer+require&packageName=rawilk%2Flaravel-app-key-rotator&pattern=endlessClouds&style=style_1&description=Rotate+app+keys+around+while+re-encrypting+data.&md=1&showWatermark=0&fontSize=100px&images=refresh) 10 | 11 | Changing your `APP_KEY` can be as simple as running `php artisan key:generate`, but what about your encrypted model data? This is where Laravel App Key Rotator comes in. This package can help with generating a new app key for you, as well as decrypting and re-encrypting your model automatically for you through an artisan command. 12 | 13 | It's also generally a good practice to rotate your app keys periodically (e.g. every 6 months) or when certain events happen, such as an employee leaving the company. See more information here: https://tighten.co/blog/app-key-and-you/ 14 | 15 | ## Basic Usage 16 | 17 | Rotating your app keys is as simple as running this artisan command: 18 | 19 | ```bash 20 | php artisan app-key-rotator:rotate 21 | ``` 22 | 23 | ## Installation 24 | 25 | You can install the package via composer: 26 | 27 | ```bash 28 | composer require rawilk/laravel-app-key-rotator 29 | ``` 30 | 31 | You can publish the config file with: 32 | 33 | ```bash 34 | php artisan vendor:publish --tag="app-key-rotator-config" 35 | ``` 36 | 37 | You can view the default configuration file here: https://github.com/rawilk/laravel-app-key-rotator/blob/main/config/app-key-rotator.php 38 | 39 | ## Documentation 40 | 41 | For documentation, please visit: https://randallwilk.dev/docs/laravel-app-key-rotator 42 | 43 | ## Testing 44 | 45 | ```bash 46 | composer test 47 | ``` 48 | 49 | ## Changelog 50 | 51 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 52 | 53 | ## Contributing 54 | 55 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 56 | 57 | ## Security 58 | 59 | If you discover any security related issues, please email randall@randallwilk.dev instead of using the issue tracker. 60 | 61 | ## Credits 62 | 63 | - [Randall Wilk](https://github.com/rawilk) 64 | - [All Contributors](../../contributors) 65 | 66 | ## Disclaimer 67 | 68 | This package is not affiliated with, maintained, authorized, endorsed or sponsored by Laravel or any of its affiliates. 69 | 70 | ## License 71 | 72 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rawilk/laravel-app-key-rotator", 3 | "description": "Rotate app keys around while re-encrypting data.", 4 | "keywords": [ 5 | "rawilk", 6 | "laravel-app-key-rotator", 7 | "app key" 8 | ], 9 | "homepage": "https://github.com/rawilk/laravel-app-key-rotator", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Randall Wilk", 14 | "email": "randall@randallwilk.dev", 15 | "homepage": "https://randallwilk.dev", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.0|^8.1|^8.2", 21 | "illuminate/support": "^9.0|^10.0", 22 | "jackiedo/dotenv-editor": "^2.0", 23 | "spatie/laravel-package-tools": "^1.13" 24 | }, 25 | "require-dev": { 26 | "laravel/pint": "^1.5", 27 | "orchestra/testbench": "^7.0|^8.0", 28 | "pestphp/pest": "^1.22", 29 | "pestphp/pest-plugin-laravel": "^1.3", 30 | "spatie/laravel-ray": "^1.31" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Rawilk\\AppKeyRotator\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Rawilk\\AppKeyRotator\\Tests\\": "tests", 40 | "Rawilk\\AppKeyRotator\\Tests\\Database\\Factories\\": "tests/database/factories" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/pest", 45 | "format": "vendor/bin/pint --dirty" 46 | }, 47 | "config": { 48 | "sort-packages": true, 49 | "allow-plugins": { 50 | "pestphp/pest-plugin": true 51 | } 52 | }, 53 | "extra": { 54 | "laravel": { 55 | "providers": [ 56 | "Rawilk\\AppKeyRotator\\AppKeyRotatorServiceProvider" 57 | ] 58 | } 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true 62 | } 63 | -------------------------------------------------------------------------------- /config/app-key-rotator.php: -------------------------------------------------------------------------------- 1 | env('OLD_APP_KEY'), 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Models 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Include any models here that have fields that need to be re-encrypted. 21 | | Each model class must implement the \Rawilk\AppKeyRotator\Contracts\ReEncryptsData 22 | | interface. 23 | | 24 | */ 25 | 'models' => [], 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Actions 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Any actions here will be run after a new app key has been generated and 33 | | saved to your .env file. 34 | | 35 | | Each action must implement the \Rawilk\AppKeyRotator\Contracts\RotatorAction interface. 36 | | 37 | | Every action receives the package's config and an instance of the AppKeyRotator 38 | | through the `handle` method. 39 | | 40 | */ 41 | 'actions' => [ 42 | \Rawilk\AppKeyRotator\Actions\ReEncryptModels::class, // a custom model re-encrypter should extend this class 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Before Actions 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Any actions here will be run BEFORE anything else when rotating the app 51 | | key. Can be useful if you want to automate something like backing 52 | | up the .env file first. 53 | | 54 | | Each action must implement the \Rawilk\AppKeyRotator\Contracts\BeforeRotatorAction interface. 55 | | 56 | | Every action receives the package's config through the `handle` method. 57 | | 58 | */ 59 | 'before_actions' => [ 60 | // \Rawilk\AppKeyRotator\Actions\BackupEnvAction::class => ['filename' => env('ENV_BACKUP_FILENAME', '.env.backup')] 61 | ], 62 | ]; 63 | -------------------------------------------------------------------------------- /src/Actions/ActionsCollection.php: -------------------------------------------------------------------------------- 1 | map(function ($actionParameters, $actionClass) { 16 | if (is_array($actionParameters) && is_numeric($actionClass)) { 17 | $actionClass = array_key_first($actionParameters); 18 | $actionParameters = $actionParameters[$actionClass]; 19 | } 20 | 21 | if (is_numeric($actionClass)) { 22 | $actionClass = $actionParameters; 23 | $actionParameters = []; 24 | } 25 | 26 | return app()->makeWith($actionClass, $actionParameters); 27 | }) 28 | ->toArray(); 29 | 30 | parent::__construct($actions); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Actions/BackupEnvAction.php: -------------------------------------------------------------------------------- 1 | filename), 19 | file_get_contents(base_path('.env')), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Actions/BeforeActionsCollection.php: -------------------------------------------------------------------------------- 1 | getModels($config['models'] ?? []); 18 | 19 | $models->each(fn (string $model) => $this->reEncryptModel($model, $appKeyRotator)); 20 | } 21 | 22 | protected function reEncryptModel(string $modelClass, AppKeyRotator $appKeyRotator): void 23 | { 24 | $encryptedProperties = $modelClass::make()->encryptedProperties(); 25 | 26 | $modelClass::query() 27 | ->select(['id', ...$encryptedProperties]) 28 | ->cursor() 29 | ->each(function (Model $model) use ($appKeyRotator, $encryptedProperties) { 30 | // We get the attributes here to prevent any accessors or mutators from trying to 31 | // encrypt/decrypt values with the wrong encryption keys. 32 | $attributes = $model->getAttributes(); 33 | 34 | foreach ($encryptedProperties as $field) { 35 | $attributes[$field] = $appKeyRotator->reEncrypt($attributes[$field]); 36 | } 37 | 38 | $model->setRawAttributes($attributes); 39 | 40 | $model->timestamps = false; 41 | 42 | $model->saveQuietly(); 43 | }); 44 | } 45 | 46 | /** 47 | * @return \Illuminate\Support\Collection 48 | */ 49 | protected function getModels(array $models): Collection 50 | { 51 | return collect($models) 52 | ->filter(function (string $model) { 53 | $instance = app($model); 54 | 55 | return $instance instanceof ReEncryptsData; 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Actions/RotateKeyAction.php: -------------------------------------------------------------------------------- 1 | false, 18 | ]); 19 | 20 | $this->env->autoBackup(false); 21 | } 22 | 23 | /** 24 | * @return array 25 | */ 26 | public function __invoke(): array 27 | { 28 | $currentKey = $this->getCurrentKey(); 29 | 30 | $this->writeToEnv( 31 | 'OLD_APP_KEY', 32 | $currentKey, 33 | 'Rotated at: ' . now()->toDateTimeString() . ' ' . now()->tzName, 34 | ); 35 | 36 | config([ 37 | 'app-key-rotator.old_app_key' => $currentKey, 38 | ]); 39 | 40 | Artisan::call('key:generate', [ 41 | '--force' => true, 42 | ]); 43 | 44 | return [ 45 | 'old' => $currentKey, 46 | 'new' => config('app.key'), 47 | ]; 48 | } 49 | 50 | private function getCurrentKey(): string 51 | { 52 | $key = config('app.key'); 53 | 54 | throw_unless($key, AppKeyNotSetException::keyNotSet()); 55 | 56 | return $key; 57 | } 58 | 59 | private function writeToEnv(string $key, $value, ?string $comment = null): void 60 | { 61 | $this->env->setKey($key, $value, $comment)->save(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AppKeyRotator.php: -------------------------------------------------------------------------------- 1 | oldAppKey = $this->normalizeKey($oldAppKey); 19 | $this->newAppKey = $this->normalizeKey($newAppKey); 20 | 21 | $this->createEncrypters(); 22 | } 23 | 24 | public function reEncrypt($value): string 25 | { 26 | return $this->newEncrypter->encrypt($this->oldEncrypter->decrypt($value)); 27 | } 28 | 29 | public function createEncrypters(): void 30 | { 31 | $cipher = config('app.cipher'); 32 | 33 | if ($this->oldAppKey) { 34 | $this->oldEncrypter = new Encrypter($this->oldAppKey, $cipher); 35 | } 36 | 37 | if ($this->newAppKey) { 38 | $this->newEncrypter = new Encrypter($this->newAppKey, $cipher); 39 | } 40 | } 41 | 42 | protected function normalizeKey(string $key): string 43 | { 44 | if (Str::startsWith($key, 'base64:')) { 45 | return base64_decode(substr($key, 7)); 46 | } 47 | 48 | return $key; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AppKeyRotatorServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-app-key-rotator') 24 | ->hasConfigFile() 25 | ->hasCommands([ 26 | RotateAppKeyCommand::class, 27 | ]); 28 | } 29 | 30 | public function packageRegistered(): void 31 | { 32 | // We'll use our custom Encrypter to allow for decrypting with the previous app key in case 33 | // using the current key fails. 34 | $this->app->singleton('encrypter', function () { 35 | $config = Container::getInstance()->make('config')['app']; 36 | 37 | if (Str::startsWith($key = $config['key'], 'base64:')) { 38 | $key = base64_decode(substr($key, 7)); 39 | } 40 | 41 | return new Encrypter($key, $config['cipher']); 42 | }); 43 | 44 | // The rest of our bindings are only needed in the console. 45 | if (! $this->app->runningInConsole()) { 46 | return; 47 | } 48 | 49 | $this->app->singleton(ActionsCollection::class, function ($app) { 50 | $actionClassNames = $app['config']->get('app-key-rotator.actions', []) ?? []; 51 | 52 | return new ActionsCollection($actionClassNames); 53 | }); 54 | 55 | $this->app->singleton(BeforeActionsCollection::class, function ($app) { 56 | $actionClassNames = $app['config']->get('app-key-rotator.before_actions', []) ?? []; 57 | 58 | return new BeforeActionsCollection($actionClassNames); 59 | }); 60 | 61 | $this->app->singleton(RotateKeyAction::class, function ($app) { 62 | return new RotateKeyAction($app['dotenv-editor']); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Commands/RotateAppKeyCommand.php: -------------------------------------------------------------------------------- 1 | runBeforeActions($beforeActions); 24 | 25 | $keys = app(RotateKeyAction::class)(); 26 | 27 | $appKeyRotator = new AppKeyRotator($keys['old'], $keys['new']); 28 | $config = config('app-key-rotator'); 29 | 30 | $actions->each(function (RotatorAction $action) use ($appKeyRotator, $config) { 31 | $action->handle($appKeyRotator, $config); 32 | }); 33 | 34 | $this->info( 35 | "App key was changed from [{$keys['old']}] to [{$keys['new']}]." 36 | ); 37 | } 38 | 39 | protected function runBeforeActions(BeforeActionsCollection $actions): void 40 | { 41 | $config = config('app-key-rotator'); 42 | 43 | $actions->each(function (BeforeRotatorAction $action) use ($config) { 44 | $action->handle($config); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Contracts/BeforeRotatorAction.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function encryptedProperties(): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Contracts/RotatorAction.php: -------------------------------------------------------------------------------- 1 | key; 19 | 20 | $this->key = Str::startsWith(config('app-key-rotator.old_app_key'), 'base64:') 21 | ? base64_decode(substr(config('app-key-rotator.old_app_key'), 7)) 22 | : config('app-key-rotator.old_app_key'); 23 | 24 | return tap(parent::decrypt($payload, $unserialize), function () use ($currentKey) { 25 | $this->key = $currentKey; 26 | }); 27 | } 28 | } 29 | } 30 | --------------------------------------------------------------------------------