tfa = $tfa; $this->otp_helper = new HOTP(); add_action('plugins_loaded', array($this, 'plugins_loaded')); add_action('admin_init', array($this, 'admin_init')); if (!is_admin()) { add_action('init', array($this, 'check_possible_reset')); } // Potentially show off-sync message for hotp add_action('admin_notices', array($this, 'tfa_show_hotp_off_sync_message')); } /** * Return whether or not this class detected and saved new settings * * @return Boolean */ public function were_settings_saved() { return $this->settings_saved; } /** * Runs upon the WP action admin_init */ public function admin_init() { $this->check_possible_reset(); global $current_user; if (!empty($_REQUEST['_tfa_activate_nonce']) && !empty($_POST['tfa_enable_tfa']) && wp_verify_nonce($_REQUEST['_tfa_activate_nonce'], 'tfa_activate') && !empty($_GET['settings-updated'])) { $this->tfa->change_tfa_enabled_status($current_user->ID, $_POST['tfa_enable_tfa']); $this->settings_saved = true; } if (!empty($_REQUEST['_tfa_algorithm_nonce']) && !empty($_POST['tfa_algorithm_type']) && !empty($_GET['settings-updated']) && wp_verify_nonce($_REQUEST['_tfa_algorithm_nonce'], 'tfa_algorithm')) { $old_algorithm = $this->get_user_otp_algorithm($current_user->ID); if ($old_algorithm != $_POST['tfa_algorithm_type']) { $this->changeUserAlgorithmTo($current_user->ID, $_POST['tfa_algorithm_type']); } $this->settings_saved = true; } if (!empty($_GET['warning_button_clicked']) && !empty($_REQUEST['resyncnonce']) && wp_verify_nonce($_REQUEST['resyncnonce'], 'tfaresync')) { delete_user_meta($current_user->ID, 'tfa_hotp_off_sync'); } } /** * Enqueue adding of JavaScript for footer */ public function add_footer() { static $added_footer = false; if ($added_footer) return; $added_footer = true; $qr_script_file = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? 'jquery-qrcode.js' : 'jquery-qrcode.min.js'; $qr_script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->tfa->includes_dir()."/jquery-qrcode/$qr_script_file"); wp_register_script('jquery-qrcode', $this->tfa->includes_url()."/jquery-qrcode/$qr_script_file", array('jquery'), $qr_script_ver); $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->tfa->includes_dir()."/totp.js"); // Adds the necessary JavaScript for rendering and updating QR codes, and handling trusted devices removal in the admin area wp_enqueue_script('simba-tfa-totp', $this->tfa->includes_url()."/totp.js", array('jquery-qrcode'), $script_ver); wp_localize_script('simba-tfa-totp', 'simbatfa_totp', $this->translation_strings()); } /** * Get textual strings used from JavaScript * * @return Array */ private function translation_strings() { // It's possible that FORCE_ADMIN_SSL will make that SSL, whilst the user is on the front-end having logged in over non-SSL - and as a result, their login cookies won't get sent, and they're not registered as logged in. $ajax_url = admin_url('admin-ajax.php'); $also_try = ''; if (!is_admin() && substr(strtolower($ajax_url), 0, 6) == 'https:' && !is_ssl()) { $also_try = 'http:'.substr($ajax_url, 6); } return apply_filters('simba_tfa_totp_translation_strings', array( 'ajax_url' => $ajax_url, 'updating' => __('Updating...', 'all-in-one-wp-security-and-firewall'), 'tfa_shared_nonce' => wp_create_nonce('tfa_shared_nonce'), 'also_try' => $also_try, 'response' => __('Response:', 'all-in-one-wp-security-and-firewall'), )); } /** * Return a link to refresh the current OTP code * * @return String */ public function refresh_current_otp_link() { return ''.__('(update)', 'all-in-one-wp-security-and-firewall').''; } /** * Echo the radio buttons for changing between TOTP/HOTP * * TODO: Hide this choice on new installs (TOTP only) * * @param Integer $user_id */ protected function print_algorithm_choice_radios($user_id) { if (!$user_id) return; $types = array( 'totp' => __('TOTP (time based - most common algorithm; used by Google Authenticator)', 'all-in-one-wp-security-and-firewall'), 'hotp' => __('HOTP (event based)', 'all-in-one-wp-security-and-firewall') ); $setting = $this->get_user_otp_algorithm($user_id); foreach ($types as $id => $name) { print '
\n"; } } /** * Print out the advanced settings box - choice of algorithm * * @param Boolean|Callable $submit_button_callback - if not a callback, then
tags will be added */ public function advanced_settings_box($submit_button_callback = false) { global $current_user; $algorithm_type = $this->get_user_otp_algorithm($current_user->ID); ?>

print_algorithm_choice_radios($current_user->ID); if ('hotp' == $algorithm_type) { $counter = $this->getUserCounter($current_user->ID); print '
'.__('Your counter on the server is currently on', 'all-in-one-wp-security-and-firewall').': '.$counter; } ?>

'; } else { call_user_func($submit_button_callback); } ?>
ID; $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true); if (!$tfa_priv_key_64) $tfa_priv_key_64 = $this->addPrivateKey($user_id); $time_now = time(); $refresh_after = 30 - ($time_now % 30); return ''.$this->generateOTP($user_id, $tfa_priv_key_64).''; } /** * Runs upon the WP 'init' action - check for possible private key reset request from the user */ public function check_possible_reset() { if (!empty($_GET['simbatfa_priv_key_reset']) && !empty($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], 'simbatfa_reset_private_key')) { $this->reset_private_key_and_emergency_codes(); exit; } } /** * Remove private key and emergency codes for the specified (or logged-in) user * * @param Boolean|Integer $user_id - WP user ID, or false for the currently logged-in user * @param Boolean $redirect - if this is not false, then a redirection will occur - where to depends upon the value of $_REQUEST['noredirect'] */ public function reset_private_key_and_emergency_codes($user_id = false, $redirect = true) { if (!$user_id) { global $current_user; $user_id = $current_user->ID; } delete_user_meta($user_id, 'tfa_priv_key_64'); delete_user_meta($user_id, 'simba_tfa_emergency_codes_64'); if (!$redirect) return; if (empty($_REQUEST['noredirect'])) { // TODO: Re-factoring wp_safe_redirect(admin_url('admin.php').'?page='. $this->tfa->get_user_settings_page_slug() .'&settings-updated=1'); } else { $url = (is_ssl() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . remove_query_arg(array('simbatfa_priv_key_reset', 'noredirect', 'nonce')); wp_redirect(esc_url_raw($url)); } } /** * Return HTML for a link to reset the current user's private key * * @return String */ public function reset_link() { // TODO: Refactoring $url_base = is_admin() ? admin_url('admin.php').'?page='. $this->tfa->get_user_settings_page_slug() .'&settings-updated=1' : (( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST']); $add_query_args = array('simbatfa_priv_key_reset' => 1); if (!is_admin()) $add_query_args['noredirect'] = 1; $url = $url_base.add_query_arg($add_query_args); $url = wp_nonce_url($url, 'simbatfa_reset_private_key', 'nonce'); return ''.__('Reset private key', 'all-in-one-wp-security-and-firewall').''; } /** * Output the current codes box * * @param Boolean|Integer $user_id */ public function current_codes_box($user_id = false) { global $current_user; if (false == $user_id) $user_id = $current_user->ID; $admin = is_admin(); $this->add_footer(); $url = preg_replace('/^https?:\/\//i', '', site_url()); // TODO Replace this with an appropriate method $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true); if (!$tfa_priv_key_64) $tfa_priv_key_64 = $this->addPrivateKey($user_id); $tfa_priv_key = trim($this->getPrivateKeyPlain($tfa_priv_key_64, $user_id), "\x00..\x1F"); $tfa_priv_key_32 = Base32::encode($tfa_priv_key); $algorithm_type = $this->get_user_otp_algorithm($user_id); if ($admin && $current_user->ID != $user_id) { $user = get_user_by('id', $user_id); $user_descrip = htmlspecialchars($user->user_nicename.' - '.$user->user_email); echo '

'.sprintf(__('Current codes (login: %s)', 'all-in-one-wp-security-and-firewall'), $user_descrip).'

'; } ?>

ID == $user_id) { echo $this->refresh_current_otp_link(); } ?>

current_otp_code($user_id); ?>

refresh_current_otp_link(); ?> : generateOTP($user_id, $tfa_priv_key_64); ?>

.

tfa_qr_code_url($algorithm_type, $url, $tfa_priv_key, $user_id); ?>

print_private_keys('full', $user_id); if ($current_user->ID == $user_id) { echo $this->reset_link($admin); } else { echo ''.__('Reset private key', 'all-in-one-wp-security-and-firewall').''; } ?>

tfa->get_premium_version_url()).'">'.__('One-time emergency codes are a feature of the Premium version of this plugin.', 'all-in-one-wp-security-and-firewall').''; echo apply_filters('simba_tfa_emergency_codes_user_settings', $default_text, $user_id); ?>
ID; $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true); if (!$tfa_priv_key_64) $tfa_priv_key_64 = $this->addPrivateKey($user_id); $tfa_priv_key = trim($this->getPrivateKeyPlain($tfa_priv_key_64, $user_id), "\x00..\x1F"); $tfa_priv_key_32 = Base32::encode($tfa_priv_key); // The first (base32) private key used to have the description "base 32 - used by Google Authenticator and Authy", and the base64 version was just described as "private key". But basically the former is what everything uses. //Private key: htmlspecialchars($tfa_priv_key) if ('full' == $type) { ?> '.htmlspecialchars($tfa_priv_key_32); ?>
user_login).'?secret='.Base32::encode($tfa_priv_key).'&issuer='.$url.'&counter='.$this->getUserCounter($user->ID); return $encode; } /** * See if HOTP is off sync, and if show, print out a message */ public function tfa_show_hotp_off_sync_message() { global $current_user; $is_off_sync = get_user_meta($current_user->ID, 'tfa_hotp_off_sync', true); if (!$is_off_sync) return; ?>




time_window_size = apply_filters('simbatfa_time_window_size', 30); $this->check_back_time_windows = apply_filters('simbatfa_check_back_time_windows', 2); $this->check_forward_time_windows = apply_filters('simbatfa_check_forward_time_windows', 1); $this->check_forward_counter_window = apply_filters('simbatfa_check_forward_counter_window', 20); $this->salt_prefix = defined('AUTH_SALT') ? AUTH_SALT : wp_salt('auth'); $this->pw_prefix = defined('AUTH_KEY') ? AUTH_KEY : get_site_option('auth_key'); } /** * Generate the current code for a specified user * * @param $user_id Integer - WordPress user ID * * @return String|Boolean - false if not set up */ public function get_current_code($user_id) { $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true); if (!$tfa_priv_key_64) return false; return $this->generateOTP($user_id, $tfa_priv_key_64); } public function print_default_hmac_radios() { $setting = $this->tfa->get_option('tfa_default_hmac'); if (!$setting) $setting = $this->default_hmac; $types = array('totp' => __('TOTP (time based - most common algorithm; used by Google Authenticator)', 'all-in-one-wp-security-and-firewall'), 'hotp' => __('HOTP (event based)', 'all-in-one-wp-security-and-firewall')); foreach ($types as $id => $name) { print ' '.'
\n"; } } public function generateOTP($user_ID, $key_b64, $length = 6, $counter = false) { $length = $length ? (int)$length : 6; $key = $this->decryptString($key_b64, $user_ID); $alg = $this->get_user_otp_algorithm($user_ID); if ('hotp' == $alg) { $db_counter = $this->getUserCounter($user_ID); $counter = $counter ? $counter : $db_counter; $otp_res = $this->otp_helper->generateByCounter($key, $counter); } else { //time() is supposed to be UTC $time = $counter ? $counter : time(); $otp_res = $this->otp_helper->generateByTime($key, $this->time_window_size, $time); } $code = $otp_res->toHotp($length); return $code; } /** * Generate a list of OTP codes based on the user, key and time window * * @param Integer $user_ID - user ID * @param String $key_b64 - the user's private key, in base64 format * * @return Array */ private function generate_otps_for_login_check($user_ID, $key_b64) { $key = trim($this->decryptString($key_b64, $user_ID)); $alg = $this->get_user_otp_algorithm($user_ID); if ('totp' == $alg) { $otp_res = $this->otp_helper->generateByTimeWindow($key, $this->time_window_size, -1*$this->check_back_time_windows, $this->check_forward_time_windows); } elseif ('hotp' == $alg) { $counter = $this->getUserCounter($user_ID); $otp_res = array(); for ($i = 0; $i < $this->check_forward_counter_window; $i++) { $otp_res[] = $this->otp_helper->generateByCounter($key, $counter+$i); } } return $otp_res; } /** * Generate a private key for the user. * * @param Integer $user_id - WordPress user ID * @param Boolean|String $key * * @return String */ public function addPrivateKey($user_id, $key = false) { // To work with Google Authenticator it has to be 10 bytes = 16 chars in base32 $code = $key ? $key : strtoupper($this->randString(10)); // Encrypt the key $code = $this->encryptString($code, $user_id); // Add private key to usermeta update_user_meta($user_id, 'tfa_priv_key_64', $code); $alg = $this->get_user_otp_algorithm($user_id); // This hook is used for generation of emergency codes to accompany the key do_action('simba_tfa_adding_private_key', $alg, $user_id, $code, $this); $this->changeUserAlgorithmTo($user_id, $alg); return $code; } /** * Port over keys that were encrypted with mcrypt and its non-compliant padding scheme, so that if the site is ever migrated to a server without mcrypt, they can still be decrypted */ public function potentially_port_private_keys() { $simba_tfa_priv_key_format = get_site_option('simba_tfa_priv_key_format', false); if ($simba_tfa_priv_key_format >= 1 || !function_exists('openssl_encrypt')) return; $attempts = 0; $successes = 0; error_log("TFA: Beginning attempt to port private key encryption over to openssl"); global $wpdb; $sql = "SELECT user_id, meta_value FROM ".$wpdb->usermeta." WHERE meta_key = 'tfa_priv_key_64'"; $user_results = $wpdb->get_results($sql); foreach ($user_results as $u) { $dec_openssl = $this->decryptString($u->meta_value, $u->user_id, true); $ported = false; if ('' == $dec_openssl) { $attempts++; $dec_default = $this->decryptString($u->meta_value, $u->user_id); if ('' != $dec_default) { $enc = $this->encryptString($dec_default, $u->user_id); if ($enc) { $ported = true; $successes++; update_user_meta($u->user_id, 'tfa_priv_key_64', $enc); } } } if ($ported) { error_log("TFA: Successfully ported the key for user with ID ".$u->user_id." over to openssl"); } else { error_log("TFA: Failed to port the key for user with ID ".$u->user_id." over to openssl"); } } if ($attempts == 0 || $successes > 0) update_site_option('simba_tfa_priv_key_format', 1); } /** * This function will attempt to encrypt all the users private keys and emergency codes * * @return boolean|WP_Error - true on success or WP_Error on failure */ public function potentially_encrypt_private_keys() { error_log("TFA: Beginning attempt to encrypt private keys"); global $wpdb; $sql = "SELECT user_id, meta_value FROM ".$wpdb->usermeta." WHERE meta_key = 'tfa_priv_key_64' AND meta_value != ''"; $user_results = $wpdb->get_results($sql); if (null === $user_results) { return new WP_Error( 'failed_to_get_priv_keys', __('Encrypt secrets feature not enabled: unable to get private keys from the database.', 'all-in-one-wp-security-and-firewall') ); } $number_ported = 0; $number_failed = 0; foreach ($user_results as $u) { $ported = false; $key = $this->decryptString($u->meta_value, $u->user_id); $enc = $this->encryptString($key, $u->user_id, true); if ($enc) { $ported = true; update_user_meta($u->user_id, 'tfa_priv_key_64', $enc); } $codes = get_user_meta($u->user_id, 'simba_tfa_emergency_codes_64', true); if (!is_array($codes)) $codes = array(); $enc_codes = array(); foreach ($codes as $code) { $plain_code = $this->decryptString($code, $u->user_id); $enc_codes[] = $this->encryptString($plain_code, $u->user_id, true); } if (!empty($enc_codes)) update_user_meta($u->user_id, 'simba_tfa_emergency_codes_64', $enc_codes); if ($ported) { $number_ported++; } else { $number_failed++; error_log("TFA: Failed to encrypt the key for user with ID ".$u->user_id); } } error_log("TFA: Number of user keys successfully encrypted: ".$number_ported.", number which failed to encrypt: ".$number_failed); return true; } public function getPrivateKeyPlain($enc, $user_ID) { $dec = $this->decryptString($enc, $user_ID); $this->potentially_port_private_keys(); return $dec; } /** * @param Integer $user_id - WP user ID * @param Boolean $generate_if_empty - generate some new codes if the list is empty * * @return String - human-usable codes, separated by ', ' (or a human-readable message, if there were none) */ public function get_emergency_codes_as_string($user_id, $generate_if_empty = false) { $codes = get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true); if (!is_array($codes)) $codes = array(); if ($generate_if_empty && empty($codes)) { $tfa_priv_key = get_user_meta($user_id, 'tfa_priv_key_64', true); $algorithm = get_user_meta($user_id, 'tfa_algorithm_type', true); do_action('simba_tfa_emergency_codes_empty', $algorithm, $user_id, $tfa_priv_key, $this); $codes = get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true); if (!is_array($codes)) $codes = array(); } $emergency_str = ''; foreach ($codes as $p_code) { $emergency_str .= $this->decryptString($p_code, $user_id).', '; } $emergency_str = rtrim($emergency_str, ', '); $emergency_str = $emergency_str ? $emergency_str : ''.__('There are no emergency codes left. You will need to reset your private key to generate new ones.', 'all-in-one-wp-security-and-firewall').''; return $emergency_str; } /** * Check a code for a user (checks the code only - does not check activation status etc.) * * @param Integer $user_id - WP user ID * @param String $user_code - the code to check * @param Boolean $allow_emergency_code - whether to check against emergency codes * * @return Boolean */ public function check_code_for_user($user_id, $user_code, $allow_emergency_code = true) { $tfa_priv_key = get_user_meta($user_id, 'tfa_priv_key_64', true); // $tfa_last_login = get_user_meta($user_id, 'tfa_last_login', true); // Unused $tfa_last_pws_arr = get_user_meta($user_id, 'tfa_last_pws', true); $tfa_last_pws = @$tfa_last_pws_arr ? $tfa_last_pws_arr : array(); $alg = $this->get_user_otp_algorithm($user_id); $current_time_window = intval(time()/30); //Give the user 1,5 minutes time span to enter/retrieve the code //Or check $this->check_forward_counter_window number of events if hotp $codes = $this->generate_otps_for_login_check($user_id, $tfa_priv_key); //A recently used code was entered; that's not OK. if (in_array($this->hash($user_code, $user_id), $tfa_last_pws)) return false; $match = false; foreach ($codes as $index => $code) { if (hash_equals(trim($code->toHotp(6)), trim($user_code))) { $match = true; $found_index = $index; break; } } // Check emergency codes if (!$match) { $emergency_codes = $allow_emergency_code ? get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true) : array(); if (!$emergency_codes) return $match; foreach ($emergency_codes as $key => $emergency_code) { $dec = trim($this->decryptString(trim($emergency_code), $user_id)); if (hash_equals($dec, trim($user_code))) { $match = true; // Remove emergency code unset($emergency_codes[$key]); break; } } // Update emergency codes array if ($match) { update_user_meta($user_id, 'simba_tfa_emergency_codes_64', $emergency_codes); do_action('simba_tfa_emergency_code_used', $user_id, $emergency_codes); } } else { //Add the used code as well so it cant be used again //Keep the two last codes $tfa_last_pws[] = $this->hash($user_code, $user_id); $nr_of_old_to_save = $alg == 'hotp' ? $this->check_forward_counter_window : $this->check_back_time_windows; if (count($tfa_last_pws) > $nr_of_old_to_save) array_splice($tfa_last_pws, 0, 1); update_user_meta($user_id, 'tfa_last_pws', $tfa_last_pws); } if ($match) { //Save the time window when the last successful login took place update_user_meta($user_id, 'tfa_last_login', $current_time_window); //Update the counter if HOTP was used if ($alg == 'hotp') { $counter = $this->getUserCounter($user_id); $enc_new_counter = $this->encryptString($counter+1, $user_id); update_user_meta($user_id, 'tfa_hotp_counter', $enc_new_counter); if ($found_index > 10) update_user_meta($user_id, 'tfa_hotp_off_sync', 1); } } return $match; } public function getUserCounter($user_ID) { $enc_counter = get_user_meta($user_ID, 'tfa_hotp_counter', true); return $enc_counter ? trim($this->decryptString(trim($enc_counter), $user_ID)) : ''; } public function changeUserAlgorithmTo($user_id, $new_algorithm) { update_user_meta($user_id, 'tfa_algorithm_type', $new_algorithm); delete_user_meta($user_id, 'tfa_hotp_off_sync'); $counter_start = rand(13, 999999999); $enc_counter_start = $this->encryptString($counter_start, $user_id); if ('hotp' == $new_algorithm) { update_user_meta($user_id, 'tfa_hotp_counter', $enc_counter_start); } else { delete_user_meta($user_id, 'tfa_hotp_counter'); } } /** * Whether HOTP or TOTP is being used * * @param Integer|Boolean $user_id - WordPress user ID, or false for the site-wide default * * @return String - 'hotp' or 'totp' */ public function get_user_otp_algorithm($user_id = false) { $setting = $user_id ? get_user_meta($user_id, 'tfa_algorithm_type', true) : false; $default_hmac = $this->tfa->get_option('tfa_default_hmac'); if (!$default_hmac) $default_hmac = $this->default_hmac; return $setting ? $setting : $default_hmac; } private function get_iv_size() { // mcrypt first, for backwards compatibility if (function_exists('mcrypt_get_iv_size')) { return $GLOBALS['simba_two_factor_authentication']->is_mcrypt_deprecated() ? @mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC) : mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC); } elseif (function_exists('openssl_cipher_iv_length')) { return openssl_cipher_iv_length('AES-128-CBC'); } throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed'); } private function encrypt($key, $string, $iv) { // Prefer OpenSSL, because it uses correct padding, and its output can be decrypted by mcrypt - whereas, the converse is not true if (function_exists('openssl_encrypt')) { return openssl_encrypt($string, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv); } elseif (function_exists('mcrypt_encrypt')) { return $GLOBALS['simba_two_factor_authentication']->is_mcrypt_deprecated() ? @mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv) : mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv); } throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed'); } private function decrypt($key, $enc, $iv, $force_openssl = false) { // Prefer mcrypt, because it can decrypt the output of both mcrypt_encrypt() and openssl_decrypt(), whereas (because of mcrypt_encrypt() using bad padding), the converse is not true if (function_exists('mcrypt_decrypt') && !$force_openssl) { return $GLOBALS['simba_two_factor_authentication']->is_mcrypt_deprecated() ? @mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv) : mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv); } elseif (function_exists('openssl_decrypt')) { $decrypted = openssl_decrypt($enc, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv); if (false === $decrypted && !$force_openssl) { $extra = function_exists('wp_debug_backtrace_summary') ? " backtrace: ".wp_debug_backtrace_summary() : ''; error_log("TFA decryption failure: was your site migrated to a server without mcrypt? You may need to install mcrypt, or disable TFA, in order to successfully decrypt data that was previously encrypted with mcrypt.$extra"); } return $decrypted; } if ($force_openssl) return false; throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed'); } public function encryptString($string, $salt_suffix, $force_encrypt = false) { $key = ($this->tfa->get_option('tfa_encrypt_secrets') && defined('SIMBA_TFA_DB_ENCRYPTION_KEY')) ? base64_decode(SIMBA_TFA_DB_ENCRYPTION_KEY) : $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix); if ($force_encrypt && defined('SIMBA_TFA_DB_ENCRYPTION_KEY')) $key = base64_decode(SIMBA_TFA_DB_ENCRYPTION_KEY); $iv_size = $this->get_iv_size(); $iv = $GLOBALS['simba_two_factor_authentication']->random_bytes($iv_size); $enc = $this->encrypt($key, $string, $iv); if (false === $enc) return false; $enc = $iv.$enc; $enc_b64 = base64_encode($enc); return $enc_b64; } private function decryptString($enc_b64, $salt_suffix, $force_openssl = false) { $key = ($this->tfa->get_option('tfa_encrypt_secrets') && defined('SIMBA_TFA_DB_ENCRYPTION_KEY')) ? base64_decode(SIMBA_TFA_DB_ENCRYPTION_KEY) : $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix); $iv_size = $this->get_iv_size(); $enc_conc = bin2hex(base64_decode($enc_b64)); $iv = hex2bin(substr($enc_conc, 0, $iv_size*2)); $enc = hex2bin(substr($enc_conc, $iv_size*2)); $string = $this->decrypt($key, $enc, $iv, $force_openssl); // Remove padding bytes return rtrim($string, "\x00..\x1F"); } private function hashAndBin($pw, $salt) { $key = $this->hash($pw, $salt); $key = pack('H*', $key); // Yes: it's a null encryption key. See: https://wordpress.org/support/topic/warning-mcrypt_decrypt-key-of-size-0-not-supported-by-this-algorithm-only-k?replies=5#post-6806922 // Basically: the original plugin had a bug here, which caused a null encryption key. This fails on PHP 5.6+. But, fixing it would break backwards compatibility for existing installs - and note that the only unknown once you have access to the encrypted data is the AUTH_SALT and AUTH_KEY constants... which means that actually the intended encryption was non-portable, + problematic if you lose your wp-config.php or try to migrate data to another site, or changes these values. (Normally changing these values only causes a compulsory re-log-in - but with the intended encryption in the original author's plugin, it'd actually cause a permanent lock-out until you disabled his plugin). If someone has read-access to the database, then it'd be reasonable to assume they have read-access to wp-config.php too: or at least, the number of attackers who can do one and not the other would be small. The "encryption's" not worth it. // In summary: this isn't encryption, and is not intended to be. return str_repeat(chr(0), 16); } private function hash($pw, $salt) { //$hash = hash_pbkdf2('sha256', $pw, $salt, 10); //$hash = crypt($pw, '$5$'.$salt.'$'); $hash = md5($salt.$pw); return $hash; } private function randString($len = 10) { $chars = '23456789QWERTYUPASDFGHJKLZXCVBNM'; $chars = str_split($chars); shuffle($chars); if (function_exists('random_int')) { $code = ''; for ($i = 1; $i <= $len; $i++) { $code .= $chars[random_int(0, count($chars)-1)]; } } else { $code = implode('', array_splice($chars, 0, $len)); } return $code; } }