'sodium', // method. 'n' => base64_encode( $nonce ), 'c' => base64_encode( $ciphertext ), ); return self::PREFIX . base64_encode( wp_json_encode( $payload ) ); } // OpenSSL fallback AES-256-GCM. $iv_length = 12; // Recommended length for GCM. $iv = random_bytes( $iv_length ); $tag = ''; $cipher = 'aes-256-gcm'; $ciphertext = openssl_encrypt( $plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag ); if ( false === $ciphertext ) { throw new \RuntimeException( 'OpenSSL encryption failed: ' . openssl_error_string() ); } $payload = array( 'm' => 'openssl', 'a' => base64_encode( $ciphertext ), 'i' => base64_encode( $iv ), 't' => base64_encode( $tag ), 'c' => $cipher, ); return self::PREFIX . base64_encode( wp_json_encode( $payload ) ); } /** * Decrypt a value if encrypted; otherwise return as-is. * * @param string|null $value Value from options. * * @return string Decrypted plaintext or original. * * @since 4.1.0 */ public static function decrypt( ?string $value ): string { if ( ! self::is_encrypted( $value ) ) { return (string) $value; } $encoded = substr( (string) $value, strlen( self::PREFIX ) ); $json = base64_decode( $encoded, true ); if ( false === $json ) { return (string) $value; // Corrupted. } $data = json_decode( $json, true ); if ( ! is_array( $data ) ) { return (string) $value; } $key = self::get_key(); if ( isset( $data['m'] ) && 'sodium' === $data['m'] && isset( $data['n'], $data['c'] ) && function_exists( 'sodium_crypto_secretbox_open' ) ) { $nonce = base64_decode( $data['n'], true ); $ciphertext = base64_decode( $data['c'], true ); if ( false === $nonce || false === $ciphertext ) { return (string) $value; } $plain = sodium_crypto_secretbox_open( $ciphertext, $nonce, $key ); return ( false === $plain ) ? (string) $value : $plain; } if ( isset( $data['m'] ) && 'openssl' === $data['m'] && isset( $data['a'], $data['i'], $data['t'], $data['c'] ) ) { $ciphertext = base64_decode( $data['a'], true ); $iv = base64_decode( $data['i'], true ); $tag = base64_decode( $data['t'], true ); if ( false === $ciphertext || false === $iv || false === $tag ) { return (string) $value; } // Always use the known-safe cipher regardless of stored payload value. $plain = openssl_decrypt( $ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag ); return ( false === $plain ) ? (string) $value : $plain; } return (string) $value; // Unknown method. } /** * Encrypt sensitive fields inside the options array (in-place) before storage. * * @param array $options Reference to full options array. * * @return void * * @since 4.1.0 */ public static function encrypt_sensitive_fields( array &$options ): void { // SMTP password. if ( isset( $options['smtp_password'] ) && is_string( $options['smtp_password'] ) && '' !== $options['smtp_password'] ) { $options['smtp_password'] = self::encrypt( $options['smtp_password'] ); } // Slack token. if ( isset( $options['slack_notifications']['all']['auth_token'] ) && is_string( $options['slack_notifications']['all']['auth_token'] ) && '' !== $options['slack_notifications']['all']['auth_token'] ) { $options['slack_notifications']['all']['auth_token'] = self::encrypt( $options['slack_notifications']['all']['auth_token'] ); } // Telegram token. if ( isset( $options['telegram_notifications']['all']['auth_token'] ) && is_string( $options['telegram_notifications']['all']['auth_token'] ) && '' !== $options['telegram_notifications']['all']['auth_token'] ) { $options['telegram_notifications']['all']['auth_token'] = self::encrypt( $options['telegram_notifications']['all']['auth_token'] ); } // PageSpeed API key. if ( isset( $options['pagespeed_api_key'] ) && is_string( $options['pagespeed_api_key'] ) && '' !== $options['pagespeed_api_key'] ) { $options['pagespeed_api_key'] = self::encrypt( $options['pagespeed_api_key'] ); } } /** * Decrypt sensitive fields (in-place) for runtime use. * * @param array $options Reference to options array. * * @return bool True if any field migrated (was plaintext and now encrypted in DB trigger needed). * * @since 4.1.0 */ public static function decrypt_sensitive_fields( array &$options ): bool { $migrated = false; // SMTP password. if ( isset( $options['smtp_password'] ) && is_string( $options['smtp_password'] ) && '' !== $options['smtp_password'] ) { if ( ! self::is_encrypted( $options['smtp_password'] ) ) { $migrated = true; // Will need re-store. } else { $options['smtp_password'] = self::decrypt( $options['smtp_password'] ); } } // Slack token. if ( isset( $options['slack_notifications']['all']['auth_token'] ) && is_string( $options['slack_notifications']['all']['auth_token'] ) && '' !== $options['slack_notifications']['all']['auth_token'] ) { if ( ! self::is_encrypted( $options['slack_notifications']['all']['auth_token'] ) ) { $migrated = true; } else { $options['slack_notifications']['all']['auth_token'] = self::decrypt( $options['slack_notifications']['all']['auth_token'] ); } } // Telegram token. if ( isset( $options['telegram_notifications']['all']['auth_token'] ) && is_string( $options['telegram_notifications']['all']['auth_token'] ) && '' !== $options['telegram_notifications']['all']['auth_token'] ) { if ( ! self::is_encrypted( $options['telegram_notifications']['all']['auth_token'] ) ) { $migrated = true; } else { $options['telegram_notifications']['all']['auth_token'] = self::decrypt( $options['telegram_notifications']['all']['auth_token'] ); } } // PageSpeed API key. if ( isset( $options['pagespeed_api_key'] ) && is_string( $options['pagespeed_api_key'] ) && '' !== $options['pagespeed_api_key'] ) { if ( ! self::is_encrypted( $options['pagespeed_api_key'] ) ) { $migrated = true; } else { $options['pagespeed_api_key'] = self::decrypt( $options['pagespeed_api_key'] ); } } return $migrated; } }