mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-21 10:15:01 -04:00
Three related data-integrity fixes: - backupEnvFile() now returns true/false based on whether the backup actually exists and is readable. rotateEncryptionKey() aborts before destroying the key when the backup could not be written to disk. - rotateEncryptionKey() and provisionThrottleKey() throw RuntimeException(Error.unable_to_read_env_file) when the .env read fails, instead of silently replacing the whole file with an empty string. This prevents a permission error from wiping all keys. - checkEncryption() and EnvProvision::run() now both roll back to the backup with abortEncryptionConversion() when the post-rotation saveAll() throws, matching the migration path (which already did this). A failing fake Appconfig is used to exercise this in the new testCheckEncryptionRollsBackWhenSaveAllFails test.
127 lines
3.7 KiB
PHP
127 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Commands;
|
|
|
|
use App\Libraries\CI3SecretConverter;
|
|
use CodeIgniter\CLI\BaseCommand;
|
|
use CodeIgniter\CLI\CLI;
|
|
use Exception;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Idempotent startup provisioning of the encryption + throttle keys.
|
|
*
|
|
* In bare-metal deployments the web runtime can auto-provision keys inline
|
|
* (see checkEncryption()/checkThrottleEncryption(), gated by .env
|
|
* writability). In Docker/Compose deployments .env is typically mounted
|
|
* read-only, so keys are minted here at container start via
|
|
* `php spark env:provision`. For legacy CI3 deployments this command
|
|
* also re-encrypts stored CI3 secrets to the CI4 cipher (shared
|
|
* implementation with the interactive ConvertToCI4 migration).
|
|
*/
|
|
class EnvProvision extends BaseCommand
|
|
{
|
|
/**
|
|
* The command's group.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $group = 'Environment';
|
|
|
|
protected $name = 'env:provision';
|
|
|
|
protected $usage = 'env:provision';
|
|
|
|
protected $description = 'Ensures the encryption and throttle keys are provisioned and converts any legacy CI3-encrypted secrets.';
|
|
|
|
public function run(array $params): void
|
|
{
|
|
helper('security');
|
|
|
|
$throttleKey = provisionThrottleKey();
|
|
CLI::write('throttle.key : ' . ($throttleKey !== '' ? 'present' : 'MISSING'), 'green');
|
|
|
|
$encryptionConfig = config('Encryption');
|
|
$key = (string) ($encryptionConfig->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) {
|
|
$plain = $converter->decryptAll($key);
|
|
$hasData = $this->anyNonEmpty($plain);
|
|
|
|
rotateEncryptionKey($key);
|
|
CLI::write('encryption.key : rotated CI3 -> CI4 key', 'green');
|
|
|
|
$encrypted = $converter->encryptAll($plain);
|
|
|
|
if ($hasData && array_diff_assoc($plain, $converter->verifyAll($encrypted)) !== []) {
|
|
abortEncryptionConversion();
|
|
throw new RuntimeException('Failed to verify converted encryption data.');
|
|
}
|
|
|
|
if ($hasData) {
|
|
try {
|
|
$converter->saveAll($encrypted);
|
|
} catch (RuntimeException $e) {
|
|
abortEncryptionConversion();
|
|
|
|
throw $e;
|
|
}
|
|
|
|
CLI::write('legacy secrets : converted and verified to CI4 cipher', 'green');
|
|
}
|
|
} else {
|
|
rotateEncryptionKey(null);
|
|
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;
|
|
}
|
|
}
|