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;
}
}