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.
This commit is contained in:
338
modules/desk_moloni/libraries/Encryption.php
Normal file
338
modules/desk_moloni/libraries/Encryption.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?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()
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user