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:
119
src/Framework/Encryption/AesEncryption.php
Normal file
119
src/Framework/Encryption/AesEncryption.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
142
src/Framework/Encryption/BasicEncryption.php
Normal file
142
src/Framework/Encryption/BasicEncryption.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
76
src/Framework/Encryption/EncryptionFactory.php
Normal file
76
src/Framework/Encryption/EncryptionFactory.php
Normal 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';
|
||||
}
|
||||
}
|
||||
36
src/Framework/Encryption/EncryptionInterface.php
Normal file
36
src/Framework/Encryption/EncryptionInterface.php
Normal 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;
|
||||
}
|
||||
50
src/Framework/Encryption/HmacService.php
Normal file
50
src/Framework/Encryption/HmacService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user