enqueue_assets( '' ); } wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false ); $totp_key = $this->get_key( $user ); $this->admin_notices(); ?>

already_active = true; $return->key = get_user_meta( $user->ID, self::SECRET_META_KEY, true ); if ( empty( $return->key ) ) { $return->key = $this->generate_key(); $return->already_active = false; } return $return; } /** * Save the options specified in `::user_two_factor_options()` * * @param integer $user_id The user ID whose options are being updated. */ public function user_two_factor_options_update( $user_id ) { if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) { check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' ); // If there is no authcode provided if ( empty( $_POST['two-factor-totp-authcode'] ) ) { $two_factor_core = ITSEC_Two_Factor::get_instance(); // Check to see if TOTP is enabled, and if not then go no further if ( ! in_array( 'Two_Factor_Totp', $two_factor_core->get_enabled_providers_for_user() ) ) { return; } } $current_key = get_user_meta( $user_id, self::SECRET_META_KEY, true ); // If the key hasn't changed or is invalid, do nothing. if ( $current_key === $_POST['two-factor-totp-key'] || ! preg_match( '/^[' . $this->_base_32_chars . ']+$/', $_POST['two-factor-totp-key'] ) ) { return; } $notices = array(); if ( empty( $_POST['two-factor-totp-authcode'] ) ) { $notices['error'][] = __( 'Two-Factor Authentication not activated, you must specify authcode to ensure it is properly set up. Please re-scan the QR code and enter the code provided by your application.', 'it-l10n-ithemes-security-pro' ); } else { if ( $this->_is_valid_authcode( $_POST['two-factor-totp-key'], $_POST['two-factor-totp-authcode'] ) ) { if ( ! update_user_meta( $user_id, self::SECRET_META_KEY, $_POST['two-factor-totp-key'] ) ) { $notices['error'][] = __( 'Unable to save Two-Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'it-l10n-ithemes-security-pro' ); } } else { $notices['error'][] = __( 'Two-Factor Authentication not activated, the authentication code you entered was not valid. Please re-scan the QR code and enter the code provided by your application.', 'it-l10n-ithemes-security-pro' ); } } if ( ! empty( $notices ) ) { update_user_meta( $user_id, self::NOTICES_META_KEY, $notices ); } } } public function ajax_new_code() { check_ajax_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' ); $site_name = get_bloginfo( 'name', 'display' ); $return = array(); $return['key'] = $this->generate_key(); $return['qrcode_url'] = $this->get_google_qr_code( $site_name . ':' . $_POST['user_login'], $return['key'], $site_name ); wp_send_json_success( $return ); } public function ajax_verify_code() { check_ajax_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' ); $user_id = (int) $_POST['user_id']; if ( ! $user_id || ! current_user_can( 'edit_user', $user_id ) ) { wp_send_json_error( __( 'You do not have permission to edit this user.', 'it-l10n-ithemes-security-pro' ) ); } if ( $this->_is_valid_authcode( $_POST['key'], $_POST['authcode'] ) ) { if ( ! update_user_meta( $user_id, self::SECRET_META_KEY, $_POST['key'] ) ) { wp_send_json_error( __( 'Unable to save two-factor secret.', 'it-l10n-ithemes-security-pro' ) ); } wp_send_json_success( __( 'Success!', 'it-l10n-ithemes-security-pro' ) ); } else { wp_send_json_error( __( 'The code you supplied is not valid.', 'it-l10n-ithemes-security-pro' ) ); } $site_name = get_bloginfo( 'name', 'display' ); $return = array(); $return['key'] = $this->generate_key(); $return['qrcode_url'] = $this->get_google_qr_code( $site_name . ':' . $_POST['user_login'], $return['key'], $site_name ); wp_send_json( $return ); } /** * Display any available admin notices. * * @param array $notices Keys are CSS class to use, values are an array of messages. */ public function admin_notices( $notices = null ) { if ( ! isset( $notices ) ) { $notices = get_user_meta( get_current_user_id(), self::NOTICES_META_KEY, true ); delete_user_meta( get_current_user_id(), self::NOTICES_META_KEY ); } if ( ! empty( $notices ) ) { foreach ( $notices as $class => $messages ) { ?>

ID, self::SECRET_META_KEY, true ); return $this->_is_valid_authcode( $key, trim( $_REQUEST['authcode'] ) ); } /** * Checks if a given code is valid for a given key, allowing for a certain amount of time drift * * @param string $key The share secret key to use. * @param string $authcode The code to test. * * @return bool Whether the code is valid within the time frame */ private function _is_valid_authcode( $key, $authcode ) { /** * Filter the maximum ticks to allow when checking valid codes. * * Ticks are the allowed offset from the correct time in 30 second increments, * so the default of 4 allows codes that are two minutes to either side of server time * * @param int $max_ticks Max ticks of time correction to allow. Default 4. */ $max_ticks = apply_filters( 'two-factor-totp-time-step-allowance', self::DEFAULT_TIME_STEP_ALLOWANCE ); // Array of all ticks to allow, sorted using absolute value to test closest match first. $ticks = range( - $max_ticks, $max_ticks ); usort( $ticks, array( $this, 'abssort' ) ); $time = time() / self::DEFAULT_TIME_STEP_SEC; foreach ( $ticks as $offset ) { $log_time = $time + $offset; if ( $this->calc_totp( $key, $log_time ) === $authcode ) { return true; } } return false; } /** * Generates key * * @param int $bitsize Nume of bits to use for key. * * @return string $bitsize long string composed of available base32 chars. */ public function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) { $bytes = ceil( $bitsize / 8 ); $secret = wp_generate_password( $bytes, true, true ); return $this->base32_encode( $secret ); } /** * Returns data in base32 encoding. * * @param string $data Data to be converted into base32. * * @return string Data in base32 encoding without padding. */ public function base32_encode( $data ) { if ( empty( $data ) ) { return ''; } $binary_string = ''; foreach ( str_split( $data ) as $character ) { $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT ); } $five_bit_sections = str_split( $binary_string, 5 ); $base32 = ''; foreach ( $five_bit_sections as $five_bit_section ) { $base32 .= $this->_base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ]; } return $base32; } /** * Pack stuff * * @param string $value The value to be packed. * * @return string Binary packed string. */ private static function pack64( $value ) { // 64bit mode (PHP_INT_SIZE == 8) if ( PHP_INT_SIZE >= 8 ) { // We can use the new 64bit pack functionality if we're on PHP 5.6.3+ if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) { return pack( 'J', $value ); } $highmap = 0xffffffff << 32; $higher = ( $value & $highmap ) >> 32; } else { // 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher and not pack anything beyond it's limits $higher = 0; } $lowmap = 0xffffffff; $lower = $value & $lowmap; return pack( 'NN', $higher, $lower ); } /** * Calculate a valid code given the shared secret key * * @param string $key The shared secret key to use for calculating code. * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size. * @param int $digits The number of digits in the returned code. * @param string $hash The hash used to calculate the code. * @param int $time_step The size of the time step. * * @return string The totp code */ private function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) { $secret = $this->base32_decode( $key ); if ( false === $step_count ) { $step_count = floor( time() / $time_step ); } $timestamp = $this->pack64( $step_count ); $hash = hash_hmac( $hash, $timestamp, $secret, true ); $offset = ord( $hash[19] ) & 0xf; $code = ( ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) | ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) | ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) | ( ord( $hash[ $offset + 3 ] ) & 0xff ) ) % pow( 10, $digits ); return str_pad( $code, $digits, '0', STR_PAD_LEFT ); } /** * Uses the Google Charts API to build a QR Code for use with an otpauth url * * @param string $name The name to display in the Authentication app. * @param string $key The secret key to share with the Authentication app. * @param string $title The title to display in the Authentication app. * @param array $opts Additional options. * * @return string A URL to use as an img src to display the QR code */ public function get_google_qr_code( $name, $key, $title = null, $opts = array() ) { // rawurlencode() $name and $title because iOS chokes otherwise $payload = urlencode( 'otpauth://totp/' . rawurlencode( $name ) . '?secret=' . $key ); if ( isset( $title ) ) { $payload .= urlencode( '&issuer=' . rawurlencode( $title ) ); } $size = isset( $opts['size'] ) ? absint( $opts['size'] ) : 200; $url = "https://qr-code.ithemes.com/?size={$size}&data={$payload}"; /** * Filter the image URL for the QR code. * * @param string $url The image src attr URL. * @param string $payload The payload to embed in the QR code. * @param string $name The name to display in the Authentication app. * @param string $title The title to display in the Authentication app. * @param array $opts Additional options. */ $url = apply_filters( 'itsec_two_factor_qr_code_url', $url, $payload, $name, $key, $title, $opts ); return $url; } /** * Whether this Two-Factor provider is configured and available for the user specified. * * @param WP_User $user WP_User object of the logged-in user. * * @return boolean */ public function is_available_for_user( $user ) { // Only available if the secret key has been saved for the user. $key = get_user_meta( $user->ID, self::SECRET_META_KEY, true ); return ! empty( $key ); } /** * Prints the form that prompts the user to authenticate. * * @param WP_User $user WP_User object of the logged-in user. */ public function authentication_page( $user ) { require_once( ABSPATH . '/wp-admin/includes/template.php' ); ?>

_base_32_chars . ']+$/', $base32_string, $match ) ) { throw new Exception( 'Invalid characters in the base32 string.' ); } $l = strlen( $base32_string ); $n = 0; $j = 0; $binary = ''; for ( $i = 0; $i < $l; $i ++ ) { $n = $n << 5; // Move buffer left by 5 to make room. $n = $n + strpos( $this->_base_32_chars, $base32_string[ $i ] ); // Add value into buffer. $j += 5; // Keep track of number of bits in buffer. if ( $j >= 8 ) { $j -= 8; $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j ); } } return $binary; } /** * Used with usort to sort an array by distance from 0 * * @param int $a First array element. * @param int $b Second array element. * * @return int -1, 0, or 1 as needed by usort */ private function abssort( $a, $b ) { $a = abs( $a ); $b = abs( $b ); if ( $a === $b ) { return 0; } return ( $a < $b ) ? - 1 : 1; } public function description() { echo '

' . sprintf( wp_kses( __( 'Use a two-factor mobile app such as Authy or Google Authenticator (Android, iOS). The mobile app generates a time-sensitive code that must be supplied when logging in.', 'it-l10n-ithemes-security-pro' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( 'https://www.authy.com/app/mobile/' ), esc_url( 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en' ), esc_url( 'https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8' ) ) . '

'; } /** * @inheritDoc */ public function get_on_board_dashicon() { return 'smartphone'; } /** * @inheritDoc */ public function get_on_board_label() { return esc_html__( 'Mobile App', 'it-l10n-ithemes-security-pro' ); } /** * @inheritDoc */ public function get_on_board_description() { return esc_html__( 'Log in to WordPress using a mobile app like Authy or Google Authenticator.', 'it-l10n-ithemes-security-pro' ); } /** * @inheritDoc */ public function has_on_board_configuration() { return true; } /** * @inheritDoc */ public function get_on_board_config( WP_User $user ) { $key = $this->get_key( $user ); $blog = get_bloginfo( 'name', 'display' ); return array( 'secret' => $key->key, 'qr' => $this->get_google_qr_code( $blog . ':' . $user->user_login, $key->key, $blog, array( 'size' => 300 ) ), ); } /** * @inheritDoc */ public function handle_ajax_on_board( WP_User $user, array $data ) { if ( $data['itsec_method'] !== 'verify-totp-code' ) { return; } if ( ! isset( $data['itsec_totp_secret'], $data['itsec_totp_code'] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid Request Format', 'it-l10n-ithemes-security-pro' ), ) ); } $secret = $data['itsec_totp_secret']; if ( $this->_is_valid_authcode( $secret, $data['itsec_totp_code'] ) ) { if ( $secret !== get_user_meta( $user->ID, self::SECRET_META_KEY, true ) && ! update_user_meta( $user->ID, self::SECRET_META_KEY, $secret ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unable to save two-factor secret.', 'it-l10n-ithemes-security-pro' ), ) ); } wp_send_json_success( array( 'message' => esc_html__( 'Success!', 'it-l10n-ithemes-security-pro' ), ) ); } else { wp_send_json_error( array( 'message' => esc_html__( 'The code you supplied is not valid.', 'it-l10n-ithemes-security-pro' ), ) ); } } public function configure_via_cli( WP_User $user, array $args ) { $key = $this->generate_key(); if ( ! update_user_meta( $user->ID, self::SECRET_META_KEY, $key ) ) { WP_CLI::error( 'Could not save Totp secret.' ); } if ( empty( $args['porcelain'] ) ) { WP_CLI::log( sprintf( 'Totp Secret: %s', $key ) ); } else { WP_CLI::log( $key ); } } }