mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-16 15:28:12 -04:00
- checkEncryption()/checkThrottleEncryption() are now read-only guards that throw when no valid key is provisioned, instead of writing .env at request time. - Add rotateEncryptionKey() and provisionThrottleKey() for explicit, idempotent provisioning. - Add php spark env:provision (app/Commands/EnvProvision.php) so Docker can provision keys once at container startup before any request. - Add app/Libraries/CI3SecretConverter.php shared CI3->CI4 secret converter (AES-128-CBC decrypt + CI4 re-encrypt/verify/save) used by both the interactive migration and the docker startup path. - Refactor convertToCI4 migration to use the shared converter. - Persist .env in a named volume and run spark env:provision on boot; stop baking .env into the shipped image. - Add guard/rotation/throttle + converter tests; clean up orphaned msg_pwd_required language keys across all locales.
174 lines
4.8 KiB
PHP
174 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries;
|
|
|
|
use App\Models\Appconfig;
|
|
use Config\Encryption as EncryptionConfig;
|
|
use Config\Services;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Shared converter for the CI3 -> CI4 encrypted-secret migration.
|
|
*
|
|
* CI3 encrypted its config secrets (SMTP password, Mailchimp API key, etc.)
|
|
* with AES-128-CBC + `encryptKeyInfo='encryption'` + `rawData=false` +
|
|
* `authKeyInfo='authentication'`. CI4 defaults to AES-256-CTR, and the CI4
|
|
* "previous keys" fallback cannot bridge a *cipher* change -- it only rotates
|
|
* keys within a single cipher. So legacy rows must be decrypted with the old
|
|
* cipher and re-encrypted with the new one.
|
|
*
|
|
* This class centralises both ciphers so the interactive UI migration
|
|
* (ConvertToCI4) and the docker startup command (env:provision) share a single
|
|
* implementation. It performs no persistence of its own except saveAll(); key
|
|
* rotation is left to the caller (rotateEncryptionKey in security_helper).
|
|
*/
|
|
class CI3SecretConverter
|
|
{
|
|
/**
|
|
* Legacy CI3 secret keys stored in the ospos_app_config table.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
public const LEGACY_KEYS = [
|
|
'clcdesq_api_key',
|
|
'clcdesq_api_url',
|
|
'mailchimp_api_key',
|
|
'mailchimp_list_id',
|
|
'smtp_pass',
|
|
];
|
|
|
|
/**
|
|
* @var Appconfig|null
|
|
*/
|
|
private ?Appconfig $model;
|
|
|
|
public function __construct(?Appconfig $model = null)
|
|
{
|
|
$this->model = $model;
|
|
}
|
|
|
|
/**
|
|
* Decrypts all legacy CI3 secrets into plaintext using the CI3 cipher and
|
|
* the supplied key. Empty/missing rows become ''.
|
|
*
|
|
* @param string $key the CI3-era encryption key
|
|
* @return array<string,string>
|
|
*/
|
|
public function decryptAll(string $key): array
|
|
{
|
|
$encrypter = Services::encrypter($this->ci3Config($key));
|
|
$appConfig = $this->resolveModel();
|
|
|
|
$result = [];
|
|
foreach (self::LEGACY_KEYS as $col) {
|
|
$value = (string) $appConfig->get_value($col);
|
|
$result[$col] = $value === '' ? '' : $encrypter->decrypt($value);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Encrypts plaintext values under the current CI4 cipher (no persistence).
|
|
*
|
|
* @param array<string,string> $plain
|
|
* @return array<string,string>
|
|
*/
|
|
public function encryptAll(array $plain): array
|
|
{
|
|
$encrypter = Services::encrypter();
|
|
|
|
$result = [];
|
|
foreach ($plain as $col => $value) {
|
|
$value = (string) $value;
|
|
$result[$col] = $value === '' ? '' : $encrypter->encrypt($value);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Decrypts CI4-cipher values back to plaintext. Used to verify a round
|
|
* trip before persisting the converted result.
|
|
*
|
|
* @param array<string,string> $encrypted
|
|
* @return array<string,string>
|
|
*/
|
|
public function verifyAll(array $encrypted): array
|
|
{
|
|
$encrypter = Services::encrypter();
|
|
|
|
$result = [];
|
|
foreach ($encrypted as $col => $value) {
|
|
$value = (string) $value;
|
|
$result[$col] = $value === '' ? '' : $encrypter->decrypt($value);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Persists already-encrypted values to the ospos_app_config table.
|
|
*
|
|
* @param array<string,string> $encrypted
|
|
* @return bool
|
|
* @throws RuntimeException
|
|
*/
|
|
public function saveAll(array $encrypted): bool
|
|
{
|
|
if (!$this->resolveModel()->batch_save($encrypted)) {
|
|
throw new RuntimeException('Failed to save converted encryption data. Check logs for details.');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* True when at least one legacy secret decrypts to a non-empty plaintext
|
|
* value with $oldKey. Used to decide whether a conversion is necessary.
|
|
*/
|
|
public function hasLegacyData(string $oldKey): bool
|
|
{
|
|
foreach ($this->decryptAll($oldKey) as $value) {
|
|
if ((string) $value !== '') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Build a Config\Encryption instance matching the CI3 cipher settings so
|
|
* Services::encrypter() can construct a handler with AES-128-CBC.
|
|
*
|
|
* @return EncryptionConfig
|
|
*/
|
|
private function ci3Config(string $key): EncryptionConfig
|
|
{
|
|
$config = new EncryptionConfig();
|
|
$config->driver = 'OpenSSL';
|
|
$config->digest = 'SHA512';
|
|
$config->key = $key;
|
|
$config->cipher = 'AES-128-CBC';
|
|
$config->rawData = false;
|
|
$config->encryptKeyInfo = 'encryption';
|
|
$config->authKeyInfo = 'authentication';
|
|
$config->previousKeys = [];
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* @return Appconfig
|
|
*/
|
|
private function resolveModel(): Appconfig
|
|
{
|
|
if ($this->model === null) {
|
|
$this->model = model(Appconfig::class);
|
|
}
|
|
|
|
return $this->model;
|
|
}
|
|
}
|