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