is_activated = get_user_meta( $user->ID, self::BACKUP_CODE_START, true ); $this->description = empty( $this->is_activated ) ? esc_html__( 'Generate non-expirable backup codes that can be used to log in once.', 'defender-security' ) : esc_html__( 'Each backup code can only be used to log in once.', 'defender-security' ); } /** * Get the name of the provider. * * @return string */ public function get_label(): string { return esc_html__( 'Backup Codes', 'defender-security' ); } /** * Returns the label for the backup codes' provider. * * @return string The label. */ public function get_login_label(): string { return $this->get_label(); } /** * Returns the login label for the backup codes' provider. * * @return string The login label. */ public function get_user_label(): string { return esc_html__( 'Backup codes', 'defender-security' ); } /** * Get the desc of the provider. * * @return string */ public function get_description(): string { return $this->description; } /** * Whether this 2FA provider is configured and codes are available for the user specified. * * @param WP_User $user The user object. * * @return bool */ public function is_available_for_user( WP_User $user ): bool { // Does this user have available codes? if ( 0 < self::get_unused_codes_for_user( $user ) ) { return true; } return false; } /** * Displays the authentication form for backup codes. */ public function authentication_form(): void { ?>

get_login_label(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

ID, self::BACKUP_CODE_VALUES, true ); if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) { return count( $backup_codes ); } return 0; } /** * Generate a random string for an auth code. * * @return string */ private static function get_code(): string { $code = ''; $chars = self::BACKUP_CODE_CHARACTERS; for ( $i = 0; $i < self::BACKUP_CODE_SIZE; $i++ ) { $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); } return $code; } /** * Show an admin notice when backup codes have run out. * * @return void */ public function admin_notices(): void { $user = wp_get_current_user(); if ( ! is_object( $user ) ) { return; } // Is User role from common list checked? if ( empty( array_intersect( $user->roles, $this->get_model()->user_roles ) ) ) { return; } // Return if the provider is not enabled. if ( ! in_array( self::$slug, $this->get_component()->get_enabled_providers_for_user( $user->ID ), true ) ) { return; } // Return if we are not out of codes. if ( $this->is_available_for_user( $user ) ) { return; } ?>

ID ) . '#wpdef-2fa-backup-codes' ) . '">' . esc_html__( 'generate', 'defender-security' ) . '' ); ?>

get_component()->is_checked_enabled_provider_by_slug( $user, self::$slug ) && $this->is_available_for_user( $user ); $attr = $cond ? '' : ' disabled'; // Check whether codes were generated before or not. if ( empty( $this->is_activated ) ) { $button_text = esc_html__( 'Generate Backup Codes', 'defender-security' ); $class = 'hidden'; $count = 0; $show_notice = 'no'; } else { $button_text = esc_html__( 'Get New Codes', 'defender-security' ); $class = $cond ? '' : 'hidden'; $count = self::get_unused_codes_for_user( $user ); $show_notice = 0 === $count ? 'yes' : 'no'; } $filename = str_replace( array( 'http://www.', 'http://', 'https://www.', 'https://', ), '', get_bloginfo( 'url' ) ); $filename = str_replace( '/', '-', $filename ); $filename = sanitize_file_name( $filename . '-backup-codes.txt' ); $this->get_controller()->render_partial( 'two-fa/providers/backup-codes', array( 'number_of_codes' => self::display_number_of_codes( $count ), 'url' => $this->get_url( 'generate_backup_codes' ), 'user' => $user, 'attr' => $attr, 'class' => $class, 'show_notice' => $show_notice, 'button_text' => $button_text, 'filename' => $filename, ) ); } /** * Generate backup codes. * * @param WP_User $user The user object. * @param array $args Optional arguments to customize code generation. * * @return array */ public static function generate_codes( WP_User $user, array $args = array() ): array { $codes = array(); $codes_hashed = array(); // Check for arguments. if ( isset( $args['number'] ) ) { $num_codes = (int) $args['number']; } else { $num_codes = self::BACKUP_CODE_COUNT; } // Add a flag if it haven't existed yet. if ( empty( get_user_meta( $user->ID, self::BACKUP_CODE_START, true ) ) ) { update_user_meta( $user->ID, self::BACKUP_CODE_START, 1 ); } if ( isset( $args['method'] ) && 'append' === $args['method'] ) { $codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true ); } for ( $i = 0; $i < $num_codes; $i++ ) { $code = self::get_code(); $codes_hashed[] = wp_hash_password( $code ); $codes[] = $code; unset( $code ); } update_user_meta( $user->ID, self::BACKUP_CODE_VALUES, $codes_hashed ); // Unhashed values. return $codes; } /** * Delete codes. * * @param WP_User $user The user object. * @param string $code_hashed The hashed code to delete. * * @return void */ public function delete_code( WP_User $user, string $code_hashed ): void { $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true ); // Delete the current code from the list since it's been used. $backup_codes = array_flip( $backup_codes ); unset( $backup_codes[ $code_hashed ] ); $backup_codes = array_values( array_flip( $backup_codes ) ); // Update the backup code master list. update_user_meta( $user->ID, self::BACKUP_CODE_VALUES, $backup_codes ); } /** * Displays the number of unused codes. * * @param int $count The number of unused codes. * * @return string */ public static function display_number_of_codes( int $count ): string { return sprintf( /* translators: %s: count */ _n( '%s unused code remaining', '%s unused codes remaining', $count, 'defender-security' ), $count ); } /** * Validates a backup code entered by the user. * * @param WP_User $user The user object. * @param string $code The backup code to validate. * * @return bool|WP_Error */ protected function validate_code( WP_User $user, string $code ) { $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true ); if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) { foreach ( $backup_codes as $code_hashed ) { if ( wp_check_password( $code, $code_hashed, $user->ID ) ) { $this->delete_code( $user, $code_hashed ); return true; } } } $lockout_message = $this->get_component()->verify_attempt( $user->ID, self::$slug ); return new WP_Error( 'opt_fail', empty( $lockout_message ) ? esc_html__( 'ERROR: Invalid verification code.', 'defender-security' ) : $lockout_message ); } /** * Validates the authentication for a user using a backup code. * * @param WP_User $user The user object. * * @return bool|WP_Error True if authenticated, WP_Error if not. */ public function validate_authentication( WP_User $user ) { $codes = defender_get_data_from_request( 'backup-codes', 'p' ) ?? false; if ( false === $codes ) { return new WP_Error( 'opt_fail', esc_html__( 'ERROR: Invalid backup code.', 'defender-security' ) ); } return $this->validate_code( $user, $codes ); } }