diff --git a/wp-content/plugins/two-factor/assets/banner-1544x500.png b/wp-content/plugins/two-factor/assets/banner-1544x500.png deleted file mode 100644 index 5b6e2081..00000000 Binary files a/wp-content/plugins/two-factor/assets/banner-1544x500.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/banner-772x250.png b/wp-content/plugins/two-factor/assets/banner-772x250.png deleted file mode 100644 index b955eeeb..00000000 Binary files a/wp-content/plugins/two-factor/assets/banner-772x250.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/icon-128x128.png b/wp-content/plugins/two-factor/assets/icon-128x128.png deleted file mode 100644 index 1f3a3c31..00000000 Binary files a/wp-content/plugins/two-factor/assets/icon-128x128.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/icon-256x256.png b/wp-content/plugins/two-factor/assets/icon-256x256.png deleted file mode 100644 index 3240b832..00000000 Binary files a/wp-content/plugins/two-factor/assets/icon-256x256.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/icon.svg b/wp-content/plugins/two-factor/assets/icon.svg deleted file mode 100644 index cc15690b..00000000 --- a/wp-content/plugins/two-factor/assets/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/wp-content/plugins/two-factor/assets/screenshot-1.png b/wp-content/plugins/two-factor/assets/screenshot-1.png deleted file mode 100644 index 001fb2ae..00000000 Binary files a/wp-content/plugins/two-factor/assets/screenshot-1.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/screenshot-2.png b/wp-content/plugins/two-factor/assets/screenshot-2.png deleted file mode 100644 index 9fb4f742..00000000 Binary files a/wp-content/plugins/two-factor/assets/screenshot-2.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/assets/screenshot-3.png b/wp-content/plugins/two-factor/assets/screenshot-3.png deleted file mode 100644 index b866bbb0..00000000 Binary files a/wp-content/plugins/two-factor/assets/screenshot-3.png and /dev/null differ diff --git a/wp-content/plugins/two-factor/class-two-factor-compat.php b/wp-content/plugins/two-factor/class-two-factor-compat.php index d7b4f46a..94e47f4d 100644 --- a/wp-content/plugins/two-factor/class-two-factor-compat.php +++ b/wp-content/plugins/two-factor/class-two-factor-compat.php @@ -11,11 +11,15 @@ * Should be used with care because ideally we wouldn't need * any integration specific code for this plugin. Everything should * be handled through clever use of hooks and best practices. + * + * @since 0.5.0 */ class Two_Factor_Compat { /** * Initialize all the custom hooks as necessary. * + * @since 0.5.0 + * * @return void */ public function init() { @@ -30,6 +34,8 @@ class Two_Factor_Compat { /** * Jetpack single sign-on wants long-lived sessions for users. * + * @since 0.5.0 + * * @param boolean $rememberme Current state of the "remember me" toggle. * * @return boolean @@ -47,6 +53,8 @@ class Two_Factor_Compat { /** * Helper to detect the presence of the active SSO module. * + * @since 0.5.0 + * * @return boolean */ public function jetpack_is_sso_active() { diff --git a/wp-content/plugins/two-factor/class-two-factor-core.php b/wp-content/plugins/two-factor/class-two-factor-core.php index 5eaa765a..d98cbfe6 100644 --- a/wp-content/plugins/two-factor/class-two-factor-core.php +++ b/wp-content/plugins/two-factor/class-two-factor-core.php @@ -1,6 +1,6 @@ init(); } + /** + * Register login page scripts. + * + * @since 0.10.0 + * + * @codeCoverageIgnore + */ + public static function login_enqueue_scripts() { + $environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : ''; + + wp_register_script( + 'two-factor-login', + plugins_url( $environment_prefix . '/providers/js/two-factor-login.js', __FILE__ ), + array(), + TWO_FACTOR_VERSION, + true + ); + + wp_register_script( + 'two-factor-login-authcode', + plugins_url( $environment_prefix . '/providers/js/two-factor-login-authcode.js', __FILE__ ), + array(), + TWO_FACTOR_VERSION, + true + ); + } + /** * Delete all plugin data on uninstall. * + * @since 0.10.0 + * * @return void */ public static function uninstall() { @@ -167,8 +209,7 @@ class Two_Factor_Core { $user_meta_keys, call_user_func( array( $provider_class, 'uninstall_user_meta_keys' ) ) ); - } catch ( Exception $e ) { - // Do nothing. + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Intentionally empty, provider may not implement this method. } } @@ -179,8 +220,7 @@ class Two_Factor_Core { $option_keys, call_user_func( array( $provider_class, 'uninstall_options' ) ) ); - } catch ( Exception $e ) { - // Do nothing. + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Intentionally empty, provider may not implement this method. } } } @@ -200,13 +240,14 @@ class Two_Factor_Core { /** * Get the registered providers of which some might not be enabled. * + * @since 0.11.0 + * * @return array List of provider keys and paths to class files. */ private static function get_default_providers() { return array( 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class-two-factor-email.php', 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class-two-factor-totp.php', - 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f.php', 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class-two-factor-backup-codes.php', 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class-two-factor-dummy.php', ); @@ -215,6 +256,8 @@ class Two_Factor_Core { /** * Get the classnames for specific providers. * + * @since 0.10.0 + * * @param array $providers List of paths to provider class files indexed by class names. * * @return array List of provider keys and classnames. @@ -230,6 +273,8 @@ class Two_Factor_Core { /** * Filters the classname for a provider. The dynamic portion of the filter is the defined providers key. * + * @since 0.9.0 + * * @param string $class The PHP Classname of the provider. * @param string $path The provided provider path to be included. */ @@ -255,9 +300,9 @@ class Two_Factor_Core { * @see Two_Factor_Core::get_enabled_providers_for_user() * @see Two_Factor_Core::get_supported_providers_for_user() * - * @since 0.1-dev + * @since 0.2.0 * - * @return array + * @return Two_Factor_Provider[] */ public static function get_providers() { $providers = self::get_default_providers(); @@ -268,23 +313,13 @@ class Two_Factor_Core { * This lets third-parties either remove providers (such as Email), or * add their own providers (such as text message or Clef). * + * @since 0.1-dev + * * @param array $providers A key-value array where the key is the class name, and * the value is the path to the file containing the class. */ $providers = apply_filters( 'two_factor_providers', $providers ); - // FIDO U2F is PHP 5.3+ only. - if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { - unset( $providers['Two_Factor_FIDO_U2F'] ); - trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - sprintf( - /* translators: %s: version number */ - __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - PHP_VERSION - ) - ); - } - // Map provider keys to classes so that we can instantiate them. $providers = self::get_providers_classes( $providers ); @@ -303,11 +338,14 @@ class Two_Factor_Core { /** * Get providers available for user which may not be enabled or configured. * + * @since 0.13.0 + * * @see Two_Factor_Core::get_enabled_providers_for_user() * @see Two_Factor_Core::get_available_providers_for_user() * * @param WP_User|int|null $user User ID. - * @return array List of provider instances indexed by provider key. + * + * @return Two_Factor_Provider[] List of provider instances indexed by provider key. */ public static function get_supported_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -325,6 +363,8 @@ class Two_Factor_Core { /** * Enable the dummy method only during debugging. * + * @since 0.5.2 + * * @param array $methods List of enabled methods. * * @return array @@ -337,9 +377,74 @@ class Two_Factor_Core { return $methods; } + /** + * Add Plugin and User Settings link to the plugin action links on the Plugins screen. + * + * @since 0.14.3 + * + * @param string[] $links An array of plugin action links. + * @return string[] Modified array with the User Settings link added. + */ + public static function add_settings_action_link( $links ) { + $plugin_settings_url = admin_url( 'options-general.php?page=two-factor-settings' ); + $plugin_settings_link = sprintf( + '%s', + esc_url( $plugin_settings_url ), + esc_html__( 'Plugin Settings', 'two-factor' ) + ); + + $user_settings_url = admin_url( 'profile.php#application-passwords-section' ); + $user_settings_link = sprintf( + '%s', + esc_url( $user_settings_url ), + esc_html__( 'User Settings', 'two-factor' ) + ); + + // Show plugin settings first, then user settings. + array_unshift( $links, $user_settings_link ); + + if ( current_user_can( 'manage_options' ) ) { + array_unshift( $links, $plugin_settings_link ); + } + + return $links; + } + + /** + * Register an error associated with the current request. + * + * @param WP_Error $error Error instance. + + * @return void + */ + private static function add_error( WP_Error $error ) { + self::$profile_errors[ $error->get_error_code() ] = $error; + } + + /** + * Attach Two-Factor profile errors to WordPress core profile update errors. + * + * @since NEXT + * + * @param WP_Error $errors WP_Error object passed by core. + * + * @return void + */ + public static function action_user_profile_update_errors( WP_Error $errors ) { + foreach ( self::$profile_errors as $profile_error ) { + foreach ( $profile_error->get_error_codes() as $code ) { + foreach ( $profile_error->get_error_messages( $code ) as $message ) { + $errors->add( $code, $message ); + } + } + } + } + /** * Check if the debug mode is enabled. * + * @since 0.5.2 + * * @return boolean */ protected static function is_wp_debug() { @@ -352,6 +457,8 @@ class Two_Factor_Core { * Fetch this from the plugin core after we introduce proper dependency injection * and get away from the singletons at the provider level (should be handled by core). * + * @since 0.5.2 + * * @param integer $user_id User ID. * * @return string @@ -372,6 +479,8 @@ class Two_Factor_Core { /** * Get the URL for resetting the secret token. * + * @since 0.5.2 + * * @param integer $user_id User ID. * @param string $action Custom two factor action key. * @@ -393,6 +502,8 @@ class Two_Factor_Core { /** * Get the two-factor revalidate URL. * + * @since 0.9.0 + * * @param bool $interim If the URL should load the interim login iframe modal. * @return string */ @@ -410,6 +521,8 @@ class Two_Factor_Core { /** * Check if a user action is valid. * + * @since 0.5.2 + * * @param integer $user_id User ID. * @param string $action User action ID. * @@ -431,6 +544,8 @@ class Two_Factor_Core { /** * Get the ID of the user being edited. * + * @since 0.5.2 + * * @return integer */ public static function current_user_being_edited() { @@ -450,6 +565,8 @@ class Two_Factor_Core { * Trigger our custom update action if a valid * action request is detected and passes the nonce check. * + * @since 0.5.2 + * * @return void */ public static function trigger_user_settings_action() { @@ -472,6 +589,8 @@ class Two_Factor_Core { * Keep track of all the authentication cookies that need to be * invalidated before the second factor authentication. * + * @since 0.5.1 + * * @param string $cookie Cookie string. * * @return void @@ -511,11 +630,14 @@ class Two_Factor_Core { * Get two-factor providers that are enabled for the specified (or current) user * but might not be configured, yet. * + * @since 0.2.0 + * * @see Two_Factor_Core::get_supported_providers_for_user() * @see Two_Factor_Core::get_available_providers_for_user() * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. - * @return array + * + * @return string[] List of keys of enabled providers for the user. */ public static function get_enabled_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -533,6 +655,8 @@ class Two_Factor_Core { /** * Filter the enabled two-factor authentication providers for this user. * + * @since 0.5.2 + * * @param array $enabled_providers The enabled providers. * @param int $user_id The user ID. */ @@ -543,11 +667,13 @@ class Two_Factor_Core { * Get all two-factor providers that are both enabled and configured * for the specified (or current) user. * + * @since 0.2.0 + * * @see Two_Factor_Core::get_supported_providers_for_user() * @see Two_Factor_Core::get_enabled_providers_for_user() * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. - * @return array List of provider instances. + * @return Two_Factor_Provider[]|WP_Error List of provider instances, or a WP_Error if all configured providers are unavailable. */ public static function get_available_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -558,6 +684,31 @@ class Two_Factor_Core { $providers = self::get_supported_providers_for_user( $user ); // Returns full objects. $enabled_providers = self::get_enabled_providers_for_user( $user ); // Returns just the keys. $configured_providers = array(); + $user_providers_raw = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true ); + + /** + * If the user had enabled providers, but none of them exist currently, + * if emailed codes is available force it to be on, so that deprecated + * or removed providers don't result in the two-factor requirement being + * removed and 'failing open'. + * + * Possible enhancement: add a filter to change the fallback method? + */ + if ( empty( $enabled_providers ) && $user_providers_raw ) { + if ( isset( $providers['Two_Factor_Email'] ) ) { + // Force Emailed codes to 'on'. + $enabled_providers[] = 'Two_Factor_Email'; + } else { + return new WP_Error( + 'no_available_2fa_methods', + __( 'Error: You have Two Factor method(s) enabled, but the provider(s) no longer exist. Please contact a site administrator for assistance.', 'two-factor' ), + array( + 'user_providers_raw' => $user_providers_raw, + 'available_providers' => array_keys( $providers ), + ) + ); + } + } foreach ( $providers as $provider_key => $provider ) { if ( in_array( $provider_key, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) { @@ -571,6 +722,8 @@ class Two_Factor_Core { /** * Fetch the provider for the request based on the user preferences. * + * @since 0.9.0 + * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. * @param null|string|object $preferred_provider Optional. The name of the provider, the provider, or empty. * @return null|object The provider @@ -596,7 +749,7 @@ class Two_Factor_Core { if ( is_string( $preferred_provider ) ) { $providers = self::get_available_providers_for_user( $user ); - if ( isset( $providers[ $preferred_provider ] ) ) { + if ( ! is_wp_error( $providers ) && isset( $providers[ $preferred_provider ] ) ) { return $providers[ $preferred_provider ]; } } @@ -608,6 +761,8 @@ class Two_Factor_Core { * Get the name of the primary provider selected by the user * and enabled for the user. * + * @since 0.12.0 + * * @param WP_User|int $user User ID or instance. * * @return string|null @@ -626,7 +781,7 @@ class Two_Factor_Core { /** * Gets the Two-Factor Auth provider for the specified|current user. * - * @since 0.1-dev + * @since 0.2.0 * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. * @return object|null @@ -643,13 +798,16 @@ class Two_Factor_Core { // If there's only one available provider, force that to be the primary. if ( empty( $available_providers ) ) { return null; + } elseif ( is_wp_error( $available_providers ) ) { + // If it returned an error, the configured methods don't exist, and it couldn't swap in a replacement. + wp_die( $available_providers ); } elseif ( 1 === count( $available_providers ) ) { $provider = key( $available_providers ); } else { $provider = self::get_primary_provider_key_selected_for_user( $user ); // If the provider specified isn't enabled, just grab the first one that is. - if ( ! isset( $available_providers[ $provider ] ) ) { + if ( empty( $provider ) || ! isset( $available_providers[ $provider ] ) ) { $provider = key( $available_providers ); } } @@ -657,6 +815,8 @@ class Two_Factor_Core { /** * Filter the two-factor authentication provider used for this user. * + * @since 0.2.0 + * * @param string $provider The provider currently being used. * @param int $user_id The user ID. */ @@ -672,7 +832,7 @@ class Two_Factor_Core { /** * Quick boolean check for whether a given user is using two-step. * - * @since 0.1-dev + * @since 0.2.0 * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. * @return bool @@ -685,7 +845,9 @@ class Two_Factor_Core { /** * Handle the browser-based login. * - * @since 0.1-dev + * @since 0.2.0 + * + * @see https://developer.wordpress.org/reference/hooks/wp_login/ * * @param string $user_login Username. * @param WP_User $user WP_User object of the logged-in user. @@ -712,6 +874,8 @@ class Two_Factor_Core { * having access to the authentication cookies which are just being set * on the first password-based authentication request. * + * @since 0.5.1 + * * @param \WP_User $user User object. * * @return void @@ -725,63 +889,70 @@ class Two_Factor_Core { } /** - * Prevent login through XML-RPC and REST API for users with at least one - * two-factor method enabled. + * Disable WP core login cookies for users that require second factor. Disable + * authenticated API requests unless explicitly enabled for the user (disabled by default). * - * @param WP_User|WP_Error $user Valid WP_User only if the previous filters + * @since 0.4.0 + * + * @param WP_User|WP_Error $user Valid WP_User only if the previous filters * have verified and confirmed the * authentication credentials. * * @return WP_User|WP_Error */ public static function filter_authenticate( $user ) { - if ( $user instanceof WP_User && self::is_api_request() && self::is_user_using_two_factor( $user->ID ) && ! self::is_user_api_login_enabled( $user->ID ) ) { - return new WP_Error( - 'invalid_application_credentials', - __( 'Error: API login for user disabled.', 'two-factor' ) - ); - } - - return $user; - } - - /** - * Prevent login cookies being set on login for Two Factor users. - * - * This makes it so that Core never sends the auth cookies. `login_form_validate_2fa()` will send them manually once the 2nd factor has been verified. - * - * @param WP_User|WP_Error $user Valid WP_User only if the previous filters - * have verified and confirmed the - * authentication credentials. - * - * @return WP_User|WP_Error - */ - public static function filter_authenticate_block_cookies( $user ) { - /* - * NOTE: The `login_init` action is checked for here to ensure we're within the regular login flow, - * rather than through an unsupported 3rd-party login process which this plugin doesn't support. - */ - if ( $user instanceof WP_User && self::is_user_using_two_factor( $user->ID ) && did_action( 'login_init' ) ) { + if ( $user instanceof WP_User && self::is_user_using_two_factor( $user->ID ) ) { + /** + * Prevent WP core from sending login cookies during `wp_set_auth_cookie()` and + * let two-factor do it after validating the second factor. + */ add_filter( 'send_auth_cookies', '__return_false', PHP_INT_MAX ); + + // Disable authentication requests for API requests for users with two-factor enabled. + if ( self::is_api_request() && ! self::is_user_api_login_enabled( $user->ID ) ) { + return new WP_Error( + 'invalid_application_credentials', + __( 'Error: API login for user disabled.', 'two-factor' ) + ); + } } return $user; } /** - * If the current user can login via API requests such as XML-RPC and REST. + * If the user can login via API requests such as XML-RPC and REST. + * + * Only logins with application passwords are permitted by default. + * + * @since 0.4.0 * * @param integer $user_id User ID. * * @return boolean */ public static function is_user_api_login_enabled( $user_id ) { - return (bool) apply_filters( 'two_factor_user_api_login_enable', false, $user_id ); + /** + * Allow or prevent logins without two-factor during + * API requests such as XML-RPC and REST. + * + * @since 0.4.0 + * + * @param boolean $enabled Whether the user can login via API requests. + * @param integer $user_id User ID. + */ + return (bool) apply_filters( + 'two_factor_user_api_login_enable', + (bool) did_action( 'application_password_did_authenticate' ), + $user_id + ); } /** * Is the current request an XML-RPC or REST request. * + * @since 0.4.0 + * * @return boolean */ public static function is_api_request() { @@ -799,7 +970,7 @@ class Two_Factor_Core { /** * Display the login form. * - * @since 0.1-dev + * @since 0.2.0 * * @param WP_User $user WP_User object of the logged-in user. */ @@ -821,6 +992,8 @@ class Two_Factor_Core { /** * Displays a message informing the user that their account has had failed login attempts. * + * @since 0.8.0 + * * @param WP_User $user WP_User object of the logged-in user. */ public static function maybe_show_last_login_failure_notice( $user ) { @@ -830,14 +1003,17 @@ class Two_Factor_Core { if ( $last_failed_two_factor_login ) { echo '
'; printf( - _n( - 'WARNING: Your account has attempted to login without providing a valid two factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', - 'WARNING: Your account has attempted to login %1$s times without providing a valid two factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', - $failed_login_count, - 'two-factor' + esc_html( + /* translators: 1: number of failed login attempts, 2: time since last failed attempt */ + _n( + 'WARNING: Your account has attempted to login %1$s time without providing a valid two factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', + 'WARNING: Your account has attempted to login %1$s times without providing a valid two factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', + $failed_login_count, + 'two-factor' + ) ), - number_format_i18n( $failed_login_count ), - human_time_diff( $last_failed_two_factor_login, time() ) + esc_html( number_format_i18n( $failed_login_count ) ), + esc_html( human_time_diff( $last_failed_two_factor_login, time() ) ) ); echo '
'; } @@ -849,7 +1025,9 @@ class Two_Factor_Core { * They were also sent an email notification in `send_password_reset_email()`, but email sent from a typical * web server is not reliable enough to trust completely. * - * @param WP_Error $errors + * @since 0.8.0 + * + * @param WP_Error $errors Error object. */ public static function maybe_show_reset_password_notice( $errors ) { if ( 'incorrect_password' !== $errors->get_error_code() ) { @@ -880,6 +1058,7 @@ class Two_Factor_Core { $errors->add( 'two_factor_password_reset', sprintf( + /* translators: %s: URL to create a new password. */ __( 'Your password was reset because of too many failed Two Factor attempts. You will need to create a new password to regain access. Please check your email for more information.', 'two-factor' ), esc_url( add_query_arg( 'action', 'lostpassword', wp_login_url() ) ) ) @@ -891,7 +1070,9 @@ class Two_Factor_Core { /** * Clear the password reset notice after the user resets their password. * - * @param WP_User $user + * @since 0.8.0 + * + * @param WP_User $user User object. */ public static function clear_password_reset_notice( $user ) { delete_user_meta( $user->ID, self::USER_PASSWORD_WAS_RESET_KEY ); @@ -900,18 +1081,19 @@ class Two_Factor_Core { /** * Generates the html form for the second step of the authentication process. * - * @since 0.1-dev + * @since 0.9.0 * * @param WP_User $user WP_User object of the logged-in user. * @param string $login_nonce A string nonce stored in usermeta. * @param string $redirect_to The URL to which the user would like to be redirected. * @param string $error_msg Optional. Login error message. * @param string|object $provider An override to the provider. + * @param string $action Action to perform. */ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg = '', $provider = null, $action = 'validate_2fa' ) { $provider = self::get_provider_for_user( $user, $provider ); if ( ! $provider ) { - wp_die( __( 'Cheatin’ uh?', 'two-factor' ) ); + wp_die( esc_html__( 'Two-factor provider not available for this user.', 'two-factor' ) ); } $provider_key = $provider->get_key(); @@ -921,6 +1103,11 @@ class Two_Factor_Core { $rememberme = intval( self::rememberme() ); + if ( is_wp_error( $available_providers ) ) { + // If it returned an error, the configured methods don't exist, and it couldn't swap in a replacement. + wp_die( $available_providers ); + } + if ( ! function_exists( 'login_header' ) ) { // We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file. require_once TWO_FACTOR_DIR . 'includes/function.login-header.php'; @@ -929,6 +1116,8 @@ class Two_Factor_Core { // Disable the language switcher. add_filter( 'login_display_language_dropdown', '__return_false' ); + wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ), array(), TWO_FACTOR_VERSION ); + login_header(); if ( ! empty( $error_msg ) ) { @@ -952,7 +1141,10 @@ class Two_Factor_Core { authentication_page( $user ); ?> - $action, 'wp-auth-id' => $user->ID, @@ -967,22 +1159,40 @@ class Two_Factor_Core { if ( $interim_login ) { $backup_link_args['interim-login'] = 1; } - ?> + + foreach ( $backup_providers as $backup_provider_key => $backup_provider ) { + $backup_link_args['provider'] = $backup_provider_key; + $links[] = array( + 'url' => self::login_url( $backup_link_args ), + 'label' => $backup_provider->get_alternative_provider_label(), + ); + } + } + + /** + * Filters the links displayed on the two-factor login form. + * + * Plugins can use this filter to modify or add links to the two-factor authentication + * login form, allowing users to select backup methods for authentication or provide documentation links. + * + * @since 0.16.0 + * + * @param array $links An array of links displayed on the two-factor login form, each with `url` and `label` keys. + */ + $links = apply_filters( 'two_factor_login_backup_links', $links ); + ?> + +

@@ -1014,41 +1224,7 @@ class Two_Factor_Core { opacity: 0.5; } - + ID, $nonce ) ) { wp_safe_redirect( home_url() ); - return; + exit; } $provider = self::get_provider_for_user( $user, $provider ); if ( ! $provider ) { - wp_die( __( 'Cheatin’ uh?', 'two-factor' ) ); + wp_die( esc_html__( 'Two-factor provider not available for this user.', 'two-factor' ) ); } // Run the provider processing. @@ -1405,7 +1605,7 @@ class Two_Factor_Core { if ( true !== $result ) { $error = ''; if ( is_wp_error( $result ) ) { - do_action( 'wp_login_failed', $user->user_login, $result ); + do_action( 'wp_login_failed', $user->user_login, $result ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core WordPress action. $error = $result->get_error_message(); } @@ -1428,7 +1628,7 @@ class Two_Factor_Core { $rememberme = true; } - $session_information_callback = static function( $session, $user_id ) use( $provider, $user ) { + $session_information_callback = static function ( $session, $user_id ) use ( $provider, $user ) { if ( $user->ID === $user_id ) { $session['two-factor-login'] = time(); $session['two-factor-provider'] = $provider->get_key(); @@ -1442,12 +1642,20 @@ class Two_Factor_Core { /* * NOTE: This filter removal is not normally required, this is included for protection against * a plugin/two factor provider which runs the `authenticate` filter during it's validation. - * Such a plugin would cause self::filter_authenticate_block_cookies() to run and add this filter. + * Such a plugin would cause self::filter_authenticate() to run and add this filter. */ remove_filter( 'send_auth_cookies', '__return_false', PHP_INT_MAX ); wp_set_auth_cookie( $user->ID, $rememberme ); + /** + * Fires after a user has been authenticated via two-factor. + * + * @since 0.5.2 + * + * @param WP_User $user The authenticated user. + * @param Two_Factor_Provider $provider The two-factor provider used for authentication. + */ do_action( 'two_factor_user_authenticated', $user, $provider ); remove_filter( 'attach_session_information', $session_information_callback ); @@ -1460,6 +1668,10 @@ class Two_Factor_Core { $customize_login = isset( $_REQUEST['customize-login'] ); if ( $customize_login ) { wp_enqueue_script( 'customize-base' ); + wp_add_inline_script( + 'customize-base', + 'setTimeout( function(){ new wp.customize.Messenger({ url: ' . wp_json_encode( esc_url( wp_customize_url() ) ) . ', channel: \'login\' }).send(\'login\') }, 1000 );' + ); } $message = '

' . __( 'You have logged in successfully.', 'two-factor' ) . '

'; $interim_login = 'success'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited @@ -1468,18 +1680,16 @@ class Two_Factor_Core { - - - ID ) ) { wp_safe_redirect( home_url() ); - return; + exit; } $provider = self::get_provider_for_user( $user, $provider ); if ( ! $provider ) { - wp_die( __( 'Cheatin’ uh?', 'two-factor' ) ); + wp_die( esc_html__( 'Two-factor provider not available for this user.', 'two-factor' ) ); } // Run the provider processing. @@ -1536,7 +1746,7 @@ class Two_Factor_Core { if ( true !== $result ) { $error = ''; if ( is_wp_error( $result ) ) { - do_action( 'wp_login_failed', $user->user_login, $result ); + do_action( 'wp_login_failed', $user->user_login, $result ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core WordPress action. $error = $result->get_error_message(); } @@ -1555,6 +1765,14 @@ class Two_Factor_Core { ) ); + /** + * Fires after a user has been revalidated via two-factor. + * + * @since 0.8.0 + * + * @param WP_User $user The revalidated user. + * @param Two_Factor_Provider $provider The two-factor provider used for revalidation. + */ do_action( 'two_factor_user_revalidated', $user, $provider ); // Must be global because that's how login_header() uses it. @@ -1569,21 +1787,23 @@ class Two_Factor_Core { user_login ), home_url(), 'https://wordpress.org/documentation/article/password-best-practices/', @@ -1742,30 +1974,36 @@ class Two_Factor_Core { ); $user_message = str_replace( "\t", '', $user_message ); - return wp_mail( $user->user_email, 'Your password was compromised and has been reset', $user_message ); + return wp_mail( $user->user_email, __( 'Your password was compromised and has been reset', 'two-factor' ), $user_message ); } /** * Notify the admin that a user's password was compromised and reset. * + * @since 0.8.0 + * * @param WP_User $user The user whose password was reset. * * @return bool `true` if the email was sent, `false` if it failed. */ public static function notify_admin_user_password_reset( $user ) { $admin_email = get_option( 'admin_email' ); - $subject = sprintf( 'Compromised password for %s has been reset', esc_html( $user->user_login ) ); + $subject = sprintf( + /* translators: %s: username */ + __( 'Compromised password for %s has been reset', 'two-factor' ), + esc_html( $user->user_login ) + ); $message = sprintf( - 'Hello, this is a notice from the Two Factor plugin to inform you that an unusually high number of failed login attempts have been detected on the %1$s account (ID %2$d). - - Those attempts successfully entered the user\'s password, and were only blocked because they entered invalid second authentication factors. - - To protect their account, the password has automatically been reset, and they have been notified that they will need to create a new one. - - If you do not wish to receive these notifications, you can disable them with the `two_factor_notify_admin_user_password_reset` filter. See %3$s for more information. - - Thank you', + /* translators: 1: username, 2: user ID, 3: URL to developer docs */ + __( + 'Hello, this is a notice from the Two Factor plugin to inform you that an unusually high number of failed login attempts have been detected on the %1$s account (ID %2$d). + Those attempts successfully entered the user\'s password, and were only blocked because they entered invalid second authentication factors. + To protect their account, the password has automatically been reset, and they have been notified that they will need to create a new one. + If you do not wish to receive these notifications, you can disable them with the `two_factor_notify_admin_user_password_reset` filter. See %3$s for more information. + Thank you', + 'two-factor' + ), esc_html( $user->user_login ), $user->ID, 'https://developer.wordpress.org/plugins/hooks/' @@ -1777,6 +2015,8 @@ class Two_Factor_Core { /** * Show the password reset error when on the login screen. + * + * @since 0.8.0 */ public static function show_password_reset_error() { $error = new WP_Error( @@ -1796,6 +2036,8 @@ class Two_Factor_Core { /** * Filter the columns on the Users admin screen. * + * @since 0.2.0 + * * @param array $columns Available columns. * @return array Updated array of columns. */ @@ -1807,6 +2049,8 @@ class Two_Factor_Core { /** * Output the 2FA column data on the Users screen. * + * @since 0.2.0 + * * @param string $output The column output. * @param string $column_name The column ID. * @param int $user_id The user ID. @@ -1824,7 +2068,6 @@ class Two_Factor_Core { $provider = self::get_primary_provider_for_user( $user_id ); return esc_html( $provider->get_label() ); } - } /** @@ -1832,13 +2075,11 @@ class Two_Factor_Core { * * This executes during the `show_user_profile` & `edit_user_profile` actions. * - * @since 0.1-dev + * @since 0.2.0 * * @param WP_User $user WP_User object of the logged-in user. */ public static function user_two_factor_options( $user ) { - $notices = []; - $providers = self::get_supported_providers_for_user( $user ); wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ), array(), TWO_FACTOR_VERSION ); @@ -1855,31 +2096,61 @@ class Two_Factor_Core { self::get_user_two_factor_revalidate_url() ); - $notices['warning two-factor-warning-revalidate-session'] = sprintf( - esc_html__( 'To update your Two-Factor options, you must first revalidate your session.', 'two-factor' ) . - ' ' . esc_html__( 'Revalidate now', 'two-factor' ) . '', - esc_url( $url ) + self::add_error( + new WP_Error( + 'two_factor_revalidate_session', + sprintf( + __( 'To update your Two-Factor options, you must first revalidate your session.', 'two-factor' ) . + ' ' . esc_html__( 'Revalidate now', 'two-factor' ) . '', + esc_url( $url ) + ), + array( + 'type' => 'warning', + ) + ) ); } if ( empty( $providers ) ) { - $notices['notice two-factor-notice-no-providers-supported'] = esc_html__( 'No providers are available for your account.', 'two-factor' ); + self::add_error( + new WP_Error( + 'two_factor_no_providers_supported', + __( 'No providers are available for your account.', 'two-factor' ), + array( + 'type' => 'notice', + ) + ) + ); } // Suggest enabling a backup method if only one method is enabled and there are more available. if ( count( $providers ) > 1 && 1 === count( $enabled_providers ) ) { - $notices['warning two-factor-warning-suggest-backup'] = esc_html__( 'To prevent being locked out of your account, consider enabling a backup method like Recovery Codes in case you lose access to your primary authentication method.', 'two-factor' ); + self::add_error( + new WP_Error( + 'two_factor_suggest_backup', + __( 'To prevent being locked out of your account, consider enabling a backup method like Recovery Codes in case you lose access to your primary authentication method.', 'two-factor' ), + array( + 'type' => 'warning', + ) + ) + ); } + + $generic_errors = array_filter( + self::$profile_errors, + static function ( WP_Error $error ) { + $error_data = $error->get_error_data(); + return empty( $error_data['provider'] ); // Where the associated provider is not set. + } + ); + ?>

- $notice ) : ?> -
-

-
- +
> + has_errors() ) { + $error_type = $error->get_error_data()['type'] ?? null; + + wp_admin_notice( + implode( '

', $error->get_error_messages() ), + array( + 'type' => is_string( $error_type ) ? $error_type : 'error', + 'additional_classes' => array( 'inline' ), + ) + ); + } + } + } + + /** + * Render the user settings. + * + * @since 0.13.0 + * + * @param WP_User $user User instance. + * @param array $providers List of available providers. + */ private static function render_user_providers_form( $user, $providers ) { - $primary_provider_key = self::get_primary_provider_key_selected_for_user( $user ); - $enabled_providers = self::get_enabled_providers_for_user( $user ); + $primary_provider_key = self::get_primary_provider_key_selected_for_user( $user ); + $available_providers = self::get_available_providers_for_user( $user ); + $recommended_provider_keys = self::get_recommended_providers( $user ); + + // Move the recommended providers first. + $recommended_providers = array_intersect_key( $providers, array_flip( $recommended_provider_keys ) ); + $providers = array_merge( $recommended_providers, $providers ); ?>

- +

+ +

+ +

+ + @@ -1919,9 +2256,14 @@ class Two_Factor_Core { get_label() ); ?> + - + - $object ) : ?> - @@ -1963,11 +2305,31 @@ class Two_Factor_Core { get_error_data(); // Return the data for the first error. + + return isset( $error_data['provider'] ) && $error_data['provider'] === $provider_key; + } + ); + } + /** * Enable a provider for a user. * * The caller is responsible for checking the user has permission to do this. * + * @since 0.8.0 + * * @param int $user_id The ID of the user. * @param string $new_provider The name of the provider class. * @@ -1999,8 +2361,10 @@ class Two_Factor_Core { * * The caller is responsible for checking the user has permission to do this. * - * @param int $user_id The ID of the user. - * @param string $provider The name of the provider class. + * @since 0.9.0 + * + * @param int $user_id The ID of the user. + * @param string $provider_to_delete The name of the provider class. * * @return bool True if the provider was disabled, false otherwise. */ @@ -2033,7 +2397,7 @@ class Two_Factor_Core { * * This executes during the `personal_options_update` & `edit_user_profile_update` actions. * - * @since 0.1-dev + * @since 0.2.0 * * @param int $user_id User ID. */ @@ -2050,48 +2414,75 @@ class Two_Factor_Core { return; } + $user = self::fetch_user( $user_id ); $providers = self::get_supported_providers_for_user( $user_id ); $enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ]; $existing_providers = self::get_enabled_providers_for_user( $user_id ); // Enable only the available providers. - $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) ); - update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, $enabled_providers ); + $enabled_providers = array_intersect_key( $providers, array_flip( $enabled_providers ) ); + + // Ensure the enabled providers are configured and can be enabled. + foreach ( $enabled_providers as $provider_key => $provider ) { + if ( ! $provider->is_available_for_user( $user ) ) { + unset( $enabled_providers[ $provider_key ] ); + + self::add_error( + new WP_Error( + 'two_factor_provider_not_configured_' . $provider_key, + sprintf( + /* translators: %s: provider label. */ + __( 'The %s method must be configured before it can be enabled.', 'two-factor' ), + esc_html( $provider->get_label() ) + ), + array( + 'provider' => $provider_key, + ) + ) + ); + } + } + + update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, array_keys( $enabled_providers ) ); // Primary provider must be enabled. $new_provider = isset( $_POST[ self::PROVIDER_USER_META_KEY ] ) ? $_POST[ self::PROVIDER_USER_META_KEY ] : ''; - if ( ! empty( $new_provider ) && in_array( $new_provider, $enabled_providers, true ) ) { + if ( ! empty( $new_provider ) && isset( $enabled_providers[ $new_provider ] ) ) { update_user_meta( $user_id, self::PROVIDER_USER_META_KEY, $new_provider ); } else { delete_user_meta( $user_id, self::PROVIDER_USER_META_KEY ); } // Have we changed the two-factor settings for the current user? Alter their session metadata. - if ( $user_id === get_current_user_id() ) { + if ( get_current_user_id() === $user_id ) { if ( $enabled_providers && ! $existing_providers && ! self::is_current_user_session_two_factor() ) { // We've enabled two-factor from a non-two-factor session, set the key but not the provider, as no provider has been used yet. - self::update_current_user_session( array( - 'two-factor-provider' => '', - 'two-factor-login' => time(), - ) ); + self::update_current_user_session( + array( + 'two-factor-provider' => '', + 'two-factor-login' => time(), + ) + ); } elseif ( $existing_providers && ! $enabled_providers ) { // We've disabled two-factor, remove session metadata. - self::update_current_user_session( array( - 'two-factor-provider' => null, - 'two-factor-login' => null, - ) ); + self::update_current_user_session( + array( + 'two-factor-provider' => null, + 'two-factor-login' => null, + ) + ); } } - // Destroy other sessions if setup 2FA for the first time, or deactivated a provider + // Destroy other sessions if setup 2FA for the first time, or deactivated a provider. if ( - // No providers, enabling one (or more) + // No providers, enabling one (or more). ( ! $existing_providers && $enabled_providers ) || // Has providers, and is disabling one (or more), but remaining with 2FA. - ( $existing_providers && $enabled_providers && array_diff( $existing_providers, $enabled_providers ) ) + ( $existing_providers && $enabled_providers && array_diff( $existing_providers, array_keys( $enabled_providers ) ) ) ) { - if ( $user_id === get_current_user_id() ) { + if ( get_current_user_id() === $user_id ) { // Keep the current session, destroy others sessions for this user. wp_destroy_other_sessions(); } else { @@ -2107,6 +2498,8 @@ class Two_Factor_Core { * * Any values set in $data that are null will be removed from the user session metadata. * + * @since 0.9.0 + * * @param array $data The data to append/remove from the current session. * @return bool */ @@ -2134,6 +2527,8 @@ class Two_Factor_Core { /** * Fetch the current user session metadata. * + * @since 0.9.0 + * * @return false|array The session array, false on error. */ public static function get_current_user_session() { @@ -2151,6 +2546,8 @@ class Two_Factor_Core { /** * Should the login session persist between sessions. * + * @since 0.5.0 + * * @return boolean */ public static function rememberme() { @@ -2160,6 +2557,13 @@ class Two_Factor_Core { $rememberme = true; } + /** + * Filters whether the login session should persist between browser sessions. + * + * @since 0.5.0 + * + * @param bool $rememberme Whether to remember the user. Default false. + */ return (bool) apply_filters( 'two_factor_rememberme', $rememberme ); } @@ -2170,12 +2574,14 @@ class Two_Factor_Core { * * @see https://core.trac.wordpress.org/ticket/58427 * + * @since 0.9.0 + * * @param array $session The Session information. * @param int $user_id The User ID for the session. * @return array */ public static function filter_session_information( $session, $user_id ) { - if ( $user_id !== get_current_user_id() ) { + if ( get_current_user_id() !== $user_id ) { return $session; } @@ -2191,3 +2597,4 @@ class Two_Factor_Core { return $session; } } + diff --git a/wp-content/plugins/two-factor/includes/Google/u2f-api.js b/wp-content/plugins/two-factor/includes/Google/u2f-api.js deleted file mode 100644 index 1036b920..00000000 --- a/wp-content/plugins/two-factor/includes/Google/u2f-api.js +++ /dev/null @@ -1,748 +0,0 @@ -//Copyright 2014-2015 Google Inc. All rights reserved. - -//Use of this source code is governed by a BSD-style -//license that can be found in the LICENSE file or at -//https://developers.google.com/open-source/licenses/bsd - -/** - * @fileoverview The U2F api. - */ -'use strict'; - - -/** - * Namespace for the U2F api. - * @type {Object} - */ -var u2f = u2f || {}; - -/** - * FIDO U2F Javascript API Version - * @number - */ -var js_api_version; - -/** - * The U2F extension id - * @const {string} - */ -// The Chrome packaged app extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the package Chrome app and does not require installing the U2F Chrome extension. - u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; -// The U2F Chrome extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the U2F Chrome extension to authenticate. -// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; - - -/** - * Message types for messages to/from the extension - * @const - * @enum {string} - */ -u2f.MessageTypes = { - 'U2F_REGISTER_REQUEST': 'u2f_register_request', - 'U2F_REGISTER_RESPONSE': 'u2f_register_response', - 'U2F_SIGN_REQUEST': 'u2f_sign_request', - 'U2F_SIGN_RESPONSE': 'u2f_sign_response', - 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', - 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' -}; - - -/** - * Response status codes - * @const - * @enum {number} - */ -u2f.ErrorCodes = { - 'OK': 0, - 'OTHER_ERROR': 1, - 'BAD_REQUEST': 2, - 'CONFIGURATION_UNSUPPORTED': 3, - 'DEVICE_INELIGIBLE': 4, - 'TIMEOUT': 5 -}; - - -/** - * A message for registration requests - * @typedef {{ - * type: u2f.MessageTypes, - * appId: ?string, - * timeoutSeconds: ?number, - * requestId: ?number - * }} - */ -u2f.U2fRequest; - - -/** - * A message for registration responses - * @typedef {{ - * type: u2f.MessageTypes, - * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), - * requestId: ?number - * }} - */ -u2f.U2fResponse; - - -/** - * An error object for responses - * @typedef {{ - * errorCode: u2f.ErrorCodes, - * errorMessage: ?string - * }} - */ -u2f.Error; - -/** - * Data object for a single sign request. - * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} - */ -u2f.Transport; - - -/** - * Data object for a single sign request. - * @typedef {Array} - */ -u2f.Transports; - -/** - * Data object for a single sign request. - * @typedef {{ - * version: string, - * challenge: string, - * keyHandle: string, - * appId: string - * }} - */ -u2f.SignRequest; - - -/** - * Data object for a sign response. - * @typedef {{ - * keyHandle: string, - * signatureData: string, - * clientData: string - * }} - */ -u2f.SignResponse; - - -/** - * Data object for a registration request. - * @typedef {{ - * version: string, - * challenge: string - * }} - */ -u2f.RegisterRequest; - - -/** - * Data object for a registration response. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: Transports, - * appId: string - * }} - */ -u2f.RegisterResponse; - - -/** - * Data object for a registered key. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: ?Transports, - * appId: ?string - * }} - */ -u2f.RegisteredKey; - - -/** - * Data object for a get API register response. - * @typedef {{ - * js_api_version: number - * }} - */ -u2f.GetJsApiVersionResponse; - - -//Low level MessagePort API support - -/** - * Sets up a MessagePort to the U2F extension using the - * available mechanisms. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - */ -u2f.getMessagePort = function(callback) { - if (typeof chrome != 'undefined' && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else if (u2f.isIosChrome_()) { - u2f.getIosPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. - u2f.getIframePort_(callback); - } -}; - -/** - * Detect chrome running on android based on the browser's useragent. - * @private - */ -u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return userAgent.indexOf('Chrome') != -1 && - userAgent.indexOf('Android') != -1; -}; - -/** - * Detect chrome running on iOS based on the browser's platform. - * @private - */ -u2f.isIosChrome_ = function() { - return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; -}; - -/** - * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback - * @private - */ -u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, - {'includeTlsChannelId': true}); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); -}; - -/** - * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback - * @private - */ -u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); -}; - -/** - * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback - * @private - */ -u2f.getIosPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedIosPort_()); - }, 0); -}; - -/** - * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port - * @constructor - * @private - */ -u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; -}; - -/** - * Format and return a sign request compliant with the JS API version supported by the extension. - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatSignRequest_ = - function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API. - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: challenge, - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API. - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - appId: appId, - challenge: challenge, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Format and return a register request compliant with the JS API version supported by the extension.. - * @param {Array} signRequests - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatRegisterRequest_ = - function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API. - for (var i = 0; i < registerRequests.length; i++) { - registerRequests[i].appId = appId; - } - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: registerRequests[0], - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API. - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - appId: appId, - registerRequests: registerRequests, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - - -/** - * Posts a message on the underlying channel. - * @param {Object} message - */ -u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); -}; - - -/** - * Emulates the HTML 5 addEventListener interface. Works only for the - * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedChromeRuntimePort_.prototype.addEventListener = - function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message' || name == 'onmessage') { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object. - handler({'data': message}); - }); - } else { - console.error('WrappedChromeRuntimePort only supports onMessage'); - } -}; - -/** - * Wrap the Authenticator app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; -} - -/** - * Launch the Authenticator intent. - * @param {Object} message - */ -u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.request=' + encodeURIComponent(JSON.stringify(message)) + - ';end'; - document.location = intentUrl; -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { - return "WrappedAuthenticatorPort_"; -}; - - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message') { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - 'message', self.onRequestUpdate_.bind(self, handler), false); - } else { - console.error('WrappedAuthenticatorPort only supports message'); - } -}; - -/** - * Callback invoked when a response is received from the Authenticator. - * @param function({data: Object}) callback - * @param {Object} message message Object - */ -u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = - function(callback, message) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject['intentURL']; - - var errorCode = messageObject['errorCode']; - var responseObject = null; - if (messageObject.hasOwnProperty('data')) { - responseObject = /** @type {Object} */ ( - JSON.parse(messageObject['data'])); - } - - callback({'data': responseObject}); -}; - -/** - * Base URL for intents to Authenticator. - * @const - * @private - */ -u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; - -/** - * Wrap the iOS client app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedIosPort_ = function() {}; - -/** - * Launch the iOS client app request - * @param {Object} message - */ -u2f.WrappedIosPort_.prototype.postMessage = function(message) { - var str = JSON.stringify(message); - var url = "u2f://auth?" + encodeURI(str); - location.replace(url); -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedIosPort_.prototype.getPortType = function() { - return "WrappedIosPort_"; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name !== 'message') { - console.error('WrappedIosPort only supports message'); - } -}; - -/** - * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback - * @private - */ -u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; - var iframe = document.createElement('iframe'); - iframe.src = iframeOrigin + '/u2f-comms.html'; - iframe.setAttribute('style', 'display:none'); - document.body.appendChild(iframe); - - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == 'ready') { - channel.port1.removeEventListener('message', ready); - callback(channel.port1); - } else { - console.error('First event on iframe port was not "ready"'); - } - }; - channel.port1.addEventListener('message', ready); - channel.port1.start(); - - iframe.addEventListener('load', function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); - }); -}; - - -//High-level JS API - -/** - * Default extension response timeout in seconds. - * @const - */ -u2f.EXTENSION_TIMEOUT_SEC = 30; - -/** - * A singleton instance for a MessagePort to the extension. - * @type {MessagePort|u2f.WrappedChromeRuntimePort_} - * @private - */ -u2f.port_ = null; - -/** - * Callbacks waiting for a port - * @type {Array} - * @private - */ -u2f.waitingForPort_ = []; - -/** - * A counter for requestIds. - * @type {number} - * @private - */ -u2f.reqCounter_ = 0; - -/** - * A map from requestIds to client callbacks - * @type {Object.} - * @private - */ -u2f.callbackMap_ = {}; - -/** - * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - * @private - */ -u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener('message', - /** @type {function(Event)} */ (u2f.responseHandler_)); - - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); - } - u2f.waitingForPort_.push(callback); - } -}; - -/** - * Handles response messages from the extension. - * @param {MessageEvent.} message - * @private - */ -u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response['requestId']; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error('Unknown or missing requestId in response.'); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response['responseData']); -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the sign request. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual sign request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual sign request in the supported API version. - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - } -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the register request. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual register request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual register request in the supported API version. - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - } -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatRegisterRequest_( - appId, registeredKeys, registerRequests, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; - - -/** - * Dispatches a message to the extension to find out the supported - * JS API version. - * If the user is on a mobile phone and is thus using Google Authenticator instead - * of the Chrome extension, don't send the request and simply return 0. - * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.getApiVersion = function(callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - // If we are using Android Google Authenticator or iOS client app, - // do not fire an intent to ask which JS API version to use. - if (port.getPortType) { - var apiVersion; - switch (port.getPortType()) { - case 'WrappedIosPort_': - case 'WrappedAuthenticatorPort_': - apiVersion = 1.1; - break; - - default: - apiVersion = 0; - break; - } - callback({ 'js_api_version': apiVersion }); - return; - } - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var req = { - type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, - timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), - requestId: reqId - }; - port.postMessage(req); - }); -}; diff --git a/wp-content/plugins/two-factor/includes/Yubico/U2F.php b/wp-content/plugins/two-factor/includes/Yubico/U2F.php deleted file mode 100644 index bbb6e9a0..00000000 --- a/wp-content/plugins/two-factor/includes/Yubico/U2F.php +++ /dev/null @@ -1,507 +0,0 @@ -appId = $appId; - $this->attestDir = $attestDir; - } - - /** - * Called to get a registration request to send to a user. - * Returns an array of one registration request and a array of sign requests. - * - * @param array $registrations List of current registrations for this - * user, to prevent the user from registering the same authenticator several - * times. - * @return array An array of two elements, the first containing a - * RegisterRequest the second being an array of SignRequest - * @throws Error - */ - public function getRegisterData(array $registrations = array()) - { - $challenge = $this->createChallenge(); - $request = new RegisterRequest($challenge, $this->appId); - $signs = $this->getAuthenticateData($registrations); - return array($request, $signs); - } - - /** - * Called to verify and unpack a registration message. - * - * @param RegisterRequest $request this is a reply to - * @param object $response response from a user - * @param bool $includeCert set to true if the attestation certificate should be - * included in the returned Registration object - * @return Registration - * @throws Error - */ - public function doRegister($request, $response, $includeCert = true) - { - if( !is_object( $request ) ) { - throw new \InvalidArgumentException('$request of doRegister() method only accepts object.'); - } - - if( !is_object( $response ) ) { - throw new \InvalidArgumentException('$response of doRegister() method only accepts object.'); - } - - if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { - throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); - } - - if( !is_bool( $includeCert ) ) { - throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.'); - } - - $rawReg = $this->base64u_decode($response->registrationData); - $regData = array_values(unpack('C*', $rawReg)); - $clientData = $this->base64u_decode($response->clientData); - $cli = json_decode($clientData); - - if($cli->challenge !== $request->challenge) { - throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE ); - } - - $registration = new Registration(); - $offs = 1; - $pubKey = substr($rawReg, $offs, PUBKEY_LEN); - $offs += PUBKEY_LEN; - // Decode the pubKey to make sure it's good. - $tmpKey = $this->pubkey_to_pem($pubKey); - if($tmpKey === null) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - $registration->publicKey = base64_encode($pubKey); - $khLen = $regData[$offs++]; - $kh = substr($rawReg, $offs, $khLen); - $offs += $khLen; - $registration->keyHandle = $this->base64u_encode($kh); - - // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes). - $certLen = 4; - $certLen += ($regData[$offs + 2] << 8); - $certLen += $regData[$offs + 3]; - - $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen)); - $offs += $certLen; - $pemCert = "-----BEGIN CERTIFICATE-----\r\n"; - $pemCert .= chunk_split(base64_encode($rawCert), 64); - $pemCert .= "-----END CERTIFICATE-----"; - if($includeCert) { - $registration->certificate = base64_encode($rawCert); - } - if($this->attestDir) { - if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) { - throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION ); - } - } - - if(!openssl_pkey_get_public($pemCert)) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - $signature = substr($rawReg, $offs); - - $dataToVerify = chr(0); - $dataToVerify .= hash('sha256', $request->appId, true); - $dataToVerify .= hash('sha256', $clientData, true); - $dataToVerify .= $kh; - $dataToVerify .= $pubKey; - - if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) { - return $registration; - } else { - throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE ); - } - } - - /** - * Called to get an authentication request. - * - * @param array $registrations An array of the registrations to create authentication requests for. - * @return array An array of SignRequest - * @throws Error - */ - public function getAuthenticateData(array $registrations) - { - $sigs = array(); - $challenge = $this->createChallenge(); - foreach ($registrations as $reg) { - if( !is_object( $reg ) ) { - throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.'); - } - - $sig = new SignRequest(); - $sig->appId = $this->appId; - $sig->keyHandle = $reg->keyHandle; - $sig->challenge = $challenge; - $sigs[] = $sig; - } - return $sigs; - } - - /** - * Called to verify an authentication response - * - * @param array $requests An array of outstanding authentication requests - * @param array $registrations An array of current registrations - * @param object $response A response from the authenticator - * @return Registration - * @throws Error - * - * The Registration object returned on success contains an updated counter - * that should be saved for future authentications. - * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of - * token cloning or similar and appropriate action should be taken. - */ - public function doAuthenticate(array $requests, array $registrations, $response) - { - if( !is_object( $response ) ) { - throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.'); - } - - if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { - throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); - } - - /** @var object|null $req */ - $req = null; - - /** @var object|null $reg */ - $reg = null; - - $clientData = $this->base64u_decode($response->clientData); - $decodedClient = json_decode($clientData); - foreach ($requests as $req) { - if( !is_object( $req ) ) { - throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.'); - } - - if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) { - break; - } - - $req = null; - } - if($req === null) { - throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST ); - } - foreach ($registrations as $reg) { - if( !is_object( $reg ) ) { - throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.'); - } - - if($reg->keyHandle === $response->keyHandle) { - break; - } - $reg = null; - } - if($reg === null) { - throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION ); - } - $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey)); - if($pemKey === null) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - - $signData = $this->base64u_decode($response->signatureData); - $dataToVerify = hash('sha256', $req->appId, true); - $dataToVerify .= substr($signData, 0, 5); - $dataToVerify .= hash('sha256', $clientData, true); - $signature = substr($signData, 5); - - if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) { - $ctr = unpack("Nctr", substr($signData, 1, 4)); - $counter = $ctr['ctr']; - /* TODO: wrap-around should be handled somehow.. */ - if($counter > $reg->counter) { - $reg->counter = $counter; - return $reg; - } else { - throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW ); - } - } else { - throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE ); - } - } - - /** - * @return array - */ - private function get_certs() - { - $files = array(); - $dir = $this->attestDir; - if($dir && $handle = opendir($dir)) { - while(false !== ($entry = readdir($handle))) { - if(is_file("$dir/$entry")) { - $files[] = "$dir/$entry"; - } - } - closedir($handle); - } - return $files; - } - - /** - * @param string $data - * @return string - */ - private function base64u_encode($data) - { - return trim(strtr(base64_encode($data), '+/', '-_'), '='); - } - - /** - * @param string $data - * @return string - */ - private function base64u_decode($data) - { - return base64_decode(strtr($data, '-_', '+/')); - } - - /** - * @param string $key - * @return null|string - */ - private function pubkey_to_pem($key) - { - if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") { - return null; - } - - /* - * Convert the public key to binary DER format first - * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 - * - * SEQUENCE(2 elem) 30 59 - * SEQUENCE(2 elem) 30 13 - * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 - * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 - * BIT STRING(520 bit) 03 42 ..key.. - */ - $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01"; - $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42"; - $der .= "\0".$key; - - $pem = "-----BEGIN PUBLIC KEY-----\r\n"; - $pem .= chunk_split(base64_encode($der), 64); - $pem .= "-----END PUBLIC KEY-----"; - - return $pem; - } - - /** - * @return string - * @throws Error - */ - private function createChallenge() - { - $challenge = openssl_random_pseudo_bytes(32, $crypto_strong ); - if( $crypto_strong !== true ) { - throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM); - } - - $challenge = $this->base64u_encode( $challenge ); - - return $challenge; - } - - /** - * Fixes a certificate where the signature contains unused bits. - * - * @param string $cert - * @return mixed - */ - private function fixSignatureUnusedBits($cert) - { - if(in_array(hash('sha256', $cert), $this->FIXCERTS)) { - $cert[strlen($cert) - 257] = "\0"; - } - return $cert; - } -} - -/** - * Class for building a registration request - * - * @package u2flib_server - */ -class RegisterRequest -{ - /** Protocol version */ - public $version = U2F_VERSION; - - /** Registration challenge */ - public $challenge; - - /** Application id */ - public $appId; - - /** - * @param string $challenge - * @param string $appId - * @internal - */ - public function __construct($challenge, $appId) - { - $this->challenge = $challenge; - $this->appId = $appId; - } -} - -/** - * Class for building up an authentication request - * - * @package u2flib_server - */ -class SignRequest -{ - /** Protocol version */ - public $version = U2F_VERSION; - - /** Authentication challenge */ - public $challenge; - - /** Key handle of a registered authenticator */ - public $keyHandle; - - /** Application id */ - public $appId; -} - -/** - * Class returned for successful registrations - * - * @package u2flib_server - */ -class Registration -{ - /** The key handle of the registered authenticator */ - public $keyHandle; - - /** The public key of the registered authenticator */ - public $publicKey; - - /** The attestation certificate of the registered authenticator */ - public $certificate; - - /** The counter associated with this registration */ - public $counter = -1; -} - -/** - * Error class, returned on errors - * - * @package u2flib_server - */ -class Error extends \Exception -{ - /** - * Override constructor and make message and code mandatory - * @param string $message - * @param int $code - * @param \Exception|null $previous - */ - public function __construct($message, $code, ?\Exception $previous = null) { - parent::__construct($message, $code, $previous); - } -} diff --git a/wp-content/plugins/two-factor/providers/class-two-factor-backup-codes.php b/wp-content/plugins/two-factor/providers/class-two-factor-backup-codes.php index 2a3345b4..465e1f8f 100644 --- a/wp-content/plugins/two-factor/providers/class-two-factor-backup-codes.php +++ b/wp-content/plugins/two-factor/providers/class-two-factor-backup-codes.php @@ -39,13 +39,36 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider { add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); parent::__construct(); } + /** + * Enqueue scripts for backup codes. + * + * @since 0.10.0 + * + * @codeCoverageIgnore + * + * @param string $hook_suffix Optional. The current admin page hook suffix. + */ + public function enqueue_assets( $hook_suffix = '' ) { + wp_register_script( + 'two-factor-backup-codes-admin', + plugins_url( 'js/backup-codes-admin.js', __FILE__ ), + array( 'jquery', 'wp-api-request' ), + TWO_FACTOR_VERSION, + true + ); + } + /** * Register the rest-api endpoints required for this provider. * + * @since 0.8.0 + * * @codeCoverageIgnore */ public function register_rest_routes() { @@ -55,11 +78,11 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider { array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_generate_codes' ), - 'permission_callback' => function( $request ) { + 'permission_callback' => function ( $request ) { return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] ); }, 'args' => array( - 'user_id' => array( + 'user_id' => array( 'required' => true, 'type' => 'integer', ), @@ -106,7 +129,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider { array( 'a' => array( 'href' => true ) ) ); ?> - +

Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes', + 'userId' => $user->ID, + ) + ); + wp_enqueue_script( 'two-factor-backup-codes-admin' ); $count = self::codes_remaining_for_user( $user ); ?> -

+

+ +

-

+
- self::NUMBER_OF_CODES, 'method' => 'replace', ); @@ -304,7 +318,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider { $i = 1; foreach ( $codes as $code ) { $download_link .= rawurlencode( "{$i}. {$code}\r\n" ); - $i++; + ++$i; } $i18n = array( @@ -327,6 +341,8 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider { /** * Returns the number of unused codes for the specified user * + * @since 0.2.0 + * * @param WP_User $user WP_User object of the logged-in user. * @return int $int The number of unused codes remaining */ @@ -348,17 +364,47 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider { public function authentication_page( $user ) { require_once ABSPATH . '/wp-admin/includes/template.php'; - $code_length = $this->get_backup_code_length( $user ); + $code_length = $this->get_backup_code_length( $user ); $code_placeholder = str_repeat( 'X', $code_length ); + ?> +

+

+ +

+ + generate_token( $user->ID ); + $token = $this->generate_token( $user->ID ); + $remote_ip = $this->get_client_ip(); + $ttl_minutes = (int) ceil( $this->user_token_ttl( $user->ID ) / MINUTE_IN_SECONDS ); - /* translators: %s: site name */ - $subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) ); - /* translators: %s: token */ - $message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) ); + $subject = wp_strip_all_tags( + sprintf( + /* translators: %s: site name */ + __( '[%s] Login confirmation code', 'two-factor' ), + wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) + ) + ); + + $message_parts = array( + __( 'Please complete the login by entering the verification code below:', 'two-factor' ), + $token, + sprintf( + /* translators: %d: number of minutes */ + __( 'This code will expire in %d minutes.', 'two-factor' ), + $ttl_minutes + ), + sprintf( + /* translators: %1$s: IP address of user, %2$s: user login */ + __( 'A user from IP address %1$s has successfully authenticated as %2$s. If this wasn\'t you, please change your password.', 'two-factor' ), + $remote_ip, + $user->user_login + ), + ); + + $message = wp_strip_all_tags( implode( "\n\n", $message_parts ) ); /** - * Filter the token email subject. + * Filters the token email subject. + * + * @since 0.5.2 * * @param string $subject The email subject line. * @param int $user_id The ID of the user. @@ -259,7 +319,9 @@ class Two_Factor_Email extends Two_Factor_Provider { $subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID ); /** - * Filter the token email message. + * Filters the token email message. + * + * @since 0.5.2 * * @param string $message The email message. * @param string $token The token. @@ -286,30 +348,33 @@ class Two_Factor_Email extends Two_Factor_Provider { $this->generate_and_email_token( $user ); } - $token_length = $this->get_token_length(); + $token_length = $this->get_token_length(); $token_placeholder = str_repeat( 'X', $token_length ); require_once ABSPATH . '/wp-admin/includes/template.php'; ?> +

+

-

+ +

- + ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { + if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state. $this->generate_and_email_token( $user ); return true; } @@ -368,7 +435,7 @@ class Two_Factor_Email extends Two_Factor_Provider { public function user_options( $user ) { $email = $user->user_email; ?> -
+

-

+

wp_strip_all_tags( __( 'Name', 'two-factor' ) ), - 'added' => wp_strip_all_tags( __( 'Added', 'two-factor' ) ), - 'last_used' => wp_strip_all_tags( __( 'Last Used', 'two-factor' ) ), - ); - } - - /** - * Prepares the list of items for displaying. - * - * @since 0.1-dev - */ - public function prepare_items() { - $columns = $this->get_columns(); - $hidden = array(); - $sortable = array(); - $primary = 'name'; - $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); - } - - /** - * Generates content for a single row of the table - * - * @since 0.1-dev - * @access protected - * - * @param object $item The current item. - * @param string $column_name The current column name. - * @return string - */ - protected function column_default( $item, $column_name ) { - switch ( $column_name ) { - case 'name': - $out = ''; - - $actions = array( - 'rename hide-if-no-js' => Two_Factor_FIDO_U2F_Admin::rename_link( $item ), - 'delete' => Two_Factor_FIDO_U2F_Admin::delete_link( $item ), - ); - - return esc_html( $item->name ) . $out . self::row_actions( $actions ); - case 'added': - return gmdate( get_option( 'date_format', 'r' ), $item->added ); - case 'last_used': - return gmdate( get_option( 'date_format', 'r' ), $item->last_used ); - default: - return 'WTF^^?'; - } - } - - /** - * Generates custom table navigation to prevent conflicting nonces. - * - * @since 0.1-dev - * @access protected - * - * @param string $which The location of the bulk actions: 'top' or 'bottom'. - */ - protected function display_tablenav( $which ) { - // Not used for the Security key list. - } - - /** - * Generates content for a single row of the table - * - * @since 0.1-dev - * @access public - * - * @param object $item The current item. - */ - public function single_row( $item ) { - ?> - - single_row_columns( $item ); ?> - - - - - - - - -
- getRegisterData( $security_keys ); - list( $req,$sigs ) = $data; - - update_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, $req ); - } catch ( Exception $e ) { - return false; - } - - wp_enqueue_style( - 'fido-u2f-admin', - plugins_url( 'css/fido-u2f-admin.css', __FILE__ ), - null, - self::asset_version() - ); - - wp_enqueue_script( - 'fido-u2f-admin', - plugins_url( 'js/fido-u2f-admin.js', __FILE__ ), - array( 'jquery', 'fido-u2f-api' ), - self::asset_version(), - true - ); - - /** - * Pass a U2F challenge and user data to our scripts - */ - - $translation_array = array( - 'user_id' => $user_id, - 'register' => array( - 'request' => $req, - 'sigs' => $sigs, - ), - 'text' => array( - 'insert' => esc_html__( 'Now insert (and tap) your Security Key.', 'two-factor' ), - 'error' => esc_html__( 'U2F request failed.', 'two-factor' ), - 'error_codes' => array( - // Map u2f.ErrorCodes to error messages. - 0 => esc_html__( 'Request OK.', 'two-factor' ), - 1 => esc_html__( 'Other U2F error.', 'two-factor' ), - 2 => esc_html__( 'Bad U2F request.', 'two-factor' ), - 3 => esc_html__( 'Unsupported U2F configuration.', 'two-factor' ), - 4 => esc_html__( 'U2F device ineligible.', 'two-factor' ), - 5 => esc_html__( 'U2F request timeout reached.', 'two-factor' ), - ), - 'u2f_not_supported' => esc_html__( 'FIDO U2F appears to be not supported by your web browser. Try using Google Chrome or Firefox.', 'two-factor' ), - ), - ); - - wp_localize_script( - 'fido-u2f-admin', - 'u2fL10n', - $translation_array - ); - - /** - * Script for admin UI - */ - - wp_enqueue_script( - 'inline-edit-key', - plugins_url( 'js/fido-u2f-admin-inline-edit.js', __FILE__ ), - array( 'jquery' ), - self::asset_version(), - true - ); - - wp_localize_script( - 'inline-edit-key', - 'inlineEditL10n', - array( - 'error' => esc_html__( 'Error while saving the changes.', 'two-factor' ), - ) - ); - } - - /** - * Return the current asset version number. - * - * Added as own helper to allow swapping the implementation once we inject - * it as a dependency. - * - * @return string - */ - protected static function asset_version() { - return Two_Factor_FIDO_U2F::asset_version(); - } - - /** - * Display the security key section in a users profile. - * - * This executes during the `show_user_security_settings` action. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public static function show_user_profile( $user ) { - if ( ! Two_Factor_FIDO_U2F::is_supported_for_user( $user ) ) { - return; - } - - wp_nonce_field( "user_security_keys-{$user->ID}", '_nonce_user_security_keys' ); - $new_key = false; - - $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user->ID ); - if ( $security_keys ) { - foreach ( $security_keys as &$security_key ) { - if ( property_exists( $security_key, 'new' ) ) { - $new_key = true; - unset( $security_key->new ); - - // If we've got a new one, update the db record to not save it there any longer. - Two_Factor_FIDO_U2F::update_security_key( $user->ID, $security_key ); - } - } - unset( $security_key ); - } - - ?> -
-

- - -

- -

- - -
- - - - - -
- - -
-

-
- - -

- - items = $security_keys; - $u2f_list_table->prepare_items(); - $u2f_list_table->display(); - $u2f_list_table->inline_edit(); - ?> -
- doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response ); - $reg->new = true; - - Two_Factor_FIDO_U2F::add_security_key( $user_id, $reg ); - } catch ( Exception $e ) { - return; - } - - delete_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY ); - - wp_safe_redirect( - add_query_arg( - array( - 'new_app_pass' => 1, - ), - wp_get_referer() - ) . '#security-keys-section' - ); - exit; - } - } - - /** - * Catch the delete security key request. - * - * This executes during the `load-profile.php` & `load-user-edit.php` actions. - * - * @since 0.1-dev - * - * @access public - * @static - */ - public static function catch_delete_security_key() { - $user_id = Two_Factor_Core::current_user_being_edited(); - - if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) { - $slug = $_REQUEST['delete_security_key']; - - check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' ); - - Two_Factor_FIDO_U2F::delete_security_key( $user_id, $slug ); - - wp_safe_redirect( remove_query_arg( 'new_app_pass', wp_get_referer() ) . '#security-keys-section' ); - exit; - } - } - - /** - * Generate a link to rename a specified security key. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param array $item The current item. - * @return string - */ - public static function rename_link( $item ) { - return sprintf( '%s', esc_html__( 'Rename', 'two-factor' ) ); - } - - /** - * Generate a link to delete a specified security key. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param array $item The current item. - * @return string - */ - public static function delete_link( $item ) { - $delete_link = add_query_arg( 'delete_security_key', $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' ); - return sprintf( '%2$s', esc_url( $delete_link ), esc_html__( 'Delete', 'two-factor' ) ); - } - - /** - * Ajax handler for quick edit saving for a security key. - * - * @since 0.1-dev - * - * @access public - * @static - */ - public static function wp_ajax_inline_save() { - check_ajax_referer( 'keyinlineeditnonce', '_inline_edit' ); - - require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php'; - $wp_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table(); - - if ( ! isset( $_POST['keyHandle'] ) ) { - wp_die(); - } - - $user_id = Two_Factor_Core::current_user_being_edited(); - $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id ); - if ( ! $security_keys ) { - wp_die(); - } - - foreach ( $security_keys as &$key ) { - if ( $key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - break; - } - } - - $key->name = $_POST['name']; - - $updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key ); - if ( ! $updated ) { - wp_die( esc_html__( 'Item not updated.', 'two-factor' ) ); - } - $wp_list_table->single_row( $key ); - wp_die(); - } -} diff --git a/wp-content/plugins/two-factor/providers/class-two-factor-fido-u2f.php b/wp-content/plugins/two-factor/providers/class-two-factor-fido-u2f.php deleted file mode 100644 index cd569997..00000000 --- a/wp-content/plugins/two-factor/providers/class-two-factor-fido-u2f.php +++ /dev/null @@ -1,404 +0,0 @@ - -

- ID ); - $data = self::$u2f->getAuthenticateData( $keys ); - update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data ); - } catch ( Exception $e ) { - ?> -

- $data, - ) - ); - - wp_enqueue_script( 'fido-u2f-login' ); - - ?> -

- - ID, self::AUTH_DATA_USER_META_KEY, true ); - - $response = json_decode( stripslashes( $_REQUEST['u2f_response'] ) ); - - $keys = self::get_security_keys( $user->ID ); - - try { - $reg = self::$u2f->doAuthenticate( $requests, $keys, $response ); - - $reg->last_used = time(); - - self::update_security_key( $user->ID, $reg ); - - return true; - } catch ( Exception $e ) { - return false; - } - } - - /** - * Whether this Two Factor provider is configured and available for the user specified. - * - * @since 0.1-dev - * - * @param WP_User $user WP_User object of the logged-in user. - * @return boolean - */ - public function is_available_for_user( $user ) { - return (bool) self::get_security_keys( $user->ID ); - } - - /** - * Inserts markup at the end of the user profile field for this provider. - * - * @since 0.1-dev - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public function user_options( $user ) { - ?> -

- -

- keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $register, 'certificate' ) || empty( $register->certificate ) - || ! property_exists( $register, 'counter' ) || ( -1 > $register->counter ) - ) { - return false; - } - - $register = array( - 'keyHandle' => $register->keyHandle, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - 'publicKey' => $register->publicKey, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - 'certificate' => $register->certificate, - 'counter' => $register->counter, - ); - - $register['name'] = __( 'New Security Key', 'two-factor' ); - $register['added'] = time(); - $register['last_used'] = $register['added']; - - return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register ); - } - - /** - * Retrieve registered security keys for a user. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @return array|bool Array of keys on success, false on failure. - */ - public static function get_security_keys( $user_id ) { - if ( ! is_numeric( $user_id ) ) { - return false; - } - - $keys = get_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY ); - if ( $keys ) { - foreach ( $keys as &$key ) { - $key = (object) $key; - } - unset( $key ); - } - - return $keys; - } - - /** - * Update registered security key. - * - * Use the $prev_value parameter to differentiate between meta fields with the - * same key and user ID. - * - * If the meta field for the user does not exist, it will be added. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @param object $data The data of registered security key. - * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. - */ - public static function update_security_key( $user_id, $data ) { - if ( ! is_numeric( $user_id ) ) { - return false; - } - - if ( - ! is_object( $data ) - || ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $data, 'certificate' ) || empty( $data->certificate ) - || ! property_exists( $data, 'counter' ) || ( -1 > $data->counter ) - ) { - return false; - } - - $keys = self::get_security_keys( $user_id ); - if ( $keys ) { - foreach ( $keys as $key ) { - if ( $key->keyHandle === $data->keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key ); - } - } - } - - return self::add_security_key( $user_id, $data ); - } - - /** - * Remove registered security key matching criteria from a user. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @param string $keyHandle Optional. Key handle. - * @return bool True on success, false on failure. - */ - public static function delete_security_key( $user_id, $keyHandle = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - global $wpdb; - - if ( ! is_numeric( $user_id ) ) { - return false; - } - - $user_id = absint( $user_id ); - if ( ! $user_id ) { - return false; - } - - $keyHandle = wp_unslash( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - $keyHandle = maybe_serialize( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - - $query = $wpdb->prepare( "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id ); - - if ( $keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - $key_handle_lookup = sprintf( ':"%s";s:', $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - - $query .= $wpdb->prepare( - ' AND meta_value LIKE %s', - '%' . $wpdb->esc_like( $key_handle_lookup ) . '%' - ); - } - - $meta_ids = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - if ( ! count( $meta_ids ) ) { - return false; - } - - foreach ( $meta_ids as $meta_id ) { - delete_metadata_by_mid( 'user', $meta_id ); - } - - return true; - } - - /** - * Return user meta keys to delete during plugin uninstall. - * - * @return array - */ - public static function uninstall_user_meta_keys() { - return array( - self::REGISTERED_KEY_USER_META_KEY, - self::AUTH_DATA_USER_META_KEY, - '_two_factor_fido_u2f_register_request', // From Two_Factor_FIDO_U2F_Admin which is not loaded during uninstall. - ); - } -} diff --git a/wp-content/plugins/two-factor/providers/class-two-factor-provider.php b/wp-content/plugins/two-factor/providers/class-two-factor-provider.php index b780c4a7..275cbae7 100644 --- a/wp-content/plugins/two-factor/providers/class-two-factor-provider.php +++ b/wp-content/plugins/two-factor/providers/class-two-factor-provider.php @@ -25,7 +25,7 @@ abstract class Two_Factor_Provider { $class_name = static::class; if ( ! isset( $instances[ $class_name ] ) ) { - $instances[ $class_name ] = new $class_name; + $instances[ $class_name ] = new $class_name(); } return $instances[ $class_name ]; @@ -37,7 +37,6 @@ abstract class Two_Factor_Provider { * @since 0.1-dev */ protected function __construct() { - return $this; } /** @@ -68,6 +67,8 @@ abstract class Two_Factor_Provider { * Prints the name of the provider. * * @since 0.1-dev + * + * @codeCoverageIgnore */ public function print_label() { echo esc_html( $this->get_label() ); @@ -98,6 +99,8 @@ abstract class Two_Factor_Provider { * Return `true` to prevent the authentication and render the * authentication page. * + * @since 0.2.0 + * * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ @@ -118,6 +121,8 @@ abstract class Two_Factor_Provider { /** * Whether this Two Factor provider is configured and available for the user specified. * + * @since 0.7.0 + * * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ @@ -126,6 +131,8 @@ abstract class Two_Factor_Provider { /** * If this provider should be available for the user. * + * @since 0.13.0 + * * @param WP_User|int $user WP_User object, user ID or null to resolve the current user. * * @return bool @@ -159,6 +166,8 @@ abstract class Two_Factor_Provider { /** * Sanitizes a numeric code to be used as an auth code. * + * @since 0.8.0 + * * @param string $field The _REQUEST field to check for the code. * @param int $length The valid expected length of the field. * @return false|string Auth code on success, false if the field is not set or not expected length. @@ -182,6 +191,8 @@ abstract class Two_Factor_Provider { /** * Return the user meta keys that need to be deletated on plugin uninstall. * + * @since 0.10.0 + * * @return array */ public static function uninstall_user_meta_keys() { @@ -191,6 +202,8 @@ abstract class Two_Factor_Provider { /** * Return the option keys that need to be deleted on plugin uninstall. * + * @since 0.10.0 + * * Note: this method doesn't have access to the instantiated provider object. * * @return array diff --git a/wp-content/plugins/two-factor/providers/class-two-factor-totp.php b/wp-content/plugins/two-factor/providers/class-two-factor-totp.php index 9b3dd084..8c47d74a 100644 --- a/wp-content/plugins/two-factor/providers/class-two-factor-totp.php +++ b/wp-content/plugins/two-factor/providers/class-two-factor-totp.php @@ -7,6 +7,8 @@ /** * Class Two_Factor_Totp + * + * @since 0.2.0 */ class Two_Factor_Totp extends Two_Factor_Provider { @@ -40,6 +42,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Class constructor. Sets up hooks, etc. * + * @since 0.2.0 + * * @codeCoverageIgnore */ protected function __construct() { @@ -51,9 +55,40 @@ class Two_Factor_Totp extends Two_Factor_Provider { parent::__construct(); } + /** + * Timestamp returned by time() + * + * @var int $now + */ + private static $now; + + /** + * Override time() in the current object for testing. + * + * @since 0.15.0 + * + * @return int + */ + private static function time() { + return self::$now ? self::$now : time(); + } + + /** + * Set up the internal state of time() invocations for deterministic generation. + * + * @since 0.15.0 + * + * @param int $now Timestamp to use when overriding time(). + */ + public static function set_time( $now ) { + self::$now = $now; + } + /** * Register the rest-api endpoints required for this provider. * + * @since 0.8.0 + * * @codeCoverageIgnore */ public function register_rest_routes() { @@ -64,7 +99,7 @@ class Two_Factor_Totp extends Two_Factor_Provider { array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'rest_delete_totp' ), - 'permission_callback' => function( $request ) { + 'permission_callback' => function ( $request ) { return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] ); }, 'args' => array( @@ -77,20 +112,20 @@ class Two_Factor_Totp extends Two_Factor_Provider { array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_setup_totp' ), - 'permission_callback' => function( $request ) { + 'permission_callback' => function ( $request ) { return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] ); }, 'args' => array( - 'user_id' => array( + 'user_id' => array( 'required' => true, 'type' => 'integer', ), - 'key' => array( + 'key' => array( 'type' => 'string', 'default' => '', 'validate_callback' => null, // Note: validation handled in ::rest_setup_totp(). ), - 'code' => array( + 'code' => array( 'type' => 'string', 'default' => '', 'validate_callback' => null, // Note: validation handled in ::rest_setup_totp(). @@ -108,6 +143,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Returns the name of the provider. + * + * @since 0.2.0 */ public function get_label() { return _x( 'Authenticator App', 'Provider Label', 'two-factor' ); @@ -125,7 +162,10 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Enqueue scripts * + * @since 0.8.0 + * * @codeCoverageIgnore + * @param string $hook_suffix Hook suffix. */ public function enqueue_assets( $hook_suffix ) { $environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : ''; @@ -137,37 +177,57 @@ class Two_Factor_Totp extends Two_Factor_Provider { TWO_FACTOR_VERSION, true ); + + wp_register_script( + 'two-factor-totp-qrcode', + plugins_url( 'js/totp-admin-qrcode.js', __FILE__ ), + array( 'two-factor-qr-code-generator' ), + TWO_FACTOR_VERSION, + true + ); + + wp_register_script( + 'two-factor-totp-admin', + plugins_url( 'js/totp-admin.js', __FILE__ ), + array( 'jquery', 'wp-api-request', 'two-factor-qr-code-generator' ), + TWO_FACTOR_VERSION, + true + ); } /** * Rest API endpoint for handling deactivation of TOTP. * + * @since 0.8.0 + * * @param WP_REST_Request $request The Rest Request object. - * @return array Success array. + * @return WP_Error|array Array of data on success, WP_Error on error. */ public function rest_delete_totp( $request ) { $user_id = $request['user_id']; $user = get_user_by( 'id', $user_id ); - $this->delete_user_totp_key( $user_id ); - if ( ! Two_Factor_Core::disable_provider_for_user( $user_id, 'Two_Factor_Totp' ) ) { return new WP_Error( 'db_error', __( 'Unable to disable TOTP provider for this user.', 'two-factor' ), array( 'status' => 500 ) ); } + $this->delete_user_totp_key( $user_id ); + ob_start(); $this->user_two_factor_options( $user ); $html = ob_get_clean(); - return [ + return array( 'success' => true, 'html' => $html, - ]; + ); } /** * REST API endpoint for setting up TOTP. * + * @since 0.8.0 + * * @param WP_REST_Request $request The Rest Request object. * @return WP_Error|array Array of data on success, WP_Error on error. */ @@ -198,15 +258,17 @@ class Two_Factor_Totp extends Two_Factor_Provider { $this->user_two_factor_options( $user ); $html = ob_get_clean(); - return [ + return array( 'success' => true, 'html' => $html, - ]; + ); } /** * Generates a URL that can be used to create a QR code. * + * @since 0.8.0 + * * @param WP_User $user The user to generate a URL for. * @param string $secret_key The secret key. * @@ -216,21 +278,27 @@ class Two_Factor_Totp extends Two_Factor_Provider { $issuer = get_bloginfo( 'name', 'display' ); /** - * Filter the Issuer for the TOTP. + * Filters the Issuer for the TOTP. * * Must follow the TOTP format for a "issuer". Do not URL Encode. * + * @since 0.8.0 + * * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#issuer + * * @param string $issuer The issuer for TOTP. */ $issuer = apply_filters( 'two_factor_totp_issuer', $issuer ); /** - * Filter the Label for the TOTP. + * Filters the Label for the TOTP. * * Must follow the TOTP format for a "label". Do not URL Encode. * + * @since 0.4.7 + * * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label + * * @param string $totp_title The label for the TOTP. * @param WP_User $user The User object. * @param string $issuer The issuer of the TOTP. This should be the prefix of the result. @@ -246,16 +314,19 @@ class Two_Factor_Totp extends Two_Factor_Provider { ); /** - * Filter the TOTP generated URL. + * Filters the TOTP generated URL. * * Must follow the TOTP format. Do not URL Encode. * + * @since 0.8.0 + * * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format + * * @param string $totp_url The TOTP URL. * @param WP_User $user The user object. */ $totp_url = apply_filters( 'two_factor_totp_url', $totp_url, $user ); - $totp_url = esc_url( $totp_url, array( 'otpauth' ) ); + $totp_url = esc_url_raw( $totp_url, array( 'otpauth' ) ); return $totp_url; } @@ -263,6 +334,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Display TOTP options on the user settings page. * + * @since 0.2.0 + * * @param WP_User $user The current user being edited. * @return void * @@ -275,132 +348,92 @@ class Two_Factor_Totp extends Two_Factor_Provider { $key = $this->get_user_totp_key( $user->ID ); - wp_enqueue_script( 'two-factor-qr-code-generator' ); - wp_enqueue_script( 'wp-api-request' ); - wp_enqueue_script( 'jquery' ); + wp_localize_script( + 'two-factor-totp-admin', + 'twoFactorTotpAdmin', + array( + 'restPath' => Two_Factor_Core::REST_NAMESPACE . '/totp', + 'userId' => $user->ID, + 'qrCodeAriaLabel' => __( 'Authenticator App QR Code', 'two-factor' ), + ) + ); + wp_enqueue_script( 'two-factor-totp-admin' ); ?>
generate_key(); $totp_url = $this->generate_qr_code_url( $user, $key ); - ?> -

- +

-

- - Loading... - - -

- - - - - -

- -

-

- - - -

- - - +
    +
  1. + +
  2. +
  3. + +

    + + + + +

    +

    + +

    +

    + +

    +
  4. +
  5. +

    +

    + + + +

    +

    + %2$s (%3$s)', + esc_attr( wp_date( 'c' ) ), + esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ), + esc_html( wp_timezone_string() ) + ) + ); + ?> +

    +
  6. +
+ $totp_url, + 'qrCodeLabel' => __( 'Authenticator App QR Code', 'two-factor' ), + ) + ); + wp_enqueue_script( 'two-factor-totp-qrcode' ); + ?>

@@ -409,24 +442,6 @@ class Two_Factor_Totp extends Two_Factor_Provider { -

@@ -436,6 +451,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Get the TOTP secret key for a user. * + * @since 0.2.0 + * * @param int $user_id User ID. * * @return string @@ -447,6 +464,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Set the TOTP secret key for a user. * + * @since 0.2.0 + * * @param int $user_id User ID. * @param string $key TOTP secret key. * @@ -459,6 +478,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Delete the TOTP secret key for a user. * + * @since 0.2.0 + * * @param int $user_id User ID. * * @return boolean If the key was deleted successfully. @@ -471,6 +492,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Check if the TOTP secret key has a proper format. * + * @since 0.2.0 + * * @param string $key TOTP secret key. * * @return boolean @@ -488,6 +511,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Validates authentication. * + * @since 0.2.0 + * * @param WP_User $user WP_User object of the logged-in user. * * @return bool Whether the user gave a valid code @@ -504,6 +529,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Validates an authentication code for a given user, preventing re-use and older TOTP keys. * + * @since 0.8.0 + * * @param WP_User $user WP_User object of the logged-in user. * @param int $code The TOTP token to validate. * @@ -535,48 +562,70 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Checks if a given code is valid for a given key, allowing for a certain amount of time drift. * + * @since 0.15.0 + * * @param string $key The share secret key to use. * @param string $authcode The code to test. + * @param string $hash The hash used to calculate the code. + * @param int $time_step The size of the time step. * * @return bool Whether the code is valid within the time frame. */ - public static function is_valid_authcode( $key, $authcode ) { - return (bool) self::get_authcode_valid_ticktime( $key, $authcode ); + public static function is_valid_authcode( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) { + return (bool) self::get_authcode_valid_ticktime( $key, $authcode, $hash, $time_step ); } /** * Checks if a given code is valid for a given key, allowing for a certain amount of time drift. * + * @since 0.15.0 + * * @param string $key The share secret key to use. * @param string $authcode The code to test. + * @param string $hash The hash used to calculate the code. + * @param int $time_step The size of the time step. * * @return false|int Returns the timestamp of the authcode on success, False otherwise. */ - public static function get_authcode_valid_ticktime( $key, $authcode ) { + public static function get_authcode_valid_ticktime( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) { /** * 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 + * so the default of 4 allows codes that are two minutes to either side of server time. * + * @since 0.2.0 * @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' ); + /** + * Filters 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. + * + * @since 0.7.0 + * + * @param int $max_ticks Max ticks of time correction to allow. Default 4. + */ $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 = floor( time() / self::DEFAULT_TIME_STEP_SEC ); + $time = (int) floor( self::time() / $time_step ); + + $digits = strlen( $authcode ); foreach ( $ticks as $offset ) { - $log_time = $time + $offset; - if ( hash_equals( self::calc_totp( $key, $log_time ), $authcode ) ) { + $log_time = (int) ( $time + $offset ); + if ( hash_equals( self::calc_totp( $key, $log_time, $digits, $hash, $time_step ), $authcode ) ) { // Return the tick timestamp. - return $log_time * self::DEFAULT_TIME_STEP_SEC; + return (int) ( $log_time * self::DEFAULT_TIME_STEP_SEC ); } } @@ -586,6 +635,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Generates key * + * @since 0.2.0 + * * @param int $bitsize Nume of bits to use for key. * * @return string $bitsize long string composed of available base32 chars. @@ -598,58 +649,94 @@ class Two_Factor_Totp extends Two_Factor_Provider { } /** - * Pack stuff + * Pack stuff. We're currently only using this to pack integers, however the generic `pack` method can handle mixed. * - * @param string $value The value to be packed. + * @since 0.2.0 + * + * @param int $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; + public static function pack64( int $value ): string { + // Native 64-bit support (modern PHP on 64-bit builds). + if ( 8 === PHP_INT_SIZE ) { + return pack( 'J', $value ); } - - $lowmap = 0xffffffff; - $lower = $value & $lowmap; - + + // 32-bit PHP fallback + $higher = ( $value >> 32 ) & 0xFFFFFFFF; + $lower = $value & 0xFFFFFFFF; + return pack( 'NN', $higher, $lower ); } + /** + * Pad a short secret with bytes from the same until it's the correct length + * for hashing. + * + * @since 0.15.0 + * + * @param string $secret Secret key to pad. + * @param int $length Byte length of the desired padded secret. + * + * @throws InvalidArgumentException If the secret or length are invalid. + * + * @return string + */ + protected static function pad_secret( $secret, $length ) { + if ( empty( $secret ) ) { + throw new InvalidArgumentException( 'Secret must be non-empty!' ); + } + + $length = intval( $length ); + if ( $length <= 0 ) { + throw new InvalidArgumentException( 'Padding length must be non-zero' ); + } + + return str_pad( $secret, $length, $secret, STR_PAD_RIGHT ); + } + /** * Calculate a valid code given the shared secret key * + * @since 0.2.0 + * * @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. * + * @throws InvalidArgumentException If the hash type is invalid. + * * @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 ); + switch ( $hash ) { + case 'sha1': + $secret = self::pad_secret( $secret, 20 ); + break; + case 'sha256': + $secret = self::pad_secret( $secret, 32 ); + break; + case 'sha512': + $secret = self::pad_secret( $secret, 64 ); + break; + default: + throw new InvalidArgumentException( 'Invalid hash type specified!' ); + } + if ( false === $step_count ) { - $step_count = floor( time() / $time_step ); + $step_count = floor( self::time() / $time_step ); } $timestamp = self::pack64( $step_count ); $hash = hash_hmac( $hash, $timestamp, $secret, true ); - $offset = ord( $hash[19] ) & 0xf; + $offset = ord( $hash[ strlen( $hash ) - 1 ] ) & 0xf; $code = ( ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) | @@ -664,6 +751,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Whether this Two Factor provider is configured and available for the user specified. * + * @since 0.2.0 + * * @param WP_User $user WP_User object of the logged-in user. * * @return boolean @@ -678,6 +767,8 @@ class Two_Factor_Totp extends Two_Factor_Provider { /** * Prints the form that prompts the user to authenticate. * + * @since 0.2.0 + * * @param WP_User $user WP_User object of the logged-in user. * * @codeCoverageIgnore @@ -685,41 +776,48 @@ class Two_Factor_Totp extends Two_Factor_Provider { public function authentication_page( $user ) { require_once ABSPATH . '/wp-admin/includes/template.php'; ?> +

+

- +

- + ' ).val( csvCodes ).css( { position: 'absolute', left: '-9999px' } ); + $( 'body' ).append( $temp ); + $temp[0].select(); + document.execCommand( 'copy' ); + $temp.remove(); + } ); + + $( '.button-two-factor-backup-codes-generate' ).click( function() { + wp.apiRequest( { + method: 'POST', + path: twoFactorBackupCodes.restPath, + data: { + user_id: parseInt( twoFactorBackupCodes.userId, 10 ) + } + } ).then( function( response ) { + var $codesList = $( '.two-factor-backup-codes-unused-codes' ), + i; + + $( '.two-factor-backup-codes-wrapper' ).show(); + $codesList.html( '' ); + $codesList.css( { 'column-count': 2, 'column-gap': '80px', 'max-width': '420px' } ); + $( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv', response.codes.join( ',' ) ); + + // Append the codes. + for ( i = 0; i < response.codes.length; i++ ) { + $codesList.append( '
  • ' + response.codes[ i ] + '
  • ' ); + } + + // Update counter. + $( '.two-factor-backup-codes-count' ).html( response.i18n.count ); + $( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link ); + } ); + } ); +}( jQuery ) ); diff --git a/wp-content/plugins/two-factor/providers/js/fido-u2f-admin-inline-edit.js b/wp-content/plugins/two-factor/providers/js/fido-u2f-admin-inline-edit.js deleted file mode 100644 index b7b0123e..00000000 --- a/wp-content/plugins/two-factor/providers/js/fido-u2f-admin-inline-edit.js +++ /dev/null @@ -1,150 +0,0 @@ -/* global window, document, jQuery, inlineEditL10n, ajaxurl */ -var inlineEditKey; - -( function( $ ) { - inlineEditKey = { - - init: function() { - var t = this, - row = $( '#security-keys-section #inline-edit' ); - - t.what = '#key-'; - - $( '#security-keys-section #the-list' ).on( 'click', 'a.editinline', function() { - inlineEditKey.edit( this ); - return false; - } ); - - // Prepare the edit row. - row.keyup( function( event ) { - if ( 27 === event.which ) { - return inlineEditKey.revert(); - } - } ); - - $( 'a.cancel', row ).click( function() { - return inlineEditKey.revert(); - } ); - - $( 'a.save', row ).click( function() { - return inlineEditKey.save( this ); - } ); - - $( 'input, select', row ).keydown( function( event ) { - if ( 13 === event.which ) { - return inlineEditKey.save( this ); - } - } ); - }, - - toggle: function( el ) { - var t = this; - - if ( 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ) { - t.revert(); - } else { - t.edit( el ); - } - }, - - edit: function( id ) { - var editRow, rowData, val, - t = this; - t.revert(); - - if ( 'object' === typeof id ) { - id = t.getId( id ); - } - - editRow = $( '#inline-edit' ).clone( true ); - rowData = $( '#inline_' + id ); - - $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '#security-keys-section .widefat thead' ).length ); - - $( t.what + id ).hide().after( editRow ).after( '' ); - - val = $( '.name', rowData ); - val.find( 'img' ).replaceWith( function() { - return this.alt; - } ); - val = val.text(); - $( ':input[name="name"]', editRow ).val( val ); - - $( editRow ).attr( 'id', 'edit-' + id ).addClass( 'inline-editor' ).show(); - $( '.ptitle', editRow ).eq( 0 ).focus(); - - return false; - }, - - save: function( id ) { - var params, fields; - - if ( 'object' === typeof id ) { - id = this.getId( id ); - } - - $( '#security-keys-section table.widefat .spinner' ).addClass( 'is-active' ); - - params = { - action: 'inline-save-key', - keyHandle: id, - user_id: window.u2fL10n.user_id - }; - - fields = $( '#edit-' + id ).find( ':input' ).serialize(); - params = fields + '&' + $.param( params ); - - // Make ajax request. - $.post( ajaxurl, params, - function( r ) { - var row, newID; - $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' ); - - if ( r ) { - if ( -1 !== r.indexOf( '' )[0].submit.call( $( '#your-profile' )[0] ); - } ); - } ); -}( jQuery ) ); diff --git a/wp-content/plugins/two-factor/providers/js/fido-u2f-login.js b/wp-content/plugins/two-factor/providers/js/fido-u2f-login.js deleted file mode 100644 index 28295307..00000000 --- a/wp-content/plugins/two-factor/providers/js/fido-u2f-login.js +++ /dev/null @@ -1,16 +0,0 @@ -/* global window, u2f, u2fL10n, jQuery */ -( function( $ ) { - if ( ! window.u2fL10n ) { - window.console.error( 'u2fL10n is not defined' ); - return; - } - - u2f.sign( u2fL10n.request[0].appId, u2fL10n.request[0].challenge, u2fL10n.request, function( data ) { - if ( data.errorCode ) { - window.console.error( 'Registration Failed', data.errorCode ); - } else { - $( '#u2f_response' ).val( JSON.stringify( data ) ); - $( '#loginform' ).submit(); - } - } ); -}( jQuery ) ); diff --git a/wp-content/plugins/two-factor/providers/js/totp-admin-qrcode.js b/wp-content/plugins/two-factor/providers/js/totp-admin-qrcode.js new file mode 100644 index 00000000..bb2cf4ef --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/totp-admin-qrcode.js @@ -0,0 +1,35 @@ +/* global twoFactorTotpQrcode, qrcode, document, window */ +( function() { + var qrGenerator = function() { + /* + * 0 = Automatically select the version, to avoid going over the limit of URL + * length. + * L = Least amount of error correction, because it's not needed when scanning + * on a monitor, and it lowers the image size. + */ + var qr = qrcode( 0, 'L' ), + svg, + title; + + qr.addData( twoFactorTotpQrcode.totpUrl ); + qr.make(); + + document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 ); + + // For accessibility, markup the SVG with a title and role. + svg = document.querySelector( '#two-factor-qr-code a svg' ); + title = document.createElement( 'title' ); + + svg.setAttribute( 'role', 'img' ); + svg.setAttribute( 'aria-label', twoFactorTotpQrcode.qrCodeLabel ); + title.innerText = twoFactorTotpQrcode.qrCodeLabel; + svg.appendChild( title ); + }; + + // Run now if the document is loaded, otherwise on DOMContentLoaded. + if ( document.readyState === 'complete' ) { + qrGenerator(); + } else { + window.addEventListener( 'DOMContentLoaded', qrGenerator ); + } +}() ); diff --git a/wp-content/plugins/two-factor/providers/js/totp-admin.js b/wp-content/plugins/two-factor/providers/js/totp-admin.js new file mode 100644 index 00000000..4d59e800 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/totp-admin.js @@ -0,0 +1,95 @@ +/* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */ +( function( $ ) { + var generateQrCode = function( totpUrl ) { + var $qrLink = $( '#two-factor-qr-code a' ), + qr, + svg, + title; + + if ( ! $qrLink.length || typeof qrcode === 'undefined' ) { + return; + } + + qr = qrcode( 0, 'L' ); + + qr.addData( totpUrl ); + qr.make(); + $qrLink.html( qr.createSvgTag( 5 ) ); + + svg = $qrLink.find( 'svg' )[ 0 ]; + if ( svg ) { + var ariaLabel = ( typeof twoFactorTotpAdmin !== 'undefined' && twoFactorTotpAdmin && twoFactorTotpAdmin.qrCodeAriaLabel ) ? twoFactorTotpAdmin.qrCodeAriaLabel : 'Authenticator App QR Code'; + title = document.createElement( 'title' ); + svg.setAttribute( 'role', 'img' ); + svg.setAttribute( 'aria-label', ariaLabel ); + title.innerText = ariaLabel; + svg.appendChild( title ); + } + }; + + var checkbox = document.getElementById( 'enabled-Two_Factor_Totp' ); + + // Focus the auth code input when the checkbox is clicked. + if ( checkbox ) { + checkbox.addEventListener( 'click', function( e ) { + if ( e.target.checked ) { + document.getElementById( 'two-factor-totp-authcode' ).focus(); + } + } ); + } + + $( '.totp-submit' ).click( function( e ) { + var key = $( '#two-factor-totp-key' ).val(), + code = $( '#two-factor-totp-authcode' ).val(); + + e.preventDefault(); + + wp.apiRequest( { + method: 'POST', + path: twoFactorTotpAdmin.restPath, + data: { + user_id: parseInt( twoFactorTotpAdmin.userId, 10 ), + key: key, + code: code, + enable_provider: true + } + } ).fail( function( response, status ) { + var errorMessage = ( response && response.responseJSON && response.responseJSON.message ) || ( response && response.statusText ) || status || '', + $error = $( '#totp-setup-error' ); + + if ( ! $error.length ) { + $error = $( '

    ' ).insertAfter( $( '.totp-submit' ) ); + } + + $error.find( 'p' ).text( errorMessage ); + + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ).trigger( 'change' ); + $( '#two-factor-totp-authcode' ).val( '' ); + } ).then( function( response ) { + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', true ).trigger( 'change' ); + $( '#two-factor-totp-options' ).html( response.html ); + } ); + } ); + + $( '.button.reset-totp-key' ).click( function( e ) { + e.preventDefault(); + + wp.apiRequest( { + method: 'DELETE', + path: twoFactorTotpAdmin.restPath, + data: { + user_id: parseInt( twoFactorTotpAdmin.userId, 10 ) + } + } ).then( function( response ) { + var totpUrl; + + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ); + $( '#two-factor-totp-options' ).html( response.html ); + + totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' ); + if ( totpUrl ) { + generateQrCode( totpUrl ); + } + } ); + } ); +}( jQuery ) ); diff --git a/wp-content/plugins/two-factor/providers/js/two-factor-login-authcode.js b/wp-content/plugins/two-factor/providers/js/two-factor-login-authcode.js new file mode 100644 index 00000000..f7e6e7aa --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/two-factor-login-authcode.js @@ -0,0 +1,38 @@ +/* global document */ +( function() { + // Enforce numeric-only input for numeric inputmode elements. + var form = document.querySelector( '#loginform' ), + inputEl = document.querySelector( 'input.authcode[inputmode="numeric"]' ), + expectedLength = ( inputEl && inputEl.dataset ) ? inputEl.dataset.digits : 0, + spaceInserted = false; + + if ( inputEl ) { + inputEl.addEventListener( + 'input', + function() { + var value = this.value.replace( /[^0-9 ]/g, '' ).replace( /^\s+/, '' ), + submitControl; + + if ( ! spaceInserted && expectedLength && value.length === Math.floor( expectedLength / 2 ) ) { + value += ' '; + spaceInserted = true; + } else if ( spaceInserted && ! this.value ) { + spaceInserted = false; + } + + this.value = value; + + // Auto-submit if it's the expected length. + if ( expectedLength && value.replace( / /g, '' ).length === parseInt( expectedLength, 10 ) ) { + if ( form && typeof form.requestSubmit === 'function' ) { + form.requestSubmit(); + submitControl = form.querySelector( '[type="submit"]' ); + if ( submitControl ) { + submitControl.disabled = true; + } + } + } + } + ); + } +}() ); diff --git a/wp-content/plugins/two-factor/providers/js/two-factor-login.js b/wp-content/plugins/two-factor/providers/js/two-factor-login.js new file mode 100644 index 00000000..6526581f --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/two-factor-login.js @@ -0,0 +1,11 @@ +/* global document, setTimeout */ +( function() { + setTimeout( function() { + var d; + try { + d = document.getElementById( 'authcode' ); + d.value = ''; + d.focus(); + } catch ( e ) {} + }, 200 ); +}() ); diff --git a/wp-content/plugins/two-factor/readme.txt b/wp-content/plugins/two-factor/readme.txt index 8143c1c4..54cd2558 100644 --- a/wp-content/plugins/two-factor/readme.txt +++ b/wp-content/plugins/two-factor/readme.txt @@ -1,22 +1,87 @@ -=== Two-Factor === -Contributors: georgestephanis, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, kasparsd, alihusnainarshad, passoniate +=== Two Factor === +Contributors: georgestephanis, kasparsd, masteradhoc, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, alihusnainarshad, passoniate Tags: 2fa, mfa, totp, authentication, security -Tested up to: 6.7 -Stable tag: 0.13.0 +Tested up to: 6.9 +Stable tag: 0.16.0 License: GPL-2.0-or-later License URI: https://spdx.org/licenses/GPL-2.0-or-later.html -Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), Universal 2nd Factor (U2F), email, and backup verification codes. +Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), email, and backup verification codes. == Description == -Use the "Two-Factor Options" section under "Users" → "Your Profile" to enable and configure one or multiple two-factor authentication providers for your account: +The Two-Factor plugin adds an extra layer of security to your WordPress login by requiring users to provide a second form of authentication in addition to their password. This helps protect against unauthorized access even if passwords are compromised. -- Email codes -- Time Based One-Time Passwords (TOTP) -- FIDO Universal 2nd Factor (U2F) -- Backup Codes -- Dummy Method (only for testing purposes) +## Setup Instructions + +**Important**: Each user must individually configure their two-factor authentication settings. + +### For Individual Users + +1. **Navigate to your profile**: Go to "Users" → "Your Profile" in the WordPress admin +2. **Find Two-Factor Options**: Scroll down to the "Two-Factor Options" section +3. **Choose your methods**: Enable one or more authentication providers (noting a site admin may have hidden one or more so what is available could vary): + - **Authenticator App (TOTP)** - Use apps like Google Authenticator, Authy, or 1Password + - **Email Codes** - Receive one-time codes via email + - **Backup Codes** - Generate one-time backup codes for emergencies + - **Dummy Method** - For testing purposes only (requires WP_DEBUG) +4. **Configure each method**: Follow the setup instructions for each enabled provider +5. **Set primary method**: Choose which method to use as your default authentication +6. **Save changes**: Click "Update Profile" to save your settings + +### For Site Administrators + +- **Plugin settings**: The plugin provides a settings page under "Settings → Two-Factor" to configure which providers should be disabled site-wide. +- **User management**: Administrators can configure 2FA for other users by editing their profiles +- **Security recommendations**: Encourage users to enable backup methods to prevent account lockouts + +## Available Authentication Methods + +### Authenticator App (TOTP) - Recommended +- **Security**: High - Time-based one-time passwords +- **Setup**: Scan QR code with authenticator app +- **Compatibility**: Works with Google Authenticator, Authy, 1Password, and other TOTP apps +- **Best for**: Most users, provides excellent security with good usability + +### Backup Codes - Recommended +- **Security**: Medium - One-time use codes +- **Setup**: Generate 10 backup codes for emergency access +- **Compatibility**: Works everywhere, no special hardware needed +- **Best for**: Emergency access when other methods are unavailable + +### Email Codes +- **Security**: Medium - One-time codes sent via email +- **Setup**: Automatic - uses your WordPress email address +- **Compatibility**: Works with any email-capable device +- **Best for**: Users who prefer email-based authentication + +### FIDO U2F Security Keys +- Deprecated and removed due to loss of browser support. + +### Dummy Method +- **Security**: None - Always succeeds +- **Setup**: Only available when WP_DEBUG is enabled +- **Purpose**: Testing and development only +- **Best for**: Developers testing the plugin + +## Important Notes + +### HTTPS Requirement +- All methods work on both HTTP and HTTPS sites + +### Browser Compatibility +- TOTP and email methods work on all devices and browsers + +### Account Recovery +- Always enable backup codes to prevent being locked out of your account +- If you lose access to all authentication methods, contact your site administrator + +### Security Best Practices +- Use multiple authentication methods when possible +- Keep backup codes in a secure location +- Regularly review and update your authentication settings + +For more information about two-factor authentication in WordPress, see the [WordPress Advanced Administration Security Guide](https://developer.wordpress.org/advanced-administration/security/mfa/). For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/). @@ -28,12 +93,30 @@ Here is a list of action and filter hooks provided by the plugin: - `two_factor_providers_for_user` filter overrides the available two-factor providers for a specific user. Array values are instances of provider classes and the user object `WP_User` is available as the second argument. - `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID. - `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow. +- `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument. - `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated. - `two_factor_email_token_length` filter overrides the default 8 character count for email tokens. -- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Providers the `WP_User` of the associated user as the second argument. +- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Provides the `WP_User` of the associated user as the second argument. +- `two_factor_rest_api_can_edit_user` filter overrides whether a user’s Two-Factor settings can be edited via the REST API. First argument is the current `$can_edit` boolean, the second argument is the user ID. +- `two_factor_before_authentication_prompt` action which receives the provider object and fires prior to the prompt shown on the authentication input form. +- `two_factor_after_authentication_prompt` action which receives the provider object and fires after the prompt shown on the authentication input form. +- `two_factor_after_authentication_input` action which receives the provider object and fires after the input shown on the authentication input form (if form contains no input, action fires immediately after `two_factor_after_authentication_prompt`). +- `two_factor_login_backup_links` filters the backup links displayed on the two-factor login form. + +== Redirect After the Two-Factor Challenge == + +To redirect users to a specific URL after completing the two-factor challenge, use WordPress Core built-in login_redirect filter. The filter works the same way as in a standard WordPress login flow: + + add_filter( 'login_redirect', function( $redirect_to, $requested_redirect_to, $user ) { + return home_url( '/dashboard/' ); + }, 10, 3 ); == Frequently Asked Questions == += What PHP and WordPress versions does the Two-Factor plugin support? = + +This plugin supports the last two major versions of WordPress and the minimum PHP version supported by those WordPress versions. + = How can I send feedback or get help with a bug? = The best place to report bugs, feature suggestions, or any other (non-security) feedback is at the Two Factor GitHub issues page. Before submitting a new issue, please search the existing issues to check if someone else has reported the same feedback. @@ -44,12 +127,133 @@ The plugin contributors and WordPress community take security bugs seriously. We To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program. += What if I lose access to all my authentication methods? = + +If you have backup codes enabled, you can use one of those to regain access. If you don't have backup codes or have used them all, you'll need to contact your site administrator to reset your account. This is why it's important to always enable backup codes and keep them in a secure location. + += Can I use this plugin with WebAuthn? = + +The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. There is an open issue to add WebAuthn support here: https://github.com/WordPress/two-factor/pull/427 + += Is there a recommended way to use passkeys or hardware security keys with Two-Factor? = + +Yes. For passkeys and hardware security keys, you can install the Two-Factor Provider: WebAuthn plugin: https://wordpress.org/plugins/two-factor-provider-webauthn/ +. It integrates directly with Two-Factor and adds WebAuthn-based authentication as an additional two-factor option for users. + == Screenshots == -1. Two-factor options under User Profile. -2. U2F Security Keys section under User Profile. -3. Email Code Authentication during WordPress Login. +1. Two-factor options under User Profile - Shows the main configuration area where users can enable different authentication methods. +2. Email Code Authentication during WordPress Login - Shows the email verification screen that appears during login. +3. Authenticator App (TOTP) setup with QR code - Demonstrates the QR code generation and manual key entry for TOTP setup. +4. Backup codes generation and management - Shows the backup codes interface for generating and managing emergency access codes. == Changelog == -See the [release history](https://github.com/wordpress/two-factor/releases). += 0.16.0 - 2026-03-27 = + +* **Breaking Changes:** Remove legacy FIDO U2F provider support by [#439](https://github.com/WordPress/two-factor/pull/439). +* **New Features:** Add a dedicated settings page for plugin configuration in wp-admin by [#764](https://github.com/WordPress/two-factor/pull/764). +* **New Features:** Add a support links filter so consumers can customize contextual recovery/help links by [#615](https://github.com/WordPress/two-factor/pull/615). +* **New Features:** Refresh backup codes UI styling and behavior by [#804](https://github.com/WordPress/two-factor/pull/804). +* **Bug Fixes:** Delete stored TOTP secrets when the TOTP provider is disabled by [#802](https://github.com/WordPress/two-factor/pull/802). +* **Bug Fixes:** Harden provider handling so login/settings checks do not fail open when expected providers disappear by [#586](https://github.com/WordPress/two-factor/pull/586). +* **Bug Fixes:** Ensure only configured providers are saved and enabled in user settings by [#798](https://github.com/WordPress/two-factor/pull/798). +* **Bug Fixes:** Improve settings-page accessibility and fix profile settings link behavior by [#828](https://github.com/WordPress/two-factor/pull/828) and [#830](https://github.com/WordPress/two-factor/pull/830). +* **Bug Fixes:** Resolve PHPCS violations in provider files by [#851](https://github.com/WordPress/two-factor/pull/851). +* **Development Updates:** Move login styles and provider scripts from inline output to enqueued/external assets by [#807](https://github.com/WordPress/two-factor/pull/807) and [#814](https://github.com/WordPress/two-factor/pull/814). +* **Development Updates:** Improve inline docs and static-analysis compatibility (WPCS/phpstan) by [#810](https://github.com/WordPress/two-factor/pull/810), [#815](https://github.com/WordPress/two-factor/pull/815), and [#817](https://github.com/WordPress/two-factor/pull/817). +* **Development Updates:** Improve unit test reliability and integrate CI code coverage reporting by [#825](https://github.com/WordPress/two-factor/pull/825), [#841](https://github.com/WordPress/two-factor/pull/841), and [#842](https://github.com/WordPress/two-factor/pull/842). +* **Development Updates:** Update readme docs and modernize CI workflow infrastructure by [#835](https://github.com/WordPress/two-factor/pull/835), [#837](https://github.com/WordPress/two-factor/pull/837), [#843](https://github.com/WordPress/two-factor/pull/843), and [#849](https://github.com/WordPress/two-factor/pull/849). +* **Dependency Updates:** Bump `qs` from 6.14.1 to 6.14.2 by [#794](https://github.com/WordPress/two-factor/pull/794). +* **Dependency Updates:** Bump `basic-ftp` from 5.0.5 to 5.2.0 by [#816](https://github.com/WordPress/two-factor/pull/816). +* **Dependency Updates:** Apply automatic lint/format updates and associated Composer package refreshes by [#799](https://github.com/WordPress/two-factor/pull/799). + += 0.15.0 - 2026-02-13 = + +* **Breaking Changes:** Trigger two-factor flow only when expected by @kasparsd in [#660](https://github.com/WordPress/two-factor/pull/660) and [#793](https://github.com/WordPress/two-factor/pull/793). +* **New Features:** Include user IP address and contextual warning in two-factor code emails by @todeveni in [#728](https://github.com/WordPress/two-factor/pull/728) +* **New Features:** Optimize email text for TOTP by @masteradhoc in [#789](https://github.com/WordPress/two-factor/pull/789) +* **New Features:** Add "Settings" action link to plugin list for quick access to profile by @hardikRathi in [#740](https://github.com/WordPress/two-factor/pull/740) +* **New Features:** Additional form hooks by @eric-michel in [#742](https://github.com/WordPress/two-factor/pull/742) +* **New Features:** Full RFC6238 Compatibility by @ericmann in [#656](https://github.com/WordPress/two-factor/pull/656) +* **New Features:** Consistent user experience for TOTP setup by @kasparsd in [#792](https://github.com/WordPress/two-factor/pull/792) +* **Documentation:** `@since` docs by @masteradhoc in [#781](https://github.com/WordPress/two-factor/pull/781) +* **Documentation:** Update user and admin docs, prepare for more screenshots by @jeffpaul in [#701](https://github.com/WordPress/two-factor/pull/701) +* **Documentation:** Add changelog & credits, update release notes by @jeffpaul in [#696](https://github.com/WordPress/two-factor/pull/696) +* **Documentation:** Clear readme.txt by @masteradhoc in [#785](https://github.com/WordPress/two-factor/pull/785) +* **Documentation:** Add date and time information above TOTP setup instructions by @masteradhoc in [#772](https://github.com/WordPress/two-factor/pull/772) +* **Documentation:** Clarify TOTP setup instructions by @masteradhoc in [#763](https://github.com/WordPress/two-factor/pull/763) +* **Documentation:** Update RELEASING.md by @jeffpaul in [#787](https://github.com/WordPress/two-factor/pull/787) +* **Development Updates:** Pause deploys to SVN trunk for merges to `master` by @kasparsd in [#738](https://github.com/WordPress/two-factor/pull/738) +* **Development Updates:** Fix CI checks for PHP compatability by @kasparsd in [#739](https://github.com/WordPress/two-factor/pull/739) +* **Development Updates:** Fix Playground refs by @kasparsd in [#744](https://github.com/WordPress/two-factor/pull/744) +* **Development Updates:** Persist existing translations when introducing new helper text in emails by @kasparsd in [#745](https://github.com/WordPress/two-factor/pull/745) +* **Development Updates:** Fix `missing_direct_file_access_protection` by @masteradhoc in [#760](https://github.com/WordPress/two-factor/pull/760) +* **Development Updates:** Fix `mismatched_plugin_name` by @masteradhoc in [#754](https://github.com/WordPress/two-factor/pull/754) +* **Development Updates:** Introduce Props Bot workflow by @jeffpaul in [#749](https://github.com/WordPress/two-factor/pull/749) +* **Development Updates:** Plugin Check: Fix Missing $domain parameter by @masteradhoc in [#753](https://github.com/WordPress/two-factor/pull/753) +* **Development Updates:** Tests: Update to supported WP version 6.8 by @masteradhoc in [#770](https://github.com/WordPress/two-factor/pull/770) +* **Development Updates:** Fix PHP 8.5 deprecated message by @masteradhoc in [#762](https://github.com/WordPress/two-factor/pull/762) +* **Development Updates:** Exclude 7.2 and 7.3 checks against trunk by @masteradhoc in [#769](https://github.com/WordPress/two-factor/pull/769) +* **Development Updates:** Fix Plugin Check errors: `MissingTranslatorsComment` & `MissingSingularPlaceholder` by @masteradhoc in [#758](https://github.com/WordPress/two-factor/pull/758) +* **Development Updates:** Add PHP 8.5 tests for latest and trunk version of WP by @masteradhoc in [#771](https://github.com/WordPress/two-factor/pull/771) +* **Development Updates:** Add `phpcs:ignore` for falsepositives by @masteradhoc in [#777](https://github.com/WordPress/two-factor/pull/777) +* **Development Updates:** Fix(totp): `otpauth` link in QR code URL by @sjinks in [#784](https://github.com/WordPress/two-factor/pull/784) +* **Development Updates:** Update deploy.yml by @masteradhoc in [#773](https://github.com/WordPress/two-factor/pull/773) +* **Development Updates:** Update required WordPress Version by @masteradhoc in [#765](https://github.com/WordPress/two-factor/pull/765) +* **Development Updates:** Fix: ensure execution stops after redirects by @sjinks in [#786](https://github.com/WordPress/two-factor/pull/786) +* **Development Updates:** Fix `WordPress.Security.EscapeOutput.OutputNotEscaped` errors by @masteradhoc in [#776](https://github.com/WordPress/two-factor/pull/776) +* **Dependency Updates:** Bump qs and express by @dependabot[bot] in [#746](https://github.com/WordPress/two-factor/pull/746) +* **Dependency Updates:** Bump lodash from 4.17.21 to 4.17.23 by @dependabot[bot] in [#750](https://github.com/WordPress/two-factor/pull/750) +* **Dependency Updates:** Bump lodash-es from 4.17.21 to 4.17.23 by @dependabot[bot] in [#748](https://github.com/WordPress/two-factor/pull/748) +* **Dependency Updates:** Bump phpunit/phpunit from 8.5.44 to 8.5.52 by @dependabot[bot] in [#755](https://github.com/WordPress/two-factor/pull/755) +* **Dependency Updates:** Bump symfony/process from 5.4.47 to 5.4.51 by @dependabot[bot] in [#756](https://github.com/WordPress/two-factor/pull/756) +* **Dependency Updates:** Bump qs and body-parser by @dependabot[bot] in [#782](https://github.com/WordPress/two-factor/pull/782) +* **Dependency Updates:** Bump webpack from 5.101.3 to 5.105.0 by @dependabot[bot] in [#780](https://github.com/WordPress/two-factor/pull/780) + += 0.14.2 - 2025-12-11 = + +* **New Features:** Add filter for rest_api_can_edit_user_and_update_two_factor_options by @gutobenn in [#689](https://github.com/WordPress/two-factor/pull/689) +* **Development Updates:** Remove Coveralls tooling and add inline coverage report by @kasparsd in [#717](https://github.com/WordPress/two-factor/pull/717) +* **Development Updates:** Update blueprint path to pull from main branch instead of a deleted f… by @georgestephanis in [#719](https://github.com/WordPress/two-factor/pull/719) +* **Development Updates:** Fix blueprint and wporg asset deploys by @kasparsd in [#734](https://github.com/WordPress/two-factor/pull/734) +* **Development Updates:** Upload release only on tag releases by @kasparsd in [#735](https://github.com/WordPress/two-factor/pull/735) +* **Development Updates:** Bump playwright and @playwright/test by @dependabot[bot] in [#721](https://github.com/WordPress/two-factor/pull/721) +* **Development Updates:** Bump tar-fs from 3.1.0 to 3.1.1 by @dependabot[bot] in [#720](https://github.com/WordPress/two-factor/pull/720) +* **Development Updates:** Bump node-forge from 1.3.1 to 1.3.2 by @dependabot[bot] in [#724](https://github.com/WordPress/two-factor/pull/724) +* **Development Updates:** Bump js-yaml by @dependabot[bot] in [#725](https://github.com/WordPress/two-factor/pull/725) +* **Development Updates:** Mark as tested with the latest WP core version by @kasparsd in [#730](https://github.com/WordPress/two-factor/pull/730) + += 0.14.1 - 2025-09-05 = + +- Don't URI encode the TOTP url for display. by @dd32 in [#711](https://github.com/WordPress/two-factor/pull/711) +- Removed the duplicate Security.md by @slvignesh05 in [#712](https://github.com/WordPress/two-factor/pull/712) +- Fixed linting issues by @sudar in [#707](https://github.com/WordPress/two-factor/pull/707) +- Update development dependencies and fix failing QR unit test by @kasparsd in [#714](https://github.com/WordPress/two-factor/pull/714) +- Trigger checkbox js change event by @gedeminas in [#688](https://github.com/WordPress/two-factor/pull/688) + += 0.14.0 - 2025-07-03 = + +* **Features:** Enable Application Passwords for REST API and XML-RPC authentication (by default) by @joostdekeijzer in [#697](https://github.com/WordPress/two-factor/pull/697) and [#698](https://github.com/WordPress/two-factor/pull/698). Previously this required two_factor_user_api_login_enable filter to be set to true which is now the default during application password auth. XML-RPC login is still disabled for regular user passwords. +* **Features:** Label recommended methods to simplify the configuration by @kasparsd in [#676](https://github.com/WordPress/two-factor/pull/676) and [#675](https://github.com/WordPress/two-factor/pull/675) +* **Documentation:** Add WP.org plugin demo by @kasparsd in [#667](https://github.com/WordPress/two-factor/pull/667) +* **Documentation:** Document supported versions of WP core and PHP by @jeffpaul in [#695](https://github.com/WordPress/two-factor/pull/695) +* **Documentation:** Document the release process by @jeffpaul in [#684](https://github.com/WordPress/two-factor/pull/684) +* **Tooling:** Remove duplicate WP.org screenshots and graphics from SVN trunk by @jeffpaul in [#683](https://github.com/WordPress/two-factor/pull/683) + += 0.13.0 - 2025-04-02 = + +- Add two_factor_providers_for_user filter to limit two-factor providers available to each user by @kasparsd in [#669](https://github.com/WordPress/two-factor/pull/669) +- Update automated testing to cover PHP 8.4 and default to PHP 8.3 by @BrookeDot in [#665](https://github.com/WordPress/two-factor/pull/665) + +[View the complete changelog details here](https://github.com/wordpress/two-factor/blob/master/CHANGELOG.md). + +== Upgrade Notice == + += 0.10.0 = +Bumps WordPress minimum supported version to 6.3 and PHP minimum to 7.2. + += 0.9.0 = +Users are now asked to re-authenticate with their two-factor before making changes to their two-factor settings. This associates each login session with the two-factor login meta data for improved handling of that session. + + diff --git a/wp-content/plugins/two-factor/settings/class-two-factor-settings.php b/wp-content/plugins/two-factor/settings/class-two-factor-settings.php new file mode 100644 index 00000000..48018835 --- /dev/null +++ b/wp-content/plugins/two-factor/settings/class-two-factor-settings.php @@ -0,0 +1,97 @@ +

    ' . esc_html__( 'Settings saved.', 'two-factor' ) . '

    '; + } + + // Build provider list for display using public core API. + $provider_instances = array(); + if ( class_exists( 'Two_Factor_Core' ) && method_exists( 'Two_Factor_Core', 'get_providers' ) ) { + $provider_instances = Two_Factor_Core::get_providers(); + if ( ! is_array( $provider_instances ) ) { + $provider_instances = array(); + } + } + + // Default to all providers enabled when the option has never been saved. + $all_provider_keys = array_keys( $provider_instances ); + $saved_enabled = get_option( 'two_factor_enabled_providers', $all_provider_keys ); + + echo '
    '; + echo '

    ' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '

    '; + echo '

    ' . esc_html__( 'Enabled Providers', 'two-factor' ) . '

    '; + echo '

    ' . esc_html__( 'Choose which Two-Factor providers are available on this site. All providers are enabled by default.', 'two-factor' ) . '

    '; + echo '
    '; + wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' ); + + echo '
    ' . esc_html__( 'Providers', 'two-factor' ) . ''; + echo ''; + + if ( empty( $provider_instances ) ) { + echo ''; + } else { + // Render a compact stacked list of provider checkboxes below the title/description. + echo ''; + echo ''; + echo ''; + } + + echo '
    ' . esc_html__( 'No providers found.', 'two-factor' ) . '
    '; + foreach ( $provider_instances as $provider_key => $instance ) { + $label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key; + + echo '

    '; + } + + echo '
    '; + echo '
    '; + + submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' ); + echo '
    '; + + echo '
    '; + } +} diff --git a/wp-content/plugins/two-factor/two-factor.php b/wp-content/plugins/two-factor/two-factor.php index 2221a75d..b1e4eca4 100644 --- a/wp-content/plugins/two-factor/two-factor.php +++ b/wp-content/plugins/two-factor/two-factor.php @@ -10,9 +10,9 @@ * @wordpress-plugin * Plugin Name: Two Factor * Plugin URI: https://wordpress.org/plugins/two-factor/ - * Description: Enable Two-Factor Authentication using time-based one-time passwords, Universal 2nd Factor (FIDO U2F, YubiKey), email, and backup verification codes. - * Version: 0.13.0 - * Requires at least: 6.3 + * Description: Enable Two-Factor Authentication using time-based one-time passwords, email, and backup verification codes. + * Requires at least: 6.8 + * Version: 0.16.0 * Requires PHP: 7.2 * Author: WordPress.org Contributors * Author URI: https://github.com/wordpress/two-factor/graphs/contributors @@ -22,6 +22,10 @@ * Network: True */ +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +} + /** * Shortcut constant to the path of this file. */ @@ -30,7 +34,7 @@ define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) ); /** * Version of the plugin. */ -define( 'TWO_FACTOR_VERSION', '0.13.0' ); +define( 'TWO_FACTOR_VERSION', '0.16.0' ); /** * Include the base class here, so that other plugins can also extend it. @@ -47,9 +51,140 @@ require_once TWO_FACTOR_DIR . 'class-two-factor-core.php'; */ require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php'; +// Load settings UI class so the settings page can be rendered. +require_once TWO_FACTOR_DIR . 'settings/class-two-factor-settings.php'; + $two_factor_compat = new Two_Factor_Compat(); Two_Factor_Core::add_hooks( $two_factor_compat ); // Delete our options and user meta during uninstall. register_uninstall_hook( __FILE__, array( Two_Factor_Core::class, 'uninstall' ) ); + +/** + * Register admin menu and plugin action links. + * + * @since 0.16 + */ +function two_factor_register_admin_hooks() { + if ( is_admin() ) { + add_action( 'admin_menu', 'two_factor_add_settings_page' ); + } + + // Load settings page assets when in admin. + // Settings assets handled inline via standard markup; no extra CSS enqueued. + + /* Enforcement filters: restrict providers based on saved enabled-providers option. */ + add_filter( 'two_factor_providers', 'two_factor_filter_enabled_providers' ); + add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_filter_enabled_providers_for_user', 10, 2 ); +} + +add_action( 'init', 'two_factor_register_admin_hooks' ); + +/** + * Add the Two Factor settings page under Settings. + * + * @since 0.16 + */ +function two_factor_add_settings_page() { + add_options_page( + __( 'Two-Factor Settings', 'two-factor' ), + __( 'Two-Factor', 'two-factor' ), + 'manage_options', + 'two-factor-settings', + 'two_factor_render_settings_page' + ); +} + + +/** + * Render the settings page via the settings class if available. + * + * @since 0.16 + */ +function two_factor_render_settings_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Prefer new settings class (keeps main file small). + if ( class_exists( 'Two_Factor_Settings' ) && is_callable( array( 'Two_Factor_Settings', 'render_settings_page' ) ) ) { + Two_Factor_Settings::render_settings_page(); + return; + } + + // Fallback: no UI available. + echo '

    ' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '

    '; + echo '

    ' . esc_html__( 'Settings not available.', 'two-factor' ) . '

    '; +} + + +/** + * Helper: retrieve the site-enabled providers option. + * Returns null when the option has never been saved (meaning all providers are allowed). + * Returns an array (possibly empty) when the admin has explicitly saved a selection. + * + * @since 0.16 + * + * @return array|null + */ +function two_factor_get_enabled_providers_option() { + $enabled = get_option( 'two_factor_enabled_providers', null ); + if ( null === $enabled ) { + return null; // Never saved — allow everything. + } + return is_array( $enabled ) ? $enabled : array(); +} + + +/** + * Filter the registered providers to only those in the site-enabled list. + * This filter receives providers in core format: classname => path. + * + * @since 0.16 + * + * @param array $providers Registered providers in classname => path format. + * @return array Filtered list of enabled providers. + */ +function two_factor_filter_enabled_providers( $providers ) { + $site_enabled = two_factor_get_enabled_providers_option(); + + // null means the option was never saved — allow all providers. + if ( null === $site_enabled ) { + return $providers; + } + + // On the settings page itself, show all providers so admins can change the selection. + if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) { + return $providers; + } + + foreach ( $providers as $key => $path ) { + if ( ! in_array( $key, $site_enabled, true ) ) { + unset( $providers[ $key ] ); + } + } + + return $providers; +} + + +/** + * Filter enabled providers for a user (classnames array) to enforce the site-enabled list. + * + * @since 0.16 + * + * @param array $enabled Enabled provider classnames for the user. + * @param int $user_id ID of the user being filtered. + * @return array Filtered list of provider classnames allowed by the site. + */ +function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) { + $site_enabled = two_factor_get_enabled_providers_option(); + + // null means the option was never saved — allow all. + if ( null === $site_enabled ) { + return $enabled; + } + + return array_values( array_intersect( (array) $enabled, $site_enabled ) ); +} diff --git a/wp-content/plugins/two-factor/user-edit.css b/wp-content/plugins/two-factor/user-edit.css index 4280ce99..83cef064 100644 --- a/wp-content/plugins/two-factor/user-edit.css +++ b/wp-content/plugins/two-factor/user-edit.css @@ -7,3 +7,70 @@ display: block; font-weight: 700; } + +.two-factor-methods-table .two-factor-method-recommended { + font-size: 0.8rem; + line-height: 1; + font-weight: 400; + border: 1px dotted; + border-radius: 0.15rem; + padding: 0.1rem 0.25rem; + margin: 0 0.15rem; +} + +#login .backup-methods-wrap { + margin-top: 16px; + padding: 0 24px; +} + +#login .backup-methods-wrap a { + text-decoration: none; +} + +#login .backup-methods-wrap ul { + list-style-position: inside; +} + +/* Prevent Jetpack from hiding our controls, see https://github.com/Automattic/jetpack/issues/3747 */ +.jetpack-sso-form-display #loginform > p, +.jetpack-sso-form-display #loginform > div { + display: block; +} + +#login form p.two-factor-prompt { + margin-bottom: 1em; +} + +#loginform .input.authcode { + letter-spacing: 0.3em; +} + +#loginform .input.authcode::placeholder { + opacity: 0.5; +} + +.two-factor-backup-codes-wrapper > :not(:last-child) { + margin-bottom: 1em; +} + +.two-factor-backup-codes-list-wrap { + background-color: #ddd; + display: inline-block; + margin-top: 24px; + padding: 20px; +} + +.two-factor-backup-codes-list-wrap .two-factor-backup-codes-unused-codes { + margin: 0; + padding: revert; +} + +.two-factor-backup-codes-list-wrap .two-factor-backup-codes-token { + letter-spacing: 0.3em; + font-family: monospace; +} + +#two-factor-qr-code { + min-width: 205px; + min-height: 205px; +}