mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-30 19:26:22 -04:00
- 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
319 lines
9.7 KiB
PHP
319 lines
9.7 KiB
PHP
<?php
|
|
|
|
use CodeIgniter\Encryption\Encryption;
|
|
use Config\Services;
|
|
|
|
/**
|
|
* Checks and initializes encryption key.
|
|
*
|
|
* This function ensures a valid encryption key exists for the application.
|
|
* It tries multiple storage locations to support different deployment scenarios:
|
|
* 1. ROOTPATH/.env - Standard location for non-containerized deployments
|
|
* 2. WRITEPATH/config/encryption.key - Fallback for Docker/container environments where .env is read-only
|
|
*
|
|
* @return bool True if encryption key is available, false if key generation/persistence failed
|
|
*/
|
|
function check_encryption(): bool
|
|
{
|
|
$old_key = config('Encryption')->key;
|
|
|
|
// Key already exists and is valid (64+ hex chars = 32+ bytes)
|
|
if (!empty($old_key) && strlen($old_key) >= 64) {
|
|
return true;
|
|
}
|
|
|
|
// Generate a new key
|
|
$encryption = new Encryption();
|
|
$key = bin2hex($encryption->createKey());
|
|
config('Encryption')->key = $key;
|
|
|
|
// Try to persist the key - attempt multiple locations
|
|
// Write both locations when possible. The writable copy is the durable one
|
|
// in containerized deployments where .env may be ephemeral.
|
|
$envPersisted = write_encryption_key_to_env($key, $old_key);
|
|
$writablePersisted = write_encryption_key_to_writable($key, $old_key);
|
|
$persisted = $envPersisted || $writablePersisted;
|
|
|
|
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.');
|
|
}
|
|
|
|
return $persisted;
|
|
}
|
|
|
|
/**
|
|
* Writes encryption key to ROOTPATH/.env file.
|
|
*
|
|
* @param string $key The new encryption key (hex-encoded)
|
|
* @param string|null $old_key The previous key to preserve for key rotation
|
|
*
|
|
* @return bool True if key was written successfully, false otherwise
|
|
*/
|
|
function write_encryption_key_to_env(string $key, ?string $old_key = null): bool
|
|
{
|
|
$config_path = ROOTPATH . '.env';
|
|
$backup_path = WRITEPATH . 'backup' . DIRECTORY_SEPARATOR . '.env.bak';
|
|
$backup_folder = WRITEPATH . 'backup';
|
|
|
|
// Ensure backup directory exists
|
|
if (!file_exists($backup_folder)) {
|
|
if (!@mkdir($backup_folder, 0750, true)) {
|
|
log_message('debug', 'Could not create backup directory');
|
|
}
|
|
}
|
|
|
|
// Create .env if it doesn't exist
|
|
if (!file_exists($config_path)) {
|
|
$example_path = ROOTPATH . '.env.example';
|
|
if (file_exists($example_path)) {
|
|
if (!@copy($example_path, $config_path)) {
|
|
log_message('debug', 'Could not copy .env.example to .env');
|
|
}
|
|
} else {
|
|
if (!@file_put_contents($config_path, "# OSPOS Configuration\n\n") !== false) {
|
|
log_message('debug', 'Could not create .env file');
|
|
}
|
|
}
|
|
@chmod($config_path, 0640);
|
|
}
|
|
|
|
// Check if .env is writable
|
|
if (!is_writable($config_path)) {
|
|
log_message('debug', '.env file is not writable');
|
|
return false;
|
|
}
|
|
|
|
// Backup existing .env
|
|
if (file_exists($config_path)) {
|
|
@copy($config_path, $backup_path);
|
|
@chmod($backup_path, 0640);
|
|
}
|
|
|
|
// Read current content
|
|
$config_file = file_get_contents($config_path);
|
|
if ($config_file === false) {
|
|
log_message('debug', 'Could not read .env file');
|
|
return false;
|
|
}
|
|
|
|
if (strpos($config_file, 'encryption.key') !== false) {
|
|
$config_file = preg_replace("/(encryption\.key.*=.*)(['\"])([^'\"]*)\\2/", "$1'$key'", $config_file);
|
|
} else {
|
|
$config_file .= "\nencryption.key = '$key'\n";
|
|
}
|
|
|
|
// Preserve old key for rotation if present
|
|
if (!empty($old_key)) {
|
|
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
|
|
$insertion_point = stripos($config_file, 'encryption.key');
|
|
if ($insertion_point !== false) {
|
|
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
|
|
}
|
|
}
|
|
|
|
// Write updated content
|
|
$result = file_put_contents($config_path, $config_file);
|
|
if ($result === false) {
|
|
log_message('debug', 'Could not write to .env file');
|
|
return false;
|
|
}
|
|
|
|
@chmod($config_path, 0640);
|
|
log_message('info', "Updated encryption key in $config_path");
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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 $old_key The previous key to preserve for key rotation
|
|
*
|
|
* @return bool True if key was written successfully, false otherwise
|
|
*/
|
|
function write_encryption_key_to_writable(string $key, ?string $old_key = null): bool
|
|
{
|
|
$key_file = WRITEPATH . 'config' . DIRECTORY_SEPARATOR . 'encryption.key';
|
|
$key_dir = dirname($key_file);
|
|
|
|
// Ensure directory exists
|
|
if (!is_dir($key_dir)) {
|
|
if (!@mkdir($key_dir, 0750, true)) {
|
|
log_message('error', 'Could not create config directory: ' . $key_dir);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if directory is writable
|
|
if (!is_writable($key_dir)) {
|
|
log_message('error', 'Config directory is not writable: ' . $key_dir);
|
|
return false;
|
|
}
|
|
|
|
// Build key data structure
|
|
$data = [
|
|
'key' => $key,
|
|
'previous_keys' => [],
|
|
'generated_at' => date('c'),
|
|
'generated_by' => 'check_encryption()',
|
|
];
|
|
|
|
if (!empty($old_key)) {
|
|
$data['previous_keys'][] = $old_key;
|
|
}
|
|
|
|
// Write key file
|
|
$content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
$result = file_put_contents($key_file, $content);
|
|
|
|
if ($result === false) {
|
|
log_message('error', 'Could not write encryption key file');
|
|
return false;
|
|
}
|
|
|
|
// Set restrictive permissions
|
|
@chmod($key_file, 0640);
|
|
|
|
log_message('info', "Stored encryption key in $key_file");
|
|
|
|
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 load_encryption_key_from_writable(): ?string
|
|
{
|
|
$key_file = WRITEPATH . 'config' . DIRECTORY_SEPARATOR . 'encryption.key';
|
|
|
|
if (!file_exists($key_file)) {
|
|
return null;
|
|
}
|
|
|
|
if (!is_readable($key_file)) {
|
|
log_message('error', 'Encryption key file exists but is not readable: ' . $key_file);
|
|
return null;
|
|
}
|
|
|
|
$content = file_get_contents($key_file);
|
|
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 abort_encryption_conversion(): void
|
|
{
|
|
$config_path = ROOTPATH . '.env';
|
|
$backup_path = WRITEPATH . '/backup/.env.bak';
|
|
|
|
if (!file_exists($backup_path)) {
|
|
return;
|
|
}
|
|
|
|
@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");
|
|
}
|
|
|
|
/**
|
|
* Removes backup file (used after successful migration).
|
|
*
|
|
* @return void
|
|
*/
|
|
function remove_backup(): void
|
|
{
|
|
$backup_path = WRITEPATH . '/backup/.env.bak';
|
|
|
|
if (file_exists($backup_path)) {
|
|
unlink($backup_path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 $encrypted_value 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 decrypt_value(?string $encrypted_value, string $default = ''): string
|
|
{
|
|
if (empty($encrypted_value)) {
|
|
return $default;
|
|
}
|
|
|
|
if (!check_encryption()) {
|
|
log_message('warning', 'Cannot decrypt value: encryption key not available');
|
|
return $default;
|
|
}
|
|
|
|
try {
|
|
$encrypter = Services::encrypter();
|
|
return $encrypter->decrypt($encrypted_value);
|
|
} catch (\CodeIgniter\Encryption\Exceptions\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 If true, return empty string on failure instead of plaintext fallback
|
|
*
|
|
* @return string The encrypted value, or empty string if encryption fails when required
|
|
*/
|
|
function encrypt_value(?string $value, bool $require = true): string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return '';
|
|
}
|
|
|
|
if (!check_encryption()) {
|
|
log_message('error', 'Cannot encrypt value: encryption key not available');
|
|
return $require ? '' : $value;
|
|
}
|
|
|
|
try {
|
|
$encrypter = Services::encrypter();
|
|
return $encrypter->encrypt($value);
|
|
} catch (\CodeIgniter\Encryption\Exceptions\EncryptionException $e) {
|
|
log_message('error', 'Encryption failed: ' . $e->getMessage());
|
|
return $require ? '' : $value;
|
|
}
|
|
} |