-
-
Notifications
You must be signed in to change notification settings - Fork 76
Better storing of credentials for hosting providers #333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
6c177ba
26c7ab4
6930465
7759be3
1ec916a
2fa1b7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| <?php | ||
| /** | ||
| * Credential Store helper for encrypting/decrypting hosting integration credentials. | ||
| * | ||
| * @package WP_Ultimo | ||
| * @subpackage Helpers | ||
| * @since 2.3.0 | ||
| */ | ||
|
|
||
| namespace WP_Ultimo\Helpers; | ||
|
|
||
| // Exit if accessed directly | ||
| defined('ABSPATH') || exit; | ||
|
|
||
| /** | ||
| * Handles encryption and decryption of hosting credentials stored in network options. | ||
| * | ||
| * @since 2.3.0 | ||
| */ | ||
| class Credential_Store { | ||
|
|
||
| /** | ||
| * The cipher method used for encryption. | ||
| * | ||
| * @var string | ||
| */ | ||
| const CIPHER_METHOD = 'aes-256-cbc'; | ||
|
|
||
| /** | ||
| * Prefix for encrypted values to identify them. | ||
| * | ||
| * @var string | ||
| */ | ||
| const ENCRYPTED_PREFIX = '$wu_enc$'; | ||
|
|
||
| /** | ||
| * Encrypt a value for storage. | ||
| * | ||
| * @since 2.3.0 | ||
| * | ||
| * @param string $value The plaintext value to encrypt. | ||
| * @return string The encrypted value. | ||
| */ | ||
| public static function encrypt(string $value): string { | ||
|
|
||
| if (empty($value)) { | ||
| return ''; | ||
| } | ||
|
|
||
| if ( ! function_exists('openssl_encrypt') || ! in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { | ||
| return self::ENCRYPTED_PREFIX . base64_encode($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | ||
| } | ||
|
|
||
| $key = self::get_encryption_key(); | ||
| $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::CIPHER_METHOD)); | ||
|
|
||
| $encrypted = openssl_encrypt($value, self::CIPHER_METHOD, $key, 0, $iv); | ||
|
|
||
| if (false === $encrypted) { | ||
| return self::ENCRYPTED_PREFIX . base64_encode($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | ||
| } | ||
|
|
||
| return self::ENCRYPTED_PREFIX . base64_encode($iv . '::' . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | ||
| } | ||
|
|
||
| /** | ||
| * Decrypt a stored value. | ||
| * | ||
| * @since 2.3.0 | ||
| * | ||
| * @param string $value The encrypted value to decrypt. | ||
| * @return string The decrypted plaintext value. | ||
| */ | ||
| public static function decrypt(string $value): string { | ||
|
|
||
| if (empty($value)) { | ||
| return ''; | ||
| } | ||
|
|
||
| if (strpos($value, self::ENCRYPTED_PREFIX) !== 0) { | ||
| return $value; | ||
| } | ||
|
|
||
| $encoded = substr($value, strlen(self::ENCRYPTED_PREFIX)); | ||
| $decoded = base64_decode($encoded); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | ||
|
|
||
| if (false === $decoded) { | ||
| return ''; | ||
| } | ||
|
|
||
| if (strpos($decoded, '::') === false) { | ||
| return $decoded; | ||
| } | ||
|
|
||
| if ( ! function_exists('openssl_decrypt') || ! in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { | ||
| return $decoded; | ||
| } | ||
|
|
||
| $parts = explode('::', $decoded, 2); | ||
|
|
||
| if (count($parts) !== 2) { | ||
| return ''; | ||
| } | ||
|
|
||
| [$iv, $encrypted] = $parts; | ||
|
|
||
| $key = self::get_encryption_key(); | ||
| $decrypted = openssl_decrypt($encrypted, self::CIPHER_METHOD, $key, 0, $iv); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| return false === $decrypted ? '' : $decrypted; | ||
| } | ||
|
|
||
| /** | ||
| * Get the encryption key derived from WordPress salts. | ||
| * | ||
| * @since 2.3.0 | ||
| * @return string | ||
| */ | ||
| private static function get_encryption_key(): string { | ||
|
|
||
| return hash('sha256', wp_salt('auth'), true); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -115,7 +115,7 @@ public function ssl_tries($max_tries, $domain) { | |||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| public function detect() { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return defined('CLOSTE_CLIENT_API_KEY') && CLOSTE_CLIENT_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||
| return (bool) $this->get_credential('CLOSTE_CLIENT_API_KEY'); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -304,25 +304,16 @@ public function test_connection(): void { | |||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| public function send_closte_api_request($endpoint, $data) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (defined('CLOSTE_CLIENT_API_KEY') === false) { | ||||||||||||||||||||||||||||||||||||||||||
| wu_log_add('integration-closte', 'CLOSTE_CLIENT_API_KEY constant not defined'); | ||||||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||||||
| 'success' => false, | ||||||||||||||||||||||||||||||||||||||||||
| 'error' => 'Closte API Key not found.', | ||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| $api_key = $this->get_credential('CLOSTE_CLIENT_API_KEY'); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (empty(CLOSTE_CLIENT_API_KEY)) { | ||||||||||||||||||||||||||||||||||||||||||
| wu_log_add('integration-closte', 'CLOSTE_CLIENT_API_KEY is empty'); | ||||||||||||||||||||||||||||||||||||||||||
| if (empty($api_key)) { | ||||||||||||||||||||||||||||||||||||||||||
| wu_log_add('integration-closte', 'CLOSTE_CLIENT_API_KEY constant not defined or empty'); | ||||||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||||||
| 'success' => false, | ||||||||||||||||||||||||||||||||||||||||||
| 'error' => 'Closte API Key is empty.', | ||||||||||||||||||||||||||||||||||||||||||
| 'error' => 'Closte API Key not found.', | ||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the log/error text to reflect credential storage, not constants. The message now mentions constants, but credentials may come from the store. A neutral “API key not configured” wording will be clearer. 🔧 Suggested wording update- if (empty($api_key)) {
- wu_log_add('integration-closte', 'CLOSTE_CLIENT_API_KEY constant not defined or empty');
+ if (empty($api_key)) {
+ wu_log_add('integration-closte', 'Closte API key not configured.');
return [
'success' => false,
- 'error' => 'Closte API Key not found.',
+ 'error' => 'Closte API key not configured.',
];
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Try different authentication methods | ||||||||||||||||||||||||||||||||||||||||||
| $api_key = CLOSTE_CLIENT_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| $post_fields = [ | ||||||||||||||||||||||||||||||||||||||||||
| 'blocking' => true, | ||||||||||||||||||||||||||||||||||||||||||
| 'timeout' => 45, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent fallback to base64-only encoding degrades security.
When OpenSSL is unavailable or encryption fails, the code silently falls back to base64 encoding without actual encryption. This could leave credentials exposed if:
Consider either:
🛡️ Proposed fix to add logging for fallback behavior
if ( ! function_exists('openssl_encrypt') || ! in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { + wu_log_add('credential-store', __('OpenSSL not available - credentials stored with obfuscation only, not encryption.', 'ultimate-multisite'), \Psr\Log\LogLevel::WARNING); return self::ENCRYPTED_PREFIX . base64_encode($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode }And for encryption failure:
if (false === $encrypted) { + wu_log_add('credential-store', __('Encryption failed - credentials stored with obfuscation only, not encryption.', 'ultimate-multisite'), \Psr\Log\LogLevel::WARNING); return self::ENCRYPTED_PREFIX . base64_encode($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode }Also applies to: 59-61
🤖 Prompt for AI Agents