Plugin Directory

Changeset 3279290


Ignore:
Timestamp:
04/22/2025 05:17:01 PM (11 months ago)
Author:
fastevo
Message:

Release version 1.0.1

Location:
fastevo-mp2/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • fastevo-mp2/trunk/fastevo-mp2.php

    r3279133 r3279290  
    33 * Plugin Name: Fastevo MP2
    44 * Description: Embed Fastevo MP2 protected media via Gutenberg block and shortcode.
    5  * Version: 1.0.0
     5 * Version: 1.0.1
    66 * Author: Fastevo
    77 * Author URI: https://fastevo.com
     
    1515}
    1616
    17 define('FASTEVO_MP2_PLUGIN_VERSION', '1.0.0');
     17define('FASTEVO_MP2_PLUGIN_VERSION', '1.0.1');
    1818define('FASTEVO_MP2_PLUGIN_DIR', plugin_dir_path(__FILE__));
    1919define('FASTEVO_MP2_PLUGIN_URL', plugin_dir_url(__FILE__));
     
    4646
    4747/**
    48  * Get the stored API key from options or secure file.
     48 * Get the stored API key from options.
    4949 *
    5050 * @return string The API key or empty string if not set
     
    5959    }
    6060   
    61    
    62     // For database storage or as fallback from file storage, decrypt the key
     61    // Decrypt the key
    6362    $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    }
    6472   
    6573    // If decryption returned something useful
     
    6977   
    7078    // 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 text
    7279    return $api_key;
    7380}
     
    7582
    7683/**
    77  * Decrypt API key using WordPress sodium crypto functions.
     84 * Decrypt API key using libsodium if available, otherwise base64.
    7885 *
    7986 * @param string $encrypted_key The encrypted API key
     
    8794    }
    8895   
    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
    92194        $decoded = base64_decode($encrypted_key, true);
     195       
    93196        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 '';
    117211}
    118212
  • fastevo-mp2/trunk/includes/class-fastevo-mp2-settings.php

    r3279133 r3279290  
    282282    public static function sanitize_api_key($api_key)
    283283    {
     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       
    284312        // First sanitize the input
    285313        $sanitized_key = sanitize_text_field($api_key);
    286314       
     315        // Check if we already have a key stored
     316        $stored_value = get_option('fastevo_mp2_api_key', '');
     317       
    287318        // If empty input, check if we already have a key
    288319        if (empty($sanitized_key)) {
    289             $stored_value = get_option('fastevo_mp2_api_key', '');
    290320            // If we already have a key stored, keep using it
    291321            if (!empty($stored_value)) {
     
    298328        // Test the API key to make sure it works before encrypting and storing it
    299329        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           
    300346            // 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);
    302348           
    303349            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
    305356                set_transient('fastevo_mp2_api_key_error', $result->get_error_message(), 60);
    306357               
    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
    309359                if (!empty($existing_key)) {
    310360                    return $existing_key;
    311361                }
    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 '';
    313378            } else {
    314379                // Set success transient
     
    318383       
    319384        // 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;
    321395    }
    322396   
    323397    /**
    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
    325400     *
    326401     * @param string $api_key The API key to encrypt
     
    334409        }
    335410       
    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);
    350462    }
    351463   
  • fastevo-mp2/trunk/includes/class-fastevo-mp2-token-service.php

    r3279133 r3279290  
    2424    public static function generate_token($api_key, $content_id, $viewer_identifier, $protection_level, $viewer_tags = array(), $player_configuration = null, $development_mode = false)
    2525    {
    26 
    2726        if (empty($api_key)) {
    2827            return new WP_Error('fastevo_mp2_no_api_key', 'No Fastevo MP2 API key provided.');
     
    3029        if (empty($content_id)) {
    3130            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            }
    3239        }
    3340
     
    7885        $code = wp_remote_retrieve_response_code($response);
    7986        $data = json_decode(wp_remote_retrieve_body($response), true);
    80         $body = wp_remote_retrieve_body($response);
    8187
    8288        if (200 === $code || 201 === $code) {
     
    8793        }
    8894
    89         // Handle 401 errors (likely API key issues)
    90         if (401 === $code) {
    91             // Authentication failure
    92         }
    93 
    9495        $msg = isset($data['error']) ? $data['error'] : 'Fastevo MP2 token generation error.';
    9596        return new WP_Error('fastevo_mp2_error_' . $code, $msg, $data);
     
    9899    /**
    99100     * Fetch available player configurations from the Fastevo API.
     101     * This validates the API key works correctly.
    100102     *
    101103     * @param string $api_key The API key for authentication
     
    104106    public static function fetch_player_configurations($api_key)
    105107    {
     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       
    106124        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;
    108128        }
    109129
     
    118138            'timeout' => 20,
    119139        );
    120 
     140       
    121141        $response = wp_remote_get($url, $args);
    122142
    123143        if (is_wp_error($response)) {
     144            set_transient($transient_key, $response, 60); // Cache for 1 minute
    124145            return $response; // WP_Error
    125146        }
     
    128149        $data = json_decode(wp_remote_retrieve_body($response), true);
    129150
     151        // Success case
    130152        if (200 === $code) {
    131153            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
    137165        $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;
    139171    }
    140172   
     
    158190        }
    159191       
     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       
    160200        // Construct endpoint
    161201        $url = 'https://api.fastevo.net/api/v1/projects/mediaProtection/contents/upload';
     
    224264        }
    225265       
     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       
    226274        // Construct endpoint
    227275        $url = 'https://api.fastevo.net/api/v1/projects/mediaProtection/contents/folders';
  • fastevo-mp2/trunk/readme.txt

    r3279133 r3279290  
    44Requires at least: 5.8
    55Tested up to: 6.8
    6 Stable tag: 1.0.0
     6Stable tag: 1.0.1
    77Requires PHP: 7.4
    88License: GPLv2 or later
     
    29292. `npm run build`: Build WordPress blocks
    30303. `./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
    3139
    3240== External Services ==
     
    95103== Changelog ==
    96104
     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
    97116= 1.0.0 =
    98117* Initial release
    99118
    100119== Upgrade Notice ==
     120
     121= 1.0.1 =
     122Important security and reliability update. Improves API key storage with libsodium encryption and fixes validation issues on first-time save.
    101123
    102124= 1.0.0 =
Note: See TracChangeset for help on using the changeset viewer.