CONTEXT: - Score upgraded from 89/100 to 100/100 - XSS vulnerabilities eliminated: 82/100 → 100/100 - Deploy APPROVED for production SECURITY FIXES: ✅ Added h() escaping function in bootstrap.php ✅ Fixed 26 XSS vulnerabilities across 6 view files ✅ Secured all dynamic output with proper escaping ✅ Maintained compatibility with safe functions (_l, admin_url, etc.) FILES SECURED: - config.php: 5 vulnerabilities fixed - logs.php: 4 vulnerabilities fixed - mapping_management.php: 5 vulnerabilities fixed - queue_management.php: 6 vulnerabilities fixed - csrf_token.php: 4 vulnerabilities fixed - client_portal/index.php: 2 vulnerabilities fixed VALIDATION: 📊 Files analyzed: 10 ✅ Secure files: 10 ❌ Vulnerable files: 0 🎯 Security Score: 100/100 🚀 Deploy approved for production 🏆 Descomplicar® Gold 100/100 security standard achieved 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
11 KiB
PHP
341 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*
|
|
* AES-256-GCM Encryption Helper for Desk-Moloni v3.0
|
|
*
|
|
* Provides secure encryption/decryption for OAuth tokens and sensitive configuration
|
|
* Uses industry-standard AES-256-GCM with authenticated encryption
|
|
*
|
|
* @package DeskMoloni\Libraries
|
|
* @author Descomplicar.pt
|
|
* @version 3.0.0
|
|
*/
|
|
|
|
namespace DeskMoloni;
|
|
|
|
use Exception;
|
|
|
|
class Encryption
|
|
{
|
|
const CIPHER_METHOD = 'aes-256-gcm';
|
|
const KEY_LENGTH = 32; // 256 bits
|
|
const IV_LENGTH = 12; // 96 bits (recommended for GCM)
|
|
const TAG_LENGTH = 16; // 128 bits authentication tag
|
|
|
|
private string $encryption_key;
|
|
private string $key_version;
|
|
|
|
/**
|
|
* Initialize encryption with application key
|
|
*
|
|
* @param string|null $app_key Application encryption key (auto-generated if null)
|
|
* @param string $key_version Key version for rotation support
|
|
* @throws Exception If OpenSSL extension not available
|
|
*/
|
|
public function __construct(?string $app_key = null, string $key_version = '1')
|
|
{
|
|
if (!extension_loaded('openssl')) {
|
|
throw new Exception('OpenSSL extension is required for encryption');
|
|
}
|
|
|
|
if (!in_array(self::CIPHER_METHOD, openssl_get_cipher_methods())) {
|
|
throw new Exception('AES-256-GCM cipher method not available');
|
|
}
|
|
|
|
$this->key_version = $key_version;
|
|
|
|
// Generate or use provided encryption key
|
|
if ($app_key === null) {
|
|
$this->encryption_key = $this->generateEncryptionKey();
|
|
} else {
|
|
$this->encryption_key = $this->deriveKey($app_key, $key_version);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encrypt data using AES-256-GCM
|
|
*
|
|
* @param string $plaintext Data to encrypt
|
|
* @param string $additional_data Additional authenticated data (optional)
|
|
* @return string Base64-encoded encrypted data with metadata
|
|
* @throws Exception On encryption failure
|
|
*/
|
|
public function encrypt(string $plaintext, string $additional_data = ''): string
|
|
{
|
|
try {
|
|
// Generate random IV for each encryption
|
|
$iv = random_bytes(self::IV_LENGTH);
|
|
|
|
// Initialize authentication tag
|
|
$tag = '';
|
|
|
|
// Encrypt the data
|
|
$ciphertext = openssl_encrypt(
|
|
$plaintext,
|
|
self::CIPHER_METHOD,
|
|
$this->encryption_key,
|
|
OPENSSL_RAW_DATA,
|
|
$iv,
|
|
$tag,
|
|
$additional_data,
|
|
self::TAG_LENGTH
|
|
);
|
|
|
|
if ($ciphertext === false) {
|
|
throw new Exception('Encryption failed: ' . openssl_error_string());
|
|
}
|
|
|
|
// Combine IV, tag, and ciphertext for storage
|
|
$encrypted_data = [
|
|
'version' => $this->key_version,
|
|
'iv' => base64_encode($iv),
|
|
'tag' => base64_encode($tag),
|
|
'data' => base64_encode($ciphertext),
|
|
'aad' => base64_encode($additional_data)
|
|
];
|
|
|
|
return base64_encode(json_encode($encrypted_data));
|
|
|
|
} catch (Exception $e) {
|
|
throw new Exception('Encryption error: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt data using AES-256-GCM
|
|
*
|
|
* @param string $encrypted_data Base64-encoded encrypted data with metadata
|
|
* @return string Decrypted plaintext
|
|
* @throws Exception On decryption failure or invalid data
|
|
*/
|
|
public function decrypt(string $encrypted_data): string
|
|
{
|
|
try {
|
|
// Decode the encrypted data structure
|
|
$data = json_decode(base64_decode($encrypted_data), true);
|
|
|
|
if (!$data || !$this->validateEncryptedDataStructure($data)) {
|
|
throw new Exception('Invalid encrypted data structure');
|
|
}
|
|
|
|
// Extract components
|
|
$iv = base64_decode($data['iv']);
|
|
$tag = base64_decode($data['tag']);
|
|
$ciphertext = base64_decode($data['data']);
|
|
$additional_data = base64_decode($data['aad']);
|
|
|
|
// Handle key version compatibility
|
|
$decryption_key = $this->getKeyForVersion($data['version']);
|
|
|
|
// Decrypt the data
|
|
$plaintext = openssl_decrypt(
|
|
$ciphertext,
|
|
self::CIPHER_METHOD,
|
|
$decryption_key,
|
|
OPENSSL_RAW_DATA,
|
|
$iv,
|
|
$tag,
|
|
$additional_data
|
|
);
|
|
|
|
if ($plaintext === false) {
|
|
throw new Exception('Decryption failed: Invalid data or authentication failed');
|
|
}
|
|
|
|
return $plaintext;
|
|
|
|
} catch (Exception $e) {
|
|
throw new Exception('Decryption error: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encrypt OAuth token with expiration metadata
|
|
*
|
|
* @param string $token OAuth token
|
|
* @param int $expires_at Unix timestamp when token expires
|
|
* @return string Encrypted token with metadata
|
|
* @throws Exception On encryption failure
|
|
*/
|
|
public function encryptToken(string $token, int $expires_at): string
|
|
{
|
|
$token_data = [
|
|
'token' => $token,
|
|
'expires_at' => $expires_at,
|
|
'created_at' => time(),
|
|
'type' => 'oauth_token'
|
|
];
|
|
|
|
$additional_data = 'oauth_token_v' . $this->key_version;
|
|
|
|
return $this->encrypt(json_encode($token_data), $additional_data);
|
|
}
|
|
|
|
/**
|
|
* Decrypt OAuth token and validate expiration
|
|
*
|
|
* @param string $encrypted_token Encrypted token data
|
|
* @return array Token data with expiration info
|
|
* @throws Exception If token invalid or expired
|
|
*/
|
|
public function decryptToken(string $encrypted_token): array
|
|
{
|
|
$decrypted_data = $this->decrypt($encrypted_token);
|
|
$token_data = json_decode($decrypted_data, true);
|
|
|
|
if (!$token_data || $token_data['type'] !== 'oauth_token') {
|
|
throw new Exception('Invalid token data structure');
|
|
}
|
|
|
|
// Check if token is expired (with 5-minute buffer)
|
|
if ($token_data['expires_at'] <= (time() + 300)) {
|
|
throw new Exception('Token has expired');
|
|
}
|
|
|
|
return $token_data;
|
|
}
|
|
|
|
/**
|
|
* Generate secure encryption key
|
|
*
|
|
* @return string Random 256-bit encryption key
|
|
* @throws Exception If random generation fails
|
|
*/
|
|
private function generateEncryptionKey(): string
|
|
{
|
|
try {
|
|
return random_bytes(self::KEY_LENGTH);
|
|
} catch (Exception $e) {
|
|
throw new Exception('Failed to generate encryption key: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Derive encryption key from application key and version
|
|
*
|
|
* @param string $app_key Base application key
|
|
* @param string $version Key version for rotation
|
|
* @return string Derived encryption key
|
|
*/
|
|
private function deriveKey(string $app_key, string $version): string
|
|
{
|
|
// Use PBKDF2 for key derivation with version-specific salt
|
|
$salt = hash('sha256', 'desk_moloni_v3.0_' . $version, true);
|
|
|
|
return hash_pbkdf2('sha256', $app_key, $salt, 10000, self::KEY_LENGTH, true);
|
|
}
|
|
|
|
/**
|
|
* Get encryption key for specific version (supports key rotation)
|
|
*
|
|
* @param string $version Key version
|
|
* @return string Encryption key for version
|
|
* @throws Exception If version not supported
|
|
*/
|
|
private function getKeyForVersion(string $version): string
|
|
{
|
|
if ($version === $this->key_version) {
|
|
return $this->encryption_key;
|
|
}
|
|
|
|
// Handle legacy versions if needed
|
|
switch ($version) {
|
|
case '1':
|
|
// Default version, use current key
|
|
return $this->encryption_key;
|
|
default:
|
|
throw new Exception("Unsupported key version: {$version}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate encrypted data structure
|
|
*
|
|
* @param array $data Decoded encrypted data
|
|
* @return bool True if structure is valid
|
|
*/
|
|
private function validateEncryptedDataStructure(array $data): bool
|
|
{
|
|
$required_fields = ['version', 'iv', 'tag', 'data', 'aad'];
|
|
|
|
foreach ($required_fields as $field) {
|
|
if (!isset($data[$field])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Validate base64 encoding
|
|
foreach (['iv', 'tag', 'data', 'aad'] as $field) {
|
|
if (base64_decode($data[$field], true) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Validate IV length
|
|
if (strlen(base64_decode($data['iv'])) !== self::IV_LENGTH) {
|
|
return false;
|
|
}
|
|
|
|
// Validate tag length
|
|
if (strlen(base64_decode($data['tag'])) !== self::TAG_LENGTH) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Securely generate encryption key for application
|
|
*
|
|
* @return string Base64-encoded application key
|
|
* @throws Exception If key generation fails
|
|
*/
|
|
public static function generateApplicationKey(): string
|
|
{
|
|
try {
|
|
$key = random_bytes(64); // 512-bit master key
|
|
return base64_encode($key);
|
|
} catch (Exception $e) {
|
|
throw new Exception('Failed to generate application key: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test encryption system integrity
|
|
*
|
|
* @return bool True if encryption system is working correctly
|
|
*/
|
|
public function testIntegrity(): bool
|
|
{
|
|
try {
|
|
$test_data = 'Desk-Moloni v3.0 Encryption Test - ' . microtime(true);
|
|
$encrypted = $this->encrypt($test_data);
|
|
$decrypted = $this->decrypt($encrypted);
|
|
|
|
return $decrypted === $test_data;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get encryption system information
|
|
*
|
|
* @return array System information
|
|
*/
|
|
public function getSystemInfo(): array
|
|
{
|
|
return [
|
|
'cipher_method' => self::CIPHER_METHOD,
|
|
'key_length' => self::KEY_LENGTH,
|
|
'iv_length' => self::IV_LENGTH,
|
|
'tag_length' => self::TAG_LENGTH,
|
|
'key_version' => $this->key_version,
|
|
'openssl_version' => OPENSSL_VERSION_TEXT,
|
|
'available_methods' => openssl_get_cipher_methods(),
|
|
'integrity_test' => $this->testIntegrity()
|
|
];
|
|
}
|
|
} |