delete_user_totp_key( $user_id ); } } /** * Get the URL for deleting the secret token. * * @param integer $user_id User ID. * * @return string * * @codeCoverageIgnore */ protected function get_token_delete_url_for_user( $user_id ) { return Two_Factor_Core::get_user_update_action_url( $user_id, self::ACTION_SECRET_DELETE ); } /** * Display TOTP options on the user settings page. * * @param WP_User $user The current user being edited. * @return false * * @codeCoverageIgnore */ public function user_two_factor_options( $user ) { if ( ! isset( $user->ID ) ) { return false; } wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false ); $key = $this->get_user_totp_key( $user->ID ); $this->admin_notices( $user->ID ); ?>
generate_key(); $site_name = get_bloginfo( 'name', 'display' ); $totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user ); ?>

is_valid_key( $key ) ) { if ( $this->is_valid_authcode( $key, $authcode ) ) { if ( ! $this->set_user_totp_key( $user_id, $key ) ) { $errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' ); } } else { $errors[] = __( 'Invalid Two Factor Authentication code.', 'two-factor' ); } } else { $errors[] = __( 'Invalid Two Factor Authentication secret key.', 'two-factor' ); } } if ( ! empty( $errors ) ) { $notices['error'] = $errors; } if ( ! empty( $notices ) ) { update_user_meta( $user_id, self::NOTICES_META_KEY, $notices ); } } } /** * Get the TOTP secret key for a user. * * @param int $user_id User ID. * * @return string */ public function get_user_totp_key( $user_id ) { return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true ); } /** * Set the TOTP secret key for a user. * * @param int $user_id User ID. * @param string $key TOTP secret key. * * @return boolean If the key was stored successfully. */ public function set_user_totp_key( $user_id, $key ) { return update_user_meta( $user_id, self::SECRET_META_KEY, $key ); } /** * Delete the TOTP secret key for a user. * * @param int $user_id User ID. * * @return boolean If the key was deleted successfully. */ public function delete_user_totp_key( $user_id ) { return delete_user_meta( $user_id, self::SECRET_META_KEY ); } /** * Check if the TOTP secret key has a proper format. * * @param string $key TOTP secret key. * * @return boolean */ public function is_valid_key( $key ) { $check = sprintf( '/^[%s]+$/', self::$base_32_chars ); if ( 1 === preg_match( $check, $key ) ) { return true; } return false; } /** * Display any available admin notices. * * @param integer $user_id User ID. * * @return void * * @codeCoverageIgnore */ public function admin_notices( $user_id ) { $notices = get_user_meta( $user_id, self::NOTICES_META_KEY, true ); if ( ! empty( $notices ) ) { delete_user_meta( $user_id, self::NOTICES_META_KEY ); foreach ( $notices as $class => $messages ) { ?>

is_valid_authcode( $this->get_user_totp_key( $user->ID ), sanitize_text_field( $_REQUEST['authcode'] ) ); } return false; } /** * 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 */ public static 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 * * @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead. * @param int $max_ticks Max ticks of time correction to allow. Default 4. */ $max_ticks = apply_filters_deprecated( 'two-factor-totp-time-step-allowance', array( self::DEFAULT_TIME_STEP_ALLOWANCE ), '0.7.0', 'two_factor_totp_time_step_allowance' ); $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( __CLASS__, 'abssort' ) ); $time = time() / self::DEFAULT_TIME_STEP_SEC; foreach ( $ticks as $offset ) { $log_time = $time + $offset; if ( hash_equals(self::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 static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) { $bytes = ceil( $bitsize / 8 ); $secret = wp_generate_password( $bytes, true, true ); return self::base32_encode( $secret ); } /** * Pack stuff * * @param string $value The value to be packed. * * @return string Binary packed string. */ public static function pack64( $value ) { // 64bit mode (PHP_INT_SIZE == 8). if ( PHP_INT_SIZE >= 8 ) { // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality. if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) { return pack( 'J', $value ); // phpcs:ignore PHPCompatibility.ParameterValues.NewPackFormat.NewFormatFound } $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 */ public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) { $secret = self::base32_decode( $key ); if ( false === $step_count ) { $step_count = floor( time() / $time_step ); } $timestamp = self::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. * * @return string A URL to use as an img src to display the QR code * * @codeCoverageIgnore */ public static function get_google_qr_code( $name, $key, $title = null ) { // Encode to support spaces, question marks and other characters. $name = rawurlencode( $name ); $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key ); if ( isset( $title ) ) { $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) ); } return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_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 = $this->get_user_totp_key( $user->ID ); return ! empty( $key ); } /** * Prints the form that prompts the user to authenticate. * * @param WP_User $user WP_User object of the logged-in user. * * @codeCoverageIgnore */ public function authentication_page( $user ) { require_once ABSPATH . '/wp-admin/includes/template.php'; ?>

= 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 static function abssort( $a, $b ) { $a = abs( $a ); $b = abs( $b ); if ( $a === $b ) { return 0; } return ( $a < $b ) ? -1 : 1; } }