label; } /** * Returns the login label for the TOTP authentication method. * * @return string The login label for the TOTP authentication method. */ public function get_login_label(): string { return esc_html__( 'TOTP Authentication', 'defender-security' ); } /** * Returns the user label for the TOTP authentication method. * * @return string The user label for the TOTP authentication method. */ public function get_user_label(): string { return esc_html__( 'TOTP', 'defender-security' ); } /** * Get the desc of the provider. * * @return string */ public function get_description(): string { return $this->description; } /** * Renders the authentication form for TOTP. */ public function authentication_form() { ?>
get_login_label(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
get_model()->app_text ); ?>
'Google Authenticator', 'microsoft-authenticator' => 'Microsoft Authenticator', 'authy' => 'Authy', ); } /** * Initializes the provider for a specific user. * * @param WP_User $user The user object. */ public function init_provider( WP_User $user ) { $is_on = $this->is_available_for_user( $user ); // $this->label will be escaped at src\view\two-fa\user-options.php. $this->label = $is_on ? sprintf( '', $this->get_component()->is_checked_enabled_provider_by_slug( $user, self::$slug ) ? '' : ' disabled', esc_html__( 'Reset Keys', 'defender-security' ) ) : ''; $this->description = $is_on ? esc_html__( 'TOTP Authentication method is active for this site', 'defender-security' ) : esc_html__( 'Use an authenticator app to sign in with a separate passcode.', 'defender-security' ); } /** * Display auth method. * * @param WP_User $user The user object. * * @throws SodiumException Exceptions thrown by the sodium functions. */ public function user_options( WP_User $user ) { // Enqueue scripts. if ( ! wp_script_is( 'clipboard', 'enqueued' ) ) { wp_enqueue_script( 'clipboard' ); } wp_enqueue_script( 'def-qrcode', defender_asset_url( '/assets/js/jquery-qrcode.min.js' ), array( 'jquery' ), '0.18.0', true ); $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', array( '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', array( '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 The secret key for the TOTP. * * @return string */ public static function generate_qr_code( $secret_key ) { $settings = new Two_Fa(); $issuer = rawurlencode( $settings->app_title ); $user = wp_get_current_user(); return 'otpauth://totp/' . $issuer . ':' . rawurlencode( $user->user_email ) . '?secret=' . $secret_key . '&issuer=' . $issuer; } /** * 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 ); } /** * Validates the TOTP authentication for a user. * * @param WP_User $user The user to validate. * * @return bool|WP_Error True if validation is successful, WP_Error otherwise. * @throws SodiumException Exceptions thrown by the sodium functions. */ public function validate_authentication( WP_User $user ) { $otp = HTTP::post( 'otp' ); if ( ! is_string( $otp ) || '' === $otp ) { $lockout_message = $this->get_component()->verify_attempt( $user->ID, self::$slug ); return new WP_Error( 'opt_fail', '' === $lockout_message ? esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' ) : $lockout_message ); } $setup_key = self::get_user_secret( $user->ID ); if ( is_wp_error( $setup_key ) ) { return new WP_Error( 'opt_fail', esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' ) ); } return self::verify_otp( $otp, $user->ID, $setup_key ); } /** * Retrieves the TOTP secret for a user. * * @param int $user_id The user ID. * * @return string|WP_Error The TOTP secret, or WP_Error on failure. * @throws SodiumException Exceptions thrown by the sodium functions. */ 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 ( is_string( $data ) && '' !== $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 ( is_string( $data ) && '' !== $data ) { return Crypt::get_decrypted_data( $data ); } } // Finally, generate a new one. return defender_generate_random_string( self::TOTP_LENGTH, self::TOTP_CHARACTERS ); } /** * Saves the TOTP setup key for a user. * * @param int $user_id The user ID. * @param string $plaintext The plaintext TOTP key. * * @return bool|WP_Error True on success, WP_Error on failure. * @throws SodiumException Exceptions thrown by the sodium functions. */ 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 The time counter. * @param int $user_id The user ID. * @param string $setup_key The TOTP setup key. * * @return string|WP_Error * @throws SodiumException Exceptions thrown by the sodium functions. */ private static function generate_otp( $counter, $user_id, $setup_key ) { if ( ! is_string( $setup_key ) || '' === $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 The TOTP code entered by the user. * @param int $user_id The user ID. * @param string $setup_key The TOTP setup key. * * @return bool|WP_Error * @throws SodiumException Exceptions thrown by the sodium functions. */ 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; } }