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