Files
opensourcepos/app/Database/Migrations/20220127000000_convertToCI4.php
objecttothis 9ee530e454 Hotfix: Fix CI3 database migration caused by regression (#4649)
* refactor: standardize function and variable names to camelCase and improve naming consistency across files

* refactor(config): remove spaces around `=` in configuration files for improved consistency and formatting as is required by .env formatting rules.

* refactor(security): extract `.env` key management logic into reusable `writeEnvKey` helper, add throttle key provisioning logic, and streamline encryption key updates

* fix(migration): improve error handling in CI3 to CI4 encryption data migration
- Secure `up` and `convertCI3EncryptedData` methods with detailed exception handling for script execution and data saving.

* fix(migration): ensure empty string is correctly handled in CI3 to CI4 encryption data conversion

* refactor(security): enhance `.env` management with durable writes, better locking, and helper abstraction
- Update `writeEnvKey` to return a success flag and handle file locks robustly.
- Introduce `atomicWriteFile` for atomic writes to prevent partial file updates.
- Add `applyEnvKeyReplacement` to streamline `.env` key insertion and updates.
- Improve throttle key provisioning with validation and runtime persistence safeguards.

* refactor(security): implement dedicated `.env` file locking for robust and cross-platform safe write operations
- Add `lockEnvFile` and `unlockEnvFile` helpers to manage `.env` mutex files.
- Refactor `.env` write logic to use lock helpers, improving reliability and preventing race conditions.
- Enhance `atomicWriteFile` for better handling of file overwrites on Windows and POSIX systems.

* fix(migration): improve encryption error handling during CI3 to CI4 data conversion
- Add conditional checks for `checkEncryption` to prevent failed key persistence.
- Introduce `abortEncryptionConversion` for cleanup on failure.
- Update `writeEnvKey` to handle and return errors gracefully.

* refactor(security): improve `atomicWriteFile` for better file locking and cross-platform durability
- Replace `uniqid` with `bin2hex(random_bytes())` for more secure temp file naming.
- Add explicit file permissions and locking for safe concurrent writes.
- Enhance error handling to ensure atomicity on both Windows and POSIX systems.

* Add env temp files to gitignore so they don't get tracked.

---------

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-08-21 21:00:48 +04:00

148 lines
4.3 KiB
PHP

<?php
namespace App\Database\Migrations;
use App\Models\Appconfig;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Forge;
use CodeIgniter\Database\Migration;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use Config\Encryption;
use Config\Services;
use ReflectionException;
class ConvertToCI4 extends Migration
{
/**
* Constructor.
*/
public function __construct(?Forge $forge = null)
{
parent::__construct($forge);
helper('security');
}
/**
* Perform a migration step.
*/
public function up(): void
{
helper('migration');
if (!executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_CI4Conversion.sql')) {
throw new DatabaseException('Migration script 3.4.0_CI4Conversion.sql failed. Check logs for details.');
}
$existingKey = config('Encryption')->key;
if (!empty($existingKey) && strlen($existingKey) < 64) {
$this->convertCI3EncryptedData();
} else {
if (!checkEncryption()) {
abortEncryptionConversion();
throw new DatabaseException('Failed to persist encryption key. Check logs for details.');
}
}
removeBackup();
}
/**
* Revert a migration step.
*/
public function down(): void {}
/**
* @throws ReflectionException
*/
private function convertCI3EncryptedData(): void
{
$appConfig = model(Appconfig::class);
$ci3EncryptedData = [
'clcdesq_api_key' => '',
'clcdesq_api_url' => '',
'mailchimp_api_key' => '',
'mailchimp_list_id' => '',
'smtp_pass' => ''
];
foreach ($ci3EncryptedData as $key => $value) {
$ci3EncryptedData[$key] = $appConfig->get_value($key);
}
$decryptedData = $this->decryptCI3Data($ci3EncryptedData);
if (!checkEncryption()) {
abortEncryptionConversion();
throw new DatabaseException('Failed to persist encryption key. Check logs for details.');
}
$ci4EncryptedData = $this->encryptData($decryptedData);
$success = empty(array_diff_assoc($decryptedData, $this->decryptData($ci4EncryptedData)));
if (!$success) {
abortEncryptionConversion();
throw new RedirectException('login'); // TODO: Need to figure out how to pass the error to the Login controller so that it gets displayed.
}
if (!$appConfig->batch_save($ci4EncryptedData)) {
abortEncryptionConversion();
throw new DatabaseException('Failed to save converted encryption data. Check logs for details.');
}
}
/**
* Decrypts CI3 encrypted data and returns the plaintext values.
*
* @param array $encryptedData Data encrypted using CI3 methodology.
* @return array Plaintext, unencrypted data.
*/
private function decryptCI3Data(array $encryptedData): array
{
$config = new Encryption();
$config->driver = 'OpenSSL';
$config->key = config('Encryption')->key;
$config->cipher = 'AES-128-CBC';
$config->rawData = false;
$config->encryptKeyInfo = 'encryption';
$config->authKeyInfo = 'authentication';
$encrypter = Services::encrypter($config);
return array_map(function ($value) use ($encrypter) {
return !empty($value) ? $encrypter->decrypt($value) : '';
}, $encryptedData);
}
/**
* Encrypts data using CI4 algorithms.
*
* @param array $plainData Data to be encrypted.
* @return array Encrypted data.
*/
private function encryptData(array $plainData): array
{
$encrypter = Services::encrypter();
return array_map(function ($value) use ($encrypter) {
return $value !== '' ? $encrypter->encrypt($value) : '';
}, $plainData);
}
/**
* Decrypts data using CI4 algorithms.
*
* @param array $encryptedData Data to be decrypted.
* @return array Decrypted data.
*/
private function decryptData(array $encryptedData): array
{
$encrypter = Services::encrypter();
return array_map(function ($value) use ($encrypter) {
return !empty($value) ? $encrypter->decrypt($value) : '';
}, $encryptedData);
}
}