Comprehensive code review using GPT-5, Gemini 3, Kimi K2.5, and Zen MCP tools across 11 critical files (~9,000 lines). Identified and fixed issues by consensus prioritization. CRITICAL fixes: - Strip passwords from transients in registration error handling - Rewrite O(3600) token verification loop to O(1) with embedded timestamp HIGH fixes: - Replace remove_all_actions() with targeted hook removal (breaks WP isolation) - Prefer wp-config.php constant for encryption key storage - Add revocation check before generating certificate download URLs - Fix security headers condition to apply to AJAX requests - Add zoho-config.php to .gitignore MEDIUM fixes: - IP spoofing: only trust proxy headers when behind configured trusted proxies - Remove unsafe-eval from CSP (keep unsafe-inline for compatibility) - Remove duplicate Master Trainer component initialization - Remove file-scope side-effect initialization in profile manager - Use WordPress current_time() for consistent timezone in cert numbers Validated as non-issues: - Path traversal (token-based system prevents) - SQL injection (proper $wpdb->prepare throughout) - OAuth CSRF (correctly implemented with hash_equals) All 7 modified PHP files pass syntax validation (php -l). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
No EOL
5.5 KiB
PHP
201 lines
No EOL
5.5 KiB
PHP
<?php
|
|
/**
|
|
* HVAC Secure Storage - Encrypted credential storage
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 1.0.7
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* HVAC_Secure_Storage class
|
|
*
|
|
* Provides encrypted storage for sensitive data like API keys
|
|
*/
|
|
class HVAC_Secure_Storage {
|
|
|
|
/**
|
|
* Encryption method
|
|
*/
|
|
const ENCRYPTION_METHOD = 'AES-256-CBC';
|
|
|
|
/**
|
|
* Get encryption key
|
|
*
|
|
* SECURITY FIX (C2): Prefer wp-config.php constant over database storage.
|
|
* Storing encryption key in the same database as encrypted data is "key under doormat".
|
|
* Define HVAC_ENCRYPTION_KEY in wp-config.php for proper key separation.
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function get_encryption_key() {
|
|
// Prefer wp-config.php constant (recommended for production)
|
|
if (defined('HVAC_ENCRYPTION_KEY') && HVAC_ENCRYPTION_KEY) {
|
|
return base64_decode(HVAC_ENCRYPTION_KEY);
|
|
}
|
|
|
|
// Fallback to database storage (legacy/development)
|
|
// Log warning in debug mode to encourage migration
|
|
if (WP_DEBUG) {
|
|
error_log('HVAC Security Warning: HVAC_ENCRYPTION_KEY not defined in wp-config.php. Using database-stored key (less secure).');
|
|
}
|
|
|
|
$key = get_option('hvac_encryption_key');
|
|
|
|
if (!$key) {
|
|
// Generate a new key if one doesn't exist
|
|
$key = base64_encode(random_bytes(32));
|
|
update_option('hvac_encryption_key', $key);
|
|
}
|
|
|
|
return base64_decode($key);
|
|
}
|
|
|
|
/**
|
|
* Encrypt data
|
|
*
|
|
* @param string $data Data to encrypt
|
|
* @return string Encrypted data
|
|
*/
|
|
public static function encrypt($data) {
|
|
if (empty($data)) {
|
|
return '';
|
|
}
|
|
|
|
$key = self::get_encryption_key();
|
|
$iv = random_bytes(openssl_cipher_iv_length(self::ENCRYPTION_METHOD));
|
|
|
|
$encrypted = openssl_encrypt($data, self::ENCRYPTION_METHOD, $key, 0, $iv);
|
|
|
|
if ($encrypted === false) {
|
|
return false;
|
|
}
|
|
|
|
return base64_encode($iv . $encrypted);
|
|
}
|
|
|
|
/**
|
|
* Decrypt data
|
|
*
|
|
* @param string $encrypted_data Encrypted data
|
|
* @return string|false Decrypted data or false on failure
|
|
*/
|
|
public static function decrypt($encrypted_data) {
|
|
if (empty($encrypted_data)) {
|
|
return '';
|
|
}
|
|
|
|
$data = base64_decode($encrypted_data);
|
|
if ($data === false) {
|
|
return false;
|
|
}
|
|
|
|
$key = self::get_encryption_key();
|
|
$iv_length = openssl_cipher_iv_length(self::ENCRYPTION_METHOD);
|
|
|
|
if (strlen($data) < $iv_length) {
|
|
return false;
|
|
}
|
|
|
|
$iv = substr($data, 0, $iv_length);
|
|
$encrypted = substr($data, $iv_length);
|
|
|
|
return openssl_decrypt($encrypted, self::ENCRYPTION_METHOD, $key, 0, $iv);
|
|
}
|
|
|
|
/**
|
|
* Store encrypted credential
|
|
*
|
|
* @param string $option_name Option name
|
|
* @param string $value Value to store
|
|
* @return bool Success
|
|
*/
|
|
public static function store_credential($option_name, $value) {
|
|
if (empty($value)) {
|
|
delete_option($option_name);
|
|
return true;
|
|
}
|
|
|
|
$encrypted = self::encrypt($value);
|
|
if ($encrypted === false) {
|
|
return false;
|
|
}
|
|
|
|
return update_option($option_name, $encrypted);
|
|
}
|
|
|
|
/**
|
|
* Retrieve encrypted credential
|
|
*
|
|
* @param string $option_name Option name
|
|
* @param string $default Default value
|
|
* @return string Decrypted value
|
|
*/
|
|
public static function get_credential($option_name, $default = '') {
|
|
$encrypted = get_option($option_name, '');
|
|
|
|
if (empty($encrypted)) {
|
|
return $default;
|
|
}
|
|
|
|
$decrypted = self::decrypt($encrypted);
|
|
return $decrypted !== false ? $decrypted : $default;
|
|
}
|
|
|
|
/**
|
|
* Migrate existing plaintext credentials to encrypted storage
|
|
*
|
|
* @return array Migration results
|
|
*/
|
|
public static function migrate_existing_credentials() {
|
|
$credentials_to_migrate = [
|
|
'hvac_zoho_client_id',
|
|
'hvac_zoho_client_secret',
|
|
'hvac_zoho_refresh_token',
|
|
'hvac_google_maps_api_key'
|
|
];
|
|
|
|
$results = [
|
|
'migrated' => 0,
|
|
'skipped' => 0,
|
|
'errors' => []
|
|
];
|
|
|
|
foreach ($credentials_to_migrate as $option_name) {
|
|
$plaintext = get_option($option_name, '');
|
|
|
|
if (empty($plaintext)) {
|
|
$results['skipped']++;
|
|
continue;
|
|
}
|
|
|
|
// Check if it's already encrypted (basic heuristic)
|
|
$decrypted = self::decrypt($plaintext);
|
|
if ($decrypted !== false && $decrypted !== $plaintext) {
|
|
$results['skipped']++;
|
|
continue;
|
|
}
|
|
|
|
// Migrate to encrypted storage
|
|
if (self::store_credential($option_name, $plaintext)) {
|
|
$results['migrated']++;
|
|
} else {
|
|
$results['errors'][] = "Failed to migrate: $option_name";
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Check if OpenSSL is available
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_encryption_available() {
|
|
return function_exists('openssl_encrypt') && function_exists('openssl_decrypt');
|
|
}
|
|
} |