Skip to content

Fix encryption key persistence for Docker environments (Issue #4554) - #4556

Open
jekkos wants to merge 2 commits into
masterfrom
fix/4554-encryption-docker-issue
Open

Fix encryption key persistence for Docker environments (Issue #4554)#4556
jekkos wants to merge 2 commits into
masterfrom
fix/4554-encryption-docker-issue

Conversation

@jekkos

@jekkos jekkos commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #4554 - Encryption key not being persisted in Docker environments, causing "forNeedsStarterKey" exceptions and inability to decrypt stored passwords after container restarts.

Problem

When running OSPOS in Docker:

  1. The encryption key is written to /app/.env which is read-only or ephemeral
  2. Container restarts reset .env to image default
  3. New ephemeral keys are generated, making previously encrypted passwords undecryptable
  4. check_encryption() always returned true even when key persistence failed
  5. No fallback storage location for containers

Solution

1. Fix check_encryption() return value (security_helper.php)

  • Now returns false when key persistence fails
  • Removes @ error suppression to properly detect write failures
  • Tries ROOTPATH/.env first, then falls back to WRITEPATH/config/encryption.key
  • Docker volumes mount writable/ as persistent storage

2. Add fallback key loading (Encryption.php)

  • Config constructor now loads key from WRITEPATH/config/encryption.key
  • Supports Docker volumes where writable/ is mounted persistently

3. Handle encryption unavailability (Config.php, Email_lib.php)

  • Changed EncrypterInterface property to nullable
  • Added try/catch around encryption operations
  • Return meaningful error messages when encryption fails
  • Log warnings when passwords are saved without encryption

4. Add language string for encryption failure

Files Changed

  • app/Helpers/security_helper.php - Key persistence with Docker fallback
  • app/Config/Encryption.php - Fallback key loading from WRITEPATH
  • app/Controllers/Config.php - Graceful encryption failure handling
  • app/Libraries/Email_lib.php - Check encryption before using encrypter
  • app/Language/en/Config.php - Error message localization

Testing

  1. Fresh Docker install → key generated and stored in WRITEPATH/config/
  2. Container restart → key reloaded from WRITEPATH/config/
  3. Password encryption/decription works across restarts

Docker Volumes

The fix uses the existing logs Docker volume which mounts to /app/writable/:

volumes:
    logs:/app/writable/logs  # This is already persistent

The encryption key is stored at /app/writable/config/encryption.key which is in the same persistent volume.

Summary by CodeRabbit

  • New Features

    • Automatic fallback and persistent storage for encryption keys (env file and writable fallback).
    • New encrypt/decrypt helpers for consistent credential handling across email, SMS, and integrations.
    • Added user-facing message for encryption failures.
  • Bug Fixes

    • More reliable encryption initialization, validation, and key recovery with logging.
    • Consistent encryption/decryption of SMTP, SMS, and Mailchimp credentials with clear failure responses.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds two-tier encryption key persistence (.env primary, writable JSON fallback); Config loads fallback when key missing; adds guarded encryptValue()/decryptValue() helpers; updates controllers, libraries, and migration to use these helpers; save endpoints return JSON failure when encryption fails.

Changes

Encryption key initialization and credential helpers

Layer / File(s) Summary
Key persistence and writable helpers
app/Helpers/security_helper.php
checkEncryption() validates/generates keys, writes to .env with backup or falls back to WRITEPATH/config/encryption.key. Added writeEncryptionKeyToEnv(), writeEncryptionKeyToWritable(), loadEncryptionKeyFromWritable(), abortEncryptionConversion(), and removeBackup().
Config-level key initialization and fallback
app/Config/Encryption.php
Constructor attempts to load a fallback key from the writable JSON file when the configured key is empty or shorter than required (loadKeyFromWritable()).
Guarded encrypt/decrypt helpers
app/Helpers/security_helper.php
Added encryptValue() and decryptValue() that ensure encryption initialization via checkEncryption(), handle empty inputs, catch EncryptionException, and return safe defaults on failure.
Controller credential handling
app/Controllers/Config.php
Controller imports and stored encrypter removed; SMTP, SMS, and Mailchimp credentials are encrypted with encryptValue() on save and decrypted with decryptValue() on read. On encryption failure controller logs and returns JSON {success:false, message: Config.encryption_failed}.
Library decryption updates
app/Libraries/Email_lib.php, app/Libraries/Mailchimp_lib.php, app/Libraries/Sms_lib.php, app/Controllers/Customers.php
Libraries and Customers now use decryptValue(...) to derive SMTP/SMS/Mailchimp secrets, removed direct encrypter/Services imports and related phpdoc, and adjusted small config wiring.
Migration helper names
app/Database/Migrations/20220127000000_convert_to_ci4.php
Migration updated to call camelCase helper names (checkEncryption(), abortEncryptionConversion(), removeBackup()).
Translation
app/Language/en/Config.php
Added encryption_failed translation key used in JSON error responses when encryption fails.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement, CodeIgniter4

Suggested reviewers

  • objecttothis

Poem

🐰 I tuck the key in two warm nooks,
If .env sleeps, the writable books,
Helpers hum to guard each flight,
Controllers fail with a polite light,
A rabbit hops — your secrets rest tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes a substantial refactoring from snake_case to camelCase function naming (checkEncryption, encryptValue, decryptValue, etc.) that, while improving code quality and PSR compliance, extends beyond the core Docker persistence issue scope. Consider separating the function naming refactoring into a dedicated PR to isolate the core Docker persistence fix from broader code style improvements, improving changeability review clarity.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main objective: fixing encryption key persistence for Docker environments by implementing fallback storage and key loading mechanisms across multiple files.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #4554 by implementing persistent key storage (WRITEPATH/config/encryption.key), fallback loading in Encryption.php constructor, error reporting via checkEncryption() return value, exception handling in encryption operations, and localization for failures.
Docstring Coverage ✅ Passed Docstring coverage is 84.21% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/4554-encryption-docker-issue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Config/Encryption.php`:
- Around line 147-152: loadKeyFromWritable() currently returns any non-empty
$data['key'], causing malformed fallback keys to be accepted; change it to fully
validate the key before returning by verifying the key field is a string,
decodes from base64 (or the expected encoding) and matches the required byte
length/format for the encrypter used, and if validation fails return null so the
constructor/encrypter initialization will ignore the bad fallback; update
loadKeyFromWritable(), and any call sites in the constructor that rely on its
result, to only install/assign a key when validation passes.

In `@app/Controllers/Config.php`:
- Around line 516-530: The code currently falls back to saving secrets (SMTP
password, msg_pwd, mailchimp_api_key, mailchimp_list_id) in plaintext when
$this->encrypter is null or check_encryption() is false; update the branches in
the SMTP block and the similar blocks handling msg_pwd and Mailchimp
(references: $passwordInput/$password, $msg_pwd, $mailchimp_api_key,
$mailchimp_list_id, $this->encrypter, check_encryption()) to refuse to store
secrets unencrypted — log an error and return a JSON failure response (same
pattern used in the encrypt() catch) instead of assigning the plaintext value so
no secret is silently downgraded to plaintext. Ensure the response messages use
lang('Config.encryption_failed') or an appropriate message and keep existing
try/catch encryption logic intact.

In `@app/Helpers/security_helper.php`:
- Around line 33-39: The current flow only writes the restart-safe copy via
write_encryption_key_to_writable($key, $old_key) when
write_encryption_key_to_env(...) fails; change this so the writable copy is
always created/mirrored regardless of whether the .env write succeeded.
Specifically, after calling write_encryption_key_to_env($key, $old_key) (and
handling its success/failure as before), always invoke
write_encryption_key_to_writable($key, $old_key) to ensure
WRITEPATH/config/encryption.key is created or updated; keep existing error
handling/logging around both write_encryption_key_to_env and
write_encryption_key_to_writable so failures are reported but the mirror is
still attempted.

In `@app/Libraries/Email_lib.php`:
- Around line 31-40: The code currently bypasses decryption when
check_encryption() is false and may forward ciphertext as SMTPPass; update the
Email_lib constructor logic so that when $smtp_pass is non-empty and
check_encryption() returns false you treat this as a decryption failure: log an
error (including context) and clear $smtp_pass (set to empty) instead of passing
the ciphertext through; modify the block around check_encryption(),
Services::encrypter(), and the EncryptionException handling to add an else
branch that logs and zeroes $smtp_pass so SMTP auth cannot proceed with
ciphertext.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd385989-2772-47c0-9e73-72b969e5f984

📥 Commits

Reviewing files that changed from the base of the PR and between b0dddc2 and 403180d.

📒 Files selected for processing (5)
  • app/Config/Encryption.php
  • app/Controllers/Config.php
  • app/Helpers/security_helper.php
  • app/Language/en/Config.php
  • app/Libraries/Email_lib.php

Comment thread app/Config/Encryption.php
Comment thread app/Controllers/Config.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Libraries/Email_lib.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Helpers/security_helper.php (1)

79-81: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Logic error: condition is inverted due to operator precedence.

The ! operator binds tighter than !==, so this evaluates as (!@file_put_contents(...)) !== false. When file_put_contents succeeds and returns bytes written (e.g., 27), !27 is false, and false !== false is false—so the log is skipped on success. When it fails and returns false, !false is true, and true !== false is true—so the log triggers on failure. However, there's also a stray ! that inverts the entire intended behavior.

Proposed fix
-            if (!`@file_put_contents`($config_path, "# OSPOS Configuration\n\n") !== false) {
+            if (`@file_put_contents`($config_path, "# OSPOS Configuration\n\n") === false) {
                 log_message('debug', 'Could not create .env file');
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Helpers/security_helper.php` around lines 79 - 81, The condition around
the file creation is inverted due to the stray '!' and operator precedence;
update the check on `@file_put_contents` so it explicitly tests for failure (===
false) rather than negating the call—i.e., remove the leading '!' and change the
if to test if `@file_put_contents`($config_path, "...") === false, then call
log_message('debug', 'Could not create .env file'); reference the symbols
file_put_contents, $config_path, and log_message in your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/Helpers/security_helper.php`:
- Around line 79-81: The condition around the file creation is inverted due to
the stray '!' and operator precedence; update the check on `@file_put_contents` so
it explicitly tests for failure (=== false) rather than negating the call—i.e.,
remove the leading '!' and change the if to test if
`@file_put_contents`($config_path, "...") === false, then call
log_message('debug', 'Could not create .env file'); reference the symbols
file_put_contents, $config_path, and log_message in your change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6335108-824d-405a-bc58-36882d1e294e

📥 Commits

Reviewing files that changed from the base of the PR and between 403180d and f7280ea.

📒 Files selected for processing (6)
  • app/Controllers/Config.php
  • app/Controllers/Customers.php
  • app/Helpers/security_helper.php
  • app/Libraries/Email_lib.php
  • app/Libraries/Mailchimp_lib.php
  • app/Libraries/Sms_lib.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/Helpers/security_helper.php (1)

33-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Always mirror the key into writable storage.

A successful .env write still skips WRITEPATH/config/encryption.key, so containerized installs can lose the key on restart even though this request succeeded.

Suggested change
-    $persisted = write_encryption_key_to_env($key, $old_key);
-
-    // Attempt 2: WRITEPATH/config/encryption.key (Docker/container fallback)
-    if (!$persisted) {
-        $persisted = write_encryption_key_to_writable($key, $old_key);
-    }
+    $envPersisted = write_encryption_key_to_env($key, $old_key);
+    $writablePersisted = write_encryption_key_to_writable($key, $old_key);
+    $persisted = $envPersisted || $writablePersisted;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Helpers/security_helper.php` around lines 33 - 39, Currently the code
stops after a successful write_encryption_key_to_env($key, $old_key) and skips
write_encryption_key_to_writable($key, $old_key), which means containerized
installs may not persist the key; change the flow so that after
write_encryption_key_to_env returns true you still call
write_encryption_key_to_writable($key, $old_key) (and handle/log its result) so
the key is mirrored into WRITEPATH/config/encryption.key regardless of .env
success; reference the functions write_encryption_key_to_env and
write_encryption_key_to_writable and ensure any errors from the writable write
are captured or logged but do not prevent the request from succeeding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Config/Encryption.php`:
- Around line 147-152: Guard access to $data['key'] before using it: update the
validation in the Encryption.php logic that currently checks
is_string($data['key']) and strlen($data['key']) to first verify
isset($data['key']) (or array_key_exists) so you don't read an undefined array
key; in other words, change the condition around $data (used in the method
handling json_decode($content)) to require isset($data['key']) &&
is_string($data['key']) && strlen($data['key']) >= 64 so the code safely
validates the key.

In `@app/Helpers/security_helper.php`:
- Around line 305-313: encrypt_value() wrongly treats the string "0" as empty
because it uses empty($value); change the guard to only treat null or
empty-string as missing (e.g. replace if (empty($value)) with if ($value ===
null || $value === '') ), so valid values like "0" are processed/returned
normally; keep the later check_encryption() behavior and the existing return
semantics for $require unchanged.

---

Duplicate comments:
In `@app/Helpers/security_helper.php`:
- Around line 33-39: Currently the code stops after a successful
write_encryption_key_to_env($key, $old_key) and skips
write_encryption_key_to_writable($key, $old_key), which means containerized
installs may not persist the key; change the flow so that after
write_encryption_key_to_env returns true you still call
write_encryption_key_to_writable($key, $old_key) (and handle/log its result) so
the key is mirrored into WRITEPATH/config/encryption.key regardless of .env
success; reference the functions write_encryption_key_to_env and
write_encryption_key_to_writable and ensure any errors from the writable write
are captured or logged but do not prevent the request from succeeding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: deec40e9-5707-4f77-a5a5-8ed2724768f0

📥 Commits

Reviewing files that changed from the base of the PR and between f7280ea and 71c164a.

📒 Files selected for processing (2)
  • app/Config/Encryption.php
  • app/Helpers/security_helper.php

Comment thread app/Config/Encryption.php
Comment thread app/Helpers/security_helper.php Outdated
@jekkos
jekkos requested a review from objecttothis May 22, 2026 14:08
Comment thread app/Config/Encryption.php
Comment thread app/Controllers/Config.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Helpers/security_helper.php Outdated
Comment thread app/Libraries/Email_lib.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
app/Helpers/security_helper.php (3)

254-272: 💤 Low value

Inconsistent empty check pattern with encryptValue().

encryptValue() at line 288 uses $value === null || $value === '' (per past review feedback about empty("0")), but decryptValue() still uses empty($encryptedValue). While encrypted values are base64 and won't be "0", using the same pattern improves consistency.

♻️ Proposed fix for consistency
 function decryptValue(?string $encryptedValue, string $default = ''): string
 {
-    if (empty($encryptedValue)) {
+    if ($encryptedValue === null || $encryptedValue === '') {
         return $default;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Helpers/security_helper.php` around lines 254 - 272, decryptValue uses
empty($encryptedValue) which is inconsistent with encryptValue's
null/empty-string check and can mis-handle values like "0"; update decryptValue
to check for $encryptedValue === null || $encryptedValue === '' instead of
empty($encryptedValue), keeping the rest of the logic (checkEncryption(),
Services::encrypter(), EncryptionException handling and log_message calls)
unchanged so behavior remains consistent with encryptValue.

69-71: ⚡ Quick win

Confusing negation logic—simplify the condition.

The !@file_put_contents(...) !== false construct works by accident but is difficult to reason about. The intent is to log when the write fails.

♻️ Proposed fix
-            if (!`@file_put_contents`($configPath, "# OSPOS Configuration\n\n") !== false) {
+            if (`@file_put_contents`($configPath, "# OSPOS Configuration\n\n") === false) {
                 log_message('debug', 'Could not create .env file');
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Helpers/security_helper.php` around lines 69 - 71, The condition around
the attempted write to $configPath is using confusing negation: replace the
current check that uses !`@file_put_contents`(...) !== false with a direct
comparison against false so failures are explicit; call
`@file_put_contents`($configPath, ...) and if that expression === false then
invoke log_message('debug', 'Could not create .env file'). This simplifies the
logic and correctly detects write failures for the file_put_contents call in
security_helper.php.

234-241: 💤 Low value

Inconsistent path separator.

Uses / directly while other functions in this file use DIRECTORY_SEPARATOR.

♻️ Proposed fix
 function removeBackup(): void
 {
-    $backupPath = WRITEPATH . '/backup/.env.bak';
+    $backupPath = WRITEPATH . 'backup' . DIRECTORY_SEPARATOR . '.env.bak';

     if (file_exists($backupPath)) {
         unlink($backupPath);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Helpers/security_helper.php` around lines 234 - 241, The removeBackup
function builds $backupPath using a hardcoded '/' which is inconsistent; change
it to build the path using DIRECTORY_SEPARATOR (e.g. compose WRITEPATH, 'backup'
and '.env.bak' with DIRECTORY_SEPARATOR) so $backupPath uses the platform-safe
separator, keeping the existing file_exists and unlink logic intact and updating
the $backupPath variable in removeBackup().
app/Controllers/Customers.php (1)

13-13: 💤 Low value

Remove unused Config\Services import.

The Services class is no longer used after refactoring decryption to use the decryptValue() helper function.

Suggested fix
 use CodeIgniter\HTTP\DownloadResponse;
 use CodeIgniter\HTTP\ResponseInterface;
 use Config\OSPOS;
-use Config\Services;
 use stdClass;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/Customers.php` at line 13, Remove the unused import of
Config\Services from the Customers controller: delete the "use Config\Services;"
statement since decryption now uses the decryptValue() helper; ensure any
references to Services in this file are also removed or replaced (look for the
Customers class and any methods previously calling Services) so there are no
leftover unused imports or undefined references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Database/Migrations/20220127000000_convert_to_ci4.php`:
- Around line 36-40: check the boolean return value of checkEncryption()
wherever it's called in the migration flow (the calls just before removeBackup()
and the later call around the encryption step) and fail fast if it returns
false: do not proceed with removeBackup() or any further migration/encryption
steps — instead log the error and abort the process by throwing an exception or
returning a failure/exit status so migration stops on persistence failure.

In `@app/Helpers/security_helper.php`:
- Around line 214-227: In abortEncryptionConversion, guard against
file_get_contents($backupPath) returning false before calling file_put_contents
to avoid writing an empty .env: check the result into a variable, log and return
on failure, and only proceed to chmod and file_put_contents when the read
succeeded; also normalize $backupPath construction to use DIRECTORY_SEPARATOR
instead of a hard-coded '/' to match other functions and avoid cross-platform
issues (refer to symbols abortEncryptionConversion, $backupPath, $configPath,
file_get_contents, file_put_contents, chmod, log_message).

---

Nitpick comments:
In `@app/Controllers/Customers.php`:
- Line 13: Remove the unused import of Config\Services from the Customers
controller: delete the "use Config\Services;" statement since decryption now
uses the decryptValue() helper; ensure any references to Services in this file
are also removed or replaced (look for the Customers class and any methods
previously calling Services) so there are no leftover unused imports or
undefined references.

In `@app/Helpers/security_helper.php`:
- Around line 254-272: decryptValue uses empty($encryptedValue) which is
inconsistent with encryptValue's null/empty-string check and can mis-handle
values like "0"; update decryptValue to check for $encryptedValue === null ||
$encryptedValue === '' instead of empty($encryptedValue), keeping the rest of
the logic (checkEncryption(), Services::encrypter(), EncryptionException
handling and log_message calls) unchanged so behavior remains consistent with
encryptValue.
- Around line 69-71: The condition around the attempted write to $configPath is
using confusing negation: replace the current check that uses
!`@file_put_contents`(...) !== false with a direct comparison against false so
failures are explicit; call `@file_put_contents`($configPath, ...) and if that
expression === false then invoke log_message('debug', 'Could not create .env
file'). This simplifies the logic and correctly detects write failures for the
file_put_contents call in security_helper.php.
- Around line 234-241: The removeBackup function builds $backupPath using a
hardcoded '/' which is inconsistent; change it to build the path using
DIRECTORY_SEPARATOR (e.g. compose WRITEPATH, 'backup' and '.env.bak' with
DIRECTORY_SEPARATOR) so $backupPath uses the platform-safe separator, keeping
the existing file_exists and unlink logic intact and updating the $backupPath
variable in removeBackup().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2792a9e3-ea13-4499-a77b-3947d4267859

📥 Commits

Reviewing files that changed from the base of the PR and between bf5af2f and 8735c07.

📒 Files selected for processing (7)
  • app/Controllers/Config.php
  • app/Controllers/Customers.php
  • app/Database/Migrations/20220127000000_convert_to_ci4.php
  • app/Helpers/security_helper.php
  • app/Libraries/Email_lib.php
  • app/Libraries/Mailchimp_lib.php
  • app/Libraries/Sms_lib.php

Comment thread app/Database/Migrations/20220127000000_convert_to_ci4.php Outdated
Comment thread app/Helpers/security_helper.php
objecttothis
objecttothis previously approved these changes May 28, 2026
@github-actions
github-actions Bot had a problem deploying to staging May 28, 2026 08:16 Failure
@github-actions

Copy link
Copy Markdown

Staging deployment failed

Check the workflow logs for details.

@objecttothis

Copy link
Copy Markdown
Member

@jekkos I approved the PR. I would look at the coderabbit comments before merging though.

@jekkos
jekkos force-pushed the fix/4554-encryption-docker-issue branch 2 times, most recently from 9c49de3 to 0b1a995 Compare June 4, 2026 08:11
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
}
}
if (strpos($configFile, 'encryption.key') !== false) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be updated to match the changes in #4571

            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);

@chmod($config_path, 0640);
if (!empty($oldKey)) {
$oldLine = "# encryption.key = '$oldKey' REMOVE IF UNNEEDED\r\n";
$insertionPoint = stripos($configFile, 'encryption.key');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs to be updated to match the changes in #4571

                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);

@jekkos
jekkos force-pushed the fix/4554-encryption-docker-issue branch 3 times, most recently from 0a4eac5 to 48e3f94 Compare June 9, 2026 21:39
@jekkos
jekkos force-pushed the fix/4554-encryption-docker-issue branch from 48e3f94 to 46bfe2a Compare June 18, 2026 18:59
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
'enable_new_look' => '',
'enable_right_bar' => '',
'enable_right_bar_tooltip' => '',
'encryption_failed' => 'Failed to encrypt data. Please check encryption configuration.',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jekkos do we need to propagate this key to the other language files with an empty '' or will weblate pick it up and propagate for us?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Somehow... Fresh install broke after bringing container down and back up. Encryption key related.

2 participants