reencryptColumn($table, $column, fn (string $value) => SignRSA::EncryptFunc($this->decryptRsa($value))); } foreach (self::APP_KEY_COLUMNS as [$table, $column]) { $failed += $this->reencryptColumn($table, $column, fn (string $value) => Crypt::encryptString(Crypt::decryptString($value))); } $failed += $this->reencryptSmtpSetting(); return $failed === 0 ? self::SUCCESS : self::FAILURE; } private function reencryptColumn(string $table, string $column, callable $reencrypt): int { if (!Schema::hasColumn($table, $column)) { $this->warn("{$table}.{$column}: column not found, skipped"); return 0; } $updated = 0; $failed = 0; DB::table($table) ->select(['id', $column]) ->whereNotNull($column) ->where($column, '!=', '') ->chunkById(200, function ($rows) use ($table, $column, $reencrypt, &$updated, &$failed) { foreach ($rows as $row) { try { $value = $reencrypt((string) $row->{$column}); } catch (Throwable) { $failed++; $this->warn("{$table}#{$row->id}.{$column}: cannot decrypt with current or previous key"); continue; } if (!$this->option('dry-run')) { DB::table($table)->where('id', $row->id)->update([$column => $value]); } $updated++; } }); $this->info("{$table}.{$column}: {$updated} re-encrypted, {$failed} failed"); return $failed; } private function reencryptSmtpSetting(): int { $setting = DB::table('setting')->where('key', 'smtp')->first(); $smtp = $setting ? json_decode((string) $setting->value, true) : null; if (empty($smtp['password'])) { return 0; } try { $smtp['password'] = SignRSA::EncryptFunc($this->decryptRsa($smtp['password'])); } catch (Throwable) { $this->warn('setting[smtp].password: cannot decrypt with current or previous key'); return 1; } if (!$this->option('dry-run')) { DB::table('setting')->where('key', 'smtp')->update(['value' => json_encode($smtp)]); } $this->info('setting[smtp].password: re-encrypted'); return 0; } private function decryptRsa(string $value): string { $plain = SignRSA::DecryptFunc($value); if ($plain === '') { throw new RuntimeException('RSA decrypt failed'); } return $plain; } }