key ?? ''); if ($key !== '' && strlen($key) >= 64) { CLI::write('encryption.key : CI4 key already present', 'green'); CLI::newLine(); return; } $converter = new CI3SecretConverter(); if ($key !== '' && strlen($key) < 64) { // DB read, safe outside the .env lock. $plain = $converter->decryptAll($key); $hasData = $this->anyNonEmpty($plain); // Backup -> rotate -> re-encrypt -> verify -> persist under a single .env lock. rotateEncryptionKeyTransaction($key, static function () use ($plain, $hasData, $converter): void { $encrypted = $converter->encryptAll($plain); if (array_diff_assoc($plain, $converter->verifyAll($encrypted)) !== []) { throw new RuntimeException('Failed to verify converted encryption data.'); } if ($hasData) { $converter->saveAll($encrypted); } }); CLI::write('encryption.key : rotated CI3 -> CI4 key', 'green'); if ($hasData) { CLI::write('legacy secrets : converted and verified to CI4 cipher', 'green'); } } else { // Fresh key (no old key to decrypt): a single atomic write suffices; // the transaction wrapper still serialises it against other workers. rotateEncryptionKeyTransaction(null, static function (): void {}); CLI::write('encryption.key : new CI4 key generated', 'green'); if ($this->legacySecretsPresent()) { CLI::write('legacy secrets : WARNING - stored CI3 secrets found but no CI3 key to decrypt them; they could not be recovered', 'yellow'); } } CLI::newLine(); CLI::write('env:provision complete.', 'green'); CLI::newLine(); } private function anyNonEmpty(array $plain): bool { foreach ($plain as $value) { if ((string) $value !== '') { return true; } } return false; } private function legacySecretsPresent(): bool { try { $appConfig = model('Appconfig'); } catch (Exception $e) { return false; } foreach (CI3SecretConverter::LEGACY_KEYS as $col) { try { if ($appConfig->get_value($col) !== '') { return true; } } catch (Exception $e) { return false; } } return false; } }