Changeset 3279290
- Timestamp:
- 04/22/2025 05:17:01 PM (11 months ago)
- Location:
- fastevo-mp2/trunk
- Files:
-
- 4 edited
-
fastevo-mp2.php (modified) (7 diffs)
-
includes/class-fastevo-mp2-settings.php (modified) (4 diffs)
-
includes/class-fastevo-mp2-token-service.php (modified) (10 diffs)
-
readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
fastevo-mp2/trunk/fastevo-mp2.php
r3279133 r3279290 3 3 * Plugin Name: Fastevo MP2 4 4 * Description: Embed Fastevo MP2 protected media via Gutenberg block and shortcode. 5 * Version: 1.0. 05 * Version: 1.0.1 6 6 * Author: Fastevo 7 7 * Author URI: https://fastevo.com … … 15 15 } 16 16 17 define('FASTEVO_MP2_PLUGIN_VERSION', '1.0. 0');17 define('FASTEVO_MP2_PLUGIN_VERSION', '1.0.1'); 18 18 define('FASTEVO_MP2_PLUGIN_DIR', plugin_dir_path(__FILE__)); 19 19 define('FASTEVO_MP2_PLUGIN_URL', plugin_dir_url(__FILE__)); … … 46 46 47 47 /** 48 * Get the stored API key from options or secure file.48 * Get the stored API key from options. 49 49 * 50 50 * @return string The API key or empty string if not set … … 59 59 } 60 60 61 62 // For database storage or as fallback from file storage, decrypt the key 61 // Decrypt the key 63 62 $decrypted_key = fastevo_mp2_decrypt_api_key($api_key); 63 64 // Handle case where decryption returned another encrypted key 65 // This can happen due to double-encryption during the validation process 66 if (!empty($decrypted_key) && (strpos($decrypted_key, 'sodium:') === 0 || strpos($decrypted_key, 'base64:') === 0)) { 67 $second_decryption = fastevo_mp2_decrypt_api_key($decrypted_key); 68 if (!empty($second_decryption)) { 69 $decrypted_key = $second_decryption; 70 } 71 } 64 72 65 73 // If decryption returned something useful … … 69 77 70 78 // Last resort: return the key as-is for backward compatibility 71 // This helps in case encryption/decryption is failing but the key is actually stored in plain text72 79 return $api_key; 73 80 } … … 75 82 76 83 /** 77 * Decrypt API key using WordPress sodium crypto functions.84 * Decrypt API key using libsodium if available, otherwise base64. 78 85 * 79 86 * @param string $encrypted_key The encrypted API key … … 87 94 } 88 95 89 // If key doesn't look encrypted (no sodium marker), try base64 first 90 if (strpos($encrypted_key, ':') === false && substr($encrypted_key, 0, 4) !== '$2y') { 91 // Try to decode from base64 (fallback method) 96 // Check for our encryption prefixes 97 if (strpos($encrypted_key, 'sodium:') === 0) { 98 // This is a sodium-encrypted key 99 $encrypted_data = substr($encrypted_key, 7); // Remove 'sodium:' prefix 100 101 // Check if sodium is available for decryption 102 if (function_exists('sodium_crypto_secretbox_open')) { 103 try { 104 // Decode the base64 data 105 $decoded = base64_decode($encrypted_data); 106 107 if ($decoded) { 108 // Create encryption key from WordPress constants (same as in encryption) 109 $wp_encryption_key = ''; 110 111 // Use WordPress auth keys if defined 112 if (defined('AUTH_KEY') && defined('SECURE_AUTH_KEY')) { 113 $wp_encryption_key = AUTH_KEY . SECURE_AUTH_KEY; 114 } 115 116 // Fallback if WordPress keys aren't available 117 if (strlen($wp_encryption_key) < 32) { 118 $wp_encryption_key = 'fastevo_mp2_default_encryption_key_please_set_wp_keys'; 119 } 120 121 // Create a proper key for sodium from the WordPress keys 122 $key = hash('sha256', $wp_encryption_key, true); 123 124 // Extract nonce and ciphertext 125 $nonce_size = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; 126 $nonce = substr($decoded, 0, $nonce_size); 127 $ciphertext = substr($decoded, $nonce_size); 128 129 // Verify we have valid data 130 if (strlen($nonce) === $nonce_size && strlen($ciphertext) > 0) { 131 // Decrypt 132 $decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $key); 133 134 if ($decrypted !== false) { 135 // Success! Return the decrypted API key 136 return $decrypted; 137 } 138 } 139 } 140 } catch (Exception $e) { 141 // Sodium decryption failed, fall through to other methods 142 } 143 } 144 } 145 else if (strpos($encrypted_key, 'base64:') === 0) { 146 // This is a base64-encoded key 147 $encoded_data = substr($encrypted_key, 7); // Remove 'base64:' prefix 148 $decoded = base64_decode($encoded_data); 149 150 if ($decoded !== false) { 151 return $decoded; 152 } 153 } 154 else { 155 // Legacy format without prefix - try different approaches 156 157 // Check if this might be a sodium-encrypted key without prefix 158 if (function_exists('sodium_crypto_secretbox_open')) { 159 try { 160 $decoded = base64_decode($encrypted_key); 161 162 if ($decoded) { 163 // Create encryption key from WordPress constants 164 $wp_encryption_key = ''; 165 166 if (defined('AUTH_KEY') && defined('SECURE_AUTH_KEY')) { 167 $wp_encryption_key = AUTH_KEY . SECURE_AUTH_KEY; 168 } 169 170 if (strlen($wp_encryption_key) < 32) { 171 $wp_encryption_key = 'fastevo_mp2_default_encryption_key_please_set_wp_keys'; 172 } 173 174 $key = hash('sha256', $wp_encryption_key, true); 175 176 $nonce_size = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; 177 $nonce = substr($decoded, 0, $nonce_size); 178 $ciphertext = substr($decoded, $nonce_size); 179 180 if (strlen($nonce) === $nonce_size && strlen($ciphertext) > 0) { 181 $decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $key); 182 183 if ($decrypted !== false) { 184 return $decrypted; 185 } 186 } 187 } 188 } catch (Exception $e) { 189 // Failed sodium decryption, continue to other methods 190 } 191 } 192 193 // Check if this is a simple base64-encoded API key 92 194 $decoded = base64_decode($encrypted_key, true); 195 93 196 if ($decoded !== false && $decoded !== $encrypted_key) { 94 return $decoded; 95 } 96 // If not base64 or decoding returns the same string, return as-is 97 return $encrypted_key; 98 } 99 100 // Check if WordPress has sodium decryption available 101 if (!function_exists('wp_sodium_decrypt')) { 102 // Without sodium functions, we can't decrypt properly - return empty to force re-auth 103 return ''; 104 } 105 106 try { 107 // Use WordPress sodium decryption 108 $decrypted = wp_sodium_decrypt($encrypted_key); 109 return $decrypted; 110 } catch (Exception $e) { 111 // Decryption failed 112 113 // If decryption fails, try base64 decode as last resort 114 $decoded = base64_decode($encrypted_key, true); 115 return ($decoded !== false) ? $decoded : ''; 116 } 197 // If it looks like an API key, return it 198 if (strpos($decoded, 'v4.local.') === 0) { 199 return $decoded; 200 } 201 } 202 203 // If key is already in API key format, return as-is 204 if (strpos($encrypted_key, 'v4.local.') === 0) { 205 return $encrypted_key; 206 } 207 } 208 209 // If all decryption methods failed, return empty string 210 return ''; 117 211 } 118 212 -
fastevo-mp2/trunk/includes/class-fastevo-mp2-settings.php
r3279133 r3279290 282 282 public static function sanitize_api_key($api_key) 283 283 { 284 // Store a static flag to track if we've already validated this key 285 // This prevents multiple validations in a single request 286 static $validated_keys = array(); 287 static $validation_in_progress = false; 288 289 // IMPORTANT: Prevent recursive validation completely 290 // This stops the validation-of-encrypted-key issue 291 if ($validation_in_progress) { 292 return $api_key; // Just return as-is, don't validate during validation 293 } 294 295 // Check if this is already an encrypted key - we shouldn't re-encrypt 296 // This is crucial for preventing double-encryption during WordPress's 297 // multiple calls to the sanitize callback 298 if (strpos($api_key, 'sodium:') === 0 || strpos($api_key, 'base64:') === 0) { 299 return $api_key; // Don't re-encrypt an already encrypted key 300 } 301 302 // Set flag to block nested validations 303 $validation_in_progress = true; 304 305 // Check if we've already validated this key in this request 306 $key_hash = md5($api_key); 307 if (isset($validated_keys[$key_hash])) { 308 $validation_in_progress = false; // Reset flag before returning 309 return $validated_keys[$key_hash]; 310 } 311 284 312 // First sanitize the input 285 313 $sanitized_key = sanitize_text_field($api_key); 286 314 315 // Check if we already have a key stored 316 $stored_value = get_option('fastevo_mp2_api_key', ''); 317 287 318 // If empty input, check if we already have a key 288 319 if (empty($sanitized_key)) { 289 $stored_value = get_option('fastevo_mp2_api_key', '');290 320 // If we already have a key stored, keep using it 291 321 if (!empty($stored_value)) { … … 298 328 // Test the API key to make sure it works before encrypting and storing it 299 329 if (class_exists('Fastevo_MP2_Token_Service')) { 330 // Important: Clear any existing transients to prevent confusion 331 delete_transient('fastevo_mp2_api_key_error'); 332 delete_transient('fastevo_mp2_api_key_success'); 333 334 // IMPORTANT: Make sure we're validating with an unencrypted key 335 // Check if the key looks like it's base64 encoded 336 $validate_with_key = $sanitized_key; 337 if (preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $validate_with_key)) { 338 $decoded = @base64_decode($validate_with_key, true); 339 if ($decoded !== false && $decoded !== $validate_with_key && 340 strpos($decoded, 'v4.local.') === 0) { 341 // This is a base64 encoded key, use the decoded version 342 $validate_with_key = $decoded; 343 } 344 } 345 300 346 // Make a simple API call to verify the key works 301 $result = Fastevo_MP2_Token_Service::fetch_player_configurations($ sanitized_key);347 $result = Fastevo_MP2_Token_Service::fetch_player_configurations($validate_with_key); 302 348 303 349 if (is_wp_error($result)) { 304 // Set a transient to show an error message 350 // Get existing key to check if we're updating or setting for first time 351 $existing_key = get_option('fastevo_mp2_api_key', ''); 352 353 // For all error cases, we'll show the error message 354 355 // Always set the error transient for display 305 356 set_transient('fastevo_mp2_api_key_error', $result->get_error_message(), 60); 306 357 307 // Return the existing value instead of the new one 308 $existing_key = get_option('fastevo_mp2_api_key', ''); 358 // If we have an existing key, return it instead of the new invalid one 309 359 if (!empty($existing_key)) { 310 360 return $existing_key; 311 361 } 312 // If we had no existing key, just save the new one anyway 362 363 // For a first-time save with an invalid key, we will NOT save it 364 // Just return an empty string which means "no key saved" 365 366 // Make the error message more helpful for common error codes 367 $error_code = $result->get_error_code(); 368 if (strpos($error_code, 'fastevo_mp2_error_401') === 0 || 369 strpos($error_code, 'fastevo_mp2_error_403') === 0) { 370 set_transient('fastevo_mp2_api_key_error', 'Invalid API key. Please check that you entered the correct key.', 60); 371 } else if (strpos($error_code, 'fastevo_mp2_error_404') === 0) { 372 set_transient('fastevo_mp2_api_key_error', 'API endpoint not found. Please contact support.', 60); 373 } else if (strpos($error_code, 'fastevo_mp2_error_5') === 0) { // Any 5xx error 374 set_transient('fastevo_mp2_api_key_error', 'Fastevo API server error. Please try again later.', 60); 375 } 376 377 return ''; 313 378 } else { 314 379 // Set success transient … … 318 383 319 384 // Encrypt the key before storing in database 320 return self::encrypt_api_key($sanitized_key); 385 $encrypted = self::encrypt_api_key($sanitized_key); 386 387 // Store this validated key to prevent duplicate validations 388 // This is crucial to avoid the race condition with encoded keys 389 $validated_keys[$key_hash] = $encrypted; 390 391 // Reset validation flag before returning 392 $validation_in_progress = false; 393 394 return $encrypted; 321 395 } 322 396 323 397 /** 324 * Encrypt API key using WordPress sodium crypto functions 398 * Encrypt API key using libsodium if available, otherwise fallback to base64 399 * Uses WordPress constants for encryption key when possible 325 400 * 326 401 * @param string $api_key The API key to encrypt … … 334 409 } 335 410 336 // Check if WordPress has sodium encryption available (WP 5.2+) 337 if (!function_exists('wp_sodium_encrypt')) { 338 // Fallback for older WordPress versions 339 return base64_encode($api_key); 340 } 341 342 try { 343 // Use WordPress sodium encryption 344 $encrypted = wp_sodium_encrypt($api_key); 345 return $encrypted; 346 } catch (Exception $e) { 347 // Fallback if encryption fails 348 return base64_encode($api_key); 349 } 411 // Check for already encrypted keys - shouldn't happen, but just in case 412 if (strpos($api_key, 'sodium:') === 0 || strpos($api_key, 'base64:') === 0) { 413 return $api_key; 414 } 415 416 // Prevent double encoding - if already base64 encoded, decode first 417 if (preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $api_key)) { 418 $decoded = @base64_decode($api_key, true); 419 if ($decoded !== false && $decoded !== $api_key && 420 strpos($decoded, 'v4.local.') === 0) { 421 $api_key = $decoded; 422 } 423 } 424 425 // Check if libsodium is available for encryption 426 if (function_exists('sodium_crypto_secretbox')) { 427 try { 428 // Create encryption key from WordPress constants 429 $wp_encryption_key = ''; 430 431 // Combine WordPress constants for a stronger key 432 if (defined('AUTH_KEY') && defined('SECURE_AUTH_KEY')) { 433 $wp_encryption_key = AUTH_KEY . SECURE_AUTH_KEY; 434 } 435 436 // Fallback if WordPress keys aren't available 437 if (strlen($wp_encryption_key) < 32) { 438 $wp_encryption_key = 'fastevo_mp2_default_encryption_key_please_set_wp_keys'; 439 } 440 441 // Create a proper key for sodium from the WordPress keys 442 $key = hash('sha256', $wp_encryption_key, true); 443 444 // Generate a random nonce 445 $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 446 447 // Encrypt the API key with sodium 448 $encrypted = sodium_crypto_secretbox($api_key, $nonce, $key); 449 450 // Format for storage - nonce + ciphertext 451 $stored = base64_encode($nonce . $encrypted); 452 453 // Mark with prefix to identify sodium encrypted keys 454 return 'sodium:' . $stored; 455 } catch (Exception $e) { 456 // If sodium encryption fails, fall back to base64 457 } 458 } 459 460 // Use base64 fallback if sodium isn't available or fails 461 return 'base64:' . base64_encode($api_key); 350 462 } 351 463 -
fastevo-mp2/trunk/includes/class-fastevo-mp2-token-service.php
r3279133 r3279290 24 24 public static function generate_token($api_key, $content_id, $viewer_identifier, $protection_level, $viewer_tags = array(), $player_configuration = null, $development_mode = false) 25 25 { 26 27 26 if (empty($api_key)) { 28 27 return new WP_Error('fastevo_mp2_no_api_key', 'No Fastevo MP2 API key provided.'); … … 30 29 if (empty($content_id)) { 31 30 return new WP_Error('fastevo_mp2_no_content_id', 'No content ID specified.'); 31 } 32 33 // Ensure we have a decrypted API key for the request 34 if (function_exists('fastevo_mp2_decrypt_api_key') && !empty($api_key)) { 35 $decrypted = fastevo_mp2_decrypt_api_key($api_key); 36 if (!empty($decrypted)) { 37 $api_key = $decrypted; 38 } 32 39 } 33 40 … … 78 85 $code = wp_remote_retrieve_response_code($response); 79 86 $data = json_decode(wp_remote_retrieve_body($response), true); 80 $body = wp_remote_retrieve_body($response);81 87 82 88 if (200 === $code || 201 === $code) { … … 87 93 } 88 94 89 // Handle 401 errors (likely API key issues)90 if (401 === $code) {91 // Authentication failure92 }93 94 95 $msg = isset($data['error']) ? $data['error'] : 'Fastevo MP2 token generation error.'; 95 96 return new WP_Error('fastevo_mp2_error_' . $code, $msg, $data); … … 98 99 /** 99 100 * Fetch available player configurations from the Fastevo API. 101 * This validates the API key works correctly. 100 102 * 101 103 * @param string $api_key The API key for authentication … … 104 106 public static function fetch_player_configurations($api_key) 105 107 { 108 // Ensure we have a decrypted API key for the request 109 if (function_exists('fastevo_mp2_decrypt_api_key') && !empty($api_key)) { 110 $decrypted = fastevo_mp2_decrypt_api_key($api_key); 111 if (!empty($decrypted)) { 112 $api_key = $decrypted; 113 } 114 } 115 116 // Cache the validation result to improve performance 117 $transient_key = 'fastevo_mp2_key_verified_' . md5($api_key); 118 $cached_result = get_transient($transient_key); 119 120 if ($cached_result !== false) { 121 return $cached_result; 122 } 123 106 124 if (empty($api_key)) { 107 return new WP_Error('fastevo_mp2_no_api_key', 'No Fastevo MP2 API key provided.'); 125 $result = new WP_Error('fastevo_mp2_no_api_key', 'No Fastevo MP2 API key provided.'); 126 set_transient($transient_key, $result, 60); // Cache failures for 1 minute 127 return $result; 108 128 } 109 129 … … 118 138 'timeout' => 20, 119 139 ); 120 140 121 141 $response = wp_remote_get($url, $args); 122 142 123 143 if (is_wp_error($response)) { 144 set_transient($transient_key, $response, 60); // Cache for 1 minute 124 145 return $response; // WP_Error 125 146 } … … 128 149 $data = json_decode(wp_remote_retrieve_body($response), true); 129 150 151 // Success case 130 152 if (200 === $code) { 131 153 if (isset($data['data']) && is_array($data['data'])) { 132 return $data['data']; 133 } 134 return array(); // Empty array if no configurations found 135 } 136 154 $result = $data['data']; 155 } else { 156 $result = array(); // Empty array (still success, just no configurations) 157 } 158 159 // Cache successful results for 5 minutes 160 set_transient($transient_key, $result, 300); 161 return $result; 162 } 163 164 // Error case 137 165 $msg = isset($data['error']) ? $data['error'] : 'Error fetching player configurations.'; 138 return new WP_Error('fastevo_mp2_error_' . $code, $msg, $data); 166 $error = new WP_Error('fastevo_mp2_error_' . $code, $msg, $data); 167 168 // Cache errors for a shorter period (1 minute) 169 set_transient($transient_key, $error, 60); 170 return $error; 139 171 } 140 172 … … 158 190 } 159 191 192 // Ensure we have a decrypted API key for the request 193 if (function_exists('fastevo_mp2_decrypt_api_key') && !empty($api_key)) { 194 $decrypted = fastevo_mp2_decrypt_api_key($api_key); 195 if (!empty($decrypted)) { 196 $api_key = $decrypted; 197 } 198 } 199 160 200 // Construct endpoint 161 201 $url = 'https://api.fastevo.net/api/v1/projects/mediaProtection/contents/upload'; … … 224 264 } 225 265 266 // Ensure we have a decrypted API key for the request 267 if (function_exists('fastevo_mp2_decrypt_api_key') && !empty($api_key)) { 268 $decrypted = fastevo_mp2_decrypt_api_key($api_key); 269 if (!empty($decrypted)) { 270 $api_key = $decrypted; 271 } 272 } 273 226 274 // Construct endpoint 227 275 $url = 'https://api.fastevo.net/api/v1/projects/mediaProtection/contents/folders'; -
fastevo-mp2/trunk/readme.txt
r3279133 r3279290 4 4 Requires at least: 5.8 5 5 Tested up to: 6.8 6 Stable tag: 1.0. 06 Stable tag: 1.0.1 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 29 29 2. `npm run build`: Build WordPress blocks 30 30 3. `./build.sh`: Create distribution package 31 32 = Security Features = 33 34 * Libsodium encryption for API key storage when available 35 * Fallback to base64 encoding when libsodium isn't available 36 * Protection against double-encryption during validation 37 * Strong error handling and validation 38 * Uses WordPress constants (AUTH_KEY, SECURE_AUTH_KEY) for encryption when available 31 39 32 40 == External Services == … … 95 103 == Changelog == 96 104 105 = 1.0.1 = 106 * Implemented libsodium encryption for secure API key storage 107 * Fixed API key validation on first-time save 108 * Added protection against double-encryption during validation 109 * Improved error handling with detailed error messages 110 * Enhanced API key validation and error reporting 111 * Added result caching to reduce API calls 112 * Fixed race condition in the API key validation process 113 * Added prefix-based encryption method detection 114 * Enhanced decryption code to handle edge cases 115 97 116 = 1.0.0 = 98 117 * Initial release 99 118 100 119 == Upgrade Notice == 120 121 = 1.0.1 = 122 Important security and reliability update. Improves API key storage with libsodium encryption and fixes validation issues on first-time save. 101 123 102 124 = 1.0.0 =
Note: See TracChangeset
for help on using the changeset viewer.