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() ]; } }