2019-09-03 11:03:32 -07:00
< ? php
namespace App\Console\Commands ;
use App\Models\Asset ;
2021-06-10 13:15:52 -07:00
use App\Models\CustomField ;
2019-09-03 11:03:32 -07:00
use App\Models\Setting ;
2021-06-10 13:15:52 -07:00
use Artisan ;
use Illuminate\Console\Command ;
2023-11-28 05:49:58 -08:00
use Illuminate\Contracts\Encryption\DecryptException ;
2021-06-10 13:15:52 -07:00
use Illuminate\Encryption\Encrypter ;
2019-09-03 11:03:32 -07:00
class RotateAppKey extends Command
{
/**
* The name and signature of the console command .
*
* @ var string
*/
2023-11-28 05:49:58 -08:00
protected $signature = ' snipeit : rotate - key
{ previous_key ? : The previous key to rotate from }
{ -- emergency : Emergency mode - rotate from . env APP_KEY to newly - generated one , modifying . env }
{ -- force : Skip interactive confirmation } ' ;
2019-09-03 11:03:32 -07:00
/**
* The console command description .
*
* @ var string
*/
2023-11-28 05:49:58 -08:00
protected $description = 'Rotates APP_KEY to a new value, optionally taking the previous key as an argument' ;
2019-09-03 11:03:32 -07:00
/**
* Create a new command instance .
*
* @ return void
*/
public function __construct ()
{
parent :: __construct ();
}
/**
* Execute the console command .
*
* @ return mixed
*/
public function handle ()
{
2023-11-28 05:49:58 -08:00
//make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both.
if ( ( ! $this -> option ( 'emergency' ) && ! $this -> argument ( 'previous_key' )) || ( $this -> option ( 'emergency' ) && $this -> argument ( 'previous_key' ))) {
$this -> error ( " Specify only one of --emergency, or an app key value, in order to rotate keys " );
return 1 ;
}
if ( $this -> option ( 'emergency' ) ) {
$msg = " \n **************************************************** \n THIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \n RE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n \n There is NO undo. \n \n Make SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n \n If you do not save the newly generated APP_KEY to your .env in this process, \n your encrypted data will no longer be decryptable. \n \n Are you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? " ;
} else {
$msg = " \n **************************************************** \n THIS WILL DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND RE-ENCRYPT THEM WITH YOUR \n APP_KEY. \n \n There is NO undo. \n \n Make SURE you have a database backup BEFORE running this command. \n \n Are you SURE you wish to continue, and have confirmed you have a database backup? " ;
}
if ( $this -> option ( 'force' ) || $this -> confirm ( $msg )) {
2019-09-03 11:03:32 -07:00
// Get the existing app_key and ciphers
// We put them in a variable since we clear the cache partway through here.
2023-11-28 05:49:58 -08:00
if ( $this -> option ( 'emergency' )) {
$old_app_key = config ( 'app.key' );
$cipher = config ( 'app.cipher' );
// Generate a new one
Artisan :: call ( 'key:generate' , [ '--show' => true ]);
$new_app_key = trim ( Artisan :: output ());
// Clear the config cache
Artisan :: call ( 'config:clear' );
// Write the new app key to the .env file
$this -> writeNewEnvironmentFileWith ( $new_app_key );
} elseif ( $this -> argument ( 'previous_key' )) {
$old_app_key = $this -> argument ( 'previous_key' );
$cipher = config ( 'app.cipher' ); // just a guess?
$new_app_key = config ( 'app.key' );
}
2019-09-03 11:03:32 -07:00
2023-11-28 05:49:58 -08:00
$this -> warn ( 'Your app cipher is: ' . $cipher );
$this -> warn ( 'Your old APP_KEY is: ' . $old_app_key );
$this -> warn ( 'Your new APP_KEY is: ' . $new_app_key );
2019-09-03 11:03:32 -07:00
// Manually create an old encrypter instance using the old app key
// and also create a new encrypter instance so we can re-crypt the field
// using the newly generated app key
$oldEncrypter = new Encrypter ( base64_decode ( substr ( $old_app_key , 7 )), $cipher );
$newEncrypter = new Encrypter ( base64_decode ( substr ( $new_app_key , 7 )), $cipher );
$fields = CustomField :: where ( 'field_encrypted' , '1' ) -> get ();
foreach ( $fields as $field ) {
$assets = Asset :: whereNotNull ( $field -> db_column ) -> get ();
foreach ( $assets as $asset ) {
2023-11-28 05:49:58 -08:00
try {
$asset -> { $field -> db_column } = $oldEncrypter -> decrypt ( $asset -> { $field -> db_column });
$this -> line ( 'DECRYPTED: ' . $field -> db_column );
} catch ( DecryptException $e ) {
$this -> line ( 'Could not decrypt ' . $field -> db_column . ' using "old key" - skipping...' );
continue ;
} catch ( \Exception $e ) {
$this -> error ( " Error decrypting " . $field -> db_column . " , reason: " . $e -> getMessage () . " . Aborting key rotation " );
throw $e ;
}
2019-09-03 11:03:32 -07:00
$asset -> { $field -> db_column } = $newEncrypter -> encrypt ( $asset -> { $field -> db_column });
$this -> line ( 'ENCRYPTED: ' . $field -> db_column );
$asset -> save ();
}
}
// Handle the LDAP password if one is provided
$setting = Setting :: first ();
2021-06-10 13:15:52 -07:00
if ( $setting -> ldap_pword != '' ) {
2023-11-28 05:49:58 -08:00
try {
$setting -> ldap_pword = $oldEncrypter -> decrypt ( $setting -> ldap_pword );
$setting -> ldap_pword = $newEncrypter -> encrypt ( $setting -> ldap_pword );
$setting -> save ();
$this -> warn ( 'LDAP password has been re-encrypted.' );
} catch ( DecryptException $e ) {
$this -> warn ( " Unable to decrypt old LDAP password; skipping " );
}
2019-09-03 11:03:32 -07:00
}
} else {
$this -> info ( 'This operation has been canceled. No changes have been made.' );
}
}
/**
* Write a new environment file with the given key .
*
* @ param string $key
* @ return void
*/
protected function writeNewEnvironmentFileWith ( $key )
{
file_put_contents ( $this -> laravel -> environmentFilePath (), preg_replace (
$this -> keyReplacementPattern (),
2023-11-28 05:49:58 -08:00
'APP_KEY="' . $key . '"' ,
2019-09-03 11:03:32 -07:00
file_get_contents ( $this -> laravel -> environmentFilePath ())
));
}
/**
* Get a regex pattern that will match env APP_KEY with any random key .
*
* @ return string
*/
protected function keyReplacementPattern ()
{
2023-11-28 05:49:58 -08:00
$escaped = '="?' . preg_quote ( $this -> laravel [ 'config' ][ 'app.key' ], '/' ) . '"?' ;
2021-06-10 13:15:52 -07:00
2019-09-03 11:03:32 -07:00
return " /^APP_KEY { $escaped } /m " ;
}
}