2FAuth/app/Console/Commands/Maintenance/FixServiceFieldEncryption.php

129 lines
4.4 KiB
PHP

<?php
namespace App\Console\Commands\Maintenance;
use App\Facades\Settings;
use App\Models\TwoFAccount;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
/**
* @codeCoverageIgnore
*/
class FixServiceFieldEncryption extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = '2fauth:fix-service-encryption';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check and encrypt 2FA accounts Service field';
/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = true;
/**
* The name of the migration that changed the data this command will try to fix
*/
protected string $relatedMigration = '2024_08_08_133136_encrypt_twofaccount_service_field';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (DB::table('migrations')->where('migration', $this->relatedMigration)->doesntExist()) {
$this->fail(sprintf('Migration %s has not been run, this command cannot be used', $this->relatedMigration));
}
if (! Settings::get('useEncryption')) {
$this->fail('Database encryption is Off, this command cannot be used');
}
$this->encryptServiceField();
}
/**
* Encrypts the Service field of all TwoFAccount records
*/
protected function encryptServiceField() : void
{
$twofaccounts = TwoFAccount::all();
$fullyEncryptedTwofaccounts = $twofaccounts->whereNotIn('service', [__('errors.indecipherable')]);
$partiallyEncryptedTwofaccounts = $twofaccounts->where('service', __('errors.indecipherable'));
if ($fullyEncryptedTwofaccounts->count() === $twofaccounts->count()) {
$this->components->info('The Service field is fully encrypted');
return;
}
else {
$this->newLine();
$this->components->warn('The Service field is not fully encrypted, although it should be.');
$this->line('ID of corresponding records in the twofaccounts table:');
$this->line($partiallyEncryptedTwofaccounts->implode('id', ', '));
if ($this->confirm('Do you want to fix encryption of those records?', true)) {
$error = 0;
$partiallyEncryptedTwofaccounts->each(function (TwoFAccount $twofaccount, int $key) use (&$error) {
// We don't want to encrypt the Service field with a different APP_KEY
// than the one used to encrypt the legacy_uri, account and secret fields, the
// model would be inconsistent.
if (str_starts_with($twofaccount->legacy_uri, 'otpauth://')) {
$rawServiceValue = $twofaccount->getRawOriginal('service');
$twofaccount->service = $rawServiceValue;
$twofaccount->save();
$this->components->task(sprintf('Fixing twofaccount record with ID #%s', $twofaccount->id));
}
else {
$error += 1;
$this->components->task(sprintf('Fixing twofaccount record with ID #%s', $twofaccount->id), function() { return false; });
$this->components->error('Wrong encryption key: The current APP_KEY cannot decipher already encrypted fields, encrypting the Service field with this key would lead to inconsistent data encryption');
}
});
$this->newLine();
if ($error > 0) {
$this->error(sprintf('%s record%s could not be fixed, see log above for details.', $error, $error > 1 ? 's' : ''));
}
//$this->line('Task completed');
}
else {
$this->components->warn('No fix applied.');
$this->line('You can re-run this command at any time to fix inconsistent records.');
}
return;
}
}
}