- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
397 lines
11 KiB
PHP
397 lines
11 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Token Manager Library
|
|
*
|
|
* Handles secure token storage and management with AES-256 encryption
|
|
*
|
|
* @package DeskMoloni
|
|
* @author Descomplicar®
|
|
* @copyright 2025 Descomplicar
|
|
* @version 3.0.0
|
|
*/
|
|
class TokenManager
|
|
{
|
|
private $CI;
|
|
|
|
// Encryption configuration
|
|
private $cipher = 'AES-256-CBC';
|
|
private $key_size = 32; // 256 bits
|
|
private $iv_size = 16; // 128 bits
|
|
|
|
// Token storage keys
|
|
private $access_token_key = 'desk_moloni_access_token_encrypted';
|
|
private $refresh_token_key = 'desk_moloni_refresh_token_encrypted';
|
|
private $token_expires_key = 'desk_moloni_token_expires';
|
|
private $token_scope_key = 'desk_moloni_token_scope';
|
|
private $encryption_key_option = 'desk_moloni_encryption_key';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->CI = &get_instance();
|
|
|
|
// Ensure encryption key exists
|
|
$this->ensure_encryption_key();
|
|
}
|
|
|
|
/**
|
|
* Save OAuth tokens securely
|
|
*
|
|
* @param array $token_data Token response from OAuth
|
|
* @return bool Success status
|
|
*/
|
|
public function save_tokens($token_data)
|
|
{
|
|
try {
|
|
// Validate required fields
|
|
if (!isset($token_data['access_token'])) {
|
|
throw new Exception('Access token is required');
|
|
}
|
|
|
|
// Calculate expiration time with 60-second buffer
|
|
$expires_in = isset($token_data['expires_in']) ? (int)$token_data['expires_in'] : 3600;
|
|
$expires_at = time() + $expires_in - 60;
|
|
|
|
// Encrypt and save access token
|
|
$encrypted_access = $this->encrypt($token_data['access_token']);
|
|
update_option($this->access_token_key, $encrypted_access);
|
|
|
|
// Encrypt and save refresh token if provided
|
|
if (isset($token_data['refresh_token'])) {
|
|
$encrypted_refresh = $this->encrypt($token_data['refresh_token']);
|
|
update_option($this->refresh_token_key, $encrypted_refresh);
|
|
}
|
|
|
|
// Save expiration and scope
|
|
update_option($this->token_expires_key, $expires_at);
|
|
update_option($this->token_scope_key, $token_data['scope'] ?? '');
|
|
|
|
// Log successful token save
|
|
log_activity('Desk-Moloni: OAuth tokens saved securely');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
log_activity('Desk-Moloni: Token save failed - ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get decrypted access token
|
|
*
|
|
* @return string|null Access token or null if not available
|
|
*/
|
|
public function get_access_token()
|
|
{
|
|
try {
|
|
$encrypted_token = get_option($this->access_token_key);
|
|
|
|
if (empty($encrypted_token)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->decrypt($encrypted_token);
|
|
|
|
} catch (Exception $e) {
|
|
log_activity('Desk-Moloni: Access token decryption failed - ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get decrypted refresh token
|
|
*
|
|
* @return string|null Refresh token or null if not available
|
|
*/
|
|
public function get_refresh_token()
|
|
{
|
|
try {
|
|
$encrypted_token = get_option($this->refresh_token_key);
|
|
|
|
if (empty($encrypted_token)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->decrypt($encrypted_token);
|
|
|
|
} catch (Exception $e) {
|
|
log_activity('Desk-Moloni: Refresh token decryption failed - ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if tokens are valid and not expired
|
|
*
|
|
* @return bool Token validity status
|
|
*/
|
|
public function are_tokens_valid()
|
|
{
|
|
// Check if access token exists
|
|
if (empty($this->get_access_token())) {
|
|
return false;
|
|
}
|
|
|
|
// Check expiration
|
|
$expires_at = get_option($this->token_expires_key);
|
|
if ($expires_at && time() >= $expires_at) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if tokens are close to expiring (within 5 minutes)
|
|
*
|
|
* @return bool True if tokens expire soon
|
|
*/
|
|
public function tokens_expire_soon()
|
|
{
|
|
$expires_at = get_option($this->token_expires_key);
|
|
|
|
if (!$expires_at) {
|
|
return false;
|
|
}
|
|
|
|
return (time() + 300) >= $expires_at; // 5 minutes
|
|
}
|
|
|
|
/**
|
|
* Get token expiration timestamp
|
|
*
|
|
* @return int|null Expiration timestamp or null
|
|
*/
|
|
public function get_token_expiration()
|
|
{
|
|
return get_option($this->token_expires_key) ?: null;
|
|
}
|
|
|
|
/**
|
|
* Get token scope
|
|
*
|
|
* @return string Token scope
|
|
*/
|
|
public function get_token_scope()
|
|
{
|
|
return get_option($this->token_scope_key) ?: '';
|
|
}
|
|
|
|
/**
|
|
* Clear all stored tokens
|
|
*
|
|
* @return bool Success status
|
|
*/
|
|
public function clear_tokens()
|
|
{
|
|
try {
|
|
update_option($this->access_token_key, '');
|
|
update_option($this->refresh_token_key, '');
|
|
update_option($this->token_expires_key, '');
|
|
update_option($this->token_scope_key, '');
|
|
|
|
log_activity('Desk-Moloni: OAuth tokens cleared');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
log_activity('Desk-Moloni: Token clear failed - ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive token status
|
|
*
|
|
* @return array Token status information
|
|
*/
|
|
public function get_token_status()
|
|
{
|
|
$expires_at = $this->get_token_expiration();
|
|
|
|
return [
|
|
'has_access_token' => !empty($this->get_access_token()),
|
|
'has_refresh_token' => !empty($this->get_refresh_token()),
|
|
'is_valid' => $this->are_tokens_valid(),
|
|
'expires_soon' => $this->tokens_expire_soon(),
|
|
'expires_at' => $expires_at,
|
|
'expires_in' => $expires_at ? max(0, $expires_at - time()) : 0,
|
|
'scope' => $this->get_token_scope(),
|
|
'formatted_expiry' => $expires_at ? date('Y-m-d H:i:s', $expires_at) : null
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Encrypt data using AES-256-CBC
|
|
*
|
|
* @param string $data Data to encrypt
|
|
* @return string Base64 encoded encrypted data with IV
|
|
*/
|
|
private function encrypt($data)
|
|
{
|
|
if (empty($data)) {
|
|
return '';
|
|
}
|
|
|
|
$key = $this->get_encryption_key();
|
|
$iv = random_bytes($this->iv_size);
|
|
|
|
$encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
|
|
|
|
if ($encrypted === false) {
|
|
throw new Exception('Encryption failed');
|
|
}
|
|
|
|
// Prepend IV to encrypted data and encode
|
|
return base64_encode($iv . $encrypted);
|
|
}
|
|
|
|
/**
|
|
* Decrypt data using AES-256-CBC
|
|
*
|
|
* @param string $encrypted_data Base64 encoded encrypted data with IV
|
|
* @return string Decrypted data
|
|
*/
|
|
private function decrypt($encrypted_data)
|
|
{
|
|
if (empty($encrypted_data)) {
|
|
return '';
|
|
}
|
|
|
|
$data = base64_decode($encrypted_data);
|
|
|
|
if ($data === false || strlen($data) < $this->iv_size) {
|
|
throw new Exception('Invalid encrypted data');
|
|
}
|
|
|
|
$key = $this->get_encryption_key();
|
|
$iv = substr($data, 0, $this->iv_size);
|
|
$encrypted = substr($data, $this->iv_size);
|
|
|
|
$decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
|
|
|
|
if ($decrypted === false) {
|
|
throw new Exception('Decryption failed');
|
|
}
|
|
|
|
return $decrypted;
|
|
}
|
|
|
|
/**
|
|
* Get or generate encryption key
|
|
*
|
|
* @return string Encryption key
|
|
*/
|
|
private function get_encryption_key()
|
|
{
|
|
$key = get_option($this->encryption_key_option);
|
|
|
|
if (empty($key)) {
|
|
throw new Exception('Encryption key not found');
|
|
}
|
|
|
|
return base64_decode($key);
|
|
}
|
|
|
|
/**
|
|
* Ensure encryption key exists
|
|
*/
|
|
private function ensure_encryption_key()
|
|
{
|
|
$existing_key = get_option($this->encryption_key_option);
|
|
|
|
if (empty($existing_key)) {
|
|
// Generate new random key
|
|
$key = random_bytes($this->key_size);
|
|
$encoded_key = base64_encode($key);
|
|
|
|
update_option($this->encryption_key_option, $encoded_key);
|
|
|
|
log_activity('Desk-Moloni: New encryption key generated');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rotate encryption key (for security maintenance)
|
|
* WARNING: This will invalidate all existing tokens
|
|
*
|
|
* @return bool Success status
|
|
*/
|
|
public function rotate_encryption_key()
|
|
{
|
|
try {
|
|
// Clear existing tokens first
|
|
$this->clear_tokens();
|
|
|
|
// Generate new key
|
|
$new_key = random_bytes($this->key_size);
|
|
$encoded_key = base64_encode($new_key);
|
|
|
|
update_option($this->encryption_key_option, $encoded_key);
|
|
|
|
log_activity('Desk-Moloni: Encryption key rotated - all tokens cleared');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
log_activity('Desk-Moloni: Key rotation failed - ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate encryption setup
|
|
*
|
|
* @return array Validation results
|
|
*/
|
|
public function validate_encryption()
|
|
{
|
|
$issues = [];
|
|
|
|
// Check if OpenSSL is available
|
|
if (!extension_loaded('openssl')) {
|
|
$issues[] = 'OpenSSL extension not loaded';
|
|
}
|
|
|
|
// Check if cipher is supported
|
|
if (!in_array($this->cipher, openssl_get_cipher_methods())) {
|
|
$issues[] = 'AES-256-CBC cipher not supported';
|
|
}
|
|
|
|
// Check if encryption key exists
|
|
try {
|
|
$this->get_encryption_key();
|
|
} catch (Exception $e) {
|
|
$issues[] = 'Encryption key not available: ' . $e->getMessage();
|
|
}
|
|
|
|
// Test encryption/decryption
|
|
try {
|
|
$test_data = 'test_token_' . time();
|
|
$encrypted = $this->encrypt($test_data);
|
|
$decrypted = $this->decrypt($encrypted);
|
|
|
|
if ($decrypted !== $test_data) {
|
|
$issues[] = 'Encryption/decryption test failed';
|
|
}
|
|
} catch (Exception $e) {
|
|
$issues[] = 'Encryption test failed: ' . $e->getMessage();
|
|
}
|
|
|
|
return [
|
|
'is_valid' => empty($issues),
|
|
'issues' => $issues,
|
|
'cipher' => $this->cipher,
|
|
'openssl_loaded' => extension_loaded('openssl'),
|
|
'supported_ciphers' => openssl_get_cipher_methods()
|
|
];
|
|
}
|
|
} |