upskill-event-manager/includes/class-hvac-secure-storage.php
ben 9f4667fbb4 fix(security): Multi-model code review - 12 security and architecture fixes
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>
2026-01-31 20:06:43 -04:00

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');
}
}