| Current Path : /home/krobertfnz/www/wp-content/plugins/wp-defender/src/component/two-factor/providers/ |
| Current File : /home/krobertfnz/www/wp-content/plugins/wp-defender/src/component/two-factor/providers/totp.php |
<?php
namespace WP_Defender\Component\Two_Factor\Providers;
use Calotes\Helper\HTTP;
use WP_Defender\Component\Two_Fa as Two_Fa_Component;
use WP_Defender\Component\Crypt;
use WP_Defender\Component\Two_Factor\Two_Factor_Provider;
use WP_Defender\Extra\Base2n;
use WP_Defender\Traits\IO;
use WP_User;
use WP_Error;
use WP_Defender\Extra\QRcode;
/**
* Class Totp
* Note: key 'defenderAuthOn' only for TOTP method.
*
* @since 2.8.0
* @package WP_Defender\Component\Two_Factor\Providers
*/
class Totp extends Two_Factor_Provider {
use IO;
/**
* 2fa provider slug.
*
* @var string
*/
public static $slug = 'totp';
/**
* @type string
*/
public const TOTP_AUTH_KEY = 'defenderAuthOn';
/**
* Used def.key before v3.4.0.
*
* @type string
*/
public const TOTP_SECRET_KEY = 'defenderAuthSecret';
/**
* Use Sodium library since v3.4.0.
*
* @type string
*/
public const TOTP_SODIUM_SECRET_KEY = 'defenderAuthSodiumSecret';
/**
* @type string
*/
public const TOTP_FORCE_KEY = 'defenderForceAuth';
/**
* @type int
*/
public const TOTP_DIGIT_COUNT = 6;
/**
* @type int
*/
public const TOTP_TIME_STEP_SEC = 30;
/**
* @type int
*/
public const TOTP_LENGTH = 16;
/**
* RFC 4648 base32 alphabet.
* @type string
*/
public const TOTP_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* @type string
*/
public const DEFAULT_CRYPTO = 'sha1';
/**
* 1 = 30 second range for authenticator.
* @type int
*/
public const TOTP_TIME_STEP_ALLOWANCE = 1;
protected $label;
protected $description;
public function __construct() {
add_action( 'wd_2fa_init_provider_' . self::$slug, [ &$this, 'init_provider' ] );
add_action( 'wd_2fa_user_options_' . self::$slug, [ &$this, 'user_options' ] );
}
/**
* Get the name of the provider.
*
* @return string
*/
public function get_label(): string {
return __( 'TOTP Authenticator App', 'wpdef' ) . $this->label;
}
/**
* @return string
*/
public function get_login_label(): string {
return __( 'TOTP Authentication', 'wpdef' );
}
/**
* @return string
*/
public function get_user_label(): string {
return __( 'TOTP', 'wpdef' );
}
/**
* Get the desc of the provider.
*
* @return string
*/
public function get_description(): string {
return $this->description;
}
public function authentication_form() {
?>
<p class="wpdef-2fa-label"><?php echo $this->get_login_label(); ?></p>
<p class="wpdef-2fa-text def-otp-text"><?php echo esc_html( $this->get_model()->app_text ); ?></p>
<input type="text" autofocus value="" autocomplete="off" name="otp" />
<button class="button button-primary float-r" type="submit"><?php _e( 'Authenticate', 'wpdef' ); ?></button>
<?php
}
/**
* @return array
*/
public function get_auth_apps(): array {
return [
'google-authenticator' => 'Google Authenticator',
'microsoft-authenticator' => 'Microsoft Authenticator',
'authy' => 'Authy',
];
}
/**
* @param WP_User $user
*/
public function init_provider( WP_User $user ) {
$is_on = $this->is_available_for_user( $user );
$this->label = $is_on
? sprintf(
/* translators: %s: style class */
__( '<button type="button" class="button reset-totp-keys button-secondary hide-if-no-js" %s>Reset Keys</button>', 'wpdef' ),
$this->get_component()->is_checked_enabled_provider_by_slug( $user, self::$slug ) ? '' : ' disabled'
)
: '';
$this->description = $is_on
? __( 'TOTP Authentication method is active for this site', 'wpdef' )
: __( 'Use an authenticator app to sign in with a separate passcode.', 'wpdef' );
}
/**
* Display auth method.
*
* @param WP_User $user
*/
public function user_options( WP_User $user ) {
if ( ! wp_script_is( 'clipboard', 'enqueued' ) ) {
wp_enqueue_script( 'clipboard' );
}
$model = $this->get_model();
$service = $this->get_component();
$default_values = $model->get_default_values();
$is_on = $this->is_available_for_user( $user );
if ( $is_on ) {
$this->get_controller()->render_partial(
'two-fa/providers/totp-enabled',
[
'url' => $this->get_url( 'disable_totp' ),
]
);
} else {
$is_success = true;
$result = self::get_user_secret( $user->ID );
if ( is_wp_error( $result ) ) {
$secret = $result->get_error_message();
$is_success = false;
} elseif ( is_bool( $result ) ) {
// Sometimes we can get a boolean value due to errors with writing to the database. In this case, we need to reset the value.
delete_user_meta( $user->ID, self::TOTP_SECRET_KEY );
// Also for new key.
delete_user_meta( $user->ID, self::TOTP_SODIUM_SECRET_KEY );
$secret = self::get_user_secret( $user->ID );
} else {
$secret = $result;
}
$this->get_controller()->render_partial(
'two-fa/providers/totp-disabled',
[
'url' => $this->get_url( 'verify_otp_for_enabling' ),
'default_message' => $default_values['message'],
'auth_apps' => $this->get_auth_apps(),
'user' => $user,
'secret_key' => $secret,
'class' => $service->is_checked_enabled_provider_by_slug( $user, self::$slug ) ? '' : 'hidden',
'is_success' => $is_success,
]
);
}
}
/**
* Generate a QR code for apps can use. Apps from get_auth_apps().
*
* @param string $secret_key
*
* @retun void
*/
public static function generate_qr_code( $secret_key ) {
$settings = new \WP_Defender\Model\Setting\Two_Fa();
$issuer = $settings->app_title;
$user = wp_get_current_user();
$chl = ( 'otpauth://totp/' . rawurlencode( $issuer ) . ':' . rawurlencode( $user->user_email )
. '?secret=' . $secret_key . '&issuer=' . rawurlencode( $issuer ) );
require_once defender_path( 'src/extra/phpqrcode/phpqrcode.php' );
QRcode::svg( $chl, false, QR_ECLEVEL_L, 4 );
}
/**
* Whether this 2FA 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( WP_User $user ) {
return (bool) get_user_meta( $user->ID, self::TOTP_AUTH_KEY, true );
}
/**
* @param WP_User $user
*
* @return bool|WP_Error
*/
public function validate_authentication( WP_User $user ) {
$otp = HTTP::post( 'otp' );
if ( empty( $otp ) ) {
$lockout_message = $this->get_component()->verify_attempt( $user->ID, self::$slug );
return new WP_Error(
'opt_fail',
empty( $lockout_message )
? __( 'Whoops, the passcode you entered was incorrect or expired.', 'wpdef' )
: $lockout_message
);
}
$setup_key = self::get_user_secret( $user->ID );
if ( is_wp_error( $setup_key ) ) {
return new WP_Error(
'opt_fail',
__( 'Whoops, the passcode you entered was incorrect or expired.', 'wpdef' )
);
}
return self::verify_otp( $otp, $user->ID, $setup_key );
}
/**
* @param int $user_id
*
* @return string|WP_Error
*/
private static function get_user_secret( $user_id ) {
// First, we check the new 'TOTP_SODIUM_SECRET_KEY' key.
$data = get_user_meta( $user_id, self::TOTP_SODIUM_SECRET_KEY, true );
if ( ! empty( $data ) ) {
return Crypt::get_decrypted_data( $data );
}
// Then check the old 'TOTP_SECRET_KEY' key.
if ( ( new Two_Fa_Component() )->maybe_update( $user_id ) ) {
// Check a new key again.
$data = get_user_meta( $user_id, self::TOTP_SODIUM_SECRET_KEY, true );
if ( ! empty( $data ) && is_string( $data ) ) {
return Crypt::get_decrypted_data( $data );
}
}
// Finally, generate a new one.
return defender_generate_random_string( self::TOTP_LENGTH, self::TOTP_CHARACTERS );
}
/**
* @param int $user_id
* @param string $plaintext
*
* @return bool|WP_Error
*/
public static function save_setup_key( $user_id, $plaintext ) {
$secret = Crypt::get_encrypted_data( $plaintext );
if ( is_wp_error( $secret ) ) {
return $secret;
}
update_user_meta( $user_id, self::TOTP_SODIUM_SECRET_KEY, $secret );
return true;
}
/**
* Generate an OTP code base on current time.
*
* @param int $counter
* @param int $user_id
* @param string $setup_key
*
* @return string|WP_Error
*/
private static function generate_otp( $counter, $user_id, $setup_key ) {
if ( empty( $setup_key ) || ! is_string( $setup_key ) ) {
$setup_key = self::get_user_secret( $user_id );
if ( is_wp_error( $setup_key ) ) {
return $setup_key;
}
}
include_once defender_path( 'src/extra/binary-to-text-php/Base2n.php' );
$base32 = new Base2n( 5, self::TOTP_CHARACTERS, false, true, true );
$secret = $base32->decode( $setup_key );
$input = floor( $counter / self::TOTP_TIME_STEP_SEC );
// According to https://tools.ietf.org/html/rfc4226#section-5.3, should be 8 bytes value.
$time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $input );
$hmac = hash_hmac( self::DEFAULT_CRYPTO, $time, $secret, true );
// Now we have 20 bytes of DEFAULT_CRYPTO, need to short it down. Getting last byte of the hmac.
$offset = ord( substr( $hmac, - 1 ) ) & 0x0F;
$four_bytes = substr( $hmac, $offset, 4 );
// Now convert it into INT.
$value = unpack( 'N', $four_bytes );
$value = $value[1];
// Make sure it always actual like 32 bits.
$value = $value & 0x7FFFFFFF;
// Close.
$code = $value % 10 ** self::TOTP_DIGIT_COUNT;
// In some case we have the 0 before, so it becomes lesser than TOTP_DIGIT_COUNT, make sure it always right.
return str_pad( (string) $code, self::TOTP_DIGIT_COUNT, '0', STR_PAD_LEFT );
}
/**
* Verify the OTP of beyond & after TOTP_TIME_STEP_SEC seconds windows.
*
* @param string $user_code
* @param int $user_id
* @param string $setup_key
*
* @return bool|WP_Error
*/
public static function verify_otp( string $user_code, int $user_id, $setup_key ) {
if ( strlen( $user_code ) < self::TOTP_DIGIT_COUNT ) {
return false;
}
for ( $i = - self::TOTP_TIME_STEP_ALLOWANCE; $i <= self::TOTP_TIME_STEP_ALLOWANCE; $i ++ ) {
$counter = 0 === $i ? time() : $i * self::TOTP_TIME_STEP_SEC + time();
$code = self::generate_otp( $counter, $user_id, $setup_key );
if ( is_wp_error( $code ) ) {
return $code;
}
if ( Crypt::compare_lines( $user_code, $code ) ) {
return true;
}
}
return false;
}
}