FixServiceFieldEncryption.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. <?php
  2. namespace App\Console\Commands\Maintenance;
  3. use App\Facades\Settings;
  4. use App\Models\TwoFAccount;
  5. use Illuminate\Console\Command;
  6. use Illuminate\Support\Facades\DB;
  7. /**
  8. * @codeCoverageIgnore
  9. */
  10. class FixServiceFieldEncryption extends Command
  11. {
  12. /**
  13. * The name and signature of the console command.
  14. *
  15. * @var string
  16. */
  17. protected $signature = '2fauth:fix-service-encryption';
  18. /**
  19. * The console command description.
  20. *
  21. * @var string
  22. */
  23. protected $description = 'Check and encrypt 2FA accounts Service field';
  24. /**
  25. * Indicates whether the command should be shown in the Artisan command list.
  26. *
  27. * @var bool
  28. */
  29. protected $hidden = true;
  30. /**
  31. * The name of the migration that changed the data this command will try to fix
  32. */
  33. protected string $relatedMigration = '2024_08_08_133136_encrypt_twofaccount_service_field';
  34. /**
  35. * Create a new command instance.
  36. *
  37. * @return void
  38. */
  39. public function __construct()
  40. {
  41. parent::__construct();
  42. }
  43. /**
  44. * Execute the console command.
  45. *
  46. * @return mixed
  47. */
  48. public function handle()
  49. {
  50. if (DB::table('migrations')->where('migration', $this->relatedMigration)->doesntExist()) {
  51. $this->fail(sprintf('Migration %s has not been run, this command cannot be used', $this->relatedMigration));
  52. }
  53. if (! Settings::get('useEncryption')) {
  54. $this->fail('Database encryption is Off, this command cannot be used');
  55. }
  56. $this->encryptServiceField();
  57. }
  58. /**
  59. * Encrypts the Service field of all TwoFAccount records
  60. */
  61. protected function encryptServiceField() : void
  62. {
  63. $twofaccounts = TwoFAccount::all();
  64. $fullyEncryptedTwofaccounts = $twofaccounts->whereNotIn('service', [__('errors.indecipherable')]);
  65. $partiallyEncryptedTwofaccounts = $twofaccounts->where('service', __('errors.indecipherable'));
  66. if ($fullyEncryptedTwofaccounts->count() === $twofaccounts->count()) {
  67. $this->components->info('The Service field is fully encrypted');
  68. return;
  69. } else {
  70. $this->newLine();
  71. $this->components->warn('The Service field is not fully encrypted, although it should be.');
  72. $this->line('ID of corresponding records in the twofaccounts table:');
  73. $this->line($partiallyEncryptedTwofaccounts->implode('id', ', '));
  74. if ($this->confirm('Do you want to fix encryption of those records?', true)) {
  75. $error = 0;
  76. $partiallyEncryptedTwofaccounts->each(function (TwoFAccount $twofaccount, int $key) use (&$error) {
  77. // We don't want to encrypt the Service field with a different APP_KEY
  78. // than the one used to encrypt the legacy_uri, account and secret fields, the
  79. // model would be inconsistent.
  80. if (str_starts_with($twofaccount->legacy_uri, 'otpauth://')) {
  81. $rawServiceValue = $twofaccount->getRawOriginal('service');
  82. $twofaccount->service = $rawServiceValue;
  83. $twofaccount->save();
  84. $this->components->task(sprintf('Fixing twofaccount record with ID #%s', $twofaccount->id));
  85. } else {
  86. $error += 1;
  87. $this->components->task(sprintf('Fixing twofaccount record with ID #%s', $twofaccount->id), function () {
  88. return false;
  89. });
  90. $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');
  91. }
  92. });
  93. $this->newLine();
  94. if ($error > 0) {
  95. $this->error(sprintf('%s record%s could not be fixed, see log above for details.', $error, $error > 1 ? 's' : ''));
  96. }
  97. //$this->line('Task completed');
  98. } else {
  99. $this->components->warn('No fix applied.');
  100. $this->line('You can re-run this command at any time to fix inconsistent records.');
  101. }
  102. return;
  103. }
  104. }
  105. }