Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
285
src/Framework/Config/EncryptedEnvLoader.php
Normal file
285
src/Framework/Config/EncryptedEnvLoader.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
|
||||
/**
|
||||
* Enhanced environment loader with encryption support
|
||||
*/
|
||||
final readonly class EncryptedEnvLoader
|
||||
{
|
||||
public function __construct(
|
||||
private EncryptionFactory $encryptionFactory,
|
||||
private RandomGenerator $randomGenerator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load environment from multiple files with encryption support
|
||||
*/
|
||||
public function loadEnvironment(FilePath|string $basePath, ?string $encryptionKey = null): Environment
|
||||
{
|
||||
$variables = [];
|
||||
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
|
||||
|
||||
// Load base .env file
|
||||
$envFile = $baseDir->join('.env');
|
||||
if ($envFile->exists()) {
|
||||
$variables = array_merge($variables, $this->parseEnvFile($envFile));
|
||||
}
|
||||
|
||||
// Load .env.secrets file if it exists and encryption key is provided
|
||||
$secretsFile = $baseDir->join('.env.secrets');
|
||||
if ($secretsFile->exists() && $encryptionKey !== null) {
|
||||
$secretsVariables = $this->parseEnvFile($secretsFile);
|
||||
$variables = array_merge($variables, $secretsVariables);
|
||||
}
|
||||
|
||||
// Load environment-specific files
|
||||
$appEnv = $variables['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'development';
|
||||
$envSpecificFile = $baseDir->join(".env.{$appEnv}");
|
||||
if ($envSpecificFile->exists()) {
|
||||
$envSpecificVariables = $this->parseEnvFile($envSpecificFile);
|
||||
$variables = array_merge($variables, $envSpecificVariables);
|
||||
}
|
||||
|
||||
return new Environment($variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single .env file
|
||||
*/
|
||||
private function parseEnvFile(FilePath|string $filePath): array
|
||||
{
|
||||
$variables = [];
|
||||
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
|
||||
|
||||
if (! $file->isReadable()) {
|
||||
return $variables;
|
||||
}
|
||||
|
||||
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines === false) {
|
||||
return $variables;
|
||||
}
|
||||
|
||||
foreach ($lines as $lineNumber => $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (str_starts_with($line, '#') || ! str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse key=value pairs
|
||||
$equalPos = strpos($line, '=');
|
||||
if ($equalPos === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(substr($line, 0, $equalPos));
|
||||
$value = trim(substr($line, $equalPos + 1));
|
||||
|
||||
// Remove surrounding quotes
|
||||
$value = $this->removeQuotes($value);
|
||||
|
||||
// Handle multiline values (basic support)
|
||||
if (str_ends_with($value, '\\')) {
|
||||
$value = rtrim($value, '\\');
|
||||
// In a full implementation, you'd continue reading the next line
|
||||
}
|
||||
|
||||
$variables[$name] = $this->castValue($value);
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove surrounding quotes from values
|
||||
*/
|
||||
private function removeQuotes(string $value): string
|
||||
{
|
||||
// Remove double quotes
|
||||
if (str_starts_with($value, '"') && str_ends_with($value, '"') && strlen($value) > 1) {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
// Remove single quotes
|
||||
if (str_starts_with($value, "'") && str_ends_with($value, "'") && strlen($value) > 1) {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast string values to appropriate types
|
||||
*/
|
||||
private function castValue(string $value): mixed
|
||||
{
|
||||
return match (strtolower($value)) {
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'null' => null,
|
||||
default => is_numeric($value) ? (str_contains($value, '.') ? (float) $value : (int) $value) : $value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a .env.secrets template file
|
||||
*/
|
||||
public function generateSecretsTemplate(FilePath|string $basePath, array $secretKeys = []): FilePath
|
||||
{
|
||||
$template = "# .env.secrets - Encrypted secrets file\n";
|
||||
$template .= "# Generated on " . date('Y-m-d H:i:s') . "\n";
|
||||
$template .= "# \n";
|
||||
$template .= "# Instructions:\n";
|
||||
$template .= "# 1. Set ENCRYPTION_KEY in your main .env file\n";
|
||||
$template .= "# 2. Use SecretManager::encryptSecret() to encrypt sensitive values\n";
|
||||
$template .= "# 3. Store encrypted values in this file with ENC[...] format\n";
|
||||
$template .= "# \n";
|
||||
$template .= "# Example:\n";
|
||||
$template .= "# SECRET_API_KEY=ENC[base64encodedencryptedvalue]\n";
|
||||
$template .= "# SECRET_DATABASE_PASSWORD=ENC[anotherencryptedvalue]\n";
|
||||
$template .= "\n";
|
||||
|
||||
// Add predefined secret keys
|
||||
$defaultSecretKeys = [
|
||||
'SECRET_ENCRYPTION_KEY',
|
||||
'SECRET_DATABASE_PASSWORD',
|
||||
'SECRET_API_KEY',
|
||||
'SECRET_JWT_SECRET',
|
||||
'SECRET_OAUTH_CLIENT_SECRET',
|
||||
'SECRET_SMTP_PASSWORD',
|
||||
'SECRET_REDIS_PASSWORD',
|
||||
];
|
||||
|
||||
$allSecretKeys = array_unique(array_merge($defaultSecretKeys, $secretKeys));
|
||||
|
||||
foreach ($allSecretKeys as $key) {
|
||||
$template .= "# {$key}=ENC[your_encrypted_value_here]\n";
|
||||
}
|
||||
|
||||
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
|
||||
$filePath = $baseDir->join('.env.secrets');
|
||||
file_put_contents($filePath->toString(), $template);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt secrets in an existing .env file
|
||||
*/
|
||||
public function encryptSecretsInFile(FilePath|string $filePath, string $encryptionKey, array $keysToEncrypt = []): int
|
||||
{
|
||||
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
|
||||
|
||||
if (! $file->exists() || ! $file->isReadable()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$encryption = $this->encryptionFactory->createBest($encryptionKey);
|
||||
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$encryptedCount = 0;
|
||||
$newLines = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmedLine = trim($line);
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (str_starts_with($trimmedLine, '#') || ! str_contains($trimmedLine, '=')) {
|
||||
$newLines[] = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$equalPos = strpos($trimmedLine, '=');
|
||||
if ($equalPos === false) {
|
||||
$newLines[] = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(substr($trimmedLine, 0, $equalPos));
|
||||
$value = trim(substr($trimmedLine, $equalPos + 1));
|
||||
|
||||
// Check if this key should be encrypted
|
||||
$shouldEncrypt = empty($keysToEncrypt) ? str_starts_with($name, 'SECRET_') : in_array($name, $keysToEncrypt, true);
|
||||
|
||||
if ($shouldEncrypt && ! $encryption->isEncrypted($value)) {
|
||||
$value = $this->removeQuotes($value);
|
||||
$encryptedValue = $encryption->encrypt($value);
|
||||
$newLines[] = "{$name}={$encryptedValue}";
|
||||
$encryptedCount++;
|
||||
} else {
|
||||
$newLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
file_put_contents($file->toString(), implode("\n", $newLines));
|
||||
|
||||
return $encryptedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate encryption setup
|
||||
*/
|
||||
public function validateEncryptionSetup(FilePath|string $basePath, ?string $encryptionKey = null): array
|
||||
{
|
||||
$issues = [];
|
||||
|
||||
// Check if encryption key is provided
|
||||
if ($encryptionKey === null) {
|
||||
$issues[] = [
|
||||
'type' => 'missing_key',
|
||||
'message' => 'No encryption key provided',
|
||||
'severity' => 'high',
|
||||
];
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
// Check key strength
|
||||
if (strlen($encryptionKey) < 32) {
|
||||
$issues[] = [
|
||||
'type' => 'weak_key',
|
||||
'message' => 'Encryption key should be at least 32 characters',
|
||||
'severity' => 'high',
|
||||
];
|
||||
}
|
||||
|
||||
// Check if .env.secrets exists
|
||||
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
|
||||
$secretsFile = $baseDir->join('.env.secrets');
|
||||
if (! $secretsFile->exists()) {
|
||||
$issues[] = [
|
||||
'type' => 'missing_secrets_file',
|
||||
'message' => '.env.secrets file not found',
|
||||
'severity' => 'medium',
|
||||
];
|
||||
}
|
||||
|
||||
// Check encryption availability
|
||||
$availableMethods = $this->encryptionFactory->getAvailableMethods();
|
||||
if (! in_array('AES', $availableMethods, true)) {
|
||||
$issues[] = [
|
||||
'type' => 'weak_encryption',
|
||||
'message' => 'OpenSSL not available, using basic encryption',
|
||||
'severity' => 'medium',
|
||||
];
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user