LibreNMS/app/Console/Commands/KeyRotate.php

169 lines
4.8 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Console\LnmsCommand;
use Artisan;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use LibreNMS\Util\EnvHelper;
use Symfony\Component\Console\Input\InputArgument;
class KeyRotate extends LnmsCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'key:rotate';
/**
* @var Encrypter
*/
private $decrypt;
/**
* @var Encrypter
*/
private $encrypt;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->addArgument('old_key', InputArgument::OPTIONAL);
$this->addOption('generate-new-key');
$this->addOption('forgot-key');
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$new = config('app.key');
$cipher = config('app.cipher');
$this->validate([
'generate-new-key' => [
'exclude_unless:old_key,null',
'boolean',
],
'forgot-key' => [
'exclude_unless:old_key,null',
'boolean',
],
'old_key' => [
'exclude_if:generate-new-key,true',
'exclude_if:forgot-key,true',
'required',
'starts_with:base64:',
Rule::notIn([$new]),
],
]);
// check for cached config
if (is_file(base_path('bootstrap/cache/config.php'))) {
Artisan::call('config:clear'); // clear config cache
$this->warn(trans('commands.key:rotate.cleared-cache'));
return 0;
}
if ($this->option('forgot-key')) {
$this->line(trans('commands.key:rotate.current_key', ['key' => $new]));
$this->error(trans('commands.key:rotate.backup_key'));
$this->newLine();
$this->error(trans('commands.key:rotate.destroy'));
if ($this->confirm(trans('commands.key:rotate.destroy_confirm'))) {
\App\Models\Config::where('config_name', 'validation.encryption.test')->delete();
return 0;
}
return 1;
}
$old = $this->argument('old_key');
if ($this->option('generate-new-key')) {
$old = $new; // use key in env as existing key
$new = 'base64:' . base64_encode(
Encrypter::generateKey($this->laravel['config']['app.cipher'])
);
}
$this->line(trans('commands.key:rotate.old_key', ['key' => $old]));
$this->line(trans('commands.key:rotate.new_key', ['key' => $new]));
$this->error(trans('commands.key:rotate.backup_keys'));
$this->newLine();
// init encrypters
$this->decrypt = $this->createEncrypter($old, $cipher);
$this->encrypt = $this->createEncrypter($new, $cipher);
$this->line(trans('commands.key:rotate.backups'));
if (! $this->confirm(trans('commands.key:rotate.confirm'))) {
return 1;
}
$success = $this->rekeyConfigData('validation.encryption.test');
if (! $success) {
$this->line(trans('commands.key:rotate.old_key', ['key' => $old]));
$this->line(trans('commands.key:rotate.new_key', ['key' => $new]));
$this->error(trans('commands.key:rotate.failed'));
return 1;
}
$this->info(trans('commands.key:rotate.success'));
if ($this->option('generate-new-key') && $this->confirm(trans('commands.key:rotate.save_key'))) {
EnvHelper::writeEnv([
'OLD_APP_KEY' => $old,
'APP_KEY' => $new,
], ['OLD_APP_KEY', 'APP_KEY']);
}
return 0;
}
private function createEncrypter(string $key, string $cipher): Encrypter
{
return new Encrypter(base64_decode(Str::after($key, 'base64:')), $cipher);
}
private function rekeyConfigData(string $key): bool
{
if (! \LibreNMS\Config::has($key)) {
return true;
}
try {
$data = $this->decrypt->decryptString(\LibreNMS\Config::get($key));
\LibreNMS\Config::persist($key, $this->encrypt->encryptString($data));
return true;
} catch (DecryptException $e) {
try {
$this->encrypt->decryptString(\LibreNMS\Config::get($key));
return true; // already rotated
} catch (DecryptException $e) {
$this->warn(trans('commands.key:rotate.decrypt-failed', ['item' => $key]));
return false;
}
}
}
}