Files
desk-moloni/modules/desk_moloni/libraries/Encryption.php
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

338 lines
11 KiB
PHP

<?php
/**
* 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()
];
}
}