From 48e3f948f50352acbda3d55de89dba54d4fc3f7d Mon Sep 17 00:00:00 2001 From: Ollama Date: Thu, 21 May 2026 21:43:14 +0200 Subject: [PATCH] Fix encryption key persistence for Docker environments The check_encryption() function now properly handles Docker/container environments where ROOTPATH/.env may be read-only or ephemeral. Changes: - Returns false when key persistence fails instead of always returning true - Removes error suppression (@) to properly detect write failures - Adds fallback to WRITEPATH/config/encryption.key for container volumes - Splits logic into separate functions for clarity and testability Fixes encryption key being lost on container restarts, which caused stored passwords to become undecryptable. GitHub-Issue: #4554 Add fallback key loading from WRITEPATH in Encryption config When encryption key is not available from .env or environment variables, the config now attempts to load from WRITEPATH/config/encryption.key. This supports Docker environments where: - .env file is read-only or ephemeral - Key was persisted to the writable volume via check_encryption() GitHub-Issue: #4554 Handle encryption unavailability gracefully in controllers Changed EncrypterInterface property to nullable and added proper error handling for cases where encryption key is not available. Changes: - Config controller: nullable encrypter property, try/catch around encryption - Email_lib: check encryption before using encrypter - Return meaningful error messages when encryption fails - Log warnings when passwords saved without encryption Users will now see clear error messages instead of unhandled exceptions when encryption key cannot be initialized. GitHub-Issue: #4554 Add encryption_failed error message to language file Added localization string for encryption failure error messages. GitHub-Issue: #4554 Add decrypt_value() and encrypt_value() helper functions Extracts the recurring decryption/encryption pattern into reusable helper functions with consistent error handling: - decrypt_value(): Safely decrypts encrypted values with try/catch - encrypt_value(): Safely encrypts values with error handling Both functions handle: - Empty/null values gracefully - Missing encryption key (logs warning) - Encryption/decryption failures (logs error, returns default) This pattern appears in 8+ locations across the codebase. GitHub-Issue: #4554 Refactor all encryption/decryption to use helper functions Replaces direct encrypter calls with decrypt_value() and encrypt_value() helpers throughout the codebase for consistent error handling: - Config controller: SMTP, SMS, Mailchimp credential encryption - Email_lib: SMTP password decryption - Sms_lib: SMS password decryption - Mailchimp_lib: API key decryption - Customers controller: Mailchimp list ID decryption Removes nullable EncrypterInterface property from Config controller as encryption is now handled via helper functions. GitHub-Issue: #4554 Address CodeRabbit feedback: validate key length, clarify encryption failure handling - loadKeyFromWritable() now validates key length >= 64 before accepting - encrypt_value() renamed param, defaults to failing encryption required - Clearer error message when credentials not saved GitHub-Issue: #4554 fix: address CodeRabbit review comments for encryption key persistence - Always mirror encryption key to both .env and WRITEPATH (Docker safety) - Guard array key access with isset() before reading in Encryption.php - Fix encrypt_value() to not treat string '0' as empty - Improve error logging for failed encryption attempts refactor: PSR-compliant naming and address objecttothis review comments - Rename functions to camelCase: checkEncryption, writeEncryptionKeyToEnv, writeEncryptionKeyToWritable, loadEncryptionKeyFromWritable, abortEncryptionConversion, removeBackup, decryptValue, encryptValue - Update all callers in Config.php, Customers.php, Migrations, Email_lib.php, Sms_lib.php, Mailchimp_lib.php - Add EncryptionException import in security_helper.php (removed FQN) - Use camelCase variables: $smtpPass, $emailConfig, $batchSaveData in affected files - Remove unnecessary inline comments (code is self-documenting) - Keep necessary docstrings for public API documentation Address remaining CodeRabbit review comments - Fix decryptValue() to use explicit null/empty check instead of empty() (handles string "0" correctly) - Guard checkEncryption() result in migration before proceeding - Check read success before writing backup restoration - Consistent DIRECTORY_SEPARATOR usage in paths GitHub-Issue: #4554 --- app/Config/Encryption.php | 50 +++ app/Controllers/Config.php | 103 +++--- app/Controllers/Customers.php | 8 +- .../20220127000000_convert_to_ci4.php | 16 +- app/Helpers/security_helper.php | 335 +++++++++++++++--- app/Language/en/Config.php | 1 + app/Libraries/Email_lib.php | 26 +- app/Libraries/Mailchimp_lib.php | 20 +- app/Libraries/Sms_lib.php | 10 +- 9 files changed, 406 insertions(+), 163 deletions(-) diff --git a/app/Config/Encryption.php b/app/Config/Encryption.php index ad45a82a1..1a16f9f00 100644 --- a/app/Config/Encryption.php +++ b/app/Config/Encryption.php @@ -106,4 +106,54 @@ class Encryption extends BaseConfig * by CI3 Encryption default configuration. */ public string $cipher = 'AES-256-CTR'; + + /** + * Constructor - loads encryption key from fallback location if not set. + * + * This supports Docker/container environments where ROOTPATH/.env may be + * read-only or ephemeral. The fallback key file is stored in WRITEPATH/config/. + */ + public function __construct() + { + parent::__construct(); + + // If key not set from .env or environment, try WRITEPATH fallback + if (empty($this->key) || strlen($this->key) < 64) { + $fallbackKey = $this->loadKeyFromWritable(); + if ($fallbackKey !== null) { + $this->key = $fallbackKey; + } + } + } + + /** + * Loads encryption key from WRITEPATH/config/encryption.key. + * + * @return string|null The encryption key if found, null otherwise + */ + private function loadKeyFromWritable(): ?string + { + $keyFile = WRITEPATH . 'config' . DIRECTORY_SEPARATOR . 'encryption.key'; + + if (!file_exists($keyFile) || !is_readable($keyFile)) { + return null; + } + + $content = file_get_contents($keyFile); + if ($content === false) { + return null; + } + + $data = json_decode($content, true); + if ( + !is_array($data) + || !isset($data['key']) + || !is_string($data['key']) + || strlen($data['key']) < 64 + ) { + return null; + } + + return $data['key']; + } } diff --git a/app/Controllers/Config.php b/app/Controllers/Config.php index df95fc82a..956e69694 100644 --- a/app/Controllers/Config.php +++ b/app/Controllers/Config.php @@ -17,11 +17,9 @@ use App\Models\Enums\Rounding_mode; use App\Models\Stock_location; use App\Models\Tax; use CodeIgniter\Database\BaseConnection; -use CodeIgniter\Encryption\EncrypterInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Database; use Config\OSPOS; -use Config\Services; use DirectoryIterator; use NumberFormatter; use ReflectionException; @@ -30,7 +28,6 @@ class Config extends Secure_Controller { protected $helpers = ['security']; private BaseConnection $db; - private EncrypterInterface $encrypter; private Barcode_lib $barcode_lib; private Sale_lib $sale_lib; private Receiving_lib $receiving_lib; @@ -62,13 +59,6 @@ class Config extends Secure_Controller $this->tax = model(Tax::class); $this->config = config(OSPOS::class)->settings; $this->db = Database::connect(); - - helper('security'); - if (check_encryption()) { - $this->encrypter = Services::encrypter(); - } else { - log_message('alert', 'Error preparing encryption key'); - } } /** @@ -256,25 +246,11 @@ class Config extends Secure_Controller // Integrations Related fields $data['mailchimp'] = []; - if (check_encryption()) { // TODO: Hungarian notation - if (!isset($this->encrypter)) { - helper('security'); - $this->encrypter = Services::encrypter(); - } + $data['mailchimp']['api_key'] = decryptValue($this->config['mailchimp_api_key'] ?? null); + $data['mailchimp']['list_id'] = decryptValue($this->config['mailchimp_list_id'] ?? null); - $data['mailchimp']['api_key'] = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key'])) - ? $this->encrypter->decrypt($this->config['mailchimp_api_key']) - : ''; - - $data['mailchimp']['list_id'] = (isset($this->config['mailchimp_list_id']) && !empty($this->config['mailchimp_list_id'])) - ? $this->encrypter->decrypt($this->config['mailchimp_list_id']) - : ''; - - // Remove any backup of .env created by check_encryption() - remove_backup(); - } else { - $data['mailchimp']['api_key'] = ''; - $data['mailchimp']['list_id'] = ''; + if (checkEncryption()) { + removeBackup(); } $data['mailchimp']['lists'] = $this->_mailchimp(); @@ -512,15 +488,23 @@ class Config extends Secure_Controller public function postSaveEmail(): ResponseInterface { $password = ''; + $passwordInput = $this->request->getPost('smtp_pass'); - if (check_encryption() && !empty($this->request->getPost('smtp_pass'))) { - $password = $this->encrypter->encrypt($this->request->getPost('smtp_pass')); + if (!empty($passwordInput)) { + $password = encryptValue($passwordInput); + if (empty($password)) { + log_message('error', 'SMTP password encryption failed - credentials not saved'); + + return $this->response->setJSON([ + 'success' => false, + 'message' => lang('Config.encryption_failed'), + ]); + } } $protocol = $this->request->getPost('protocol'); $mailpath = $this->request->getPost('mailpath'); - // Validate mailpath: required for sendmail, optional for others but must be safe if provided $isMailpathRequired = ($protocol === 'sendmail'); $isMailpathProvided = !empty($mailpath); $isMailpathValid = $isMailpathProvided && preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $mailpath); @@ -528,7 +512,7 @@ class Config extends Secure_Controller if (($isMailpathRequired && !$isMailpathProvided) || ($isMailpathProvided && !$isMailpathValid)) { return $this->response->setJSON([ 'success' => false, - 'message' => lang('Config.mailpath_invalid') + 'message' => lang('Config.mailpath_invalid'), ]); } @@ -540,7 +524,7 @@ class Config extends Secure_Controller 'smtp_pass' => $password, 'smtp_port' => $this->request->getPost('smtp_port', FILTER_SANITIZE_NUMBER_INT), 'smtp_timeout' => $this->request->getPost('smtp_timeout', FILTER_SANITIZE_NUMBER_INT), - 'smtp_crypto' => $this->request->getPost('smtp_crypto') + 'smtp_crypto' => $this->request->getPost('smtp_crypto'), ]; $success = $this->appconfig->batch_save($batch_save_data); @@ -558,16 +542,25 @@ class Config extends Secure_Controller public function postSaveMessage(): ResponseInterface { $password = ''; + $passwordInput = $this->request->getPost('msg_pwd'); - if (check_encryption() && !empty($this->request->getPost('msg_pwd'))) { - $password = $this->encrypter->encrypt($this->request->getPost('msg_pwd')); + if (!empty($passwordInput)) { + $password = encryptValue($passwordInput); + if (empty($password)) { + log_message('error', 'SMS password encryption failed'); + + return $this->response->setJSON([ + 'success' => false, + 'message' => lang('Config.encryption_failed'), + ]); + } } $batch_save_data = [ 'msg_msg' => $this->request->getPost('msg_msg'), 'msg_uid' => $this->request->getPost('msg_uid'), 'msg_pwd' => $password, - 'msg_src' => $this->request->getPost('msg_src') + 'msg_src' => $this->request->getPost('msg_src'), ]; $success = $this->appconfig->batch_save($batch_save_data); @@ -623,24 +616,38 @@ class Config extends Secure_Controller */ public function postSaveMailchimp(): ResponseInterface { - $api_key = ''; - $list_id = ''; + $apiKey = ''; + $listId = ''; - if (check_encryption()) { - $api_key_unencrypted = $this->request->getPost('mailchimp_api_key'); - if (!empty($api_key_unencrypted)) { - $api_key = $this->encrypter->encrypt($api_key_unencrypted); - } + $apiKeyInput = $this->request->getPost('mailchimp_api_key'); + if (!empty($apiKeyInput)) { + $apiKey = encryptValue($apiKeyInput); + if (empty($apiKey)) { + log_message('error', 'Mailchimp API key encryption failed'); - $list_id_unencrypted = $this->request->getPost('mailchimp_list_id'); - if (!empty($list_id_unencrypted)) { - $list_id = $this->encrypter->encrypt($list_id_unencrypted); + return $this->response->setJSON([ + 'success' => false, + 'message' => lang('Config.encryption_failed'), + ]); } } - $batch_save_data = ['mailchimp_api_key' => $api_key, 'mailchimp_list_id' => $list_id]; + $listIdInput = $this->request->getPost('mailchimp_list_id'); + if (!empty($listIdInput)) { + $listId = encryptValue($listIdInput); + if (empty($listId)) { + log_message('error', 'Mailchimp list ID encryption failed'); - $success = $this->appconfig->batch_save($batch_save_data); + return $this->response->setJSON([ + 'success' => false, + 'message' => lang('Config.encryption_failed'), + ]); + } + } + + $batchSaveData = ['mailchimp_api_key' => $apiKey, 'mailchimp_list_id' => $listId]; + + $success = $this->appconfig->batch_save($batchSaveData); return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); } diff --git a/app/Controllers/Customers.php b/app/Controllers/Customers.php index 55a872a61..81c6c1d4b 100644 --- a/app/Controllers/Customers.php +++ b/app/Controllers/Customers.php @@ -31,13 +31,7 @@ class Customers extends Persons $this->tax_code = model(Tax_code::class); $this->config = config(OSPOS::class)->settings; - $encrypter = Services::encrypter(); - - if (!empty($this->config['mailchimp_list_id'])) { - $this->_list_id = $encrypter->decrypt($this->config['mailchimp_list_id']); - } else { - $this->_list_id = ''; - } + $this->_list_id = decryptValue($this->config['mailchimp_list_id'] ?? null); } /** diff --git a/app/Database/Migrations/20220127000000_convert_to_ci4.php b/app/Database/Migrations/20220127000000_convert_to_ci4.php index 65fc9f9e1..6604fc751 100644 --- a/app/Database/Migrations/20220127000000_convert_to_ci4.php +++ b/app/Database/Migrations/20220127000000_convert_to_ci4.php @@ -33,10 +33,12 @@ class Convert_to_ci4 extends Migration if (!empty(config('Encryption')->key)) { $this->convert_ci3_encrypted_data(); } else { - check_encryption(); + if (!checkEncryption()) { + throw new \RuntimeException('Unable to persist encryption key. Migration cannot proceed without encryption.'); + } } - remove_backup(); + removeBackup(); } /** @@ -66,15 +68,19 @@ class Convert_to_ci4 extends Migration $decrypted_data = $this->decrypt_ci3_data($ci3_encrypted_data); - check_encryption(); + if (!checkEncryption()) { + abortEncryptionConversion(); + removeBackup(); + throw new RedirectException('login'); + } try { $ci4_encrypted_data = $this->encrypt_data($decrypted_data); $success = empty(array_diff_assoc($decrypted_data, $this->decrypt_data($ci4_encrypted_data))); if (!$success) { - abort_encryption_conversion(); - remove_backup(); + abortEncryptionConversion(); + removeBackup(); throw new RedirectException('login'); } diff --git a/app/Helpers/security_helper.php b/app/Helpers/security_helper.php index 5311b4ce9..12f8f0c2b 100644 --- a/app/Helpers/security_helper.php +++ b/app/Helpers/security_helper.php @@ -1,95 +1,312 @@ key; + $oldKey = config('Encryption')->key; - if ((empty($old_key)) || (strlen($old_key) < 64)) { - $encryption = new Encryption(); - $key = bin2hex($encryption->createKey()); - config('Encryption')->key = $key; + if (!empty($oldKey) && strlen($oldKey) >= 64) { + return true; + } - $config_path = ROOTPATH . '.env'; - $backup_path = WRITEPATH . '/backup/.env.bak'; - $backup_folder = WRITEPATH . '/backup'; + $encryption = new Encryption(); + $key = bin2hex($encryption->createKey()); + config('Encryption')->key = $key; - if (!file_exists($backup_folder)) { - @mkdir($backup_folder, 0750, true); - } + $envPersisted = writeEncryptionKeyToEnv($key, $oldKey); + $writablePersisted = writeEncryptionKeyToWritable($key, $oldKey); + $persisted = $envPersisted || $writablePersisted; - if (!file_exists($config_path)) { - $example_path = ROOTPATH . '.env.example'; - if (file_exists($example_path)) { - @copy($example_path, $config_path); - } else { - @file_put_contents($config_path, "# OSPOS Configuration\n\n"); - } - @chmod($config_path, 0640); - } + if ($persisted) { + log_message('info', 'Encryption key initialized successfully'); + } else { + log_message('error', 'Failed to persist encryption key to any location. Encryption may not survive container restarts.'); + } - if (file_exists($config_path)) { - @copy($config_path, $backup_path); - @chmod($backup_path, 0640); - @chmod($config_path, 0640); + return $persisted; +} - $config_file = file_get_contents($config_path); +/** + * Writes encryption key to ROOTPATH/.env file. + * + * @param string $key The new encryption key (hex-encoded) + * @param string|null $oldKey The previous key to preserve for key rotation + * + * @return bool True if key was written successfully, false otherwise + */ +function writeEncryptionKeyToEnv(string $key, ?string $oldKey = null): bool +{ + $configPath = ROOTPATH . '.env'; + $backupPath = WRITEPATH . 'backup' . DIRECTORY_SEPARATOR . '.env.bak'; + $backupFolder = WRITEPATH . 'backup'; - if (preg_match('/^\s*encryption\.key\s*=/m', $config_file)) { - $config_file = preg_replace("/^(\s*encryption\.key\s*=\s*).*/m", "\$1'$key'", $config_file, 1); - } else { - $config_file .= "\nencryption.key = '$key'\n"; - } - - if (!empty($old_key)) { - $old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n"; - if (preg_match('/^encryption\.key\s*=/m', $config_file, $matches, PREG_OFFSET_CAPTURE)) { - $config_file = substr_replace($config_file, $old_line, $matches[0][1], 0); - } - } - - @file_put_contents($config_path, $config_file); - @chmod($config_path, 0640); - - log_message('info', "Updated encryption key in $config_path"); + if (!file_exists($backupFolder)) { + if (!@mkdir($backupFolder, 0750, true)) { + log_message('debug', 'Could not create backup directory'); } } + if (!file_exists($configPath)) { + $examplePath = ROOTPATH . '.env.example'; + if (file_exists($examplePath)) { + if (!@copy($examplePath, $configPath)) { + log_message('debug', 'Could not copy .env.example to .env'); + } + } else { + if (!@file_put_contents($configPath, "# OSPOS Configuration\n\n") !== false) { + log_message('debug', 'Could not create .env file'); + } + } + @chmod($configPath, 0640); + } + + if (!is_writable($configPath)) { + log_message('debug', '.env file is not writable'); + return false; + } + + if (file_exists($configPath)) { + @copy($configPath, $backupPath); + @chmod($backupPath, 0640); + } + + $configFile = file_get_contents($configPath); + if ($configFile === false) { + log_message('debug', 'Could not read .env file'); + return false; + } + + if (strpos($configFile, 'encryption.key') !== false) { + $configFile = preg_replace("/(encryption\.key.*=.*)(['\"])([^'\"]*)\\2/", "$1'$key'", $configFile); + } else { + $configFile .= "\nencryption.key = '$key'\n"; + } + + if (!empty($oldKey)) { + $oldLine = "# encryption.key = '$oldKey' REMOVE IF UNNEEDED\r\n"; + $insertionPoint = stripos($configFile, 'encryption.key'); + if ($insertionPoint !== false) { + $configFile = substr_replace($configFile, $oldLine, $insertionPoint, 0); + } + } + + $result = file_put_contents($configPath, $configFile); + if ($result === false) { + log_message('debug', 'Could not write to .env file'); + return false; + } + + @chmod($configPath, 0640); + log_message('info', "Updated encryption key in $configPath"); + return true; } /** - * @return void + * Writes encryption key to WRITEPATH/config/encryption.key file. + * + * This is the fallback location for Docker/container environments where + * the ROOTPATH/.env file may be read-only or ephemeral. + * + * @param string $key The new encryption key (hex-encoded) + * @param string|null $oldKey The previous key to preserve for key rotation + * + * @return bool True if key was written successfully, false otherwise */ -function abort_encryption_conversion(): void +function writeEncryptionKeyToWritable(string $key, ?string $oldKey = null): bool { - $config_path = ROOTPATH . '.env'; - $backup_path = WRITEPATH . '/backup/.env.bak'; + $keyFile = WRITEPATH . 'config' . DIRECTORY_SEPARATOR . 'encryption.key'; + $keyDir = dirname($keyFile); - if (!file_exists($backup_path)) { - return; + if (!is_dir($keyDir)) { + if (!@mkdir($keyDir, 0750, true)) { + log_message('error', 'Could not create config directory: ' . $keyDir); + return false; + } } - @chmod($config_path, 0640); - $config_file = file_get_contents($backup_path); - @file_put_contents($config_path, $config_file); - log_message('info', "Restored $config_path from backup"); + if (!is_writable($keyDir)) { + log_message('error', 'Config directory is not writable: ' . $keyDir); + return false; + } + + $data = [ + 'key' => $key, + 'previous_keys' => [], + 'generated_at' => date('c'), + 'generated_by' => 'checkEncryption()', + ]; + + if (!empty($oldKey)) { + $data['previous_keys'][] = $oldKey; + } + + $content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $result = file_put_contents($keyFile, $content); + + if ($result === false) { + log_message('error', 'Could not write encryption key file'); + return false; + } + + @chmod($keyFile, 0640); + + log_message('info', "Stored encryption key in $keyFile"); + + return true; } /** + * Loads encryption key from WRITEPATH/config/encryption.key file. + * + * This is the fallback key loader for Docker/container environments. + * + * @return string|null The encryption key if found, null otherwise + */ +function loadEncryptionKeyFromWritable(): ?string +{ + $keyFile = WRITEPATH . 'config' . DIRECTORY_SEPARATOR . 'encryption.key'; + + if (!file_exists($keyFile)) { + return null; + } + + if (!is_readable($keyFile)) { + log_message('error', 'Encryption key file exists but is not readable: ' . $keyFile); + return null; + } + + $content = file_get_contents($keyFile); + if ($content === false) { + log_message('error', 'Could not read encryption key file'); + return null; + } + + $data = json_decode($content, true); + if (!is_array($data) || empty($data['key'])) { + log_message('error', 'Encryption key file has invalid format'); + return null; + } + + log_message('info', 'Loaded encryption key from WRITEPATH config'); + + return $data['key']; +} + +/** + * Restores .env from backup (used by migration rollback). + * * @return void */ -function remove_backup(): void +function abortEncryptionConversion(): void { - $backup_path = WRITEPATH . '/backup/.env.bak'; - if (!file_exists($backup_path)) { + $configPath = ROOTPATH . '.env'; + $backupPath = WRITEPATH . 'backup' . DIRECTORY_SEPARATOR . '.env.bak'; + + if (!file_exists($backupPath)) { return; } - @unlink($backup_path); - log_message('info', "Removed $backup_path"); + + $configFile = file_get_contents($backupPath); + if ($configFile === false) { + log_message('error', 'Could not read backup file for restoration'); + return; + } + + if (file_put_contents($configPath, $configFile) !== false) { + @chmod($configPath, 0640); + log_message('info', "Restored $configPath from backup"); + } else { + log_message('error', "Failed to restore $configPath from backup"); + } } + +/** + * Removes backup file (used after successful migration). + * + * @return void + */ +function removeBackup(): void +{ + $backupPath = WRITEPATH . 'backup' . DIRECTORY_SEPARATOR . '.env.bak'; + + if (file_exists($backupPath)) { + unlink($backupPath); + } +} + +/** + * Decrypts an encrypted value with proper error handling. + * + * This function provides a consistent decryption pattern across the codebase, + * handling cases where encryption key may not be available or decryption fails. + * + * @param string|null $encryptedValue The encrypted value to decrypt + * @param string $default Default value to return if decryption fails + * + * @return string The decrypted value, or default if decryption fails + */ +function decryptValue(?string $encryptedValue, string $default = ''): string +{ + if ($encryptedValue === null || $encryptedValue === '') { + return $default; + } + + if (!checkEncryption()) { + log_message('warning', 'Cannot decrypt value: encryption key not available'); + return $default; + } + + try { + $encrypter = Services::encrypter(); + return $encrypter->decrypt($encryptedValue); + } catch (EncryptionException $e) { + log_message('error', 'Decryption failed: ' . $e->getMessage()); + return $default; + } +} + +/** + * Encrypts a value with proper error handling. + * + * This function provides a consistent encryption pattern across the codebase, + * handling cases where encryption key may not be available. + * + * @param string|null $value The value to encrypt + * @param bool $require Whether encryption is required (returns empty string on failure) + * If false, returns original value on failure + * + * @return string The encrypted value, or empty string/original value if encryption fails + */ +function encryptValue(?string $value, bool $require = true): string +{ + if ($value === null || $value === '') { + return ''; + } + + if (!checkEncryption()) { + log_message('error', 'Cannot encrypt value: encryption key not available'); + return $require ? '' : $value; + } + + try { + $encrypter = Services::encrypter(); + return $encrypter->encrypt($value); + } catch (EncryptionException $e) { + log_message('error', 'Encryption failed: ' . $e->getMessage()); + return $require ? '' : $value; + } +} \ No newline at end of file diff --git a/app/Language/en/Config.php b/app/Language/en/Config.php index 1d32112b9..37efcd443 100644 --- a/app/Language/en/Config.php +++ b/app/Language/en/Config.php @@ -122,6 +122,7 @@ return [ "email_smtp_port" => "SMTP Port", "email_smtp_timeout" => "SMTP Timeout (s)", "email_smtp_user" => "SMTP Username", + "encryption_failed" => "Failed to encrypt data. Please check encryption configuration.", "enable_avatar" => "", "enable_avatar_tooltip" => "", "enable_dropdown_tooltip" => "", diff --git a/app/Libraries/Email_lib.php b/app/Libraries/Email_lib.php index eb8a9c24c..aec86055c 100644 --- a/app/Libraries/Email_lib.php +++ b/app/Libraries/Email_lib.php @@ -3,11 +3,7 @@ namespace app\Libraries; use CodeIgniter\Email\Email; -use CodeIgniter\Encryption\Encryption; -use CodeIgniter\Encryption\EncrypterInterface; -use CodeIgniter\Encryption\Exceptions\EncryptionException; use Config\OSPOS; -use Config\Services; /** @@ -26,21 +22,9 @@ class Email_lib $this->email = new Email(); $this->config = config(OSPOS::class)->settings; - $encrypter = Services::encrypter(); + $smtpPass = decryptValue($this->config['smtp_pass'] ?? null); - $smtp_pass = $this->config['smtp_pass']; - if (!empty($smtp_pass) && check_encryption()) { - try { - $smtp_pass = $encrypter->decrypt($smtp_pass); - } catch (\EncryptionException $e) { - // Decryption failed, use the original value - log_message('error', 'SMTP password decryption failed: ' . $e->getMessage()); - $smtp_pass = ''; - } - - } - - $email_config = [ + $emailConfig = [ 'mailType' => 'html', 'userAgent' => 'OSPOS', 'validate' => true, @@ -48,12 +32,12 @@ class Email_lib 'mailPath' => $this->config['mailpath'], 'SMTPHost' => $this->config['smtp_host'], 'SMTPUser' => $this->config['smtp_user'], - 'SMTPPass' => $smtp_pass, + 'SMTPPass' => $smtpPass, 'SMTPPort' => (int)$this->config['smtp_port'], 'SMTPTimeout' => (int)$this->config['smtp_timeout'], - 'SMTPCrypto' => $this->config['smtp_crypto'] + 'SMTPCrypto' => $this->config['smtp_crypto'], ]; - $this->email->initialize($email_config); + $this->email->initialize($emailConfig); } /** diff --git a/app/Libraries/Mailchimp_lib.php b/app/Libraries/Mailchimp_lib.php index a3b2b3937..cd969982a 100644 --- a/app/Libraries/Mailchimp_lib.php +++ b/app/Libraries/Mailchimp_lib.php @@ -2,9 +2,7 @@ namespace app\Libraries; -use CodeIgniter\Encryption\EncrypterInterface; use Config\OSPOS; -use Config\Services; /** * MailChimp API v3 REST client Connector @@ -14,8 +12,6 @@ use Config\Services; * Inspired by the work of: * - Rajitha Bandara: https://github.com/rajitha-bandara/ci-mailchimp-v3-rest-client * - Stefan Ashwell: https://github.com/stef686/codeigniter-mailchimp-api-v3 - * - * @property encrypterinterface encrypter */ class MailchimpConnector { @@ -40,23 +36,19 @@ class MailchimpConnector { $config = config(OSPOS::class)->settings; - $encrypter = Services::encrypter(); - - $mailchimp_api_key = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key'])) - ? $this->config['mailchimp_api_key'] - : ''; + $mailchimp_api_key = $config['mailchimp_api_key'] ?? ''; if (!empty($mailchimp_api_key)) { $this->_api_key = empty($api_key) - ? $encrypter->decrypt($mailchimp_api_key) // TODO: Hungarian notation - : $api_key; // TODO: Hungarian notation + ? decryptValue($mailchimp_api_key) + : $api_key; } - if (!empty($this->_api_key)) { // TODO: Hungarian notation + if (!empty($this->_api_key)) { // Replace with correct datacenter obtained from the last part of the api key - $strings = explode('-', $this->_api_key); // TODO: Hungarian notation + $strings = explode('-', $this->_api_key); if (is_array($strings) && !empty($strings[1])) { - $this->_api_endpoint = str_replace('', $strings[1], $this->_api_endpoint); // TODO: Hungarian notation + $this->_api_endpoint = str_replace('', $strings[1], $this->_api_endpoint); } } } diff --git a/app/Libraries/Sms_lib.php b/app/Libraries/Sms_lib.php index 9ba9eeff6..08d88936d 100644 --- a/app/Libraries/Sms_lib.php +++ b/app/Libraries/Sms_lib.php @@ -2,10 +2,7 @@ namespace app\Libraries; -use CodeIgniter\Encryption\Encryption; -use CodeIgniter\Encryption\EncrypterInterface; use Config\OSPOS; -use Config\Services; /** @@ -24,12 +21,7 @@ class Sms_lib { $config = config(OSPOS::class)->settings; - $encrypter = Services::encrypter(); - - $password = $config['msg_pwd']; - if (!empty($password)) { - $password = $encrypter->decrypt($password); - } + $password = decryptValue($config['msg_pwd'] ?? null); $username = $config['msg_uid']; $originator = $config['msg_src'];