Skip to content

Fix/secure mode session security #40

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

Merged
merged 9 commits into from
Mar 24, 2025
111 changes: 72 additions & 39 deletions tawkto/tawkto.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ class TawkTo_Settings {
const TAWK_VISIBILITY_OPTIONS = 'tawkto-visibility-options';
const TAWK_PRIVACY_OPTIONS = 'tawkto-privacy-options';
const TAWK_SECURITY_OPTIONS = 'tawkto-security-options';
const TAWK_CONFIG_VERSION = 'tawkto-config-version';
const TAWK_ACTION_SET_WIDGET = 'tawkto-set-widget';
const TAWK_ACTION_REMOVE_WIDGET = 'tawkto-remove-widget';
const CIPHER = 'AES-256-CBC';
const CIPHER_IV_LENGTH = 16;
const NO_CHANGE = 'nochange';
const TAWK_API_KEY = 'tawkto-js-api-key';

/**
* @var $plugin_ver Plugin version
Expand Down Expand Up @@ -132,6 +132,7 @@ public function admin_init() {
register_setting( 'tawk_options', self::TAWK_VISIBILITY_OPTIONS, array( &$this, 'validate_visibility_options' ) );
register_setting( 'tawk_options', self::TAWK_PRIVACY_OPTIONS, array( &$this, 'validate_privacy_options' ) );
register_setting( 'tawk_options', self::TAWK_SECURITY_OPTIONS, array( &$this, 'validate_security_options' ) );
register_setting( 'tawk_options', self::TAWK_CONFIG_VERSION, array( &$this, 'update_config_version' ) );
}

/**
Expand Down Expand Up @@ -326,6 +327,15 @@ public function validate_security_options( $input ) {
return $security;
}

/**
* Updates the config version
*
* @return int
*/
public function update_config_version() {
return get_option( self::TAWK_CONFIG_VERSION, 0 ) + 1;
}

/**
* Adds the tawk.to plugin settings in the admin menu.
*/
Expand Down Expand Up @@ -418,20 +428,20 @@ private static function validate_js_api_key( &$fields ) {
return;
}

delete_transient( self::TAWK_API_KEY );

if ( '' === $fields['js_api_key'] ) {
return;
}

try {
if ( 40 !== strlen( $fields['js_api_key'] ) ) {
throw new Exception( 'Invalid key. Please provide value with 40 characters' );
}
$fields['js_api_key'] = trim( $fields['js_api_key'] );

if ( 40 !== strlen( $fields['js_api_key'] ) ) {
self::show_tawk_options_error( 'Invalid API key' );
}

try {
$fields['js_api_key'] = self::get_encrypted_data( $fields['js_api_key'] );
} catch ( Exception $e ) {
self::show_tawk_options_error( 'Javascript API Key: ' . $e->getMessage() );
self::show_tawk_options_error( 'Error saving Javascript API Key' );

unset( $fields['js_api_key'] );
}
Expand Down Expand Up @@ -524,12 +534,12 @@ private static function get_encrypted_data( $data ) {
* @param string $data - Data to be decrypted.
* @return string
*/
private static function get_decrypted_data( $data ) {
public static function get_decrypted_data( $data ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$decoded_data = base64_decode( $data );

if ( false === $decoded_data ) {
return '';
return null;
}

$iv = substr( $decoded_data, 0, self::CIPHER_IV_LENGTH );
Expand All @@ -538,35 +548,12 @@ private static function get_decrypted_data( $data ) {
$decrypted_data = openssl_decrypt( $encrypted_data, self::CIPHER, SECURE_AUTH_KEY, 0, $iv );

if ( false === $decrypted_data ) {
return '';
return null;
}

return $decrypted_data;
}

/**
* Retrieves JS API Key
*
* @return string
*/
public static function get_js_api_key() {
if ( ! empty( get_transient( self::TAWK_API_KEY ) ) ) {
return get_transient( self::TAWK_API_KEY );
}

$security = get_option( self::TAWK_SECURITY_OPTIONS );

if ( ! isset( $security['js_api_key'] ) ) {
return '';
}

$key = self::get_decrypted_data( $security['js_api_key'] );

set_transient( self::TAWK_API_KEY, $key, 60 * 60 );

return $key;
}

/**
* Adds settings error
*
Expand Down Expand Up @@ -599,6 +586,7 @@ private static function show_tawk_options_error( $message ) {
*/
class TawkTo {
const PLUGIN_VERSION_VARIABLE = 'tawkto-version';
const TAWK_VISITOR_SESSION = 'tawkto-visitor-session';

/**
* @var $plugin_version Plugin version
Expand Down Expand Up @@ -658,9 +646,8 @@ public static function deactivate() {
delete_option( TawkTo_Settings::TAWK_VISIBILITY_OPTIONS );
delete_option( TawkTo_Settings::TAWK_PRIVACY_OPTIONS );
delete_option( TawkTo_Settings::TAWK_SECURITY_OPTIONS );
delete_option( TawkTo_Settings::TAWK_CONFIG_VERSION );
delete_option( self::PLUGIN_VERSION_VARIABLE );

delete_transient( TawkTo_Settings::TAWK_API_KEY );
}

/**
Expand All @@ -683,16 +670,62 @@ public function get_current_customer_details() {
'email' => $current_user->user_email,
);

$js_api_key = TawkTo_Settings::get_js_api_key();
if ( ! empty( $user_info['email'] ) && ! empty( $js_api_key ) ) {
$user_info['hash'] = hash_hmac( 'sha256', $user_info['email'], $js_api_key );
$hash = self::get_visitor_hash( $user_info['email'] );
if ( null !== $hash ) {
$user_info['hash'] = $hash;
}

return wp_json_encode( $user_info );
}
return null;
}

/**
* Retrieves visitor hash
*
* @param string $email - Visitor email address.
* @return string
*/
public static function get_visitor_hash( $email ) {
if ( session_status() === PHP_SESSION_NONE ) {
session_start();
}

$config_version = get_option( TawkTo_Settings::TAWK_CONFIG_VERSION, 0 );

if ( isset( $_SESSION[ self::TAWK_VISITOR_SESSION ] ) ) {
$current_session = $_SESSION[ self::TAWK_VISITOR_SESSION ];

if ( isset( $current_session['hash'] ) &&
$current_session['email'] === $email &&
$current_session['config_version'] === $config_version ) {
return $current_session['hash'];
}
}

$security = get_option( TawkTo_Settings::TAWK_SECURITY_OPTIONS );

if ( empty( $security['js_api_key'] ) ) {
return null;
}

$key = TawkTo_Settings::get_decrypted_data( $security['js_api_key'] );

if ( null === $key ) {
return null;
}

$hash = hash_hmac( 'sha256', $email, $key );

$_SESSION[ self::TAWK_VISITOR_SESSION ] = array(
'hash' => $hash,
'email' => $email,
'config_version' => $config_version,
);

return $hash;
}

/**
* Creates the embed code
*/
Expand Down