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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Framework\Encryption;
use App\Framework\Random\RandomGenerator;
use RuntimeException;
/**
* AES-256-CBC encryption implementation
*/
final readonly class AesEncryption implements EncryptionInterface
{
private const METHOD = 'AES-256-CBC';
private const PREFIX = 'ENC[';
private const SUFFIX = ']';
private const IV_LENGTH = 16;
private const KEY_LENGTH = 32;
public function __construct(
private RandomGenerator $randomGenerator,
private string $encryptionKey
) {
if (! $this->validateKey($this->encryptionKey)) {
throw new RuntimeException('Invalid encryption key length for AES-256');
}
if (! extension_loaded('openssl')) {
throw new RuntimeException('OpenSSL extension is required for AES encryption');
}
}
public function encrypt(string $data): string
{
$iv = $this->randomGenerator->bytes(self::IV_LENGTH);
$encrypted = openssl_encrypt($data, self::METHOD, $this->encryptionKey, 0, $iv);
if ($encrypted === false) {
throw new RuntimeException('Failed to encrypt data: ' . openssl_error_string());
}
$payload = base64_encode($iv . $encrypted);
return self::PREFIX . $payload . self::SUFFIX;
}
public function decrypt(string $encryptedData): string
{
if (! $this->isEncrypted($encryptedData)) {
throw new RuntimeException('Data does not appear to be encrypted with this method');
}
// Remove prefix and suffix
$payload = substr(
$encryptedData,
strlen(self::PREFIX),
-strlen(self::SUFFIX)
);
$data = base64_decode($payload);
if ($data === false) {
throw new RuntimeException('Invalid base64 encoding in encrypted data');
}
if (strlen($data) < self::IV_LENGTH) {
throw new RuntimeException('Encrypted data is too short to contain valid IV');
}
$iv = substr($data, 0, self::IV_LENGTH);
$encrypted = substr($data, self::IV_LENGTH);
$decrypted = openssl_decrypt($encrypted, self::METHOD, $this->encryptionKey, 0, $iv);
if ($decrypted === false) {
throw new RuntimeException('Failed to decrypt data: ' . openssl_error_string());
}
return $decrypted;
}
public function isEncrypted(string $data): bool
{
return str_starts_with($data, self::PREFIX) && str_ends_with($data, self::SUFFIX);
}
public function getMethod(): string
{
return self::METHOD;
}
public function validateKey(string $key): bool
{
return strlen($key) >= self::KEY_LENGTH;
}
/**
* Generate a new encryption key suitable for AES-256
*/
public function generateKey(): string
{
return base64_encode($this->randomGenerator->bytes(self::KEY_LENGTH));
}
/**
* Get encryption metadata for debugging/logging
*/
public function getMetadata(): array
{
return [
'method' => self::METHOD,
'key_length' => self::KEY_LENGTH,
'iv_length' => self::IV_LENGTH,
'prefix' => self::PREFIX,
'suffix' => self::SUFFIX,
'openssl_version' => OPENSSL_VERSION_TEXT,
];
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Framework\Encryption;
use App\Framework\Random\RandomGenerator;
use RuntimeException;
/**
* Basic encryption fallback for environments without OpenSSL
* WARNING: This is NOT cryptographically secure and should only be used for development!
*/
final readonly class BasicEncryption implements EncryptionInterface
{
private const METHOD = 'BASIC-XOR';
private const PREFIX = 'BASIC[';
private const SUFFIX = ']';
private const MIN_KEY_LENGTH = 16;
public function __construct(
private RandomGenerator $randomGenerator,
private string $encryptionKey
) {
if (! $this->validateKey($this->encryptionKey)) {
throw new RuntimeException('Encryption key must be at least ' . self::MIN_KEY_LENGTH . ' characters');
}
}
public function encrypt(string $data): string
{
// Generate a random salt
$salt = $this->randomGenerator->bytes(8);
$saltHex = bin2hex($salt);
// Create expanded key from base key + salt
$expandedKey = hash('sha256', $this->encryptionKey . $saltHex, true);
// Simple XOR encryption (NOT secure, only for fallback!)
$encrypted = $this->xorEncrypt($data, $expandedKey);
// Combine salt + encrypted data
$payload = base64_encode($salt . $encrypted);
return self::PREFIX . $payload . self::SUFFIX;
}
public function decrypt(string $encryptedData): string
{
if (! $this->isEncrypted($encryptedData)) {
throw new RuntimeException('Data does not appear to be encrypted with this method');
}
// Remove prefix and suffix
$payload = substr(
$encryptedData,
strlen(self::PREFIX),
-strlen(self::SUFFIX)
);
$data = base64_decode($payload);
if ($data === false) {
throw new RuntimeException('Invalid base64 encoding in encrypted data');
}
if (strlen($data) < 8) {
throw new RuntimeException('Encrypted data is too short to contain valid salt');
}
// Extract salt and encrypted data
$salt = substr($data, 0, 8);
$encrypted = substr($data, 8);
$saltHex = bin2hex($salt);
// Recreate expanded key
$expandedKey = hash('sha256', $this->encryptionKey . $saltHex, true);
// Decrypt using XOR
return $this->xorDecrypt($encrypted, $expandedKey);
}
public function isEncrypted(string $data): bool
{
return str_starts_with($data, self::PREFIX) && str_ends_with($data, self::SUFFIX);
}
public function getMethod(): string
{
return self::METHOD;
}
public function validateKey(string $key): bool
{
return strlen($key) >= self::MIN_KEY_LENGTH;
}
/**
* Simple XOR encryption (NOT cryptographically secure!)
*/
private function xorEncrypt(string $data, string $key): string
{
$result = '';
$keyLen = strlen($key);
$dataLen = strlen($data);
for ($i = 0; $i < $dataLen; $i++) {
$result .= $data[$i] ^ $key[$i % $keyLen];
}
return $result;
}
/**
* Simple XOR decryption (same as encryption for XOR)
*/
private function xorDecrypt(string $encrypted, string $key): string
{
return $this->xorEncrypt($encrypted, $key); // XOR is symmetric
}
/**
* Generate a new encryption key suitable for basic encryption
*/
public function generateKey(): string
{
return base64_encode($this->randomGenerator->bytes(self::MIN_KEY_LENGTH));
}
/**
* Get encryption metadata for debugging/logging
*/
public function getMetadata(): array
{
return [
'method' => self::METHOD,
'key_length' => self::MIN_KEY_LENGTH,
'prefix' => self::PREFIX,
'suffix' => self::SUFFIX,
'warning' => 'NOT CRYPTOGRAPHICALLY SECURE - DEVELOPMENT ONLY',
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Framework\Encryption;
use App\Framework\Random\RandomGenerator;
use RuntimeException;
/**
* Factory for creating encryption instances
*/
final readonly class EncryptionFactory
{
public function __construct(
private RandomGenerator $randomGenerator
) {
}
/**
* Create the best available encryption method
*/
public function createBest(string $encryptionKey): EncryptionInterface
{
if (extension_loaded('openssl')) {
return new AesEncryption($this->randomGenerator, $encryptionKey);
}
// Fallback to basic encryption with warning
error_log('[WARNING] OpenSSL not available, using BasicEncryption (NOT secure for production!)');
return new BasicEncryption($this->randomGenerator, $encryptionKey);
}
/**
* Create specific encryption method
*/
public function create(string $method, string $encryptionKey): EncryptionInterface
{
return match (strtoupper($method)) {
'AES', 'AES-256-CBC' => new AesEncryption($this->randomGenerator, $encryptionKey),
'BASIC', 'BASIC-XOR' => new BasicEncryption($this->randomGenerator, $encryptionKey),
default => throw new RuntimeException("Unsupported encryption method: {$method}")
};
}
/**
* Get available encryption methods
*/
public function getAvailableMethods(): array
{
$methods = ['BASIC'];
if (extension_loaded('openssl')) {
$methods[] = 'AES';
}
return $methods;
}
/**
* Check if a method is available
*/
public function isMethodAvailable(string $method): bool
{
return in_array(strtoupper($method), $this->getAvailableMethods(), true);
}
/**
* Get recommended method for current environment
*/
public function getRecommendedMethod(): string
{
return extension_loaded('openssl') ? 'AES' : 'BASIC';
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Framework\Encryption;
/**
* Interface for encryption strategies
*/
interface EncryptionInterface
{
/**
* Encrypt data with optional prefix/suffix markers
*/
public function encrypt(string $data): string;
/**
* Decrypt data, automatically handling markers
*/
public function decrypt(string $encryptedData): string;
/**
* Check if data appears to be encrypted
*/
public function isEncrypted(string $data): bool;
/**
* Get the encryption method identifier
*/
public function getMethod(): string;
/**
* Validate if encryption key is suitable for this method
*/
public function validateKey(string $key): bool;
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Framework\Encryption;
use App\Framework\Core\ValueObjects\Hash;
use App\Framework\Core\ValueObjects\HashAlgorithm;
use InvalidArgumentException;
/**
* HMAC service for webhook signature verification
* Extends the existing Encryption module with HMAC functionality using framework Hash objects
*/
final readonly class HmacService
{
/**
* Generate HMAC signature using Hash value objects
*/
public function generateHmac(string $payload, string $secret, HashAlgorithm $algorithm = HashAlgorithm::SHA256): Hash
{
if (! $algorithm->isAvailable()) {
throw new InvalidArgumentException("HMAC algorithm {$algorithm->value} is not available");
}
$hmac = hash_hmac($algorithm->value, $payload, $secret);
return Hash::fromString($hmac, $algorithm);
}
/**
* Verify HMAC signature with timing-safe comparison
*/
public function verifyHmac(string $payload, Hash $expectedHmac, string $secret): bool
{
$actualHmac = $this->generateHmac($payload, $secret, $expectedHmac->getAlgorithm());
return $expectedHmac->equals($actualHmac);
}
/**
* Verify HMAC with string signature (timing-safe)
*/
public function verifyHmacString(string $payload, string $signature, string $secret, HashAlgorithm $algorithm = HashAlgorithm::SHA256): bool
{
$expectedHmac = hash_hmac($algorithm->value, $payload, $secret);
return hash_equals($expectedHmac, $signature);
}
}