diff --git a/wp-content/plugins/two-factor/assets/banner-1544x500.png b/wp-content/plugins/two-factor/assets/banner-1544x500.png new file mode 100644 index 0000000000000000000000000000000000000000..5b6e2081cf6d604d3b27df7f2bf1bd3554056698 Binary files /dev/null and b/wp-content/plugins/two-factor/assets/banner-1544x500.png differ diff --git a/wp-content/plugins/two-factor/assets/banner-772x250.png b/wp-content/plugins/two-factor/assets/banner-772x250.png new file mode 100644 index 0000000000000000000000000000000000000000..b955eeeb3cd3c5cdcdfaafe866ec76b9acc8041c Binary files /dev/null and b/wp-content/plugins/two-factor/assets/banner-772x250.png differ diff --git a/wp-content/plugins/two-factor/assets/icon-128x128.png b/wp-content/plugins/two-factor/assets/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..1f3a3c314d553dec31fb6b3b788d855aa62bab6f Binary files /dev/null and b/wp-content/plugins/two-factor/assets/icon-128x128.png differ diff --git a/wp-content/plugins/two-factor/assets/icon-256x256.png b/wp-content/plugins/two-factor/assets/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..3240b832c92997756aaf75e7045372ceaef18272 Binary files /dev/null and b/wp-content/plugins/two-factor/assets/icon-256x256.png differ diff --git a/wp-content/plugins/two-factor/assets/icon.svg b/wp-content/plugins/two-factor/assets/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..cc15690b795c8af5bcf5b715a681aaaf999c85e3 --- /dev/null +++ b/wp-content/plugins/two-factor/assets/icon.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"> + <g fill="none" fill-rule="evenodd"> + <path fill="#CCC" d="M98 150a60 60 0 1 1 60 0v60a8 8 0 0 1-8 8h-44a8 8 0 0 1-8-8v-60z"/> + <path fill="#0073AA" d="M116 132a36 36 0 1 1 24 0v64.7a4 4 0 0 1-4 4h-16a4 4 0 0 1-4-4v-64-.7z"/> + </g> +</svg> diff --git a/wp-content/plugins/two-factor/assets/screenshot-1.png b/wp-content/plugins/two-factor/assets/screenshot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..545b45ee70dea58f9fd70c7fee3e85ba67fdb683 Binary files /dev/null and b/wp-content/plugins/two-factor/assets/screenshot-1.png differ diff --git a/wp-content/plugins/two-factor/assets/screenshot-2.png b/wp-content/plugins/two-factor/assets/screenshot-2.png new file mode 100644 index 0000000000000000000000000000000000000000..b98358007515bf6fb0640a487737a1e5b3619e4a Binary files /dev/null and b/wp-content/plugins/two-factor/assets/screenshot-2.png differ diff --git a/wp-content/plugins/two-factor/class.two-factor-core.php b/wp-content/plugins/two-factor/class.two-factor-core.php new file mode 100644 index 0000000000000000000000000000000000000000..58d179b6561460aded06f8701ea34a3aceb54cf1 --- /dev/null +++ b/wp-content/plugins/two-factor/class.two-factor-core.php @@ -0,0 +1,812 @@ +<?php +/** + * Class for creating two factor authorization. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +class Two_Factor_Core { + + /** + * The user meta provider key. + * + * @type string + */ + const PROVIDER_USER_META_KEY = '_two_factor_provider'; + + /** + * The user meta enabled providers key. + * + * @type string + */ + const ENABLED_PROVIDERS_USER_META_KEY = '_two_factor_enabled_providers'; + + /** + * The user meta nonce key. + * + * @type string + */ + const USER_META_NONCE_KEY = '_two_factor_nonce'; + + /** + * Set up filters and actions. + * + * @since 0.1-dev + */ + public static function add_hooks() { + add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) ); + add_action( 'init', array( __CLASS__, 'get_providers' ) ); + add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 ); + add_action( 'login_form_validate_2fa', array( __CLASS__, 'login_form_validate_2fa' ) ); + add_action( 'login_form_backup_2fa', array( __CLASS__, 'backup_2fa' ) ); + add_action( 'show_user_profile', array( __CLASS__, 'user_two_factor_options' ) ); + add_action( 'edit_user_profile', array( __CLASS__, 'user_two_factor_options' ) ); + add_action( 'personal_options_update', array( __CLASS__, 'user_two_factor_options_update' ) ); + add_action( 'edit_user_profile_update', array( __CLASS__, 'user_two_factor_options_update' ) ); + add_filter( 'manage_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) ); + add_filter( 'wpmu_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) ); + add_filter( 'manage_users_custom_column', array( __CLASS__, 'manage_users_custom_column' ), 10, 3 ); + + // Run only after the core wp_authenticate_username_password() check. + add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate' ), 50 ); + } + + /** + * Loads the plugin's text domain. + * + * Sites on WordPress 4.6+ benefit from just-in-time loading of translations. + */ + public static function load_textdomain() { + load_plugin_textdomain( 'two-factor' ); + } + + /** + * For each provider, include it and then instantiate it. + * + * @since 0.1-dev + * + * @return array + */ + public static function get_providers() { + $providers = 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', + ); + + /** + * Filter the supplied providers. + * + * This lets third-parties either remove providers (such as Email), or + * add their own providers (such as text message or Clef). + * + * @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( sprintf( // WPCS: XSS OK. + /* translators: %s: version number */ + __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), + PHP_VERSION + ) ); + } + + /** + * For each filtered provider, + */ + foreach ( $providers as $class => $path ) { + include_once( $path ); + + /** + * Confirm that it's been successfully included before instantiating. + */ + if ( class_exists( $class ) ) { + try { + $providers[ $class ] = call_user_func( array( $class, 'get_instance' ) ); + } catch ( Exception $e ) { + unset( $providers[ $class ] ); + } + } + } + + return $providers; + } + + /** + * Get all Two-Factor Auth providers that are enabled for the specified|current user. + * + * @param WP_User $user WP_User object of the logged-in user. + * @return array + */ + public static function get_enabled_providers_for_user( $user = null ) { + if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) { + $user = wp_get_current_user(); + } + + $providers = self::get_providers(); + $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true ); + if ( empty( $enabled_providers ) ) { + $enabled_providers = array(); + } + $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) ); + + return $enabled_providers; + } + + /** + * Get all Two-Factor Auth providers that are both enabled and configured for the specified|current user. + * + * @param WP_User $user WP_User object of the logged-in user. + * @return array + */ + public static function get_available_providers_for_user( $user = null ) { + if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) { + $user = wp_get_current_user(); + } + + $providers = self::get_providers(); + $enabled_providers = self::get_enabled_providers_for_user( $user ); + $configured_providers = array(); + + foreach ( $providers as $classname => $provider ) { + if ( in_array( $classname, $enabled_providers ) && $provider->is_available_for_user( $user ) ) { + $configured_providers[ $classname ] = $provider; + } + } + + return $configured_providers; + } + + /** + * Gets the Two-Factor Auth provider for the specified|current user. + * + * @since 0.1-dev + * + * @param int $user_id Optional. User ID. Default is 'null'. + * @return object|null + */ + public static function get_primary_provider_for_user( $user_id = null ) { + if ( empty( $user_id ) || ! is_numeric( $user_id ) ) { + $user_id = get_current_user_id(); + } + + $providers = self::get_providers(); + $available_providers = self::get_available_providers_for_user( get_userdata( $user_id ) ); + + // If there's only one available provider, force that to be the primary. + if ( empty( $available_providers ) ) { + return null; + } elseif ( 1 === count( $available_providers ) ) { + $provider = key( $available_providers ); + } else { + $provider = get_user_meta( $user_id, self::PROVIDER_USER_META_KEY, true ); + + // If the provider specified isn't enabled, just grab the first one that is. + if ( ! isset( $available_providers[ $provider ] ) ) { + $provider = key( $available_providers ); + } + } + + /** + * Filter the two-factor authentication provider used for this user. + * + * @param string $provider The provider currently being used. + * @param int $user_id The user ID. + */ + $provider = apply_filters( 'two_factor_primary_provider_for_user', $provider, $user_id ); + + if ( isset( $providers[ $provider ] ) ) { + return $providers[ $provider ]; + } + + return null; + } + + /** + * Quick boolean check for whether a given user is using two-step. + * + * @since 0.1-dev + * + * @param int $user_id Optional. User ID. Default is 'null'. + * @return bool + */ + public static function is_user_using_two_factor( $user_id = null ) { + $provider = self::get_primary_provider_for_user( $user_id ); + return ! empty( $provider ); + } + + /** + * Handle the browser-based login. + * + * @since 0.1-dev + * + * @param string $user_login Username. + * @param WP_User $user WP_User object of the logged-in user. + */ + public static function wp_login( $user_login, $user ) { + if ( ! self::is_user_using_two_factor( $user->ID ) ) { + return; + } + + wp_clear_auth_cookie(); + + self::show_two_factor_login( $user ); + exit; + } + + /** + * Prevent login through XML-RPC and REST API for users with at least one + * two-factor method enabled. + * + * @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; + } + + /** + * If the current user can login via API requests such as XML-RPC and REST. + * + * @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 ); + } + + /** + * Is the current request an XML-RPC or REST request. + * + * @return boolean + */ + public static function is_api_request() { + if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { + return true; + } + + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return true; + } + + return false; + } + + /** + * Display the login form. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + */ + public static function show_two_factor_login( $user ) { + if ( ! $user ) { + $user = wp_get_current_user(); + } + + $login_nonce = self::create_login_nonce( $user->ID ); + if ( ! $login_nonce ) { + wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) ); + } + + $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url(); + + self::login_html( $user, $login_nonce['key'], $redirect_to ); + } + + /** + * Display the Backup code 2fa screen. + * + * @since 0.1-dev + */ + public static function backup_2fa() { + if ( ! isset( $_GET['wp-auth-id'], $_GET['wp-auth-nonce'], $_GET['provider'] ) ) { + return; + } + + $user = get_userdata( $_GET['wp-auth-id'] ); + if ( ! $user ) { + return; + } + + $nonce = $_GET['wp-auth-nonce']; + if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) { + wp_safe_redirect( get_bloginfo( 'url' ) ); + exit; + } + + $providers = self::get_available_providers_for_user( $user ); + if ( isset( $providers[ $_GET['provider'] ] ) ) { + $provider = $providers[ $_GET['provider'] ]; + } else { + wp_die( esc_html__( 'Cheatin’ uh?', 'two-factor' ), 403 ); + } + + self::login_html( $user, $_GET['wp-auth-nonce'], $_GET['redirect_to'], '', $provider ); + + exit; + } + + /** + * Generates the html form for the second step of the authentication process. + * + * @since 0.1-dev + * + * @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. + */ + public static function login_html( $user, $login_nonce, $redirect_to, $error_msg = '', $provider = null ) { + if ( empty( $provider ) ) { + $provider = self::get_primary_provider_for_user( $user->ID ); + } elseif ( is_string( $provider ) && method_exists( $provider, 'get_instance' ) ) { + $provider = call_user_func( array( $provider, 'get_instance' ) ); + } + + $provider_class = get_class( $provider ); + + $available_providers = self::get_available_providers_for_user( $user ); + $backup_providers = array_diff_key( $available_providers, array( $provider_class => null ) ); + $interim_login = isset( $_REQUEST['interim-login'] ); // WPCS: CSRF ok. + + $rememberme = 0; + if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) { + $rememberme = 1; + } + + 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. + include_once( TWO_FACTOR_DIR . 'includes/function.login-header.php' ); + } + + login_header(); + + if ( ! empty( $error_msg ) ) { + echo '<div id="login_error"><strong>' . esc_html( $error_msg ) . '</strong><br /></div>'; + } + ?> + + <form name="validate_2fa_form" id="loginform" action="<?php echo esc_url( self::login_url( array( 'action' => 'validate_2fa' ), 'login_post' ) ); ?>" method="post" autocomplete="off"> + <input type="hidden" name="provider" id="provider" value="<?php echo esc_attr( $provider_class ); ?>" /> + <input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>" /> + <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr( $login_nonce ); ?>" /> + <?php if ( $interim_login ) { ?> + <input type="hidden" name="interim-login" value="1" /> + <?php } else { ?> + <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" /> + <?php } ?> + <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>" /> + + <?php $provider->authentication_page( $user ); ?> + </form> + + <?php + if ( 1 === count( $backup_providers ) ) : + $backup_classname = key( $backup_providers ); + $backup_provider = $backup_providers[ $backup_classname ]; + $login_url = self::login_url( + array( + 'action' => 'backup_2fa', + 'provider' => $backup_classname, + 'wp-auth-id' => $user->ID, + 'wp-auth-nonce' => $login_nonce, + 'redirect_to' => $redirect_to, + 'rememberme' => $rememberme, + ) + ); + ?> + <div class="backup-methods-wrap"> + <p class="backup-methods"> + <a href="<?php echo esc_url( $login_url ); ?>"> + <?php + echo esc_html( + sprintf( + // translators: %s: Two-factor method name. + __( 'Or, use your backup method: %s →', 'two-factor' ), + $backup_provider->get_label() + ) + ); + ?> + </a> + </p> + </div> + <?php elseif ( 1 < count( $backup_providers ) ) : ?> + <div class="backup-methods-wrap"> + <p class="backup-methods"> + <a href="javascript:;" onclick="document.querySelector('ul.backup-methods').style.display = 'block';"> + <?php esc_html_e( 'Or, use a backup method…', 'two-factor' ); ?> + </a> + </p> + <ul class="backup-methods"> + <?php + foreach ( $backup_providers as $backup_classname => $backup_provider ) : + $login_url = self::login_url( + array( + 'action' => 'backup_2fa', + 'provider' => $backup_classname, + 'wp-auth-id' => $user->ID, + 'wp-auth-nonce' => $login_nonce, + 'redirect_to' => $redirect_to, + 'rememberme' => $rememberme, + ) + ); + ?> + <li> + <a href="<?php echo esc_url( $login_url ); ?>"> + <?php $backup_provider->print_label(); ?> + </a> + </li> + <?php endforeach; ?> + </ul> + </div> + <?php endif; ?> + + <p id="backtoblog"> + <a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'two-factor' ); ?>"> + <?php + echo esc_html( + sprintf( + // translators: %s: site name. + __( '← Back to %s', 'two-factor' ), + get_bloginfo( 'title', 'display' ) + ) + ); + ?> + </a> + </p> + </div> + <style> + /* @todo: migrate to an external stylesheet. */ + .backup-methods-wrap { + margin-top: 16px; + padding: 0 24px; + } + .backup-methods-wrap a { + color: #999; + text-decoration: none; + } + ul.backup-methods { + display: none; + padding-left: 1.5em; + } + /* 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; + } + </style> + + <?php + /** This action is documented in wp-login.php */ + do_action( 'login_footer' ); ?> + <div class="clear"></div> + </body> + </html> + <?php + } + + /** + * Generate the two-factor login form URL. + * + * @param array $params List of query argument pairs to add to the URL. + * @param string $scheme URL scheme context. + * + * @return string + */ + public static function login_url( $params = array(), $scheme = 'login' ) { + if ( ! is_array( $params ) ) { + $params = array(); + } + + $params = urlencode_deep( $params ); + + return add_query_arg( $params, site_url( 'wp-login.php', $scheme ) ); + } + + /** + * Create the login nonce. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + * @return array + */ + public static function create_login_nonce( $user_id ) { + $login_nonce = array(); + try { + $login_nonce['key'] = bin2hex( random_bytes( 32 ) ); + } catch (Exception $ex) { + $login_nonce['key'] = wp_hash( $user_id . mt_rand() . microtime(), 'nonce' ); + } + $login_nonce['expiration'] = time() + HOUR_IN_SECONDS; + + if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce ) ) { + return false; + } + + return $login_nonce; + } + + /** + * Delete the login nonce. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + * @return bool + */ + public static function delete_login_nonce( $user_id ) { + return delete_user_meta( $user_id, self::USER_META_NONCE_KEY ); + } + + /** + * Verify the login nonce. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + * @param string $nonce Login nonce. + * @return bool + */ + public static function verify_login_nonce( $user_id, $nonce ) { + $login_nonce = get_user_meta( $user_id, self::USER_META_NONCE_KEY, true ); + if ( ! $login_nonce ) { + return false; + } + + if ( $nonce !== $login_nonce['key'] || time() > $login_nonce['expiration'] ) { + self::delete_login_nonce( $user_id ); + return false; + } + + return true; + } + + /** + * Login form validation. + * + * @since 0.1-dev + */ + public static function login_form_validate_2fa() { + if ( ! isset( $_POST['wp-auth-id'], $_POST['wp-auth-nonce'] ) ) { + return; + } + + $user = get_userdata( $_POST['wp-auth-id'] ); + if ( ! $user ) { + return; + } + + $nonce = $_POST['wp-auth-nonce']; + if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) { + wp_safe_redirect( get_bloginfo( 'url' ) ); + exit; + } + + if ( isset( $_POST['provider'] ) ) { + $providers = self::get_available_providers_for_user( $user ); + if ( isset( $providers[ $_POST['provider'] ] ) ) { + $provider = $providers[ $_POST['provider'] ]; + } else { + wp_die( esc_html__( 'Cheatin’ uh?', 'two-factor' ), 403 ); + } + } else { + $provider = self::get_primary_provider_for_user( $user->ID ); + } + + // Allow the provider to re-send codes, etc. + if ( true === $provider->pre_process_authentication( $user ) ) { + $login_nonce = self::create_login_nonce( $user->ID ); + if ( ! $login_nonce ) { + wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) ); + } + + self::login_html( $user, $login_nonce['key'], $_REQUEST['redirect_to'], '', $provider ); + exit; + } + + // Ask the provider to verify the second factor. + if ( true !== $provider->validate_authentication( $user ) ) { + do_action( 'wp_login_failed', $user->user_login ); + + $login_nonce = self::create_login_nonce( $user->ID ); + if ( ! $login_nonce ) { + wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) ); + } + + self::login_html( $user, $login_nonce['key'], $_REQUEST['redirect_to'], esc_html__( 'ERROR: Invalid verification code.', 'two-factor' ), $provider ); + exit; + } + + self::delete_login_nonce( $user->ID ); + + $rememberme = false; + if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) { + $rememberme = true; + } + + wp_set_auth_cookie( $user->ID, $rememberme ); + + // Must be global because that's how login_header() uses it. + global $interim_login; + $interim_login = isset( $_REQUEST['interim-login'] ); // WPCS: override ok. + + if ( $interim_login ) { + $customize_login = isset( $_REQUEST['customize-login'] ); + if ( $customize_login ) { + wp_enqueue_script( 'customize-base' ); + } + $message = '<p class="message">' . __( 'You have logged in successfully.', 'two-factor' ) . '</p>'; + $interim_login = 'success'; // WPCS: override ok. + login_header( '', $message ); ?> + </div> + <?php + /** This action is documented in wp-login.php */ + do_action( 'login_footer' ); ?> + <?php if ( $customize_login ) : ?> + <script type="text/javascript">setTimeout( function(){ new wp.customize.Messenger({ url: '<?php echo wp_customize_url(); /* WPCS: XSS OK. */ ?>', channel: 'login' }).send('login') }, 1000 );</script> + <?php endif; ?> + </body></html> + <?php + exit; + } + $redirect_to = apply_filters( 'login_redirect', $_REQUEST['redirect_to'], $_REQUEST['redirect_to'], $user ); + wp_safe_redirect( $redirect_to ); + + exit; + } + + /** + * Filter the columns on the Users admin screen. + * + * @param array $columns Available columns. + * @return array Updated array of columns. + */ + public static function filter_manage_users_columns( array $columns ) { + $columns['two-factor'] = __( 'Two-Factor', 'two-factor' ); + return $columns; + } + + /** + * Output the 2FA column data on the Users screen. + * + * @param string $output The column output. + * @param string $column_name The column ID. + * @param int $user_id The user ID. + * @return string The column output. + */ + public static function manage_users_custom_column( $output, $column_name, $user_id ) { + + if ( 'two-factor' !== $column_name ) { + return $output; + } + + if ( ! self::is_user_using_two_factor( $user_id ) ) { + return sprintf( '<span class="dashicons-before dashicons-no-alt">%s</span>', esc_html__( 'Disabled', 'two-factor' ) ); + } else { + $provider = self::get_primary_provider_for_user( $user_id ); + return esc_html( $provider->get_label() ); + } + + } + + /** + * Add user profile fields. + * + * This executes during the `show_user_profile` & `edit_user_profile` actions. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + */ + public static function user_two_factor_options( $user ) { + wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ) ); + + $enabled_providers = array_keys( self::get_available_providers_for_user( $user ) ); + $primary_provider = self::get_primary_provider_for_user( $user->ID ); + + if ( ! empty( $primary_provider ) && is_object( $primary_provider ) ) { + $primary_provider_key = get_class( $primary_provider ); + } else { + $primary_provider_key = null; + } + + wp_nonce_field( 'user_two_factor_options', '_nonce_user_two_factor_options', false ); + + ?> + <input type="hidden" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php /* Dummy input so $_POST value is passed when no providers are enabled. */ ?>" /> + <table class="form-table" id="two-factor-options"> + <tr> + <th> + <?php esc_html_e( 'Two-Factor Options', 'two-factor' ); ?> + </th> + <td> + <table class="two-factor-methods-table"> + <thead> + <tr> + <th class="col-enabled" scope="col"><?php esc_html_e( 'Enabled', 'two-factor' ); ?></th> + <th class="col-primary" scope="col"><?php esc_html_e( 'Primary', 'two-factor' ); ?></th> + <th class="col-name" scope="col"><?php esc_html_e( 'Name', 'two-factor' ); ?></th> + </tr> + </thead> + <tbody> + <?php foreach ( self::get_providers() as $class => $object ) : ?> + <tr> + <th scope="row"><input type="checkbox" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php echo esc_attr( $class ); ?>" <?php checked( in_array( $class, $enabled_providers ) ); ?> /></th> + <th scope="row"><input type="radio" name="<?php echo esc_attr( self::PROVIDER_USER_META_KEY ); ?>" value="<?php echo esc_attr( $class ); ?>" <?php checked( $class, $primary_provider_key ); ?> /></th> + <td> + <?php $object->print_label(); ?> + <?php do_action( 'two-factor-user-options-' . $class, $user ); ?> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + </td> + </tr> + </table> + <?php + /** + * Fires after the Two Factor methods table. + * + * To be used by Two Factor methods to add settings UI. + * + * @since 0.1-dev + */ + do_action( 'show_user_security_settings', $user ); + } + + /** + * Update the user meta value. + * + * This executes during the `personal_options_update` & `edit_user_profile_update` actions. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + */ + public static function user_two_factor_options_update( $user_id ) { + if ( isset( $_POST['_nonce_user_two_factor_options'] ) ) { + check_admin_referer( 'user_two_factor_options', '_nonce_user_two_factor_options' ); + + if ( ! isset( $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ] ) || + ! is_array( $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ] ) ) { + return; + } + + $providers = self::get_providers(); + + $enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ]; + + // 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 ); + + // 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 ) ) { + update_user_meta( $user_id, self::PROVIDER_USER_META_KEY, $new_provider ); + } + } + } +} diff --git a/wp-content/plugins/two-factor/includes/Google/u2f-api.js b/wp-content/plugins/two-factor/includes/Google/u2f-api.js new file mode 100644 index 0000000000000000000000000000000000000000..9244d14e74066eda7e6ad1d9912efbfaf540aad9 --- /dev/null +++ b/wp-content/plugins/two-factor/includes/Google/u2f-api.js @@ -0,0 +1,748 @@ +//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 messsages 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.Transport>} + */ +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<u2f.SignRequest>} 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<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} 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<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + * |function((u2f.Error|u2f.SignResponse)))>} + * @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.<u2f.Response>} 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<u2f.RegisteredKey>} 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<u2f.RegisteredKey>} 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<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} 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<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} 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 new file mode 100644 index 0000000000000000000000000000000000000000..a11c78fbac34bad29e021822386fa169bd9fbc6c --- /dev/null +++ b/wp-content/plugins/two-factor/includes/Yubico/U2F.php @@ -0,0 +1,507 @@ +<?php +/* Copyright (c) 2014 Yubico AB + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +namespace u2flib_server; + +/** Constant for the version of the u2f protocol */ +const U2F_VERSION = "U2F_V2"; + +/** Error for the authentication message not matching any outstanding + * authentication request */ +const ERR_NO_MATCHING_REQUEST = 1; + +/** Error for the authentication message not matching any registration */ +const ERR_NO_MATCHING_REGISTRATION = 2; + +/** Error for the signature on the authentication message not verifying with + * the correct key */ +const ERR_AUTHENTICATION_FAILURE = 3; + +/** Error for the challenge in the registration message not matching the + * registration challenge */ +const ERR_UNMATCHED_CHALLENGE = 4; + +/** Error for the attestation signature on the registration message not + * verifying */ +const ERR_ATTESTATION_SIGNATURE = 5; + +/** Error for the attestation verification not verifying */ +const ERR_ATTESTATION_VERIFICATION = 6; + +/** Error for not getting good random from the system */ +const ERR_BAD_RANDOM = 7; + +/** Error when the counter is lower than expected */ +const ERR_COUNTER_TOO_LOW = 8; + +/** Error decoding public key */ +const ERR_PUBKEY_DECODE = 9; + +/** Error user-agent returned error */ +const ERR_BAD_UA_RETURNING = 10; + +/** Error old OpenSSL version */ +const ERR_OLD_OPENSSL = 11; + +/** @internal */ +const PUBKEY_LEN = 65; + +class U2F +{ + /** @var string */ + private $appId; + + /** @var null|string */ + private $attestDir; + + /** @internal */ + private $FIXCERTS = array( + '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8', + 'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f', + '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae', + 'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb', + '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897', + 'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511' + ); + + /** + * @param string $appId Application id for the running application + * @param string|null $attestDir Directory where trusted attestation roots may be found + * @throws Error If OpenSSL older than 1.0.0 is used + */ + public function __construct($appId, $attestDir = null) + { + if(OPENSSL_VERSION_NUMBER < 0x10000000) { + throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL); + } + $this->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/includes/function.login-header.php b/wp-content/plugins/two-factor/includes/function.login-header.php new file mode 100644 index 0000000000000000000000000000000000000000..647a0e6d94b9a220d64bfaad0dad0be6a0db261c --- /dev/null +++ b/wp-content/plugins/two-factor/includes/function.login-header.php @@ -0,0 +1,227 @@ +<?php +/** + * Extracted from wp-login.php since that file also loads WP core which we already have. + */ + +/** + * Output the login page header. + * + * @param string $title Optional. WordPress login Page title to display in the `<title>` element. + * Default 'Log In'. + * @param string $message Optional. Message to display in header. Default empty. + * @param WP_Error $wp_error Optional. The error to pass. Default is a WP_Error instance. + */ +function login_header( $title = 'Log In', $message = '', $wp_error = null ) { + global $error, $interim_login, $action; + + // Don't index any of these forms + add_action( 'login_head', 'wp_no_robots' ); + + add_action( 'login_head', 'wp_login_viewport_meta' ); + + if ( ! is_wp_error( $wp_error ) ) { + $wp_error = new WP_Error(); + } + + // Shake it! + $shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password' ); + /** + * Filters the error codes array for shaking the login form. + * + * @since 3.0.0 + * + * @param array $shake_error_codes Error codes that shake the login form. + */ + $shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes ); + + if ( $shake_error_codes && $wp_error->get_error_code() && in_array( $wp_error->get_error_code(), $shake_error_codes ) ) + add_action( 'login_head', 'wp_shake_js', 12 ); + + $login_title = get_bloginfo( 'name', 'display' ); + + /* translators: Login screen title. 1: Login screen name, 2: Network or site name */ + $login_title = sprintf( __( '%1$s ‹ %2$s — WordPress' ), $title, $login_title ); + + /** + * Filters the title tag content for login page. + * + * @since 4.9.0 + * + * @param string $login_title The page title, with extra context added. + * @param string $title The original page title. + */ + $login_title = apply_filters( 'login_title', $login_title, $title ); + + ?><!DOCTYPE html> + <!--[if IE 8]> + <html xmlns="http://www.w3.org/1999/xhtml" class="ie8" <?php language_attributes(); ?>> + <![endif]--> + <!--[if !(IE 8) ]><!--> + <html xmlns="http://www.w3.org/1999/xhtml" <?php language_attributes(); ?>> + <!--<![endif]--> + <head> + <meta http-equiv="Content-Type" content="<?php bloginfo('html_type'); ?>; charset=<?php bloginfo('charset'); ?>" /> + <title><?php echo $login_title; ?></title> + <?php + + wp_enqueue_style( 'login' ); + + /* + * Remove all stored post data on logging out. + * This could be added by add_action('login_head'...) like wp_shake_js(), + * but maybe better if it's not removable by plugins + */ + if ( 'loggedout' == $wp_error->get_error_code() ) { + ?> + <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script> + <?php + } + + /** + * Enqueue scripts and styles for the login page. + * + * @since 3.1.0 + */ + do_action( 'login_enqueue_scripts' ); + + /** + * Fires in the login page header after scripts are enqueued. + * + * @since 2.1.0 + */ + do_action( 'login_head' ); + + if ( is_multisite() ) { + $login_header_url = network_home_url(); + $login_header_title = get_network()->site_name; + } else { + $login_header_url = __( 'https://wordpress.org/' ); + $login_header_title = __( 'Powered by WordPress' ); + } + + /** + * Filters link URL of the header logo above login form. + * + * @since 2.1.0 + * + * @param string $login_header_url Login header logo URL. + */ + $login_header_url = apply_filters( 'login_headerurl', $login_header_url ); + + /** + * Filters the title attribute of the header logo above login form. + * + * @since 2.1.0 + * + * @param string $login_header_title Login header logo title attribute. + */ + $login_header_title = apply_filters( 'login_headertitle', $login_header_title ); + + /* + * To match the URL/title set above, Multisite sites have the blog name, + * while single sites get the header title. + */ + if ( is_multisite() ) { + $login_header_text = get_bloginfo( 'name', 'display' ); + } else { + $login_header_text = $login_header_title; + } + + $classes = array( 'login-action-' . $action, 'wp-core-ui' ); + if ( is_rtl() ) + $classes[] = 'rtl'; + if ( $interim_login ) { + $classes[] = 'interim-login'; + ?> + <style type="text/css">html{background-color: transparent;}</style> + <?php + + if ( 'success' === $interim_login ) + $classes[] = 'interim-login-success'; + } + $classes[] =' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) ); + + /** + * Filters the login page body classes. + * + * @since 3.5.0 + * + * @param array $classes An array of body classes. + * @param string $action The action that brought the visitor to the login page. + */ + $classes = apply_filters( 'login_body_class', $classes, $action ); + + ?> + </head> + <body class="login <?php echo esc_attr( implode( ' ', $classes ) ); ?>"> + <?php + /** + * Fires in the login page header after the body tag is opened. + * + * @since 4.6.0 + */ + do_action( 'login_header' ); + ?> + <div id="login"> + <h1><a href="<?php echo esc_url( $login_header_url ); ?>" title="<?php echo esc_attr( $login_header_title ); ?>" tabindex="-1"><?php echo $login_header_text; ?></a></h1> + <?php + + unset( $login_header_url, $login_header_title ); + + /** + * Filters the message to display above the login form. + * + * @since 2.1.0 + * + * @param string $message Login message text. + */ + $message = apply_filters( 'login_message', $message ); + if ( !empty( $message ) ) + echo $message . "\n"; + + // In case a plugin uses $error rather than the $wp_errors object + if ( !empty( $error ) ) { + $wp_error->add('error', $error); + unset($error); + } + + if ( $wp_error->get_error_code() ) { + $errors = ''; + $messages = ''; + foreach ( $wp_error->get_error_codes() as $code ) { + $severity = $wp_error->get_error_data( $code ); + foreach ( $wp_error->get_error_messages( $code ) as $error_message ) { + if ( 'message' == $severity ) + $messages .= ' ' . $error_message . "<br />\n"; + else + $errors .= ' ' . $error_message . "<br />\n"; + } + } + if ( ! empty( $errors ) ) { + /** + * Filters the error messages displayed above the login form. + * + * @since 2.1.0 + * + * @param string $errors Login error message. + */ + echo '<div id="login_error">' . apply_filters( 'login_errors', $errors ) . "</div>\n"; + } + if ( ! empty( $messages ) ) { + /** + * Filters instructional messages displayed above the login form. + * + * @since 2.5.0 + * + * @param string $messages Login messages. + */ + echo '<p class="message">' . apply_filters( 'login_messages', $messages ) . "</p>\n"; + } + } +} // End of login_header() + +function wp_login_viewport_meta() { + ?> + <meta name="viewport" content="width=device-width" /> + <?php +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..62ca51bbb686e49c5d88cb7f05c81d8388b96831 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-backup-codes.php @@ -0,0 +1,335 @@ +<?php +/** + * Class for creating a backup codes provider. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +class Two_Factor_Backup_Codes extends Two_Factor_Provider { + + /** + * The user meta backup codes key. + * + * @type string + */ + const BACKUP_CODES_META_KEY = '_two_factor_backup_codes'; + + /** + * The number backup codes. + * + * @type int + */ + const NUMBER_OF_CODES = 10; + + /** + * Ensures only one instance of this class exists in memory at any one time. + * + * @since 0.1-dev + */ + static function get_instance() { + static $instance; + $class = __CLASS__; + if ( ! is_a( $instance, $class ) ) { + $instance = new $class; + } + return $instance; + } + + /** + * Class constructor. + * + * @since 0.1-dev + */ + protected function __construct() { + add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) ); + add_action( 'admin_notices', array( $this, 'admin_notices' ) ); + add_action( 'wp_ajax_two_factor_backup_codes_generate', array( $this, 'ajax_generate_json' ) ); + + return parent::__construct(); + } + + /** + * Displays an admin notice when backup codes have run out. + * + * @since 0.1-dev + */ + public function admin_notices() { + $user = wp_get_current_user(); + + // Return if the provider is not enabled. + if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ) ) ) { + return; + } + + // Return if we are not out of codes. + if ( $this->is_available_for_user( $user ) ) { + return; + } + ?> + <div class="error"> + <p> + <span> + <?php + printf( // WPCS: XSS OK. + __( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'two-factor' ), + esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' ) + ); + ?> + <span> + </p> + </div> + <?php + } + + /** + * Returns the name of the provider. + * + * @since 0.1-dev + */ + public function get_label() { + return _x( 'Backup Verification Codes (Single Use)', 'Provider Label', 'two-factor' ); + } + + /** + * Whether this Two Factor provider is configured and codes are 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 ) { + // Does this user have available codes? + if ( 0 < self::codes_remaining_for_user( $user ) ) { + return true; + } + return false; + } + + /** + * 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 ) { + $ajax_nonce = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID ); + $count = self::codes_remaining_for_user( $user ); + ?> + <p id="two-factor-backup-codes"> + <button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js"> + <?php esc_html_e( 'Generate Verification Codes', 'two-factor' ); ?> + </button> + <span class="two-factor-backup-codes-count"><?php + echo esc_html( sprintf( + /* translators: %s: count */ + _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), + $count + ) ); + ?></span> + </p> + <div class="two-factor-backup-codes-wrapper" style="display:none;"> + <ol class="two-factor-backup-codes-unused-codes"></ol> + <p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'two-factor' ); ?></p> + <p> + <a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a> + <p> + </div> + <script type="text/javascript"> + ( function( $ ) { + $( '.button-two-factor-backup-codes-generate' ).click( function() { + $.ajax( { + method: 'POST', + url: ajaxurl, + data: { + action: 'two_factor_backup_codes_generate', + user_id: '<?php echo esc_js( $user->ID ); ?>', + nonce: '<?php echo esc_js( $ajax_nonce ); ?>' + }, + dataType: 'JSON', + success: function( response ) { + var $codesList = $( '.two-factor-backup-codes-unused-codes' ); + + $( '.two-factor-backup-codes-wrapper' ).show(); + $codesList.html( '' ); + + // Append the codes. + for ( i = 0; i < response.data.codes.length; i++ ) { + $codesList.append( '<li>' + response.data.codes[ i ] + '</li>' ); + } + + // Update counter. + $( '.two-factor-backup-codes-count' ).html( response.data.i18n.count ); + + // Build the download link + var txt_data = 'data:application/text;charset=utf-8,' + '\n'; + txt_data += response.data.i18n.title.replace( /%s/g, document.domain ) + '\n\n'; + + for ( i = 0; i < response.data.codes.length; i++ ) { + txt_data += i + 1 + '. ' + response.data.codes[ i ] + '\n'; + } + + $( '#two-factor-backup-codes-download-link' ).attr( 'href', encodeURI( txt_data ) ); + } + } ); + } ); + } )( jQuery ); + </script> + <?php + } + + /** + * Generates backup codes & updates the user meta. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @param array $args Optional arguments for assinging new codes. + * @return array + */ + public function generate_codes( $user, $args = '' ) { + $codes = array(); + $codes_hashed = array(); + + // Check for arguments. + if ( isset( $args['number'] ) ) { + $num_codes = (int) $args['number']; + } else { + $num_codes = self::NUMBER_OF_CODES; + } + + // Append or replace (default). + if ( isset( $args['method'] ) && 'append' === $args['method'] ) { + $codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true ); + } + + for ( $i = 0; $i < $num_codes; $i++ ) { + $code = $this->get_code(); + $codes_hashed[] = wp_hash_password( $code ); + $codes[] = $code; + unset( $code ); + } + + update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed ); + + // Unhashed. + return $codes; + } + + /** + * Generates a JSON object of backup codes. + * + * @since 0.1-dev + */ + public function ajax_generate_json() { + $user = get_user_by( 'id', sanitize_text_field( $_POST['user_id'] ) ); + check_ajax_referer( 'two-factor-backup-codes-generate-json-' . $user->ID, 'nonce' ); + + // Setup the return data. + $codes = $this->generate_codes( $user ); + $count = self::codes_remaining_for_user( $user ); + $i18n = array( + /* translators: %s: count */ + 'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), $count ) ), + /* translators: %s: the site's domain */ + 'title' => esc_html__( 'Two-Factor Backup Codes for %s', 'two-factor' ), + ); + + // Send the response. + wp_send_json_success( array( 'codes' => $codes, 'i18n' => $i18n ) ); + } + + /** + * Returns the number of unused codes for the specified user + * + * @param WP_User $user WP_User object of the logged-in user. + * @return int $int The number of unused codes remaining + */ + public static function codes_remaining_for_user( $user ) { + $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true ); + if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) { + return count( $backup_codes ); + } + return 0; + } + + /** + * Prints the form that prompts the user to authenticate. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + */ + public function authentication_page( $user ) { + require_once( ABSPATH . '/wp-admin/includes/template.php' ); + ?> + <p><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p><br/> + <p> + <label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label> + <input type="tel" name="two-factor-backup-code" id="authcode" class="input" value="" size="20" pattern="[0-9]*" /> + </p> + <?php + submit_button( __( 'Submit', 'two-factor' ) ); + } + + /** + * Validates the users input token. + * + * In this class we just return true. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + public function validate_authentication( $user ) { + return $this->validate_code( $user, $_POST['two-factor-backup-code'] ); + } + + /** + * Validates a backup code. + * + * Backup Codes are single use and are deleted upon a successful validation. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @param int $code The backup code. + * @return boolean + */ + public function validate_code( $user, $code ) { + $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true ); + + if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) { + foreach ( $backup_codes as $code_index => $code_hashed ) { + if ( wp_check_password( $code, $code_hashed, $user->ID ) ) { + $this->delete_code( $user, $code_hashed ); + return true; + } + } + } + return false; + } + + /** + * Deletes a backup code. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @param string $code_hashed The hashed the backup code. + */ + public function delete_code( $user, $code_hashed ) { + $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true ); + + // Delete the current code from the list since it's been used. + $backup_codes = array_flip( $backup_codes ); + unset( $backup_codes[ $code_hashed ] ); + $backup_codes = array_values( array_flip( $backup_codes ) ); + + // Update the backup code master list. + update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes ); + } +} diff --git a/wp-content/plugins/two-factor/providers/class.two-factor-dummy.php b/wp-content/plugins/two-factor/providers/class.two-factor-dummy.php new file mode 100644 index 0000000000000000000000000000000000000000..c28ccca7161786f1db1b99ae5991d601ea19ce89 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-dummy.php @@ -0,0 +1,93 @@ +<?php +/** + * Class for creating a dummy provider. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +class Two_Factor_Dummy extends Two_Factor_Provider { + + /** + * Ensures only one instance of this class exists in memory at any one time. + * + * @since 0.1-dev + */ + static function get_instance() { + static $instance; + $class = __CLASS__; + if ( ! is_a( $instance, $class ) ) { + $instance = new $class; + } + return $instance; + } + + /** + * Class constructor. + * + * @since 0.1-dev + */ + protected function __construct() { + add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) ); + return parent::__construct(); + } + + /** + * Returns the name of the provider. + * + * @since 0.1-dev + */ + public function get_label() { + return _x( 'Dummy Method', 'Provider Label', 'two-factor' ); + } + + /** + * Prints the form that prompts the user to authenticate. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + */ + public function authentication_page( $user ) { + require_once( ABSPATH . '/wp-admin/includes/template.php' ); + ?> + <p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p> + <?php + submit_button( __( 'Yup.', 'two-factor' ) ); + } + + /** + * Validates the users input token. + * + * In this class we just return true. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + public function validate_authentication( $user ) { + return true; + } + + /** + * 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 true; + } + + /** + * 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 ) {} +} diff --git a/wp-content/plugins/two-factor/providers/class.two-factor-email.php b/wp-content/plugins/two-factor/providers/class.two-factor-email.php new file mode 100644 index 0000000000000000000000000000000000000000..177885717a5f144406009cd0ce39cb9de8b00909 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-email.php @@ -0,0 +1,263 @@ +<?php +/** + * Class for creating an email provider. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +class Two_Factor_Email extends Two_Factor_Provider { + + /** + * The user meta token key. + * + * @type string + */ + const TOKEN_META_KEY = '_two_factor_email_token'; + + /** + * Name of the input field used for code resend. + * + * @var string + */ + const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend'; + + /** + * Ensures only one instance of this class exists in memory at any one time. + * + * @since 0.1-dev + */ + static function get_instance() { + static $instance; + $class = __CLASS__; + if ( ! is_a( $instance, $class ) ) { + $instance = new $class; + } + return $instance; + } + + /** + * Class constructor. + * + * @since 0.1-dev + */ + protected function __construct() { + add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) ); + return parent::__construct(); + } + + /** + * Returns the name of the provider. + * + * @since 0.1-dev + */ + public function get_label() { + return _x( 'Email', 'Provider Label', 'two-factor' ); + } + + /** + * Generate the user token. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + * @return string + */ + public function generate_token( $user_id ) { + $token = $this->get_code(); + update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) ); + return $token; + } + + /** + * Check if user has a valid token already. + * + * @param int $user_id User ID. + * @return boolean If user has a valid email token. + */ + public function user_has_token( $user_id ) { + $hashed_token = $this->get_user_token( $user_id ); + + if ( ! empty( $hashed_token ) ) { + return true; + } else { + return false; + } + } + + /** + * Get the authentication token for the user. + * + * @param int $user_id User ID. + * + * @return string|boolean User token or `false` if no token found. + */ + public function get_user_token( $user_id ) { + $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true ); + + if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) { + return $hashed_token; + } + + return false; + } + + /** + * Validate the user token. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + * @param string $token User token. + * @return boolean + */ + public function validate_token( $user_id, $token ) { + $hashed_token = $this->get_user_token( $user_id ); + + // Bail if token is empty or it doesn't match. + if ( empty( $hashed_token ) || ( wp_hash( $token ) !== $hashed_token ) ) { + return false; + } + + // Ensure that the token can't be re-used. + $this->delete_token( $user_id ); + + return true; + } + + /** + * Delete the user token. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + */ + public function delete_token( $user_id ) { + delete_user_meta( $user_id, self::TOKEN_META_KEY ); + } + + /** + * Generate and email the user token. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return bool Whether the email contents were sent successfully. + */ + public function generate_and_email_token( $user ) { + $token = $this->generate_token( $user->ID ); + + /* translators: %s: site name */ + $subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), get_bloginfo( 'name' ) ) ); + /* translators: %s: token */ + $message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) ); + + return wp_mail( $user->user_email, $subject, $message ); + } + + /** + * Prints the form that prompts the user to authenticate. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + */ + public function authentication_page( $user ) { + if ( ! $user ) { + return; + } + + if ( ! $this->user_has_token( $user->ID ) ) { + $this->generate_and_email_token( $user ); + } + + require_once( ABSPATH . '/wp-admin/includes/template.php' ); + ?> + <p><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p> + <p> + <label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label> + <input type="tel" name="two-factor-email-code" id="authcode" class="input" value="" size="20" pattern="[0-9]*" /> + <?php submit_button( __( 'Log In', 'two-factor' ) ); ?> + </p> + <p class="two-factor-email-resend"> + <input type="submit" class="button" name="<?php echo esc_attr( self::INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'two-factor' ); ?>" /> + </p> + <script type="text/javascript"> + setTimeout( function(){ + var d; + try{ + d = document.getElementById('authcode'); + d.value = ''; + d.focus(); + } catch(e){} + }, 200); + </script> + <?php + } + + /** + * Send the email code if missing or requested. Stop the authentication + * validation if a new token has been generated and sent. + * + * @param WP_USer $user WP_User object of the logged-in user. + * @return boolean + */ + public function pre_process_authentication( $user ) { + if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { + $this->generate_and_email_token( $user ); + return true; + } + + return false; + } + + /** + * Validates the users input token. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + public function validate_authentication( $user ) { + if ( ! isset( $user->ID ) || ! isset( $_REQUEST['two-factor-email-code'] ) ) { + return false; + } + + return $this->validate_token( $user->ID, $_REQUEST['two-factor-email-code'] ); + } + + /** + * 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 true; + } + + /** + * 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 ) { + $email = $user->user_email; + ?> + <div> + <?php + echo esc_html( sprintf( + /* translators: %s: email address */ + __( 'Authentication codes will be sent to %s.', 'two-factor' ), + $email + ) ); + ?> + </div> + <?php + } +} diff --git a/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f-admin-list-table.php b/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f-admin-list-table.php new file mode 100644 index 0000000000000000000000000000000000000000..7942c9a9413919eb0603a128e7004d4df61486b3 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f-admin-list-table.php @@ -0,0 +1,152 @@ +<?php +// Load the parent class if it doesn't exist. +if ( ! class_exists( 'WP_List_Table' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; +} + +/** + * Class for displaying the list of security key items. + * + * @since 0.1-dev + * @access private + * + * @package Two_Factor + */ +class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table { + + /** + * Get a list of columns. + * + * @since 0.1-dev + * + * @return array + */ + public function get_columns() { + return array( + 'name' => 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 = '<div class="hidden" id="inline_' . esc_attr( $item->keyHandle ) . '">'; + $out .= '<div class="name">' . esc_html( $item->name ) . '</div>'; + $out .= '</div>'; + + $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 date( get_option( 'date_format', 'r' ), $item->added ); + case 'last_used': + return date( 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 ) { + ?> + <tr id="key-<?php echo esc_attr( $item->keyHandle ); ?>"> + <?php $this->single_row_columns( $item ); ?> + </tr> + <?php + } + + /** + * Outputs the hidden row displayed when inline editing + * + * @since 0.1-dev + */ + public function inline_edit() { + ?> + <table style="display: none"> + <tbody id="inlineedit"> + <tr id="inline-edit" class="inline-edit-row" style="display: none"> + <td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange"> + <fieldset> + <div class="inline-edit-col"> + <h4><?php esc_html_e( 'Quick Edit', 'two-factor' ); ?></h4> + + <label> + <span class="title"><?php esc_html_e( 'Name', 'two-factor' ); ?></span> + <span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span> + </label> + </div> + </fieldset> + <?php + $core_columns = array( 'name' => true, 'added' => true, 'last_used' => true ); + list( $columns ) = $this->get_column_info(); + foreach ( $columns as $column_name => $column_display_name ) { + if ( isset( $core_columns[ $column_name ] ) ) { + continue; + } + + /** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */ + do_action( 'quick_edit_custom_box', $column_name, 'edit-security-keys' ); + } + ?> + <p class="inline-edit-save submit"> + <a href="#inline-edit" class="cancel button-secondary alignleft"><?php esc_html_e( 'Cancel', 'two-factor' ); ?></a> + <a href="#inline-edit" class="save button-primary alignright"><?php esc_html_e( 'Update', 'two-factor' ); ?></a> + <span class="spinner"></span> + <span class="error" style="display:none;"></span> + <?php wp_nonce_field( 'keyinlineeditnonce', '_inline_edit', false ); ?> + <br class="clear" /> + </p> + </td> + </tr> + </tbody> + </table> + <?php + } +} diff --git a/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f-admin.php b/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f-admin.php new file mode 100644 index 0000000000000000000000000000000000000000..d4c4ac85434abcd60892bd30766fbe290d8c019a --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f-admin.php @@ -0,0 +1,341 @@ +<?php +/** + * Class for registering & modifying FIDO U2F security keys. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +class Two_Factor_FIDO_U2F_Admin { + + /** + * The user meta register data. + * + * @type string + */ + const REGISTER_DATA_USER_META_KEY = '_two_factor_fido_u2f_register_request'; + + /** + * Add various hooks. + * + * @since 0.1-dev + * + * @access public + * @static + */ + public static function add_hooks() { + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); + add_action( 'show_user_security_settings', array( __CLASS__, 'show_user_profile' ) ); + add_action( 'personal_options_update', array( __CLASS__, 'catch_submission' ), 0 ); + add_action( 'edit_user_profile_update', array( __CLASS__, 'catch_submission' ), 0 ); + add_action( 'load-profile.php', array( __CLASS__, 'catch_delete_security_key' ) ); + add_action( 'load-user-edit.php', array( __CLASS__, 'catch_delete_security_key' ) ); + add_action( 'wp_ajax_inline-save-key', array( __CLASS__, 'wp_ajax_inline_save' ) ); + } + + /** + * Enqueue assets. + * + * @since 0.1-dev + * + * @access public + * @static + * + * @param string $hook Current page. + */ + public static function enqueue_assets( $hook ) { + if ( ! in_array( $hook, array( 'user-edit.php', 'profile.php' ) ) ) { + return; + } + + $user_id = get_current_user_id(); + $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id ); + + // @todo Ensure that scripts don't fail because of missing u2fL10n + try { + $data = Two_Factor_FIDO_U2F::$u2f->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( + '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 ) { + 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 ); + } + + ?> + <div class="security-keys" id="security-keys-section"> + <h3><?php esc_html_e( 'Security Keys', 'two-factor' ); ?></h3> + + <?php if ( ! is_ssl() ) : ?> + <p class="u2f-error-https"> + <em><?php esc_html_e( 'U2F requires an HTTPS connection. You won\'t be able to add new security keys over HTTP.', 'two-factor' ); ?></em> + </p> + <?php endif; ?> + + <div class="register-security-key"> + <input type="hidden" name="do_new_security_key" id="do_new_security_key" /> + <input type="hidden" name="u2f_response" id="u2f_response" /> + <button type="button" class="button button-secondary" id="register_security_key"><?php echo esc_html( _x( 'Register New Key', 'security key', 'two-factor' ) ); ?></button> + <span class="spinner"></span> + <span class="security-key-status"></span> + </div> + + <?php if ( $new_key ) : ?> + <div class="notice notice-success is-dismissible"> + <p class="new-security-key"><?php esc_html_e( 'Your new security key registered.', 'two-factor' ); ?></p> + </div> + <?php endif; ?> + + <p><a href="https://support.google.com/accounts/answer/6103523"><?php esc_html_e( 'You can find FIDO U2F Security Key devices for sale from here.', 'two-factor' ); ?></a></p> + + <?php + require( TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f-admin-list-table.php' ); + $u2f_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table(); + $u2f_list_table->items = $security_keys; + $u2f_list_table->prepare_items(); + $u2f_list_table->display(); + $u2f_list_table->inline_edit(); + ?> + </div> + <?php + } + + /** + * Catch the non-ajax submission from the new form. + * + * This executes during the `personal_options_update` & `edit_user_profile_update` actions. + * + * @since 0.1-dev + * + * @access public + * @static + * + * @param int $user_id User ID. + * @return false + */ + public static function catch_submission( $user_id ) { + if ( ! empty( $_REQUEST['do_new_security_key'] ) ) { + check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' ); + + try { + $response = json_decode( stripslashes( $_POST['u2f_response'] ) ); + $reg = Two_Factor_FIDO_U2F::$u2f->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 false; + } + + 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 = get_current_user_id(); + if ( ! 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' ); + } + } + + /** + * 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( '<a href="#" class="editinline">%s</a>', 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 ); + $delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' ); + return sprintf( '<a href="%1$s">%2$s</a>', 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( 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 = get_current_user_id(); + + $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'] ) { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..54644251369ffce55bc88a61879831e7b002fdcb --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-fido-u2f.php @@ -0,0 +1,384 @@ +<?php +/** + * Class for creating a FIDO Universal 2nd Factor provider. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +class Two_Factor_FIDO_U2F extends Two_Factor_Provider { + + /** + * U2F Library + * + * @var u2flib_server\U2F + */ + public static $u2f; + + /** + * The user meta registered key. + * + * @type string + */ + const REGISTERED_KEY_USER_META_KEY = '_two_factor_fido_u2f_registered_key'; + + /** + * The user meta authenticate data. + * + * @type string + */ + const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request'; + + /** + * Version number for the bundled assets. + * + * @var string + */ + const U2F_ASSET_VERSION = '0.2.0'; + + /** + * Ensures only one instance of this class exists in memory at any one time. + * + * @return \Two_Factor_FIDO_U2F + */ + static function get_instance() { + static $instance; + + if ( ! isset( $instance ) ) { + $instance = new self(); + } + + return $instance; + } + + /** + * Class constructor. + * + * @since 0.1-dev + */ + protected function __construct() { + if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) { + return; + } + + require_once( TWO_FACTOR_DIR . 'includes/Yubico/U2F.php' ); + self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() ); + + require_once( TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f-admin.php' ); + Two_Factor_FIDO_U2F_Admin::add_hooks(); + + wp_register_script( + 'fido-u2f-api', + plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ), + null, + self::asset_version(), + true + ); + + wp_register_script( + 'fido-u2f-login', + plugins_url( 'js/fido-u2f-login.js', __FILE__ ), + array( 'jquery', 'fido-u2f-api' ), + self::asset_version(), + true + ); + + add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) ); + + return parent::__construct(); + } + + /** + * Get the asset version number. + * + * TODO: There should be a plugin-level helper for getting the current plugin version. + * + * @return string + */ + public static function asset_version() { + return self::U2F_ASSET_VERSION; + } + + /** + * Return the U2F AppId. U2F requires the AppID to use HTTPS + * and a top-level domain. + * + * @return string AppID URI + */ + public static function get_u2f_app_id() { + $url_parts = wp_parse_url( home_url() ); + + if ( ! empty( $url_parts['port'] ) ) { + return sprintf( 'https://%s:%d', $url_parts['host'], $url_parts['port'] ); + } else { + return sprintf( 'https://%s', $url_parts['host'] ); + } + } + + /** + * Returns the name of the provider. + * + * @since 0.1-dev + */ + public function get_label() { + return _x( 'FIDO Universal 2nd Factor (U2F)', 'Provider Label', 'two-factor' ); + } + + /** + * Enqueue assets for login form. + * + * @since 0.1-dev + */ + public function login_enqueue_assets() { + wp_enqueue_script( 'fido-u2f-login' ); + } + + /** + * Prints the form that prompts the user to authenticate. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return null + */ + public function authentication_page( $user ) { + require_once( ABSPATH . '/wp-admin/includes/template.php' ); + + // U2F doesn't work without HTTPS + if ( ! is_ssl() ) { + ?> + <p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p> + <?php + + return; + } + + try { + $keys = self::get_security_keys( $user->ID ); + $data = self::$u2f->getAuthenticateData( $keys ); + update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data ); + } catch ( Exception $e ) { + ?> + <p><?php esc_html_e( 'An error occurred while creating authentication data.', 'two-factor' ); ?></p> + <?php + return null; + } + + wp_localize_script( + 'fido-u2f-login', + 'u2fL10n', + array( + 'request' => $data, + ) + ); + + wp_enqueue_script( 'fido-u2f-login' ); + + ?> + <p><?php esc_html_e( 'Now insert (and tap) your Security Key.', 'two-factor' ); ?></p> + <input type="hidden" name="u2f_response" id="u2f_response" /> + <?php + } + + /** + * Validates the users input token. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + public function validate_authentication( $user ) { + $requests = get_user_meta( $user->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 = current_time( 'timestamp' ); + + 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 ) { + ?> + <p> + <?php esc_html_e( 'Requires an HTTPS connection. Configure your security keys in the "Security Keys" section below.', 'two-factor' ); ?> + </p> + <?php + } + + /** + * Add registered security key to a user. + * + * @since 0.1-dev + * + * @param int $user_id User ID. + * @param object $register The data of registered security key. + * @return int|bool Meta ID on success, false on failure. + */ + public static function add_security_key( $user_id, $register ) { + if ( ! is_numeric( $user_id ) ) { + return false; + } + + if ( + ! is_object( $register ) + || ! property_exists( $register, 'keyHandle' ) || empty( $register->keyHandle ) + || ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) + || ! property_exists( $register, 'certificate' ) || empty( $register->certificate ) + || ! property_exists( $register, 'counter' ) || ( -1 > $register->counter ) + ) { + return false; + } + + $register = array( + 'keyHandle' => $register->keyHandle, + 'publicKey' => $register->publicKey, + 'certificate' => $register->certificate, + 'counter' => $register->counter, + ); + + $register['name'] = __( 'New Security Key', 'two-factor' ); + $register['added'] = current_time( 'timestamp' ); + $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 ) + || ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) + || ! 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 ) { + 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 ) { + global $wpdb; + + if ( ! is_numeric( $user_id ) ) { + return false; + } + + $user_id = absint( $user_id ); + if ( ! $user_id ) { + return false; + } + + $table = $wpdb->usermeta; + + $keyHandle = wp_unslash( $keyHandle ); + $keyHandle = maybe_serialize( $keyHandle ); + + $query = $wpdb->prepare( "SELECT umeta_id FROM $table WHERE meta_key = '%s' AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id ); + + if ( $keyHandle ) { + $query .= $wpdb->prepare( ' AND meta_value LIKE %s', '%:"' . $keyHandle . '";s:%' ); + } + + $meta_ids = $wpdb->get_col( $query ); + if ( ! count( $meta_ids ) ) { + return false; + } + + foreach ( $meta_ids as $meta_id ) { + delete_metadata_by_mid( 'user', $meta_id ); + } + + return true; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..97a4a7fcd59aac7c8a668f36e10e946b7f0d0f09 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-provider.php @@ -0,0 +1,96 @@ +<?php +/** + * Abstract class for creating two factor authentication providers. + * + * @since 0.1-dev + * + * @package Two_Factor + */ +abstract class Two_Factor_Provider { + + /** + * Class constructor. + * + * @since 0.1-dev + */ + protected function __construct() { + return $this; + } + + /** + * Returns the name of the provider. + * + * @since 0.1-dev + * + * @return string + */ + abstract function get_label(); + + /** + * Prints the name of the provider. + * + * @since 0.1-dev + */ + public function print_label() { + echo esc_html( $this->get_label() ); + } + + /** + * Prints the form that prompts the user to authenticate. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + */ + abstract function authentication_page( $user ); + + /** + * Allow providers to do extra processing before the authentication. + * Return `true` to prevent the authentication and render the + * authentication page. + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + public function pre_process_authentication( $user ) { + return false; + } + + /** + * Validates the users input token. + * + * @since 0.1-dev + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + abstract function validate_authentication( $user ); + + /** + * Whether this Two Factor provider is configured and available for the user specified. + * + * @param WP_User $user WP_User object of the logged-in user. + * @return boolean + */ + abstract function is_available_for_user( $user ); + + /** + * Generate a random eight-digit string to send out as an auth code. + * + * @since 0.1-dev + * + * @param int $length The code length. + * @param string|array $chars Valid auth code characters. + * @return string + */ + public function get_code( $length = 8, $chars = '1234567890' ) { + $code = ''; + if ( is_array( $chars ) ) { + $chars = implode( '', $chars ); + } + for ( $i = 0; $i < $length; $i++ ) { + $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); + } + return $code; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..d9a56781a7cc86e7a1f5d2b6d37c47740545b5ff --- /dev/null +++ b/wp-content/plugins/two-factor/providers/class.two-factor-totp.php @@ -0,0 +1,509 @@ +<?php +/** + * Class for creating a Time Based One-Time Password provider. + * + * @package Two_Factor + */ + +/** + * Class Two_Factor_Totp + */ +class Two_Factor_Totp extends Two_Factor_Provider { + + /** + * The user meta token key. + * + * @var string + */ + const SECRET_META_KEY = '_two_factor_totp_key'; + + /** + * The user meta token key. + * + * @var string + */ + const NOTICES_META_KEY = '_two_factor_totp_notices'; + + const DEFAULT_KEY_BIT_SIZE = 160; + const DEFAULT_CRYPTO = 'sha1'; + const DEFAULT_DIGIT_COUNT = 6; + const DEFAULT_TIME_STEP_SEC = 30; + const DEFAULT_TIME_STEP_ALLOWANCE = 4; + private static $_base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + + /** + * Class constructor. Sets up hooks, etc. + */ + protected function __construct() { + add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_two_factor_options' ) ); + add_action( 'personal_options_update', array( $this, 'user_two_factor_options_update' ) ); + add_action( 'edit_user_profile_update', array( $this, 'user_two_factor_options_update' ) ); + return parent::__construct(); + } + + /** + * Ensures only one instance of this class exists in memory at any one time. + */ + public static function get_instance() { + static $instance; + if ( ! isset( $instance ) ) { + $instance = new self(); + } + return $instance; + } + + /** + * Returns the name of the provider. + */ + public function get_label() { + return _x( 'Time Based One-Time Password (Google Authenticator)', 'Provider Label', 'two-factor' ); + } + + /** + * Display TOTP options on the user settings page. + * + * @param WP_User $user The current user being edited. + * @return false + */ + public function user_two_factor_options( $user ) { + if ( ! isset( $user->ID ) ) { + return false; + } + + wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false ); + + $key = $this->get_user_totp_key( $user->ID ); + $this->admin_notices(); + + ?> + <div id="two-factor-totp-options"> + <?php if ( empty( $key ) ) : + $key = $this->generate_key(); + $site_name = get_bloginfo( 'name', 'display' ); + $totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user ); + ?> + <p> + <?php esc_html_e( 'Please scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.', 'two-factor' ); ?> + </p> + <p> + <img src="<?php echo esc_url( $this->get_google_qr_code( $totp_title, $key, $site_name ) ); ?>" id="two-factor-totp-qrcode" /> + </p> + <p> + <code><?php echo esc_html( $key ); ?></code> + </p> + <p> + <input type="hidden" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" /> + <label for="two-factor-totp-authcode"> + <?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?> + <input type="tel" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9]*" /> + </label> + <input type="submit" class="button" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" /> + </p> + <?php else : ?> + <p class="success"> + <?php esc_html_e( 'Secret key configured and registered.', 'two-factor' ); ?> + </p> + <p> + <input type="submit" class="button" name="two-factor-totp-delete" value="<?php esc_attr_e( 'Reset Key', 'two-factor' ); ?>" /> + <em class="description"> + <?php esc_html_e( 'You will have to re-scan the QR code on all devices as the previous codes will stop working.', 'two-factor' ); ?> + </em> + </p> + <?php endif; ?> + </div> + <?php + } + + /** + * Save the options specified in `::user_two_factor_options()` + * + * @param integer $user_id The user ID whose options are being updated. + * @return false + */ + public function user_two_factor_options_update( $user_id ) { + $notices = array(); + $errors = array(); + + $current_key = $this->get_user_totp_key( $user_id ); + + if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) { + check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' ); + + // Delete the secret key. + if ( ! empty( $current_key ) && isset( $_POST['two-factor-totp-delete'] ) ) { + $this->delete_user_totp_key( $user_id ); + } + + // Validate and store a new secret key. + if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) { + if ( $this->is_valid_key( $_POST['two-factor-totp-key'] ) ) { + if ( $this->is_valid_authcode( $_POST['two-factor-totp-key'], $_POST['two-factor-totp-authcode'] ) ) { + if ( ! $this->set_user_totp_key( $user_id, $_POST['two-factor-totp-key'] ) ) { + $errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' ); + } + } else { + $errors[] = __( 'Invalid Two Factor Authentication code.', 'two-factor' ); + } + } else { + $errors[] = __( 'Invalid Two Factor Authentication secret key.', 'two-factor' ); + } + } + + if ( ! empty( $errors ) ) { + $notices['error'] = $errors; + } + + if ( ! empty( $notices ) ) { + update_user_meta( $user_id, self::NOTICES_META_KEY, $notices ); + } + } + } + + /** + * Get the TOTP secret key for a user. + * + * @param int $user_id User ID. + * + * @return string + */ + public function get_user_totp_key( $user_id ) { + return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true ); + } + + /** + * Set the TOTP secret key for a user. + * + * @param int $user_id User ID. + * @param string $key TOTP secret key. + * + * @return boolean If the key was stored successfully. + */ + public function set_user_totp_key( $user_id, $key ) { + return update_user_meta( $user_id, self::SECRET_META_KEY, $key ); + } + + /** + * Delete the TOTP secret key for a user. + * + * @param int $user_id User ID. + * + * @return boolean If the key was deleted successfully. + */ + public function delete_user_totp_key( $user_id ) { + return delete_user_meta( $user_id, self::SECRET_META_KEY ); + } + + /** + * Check if the TOTP secret key has a proper format. + * + * @param string $key TOTP secret key. + * + * @return boolean + */ + public function is_valid_key( $key ) { + $check = sprintf( '/^[%s]+$/', self::$_base_32_chars ); + + if ( 1 === preg_match( $check, $key ) ) { + return true; + } + + return false; + } + + /** + * Display any available admin notices. + */ + public function admin_notices() { + $notices = get_user_meta( get_current_user_id(), self::NOTICES_META_KEY, true ); + + if ( ! empty( $notices ) ) { + delete_user_meta( get_current_user_id(), self::NOTICES_META_KEY ); + foreach ( $notices as $class => $messages ) { + ?> + <div class="<?php echo esc_attr( $class ) ?>"> + <?php + foreach ( $messages as $msg ) { + ?> + <p> + <span><?php echo esc_html( $msg ); ?><span> + </p> + <?php + } + ?> + </div> + <?php + } + } + } + + /** + * Validates authentication. + * + * @param WP_User $user WP_User object of the logged-in user. + * + * @return bool Whether the user gave a valid code + */ + public function validate_authentication( $user ) { + if ( ! empty( $_REQUEST['authcode'] ) ) { // WPCS: CSRF ok, nonce verified by login_form_validate_2fa(). + return $this->is_valid_authcode( + $this->get_user_totp_key( $user->ID ), + sanitize_text_field( $_REQUEST['authcode'] ) // WPCS: CSRF ok, nonce verified by login_form_validate_2fa(). + ); + } + + return false; + } + + /** + * Checks if a given code is valid for a given key, allowing for a certain amount of time drift + * + * @param string $key The share secret key to use. + * @param string $authcode The code to test. + * + * @return bool Whether the code is valid within the time frame + */ + public static function is_valid_authcode( $key, $authcode ) { + /** + * Filter the maximum ticks to allow when checking valid codes. + * + * Ticks are the allowed offset from the correct time in 30 second increments, + * so the default of 4 allows codes that are two minutes to either side of server time + * + * @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 = time() / self::DEFAULT_TIME_STEP_SEC; + + foreach ( $ticks as $offset ) { + $log_time = $time + $offset; + if ( self::calc_totp( $key, $log_time ) === $authcode ) { + return true; + } + } + return false; + } + + /** + * Generates key + * + * @param int $bitsize Nume of bits to use for key. + * + * @return string $bitsize long string composed of available base32 chars. + */ + public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) { + $bytes = ceil( $bitsize / 8 ); + $secret = wp_generate_password( $bytes, true, true ); + + return self::base32_encode( $secret ); + } + + /** + * Pack stuff + * + * @param string $value The value to be packed. + * + * @return string Binary packed string. + */ + public static function pack64( $value ) { + // 64bit mode (PHP_INT_SIZE == 8). + if ( PHP_INT_SIZE >= 8 ) { + // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality. + if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) { + return pack( 'J', $value ); + } + $highmap = 0xffffffff << 32; + $higher = ( $value & $highmap ) >> 32; + } else { + /* + * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher + * and not pack anything beyond it's limits. + */ + $higher = 0; + } + + $lowmap = 0xffffffff; + $lower = $value & $lowmap; + + return pack( 'NN', $higher, $lower ); + } + + /** + * Calculate a valid code given the shared secret key + * + * @param string $key The shared secret key to use for calculating code. + * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size. + * @param int $digits The number of digits in the returned code. + * @param string $hash The hash used to calculate the code. + * @param int $time_step The size of the time step. + * + * @return string The totp code + */ + public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) { + $secret = self::base32_decode( $key ); + + if ( false === $step_count ) { + $step_count = floor( time() / $time_step ); + } + + $timestamp = self::pack64( $step_count ); + + $hash = hash_hmac( $hash, $timestamp, $secret, true ); + + $offset = ord( $hash[19] ) & 0xf; + + $code = ( + ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) | + ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) | + ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) | + ( ord( $hash[ $offset + 3 ] ) & 0xff ) + ) % pow( 10, $digits ); + + return str_pad( $code, $digits, '0', STR_PAD_LEFT ); + } + + /** + * Uses the Google Charts API to build a QR Code for use with an otpauth url + * + * @param string $name The name to display in the Authentication app. + * @param string $key The secret key to share with the Authentication app. + * @param string $title The title to display in the Authentication app. + * + * @return string A URL to use as an img src to display the QR code + */ + public static function get_google_qr_code( $name, $key, $title = null ) { + // Encode to support spaces, question marks and other characters. + $name = rawurlencode( $name ); + $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key ); + if ( isset( $title ) ) { + $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) ); + } + return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url; + } + + /** + * Whether this Two Factor provider is configured and available for the user specified. + * + * @param WP_User $user WP_User object of the logged-in user. + * + * @return boolean + */ + public function is_available_for_user( $user ) { + // Only available if the secret key has been saved for the user. + $key = $this->get_user_totp_key( $user->ID ); + + return ! empty( $key ); + } + + /** + * Prints the form that prompts the user to authenticate. + * + * @param WP_User $user WP_User object of the logged-in user. + */ + public function authentication_page( $user ) { + require_once( ABSPATH . '/wp-admin/includes/template.php' ); + ?> + <p> + <label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label> + <input type="tel" name="authcode" id="authcode" class="input" value="" size="20" pattern="[0-9]*" /> + </p> + <script type="text/javascript"> + setTimeout( function(){ + var d; + try{ + d = document.getElementById('authcode'); + d.value = ''; + d.focus(); + } catch(e){} + }, 200); + </script> + <?php + submit_button( __( 'Authenticate', 'two-factor' ) ); + } + + /** + * Returns a base32 encoded string. + * + * @param string $string String to be encoded using base32. + * + * @return string base32 encoded string without padding. + */ + public static function base32_encode( $string ) { + if ( empty( $string ) ) { + return ''; + } + + $binary_string = ''; + + foreach ( str_split( $string ) as $character ) { + $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT ); + } + + $five_bit_sections = str_split( $binary_string, 5 ); + $base32_string = ''; + + foreach ( $five_bit_sections as $five_bit_section ) { + $base32_string .= self::$_base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ]; + } + + return $base32_string; + } + + /** + * Decode a base32 string and return a binary representation + * + * @param string $base32_string The base 32 string to decode. + * + * @throws Exception If string contains non-base32 characters. + * + * @return string Binary representation of decoded string + */ + public static function base32_decode( $base32_string ) { + + $base32_string = strtoupper( $base32_string ); + + if ( ! preg_match( '/^[' . self::$_base_32_chars . ']+$/', $base32_string, $match ) ) { + throw new Exception( 'Invalid characters in the base32 string.' ); + } + + $l = strlen( $base32_string ); + $n = 0; + $j = 0; + $binary = ''; + + for ( $i = 0; $i < $l; $i++ ) { + + $n = $n << 5; // Move buffer left by 5 to make room. + $n = $n + strpos( self::$_base_32_chars, $base32_string[ $i ] ); // Add value into buffer. + $j += 5; // Keep track of number of bits in buffer. + + if ( $j >= 8 ) { + $j -= 8; + $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j ); + } + } + + return $binary; + } + + /** + * Used with usort to sort an array by distance from 0 + * + * @param int $a First array element. + * @param int $b Second array element. + * + * @return int -1, 0, or 1 as needed by usort + */ + private static function abssort( $a, $b ) { + $a = abs( $a ); + $b = abs( $b ); + if ( $a === $b ) { + return 0; + } + return ($a < $b) ? -1 : 1; + } +} diff --git a/wp-content/plugins/two-factor/providers/css/fido-u2f-admin.css b/wp-content/plugins/two-factor/providers/css/fido-u2f-admin.css new file mode 100644 index 0000000000000000000000000000000000000000..947dbf43255ea44863424506c2419d6c35c2656b --- /dev/null +++ b/wp-content/plugins/two-factor/providers/css/fido-u2f-admin.css @@ -0,0 +1,10 @@ +#security-keys-section .wp-list-table { + margin-bottom: 2em; +} +#security-keys-section .register-security-key .spinner { + float: none; +} +#security-keys-section .security-key-status { + vertical-align: middle; + font-style: italic; +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..8f40372babda610ccee947155f213f29b10df4de --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/fido-u2f-admin-inline-edit.js @@ -0,0 +1,145 @@ +/* global 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; + 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ? t.revert() : 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( '<tr class="hidden"></tr>' ); + + 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 + }; + + fields = $( '#edit-' + id ).find( ':input' ).serialize(); + params = fields + '&' + $.param( params ); + + // Make ajax request. + $.post( ajaxurl, params, + function( r ) { + var row, newID, optionValue; + $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' ); + + if ( r ) { + if ( -1 !== r.indexOf( '<tr' ) ) { + $( inlineEditKey.what + id ).siblings( 'tr.hidden' ).addBack().remove(); + newID = $( r ).attr( 'id' ); + + $( '#edit-' + id ).before( r ).remove(); + + if ( newID ) { + optionValue = newID.replace( 'key-', '' ); + row = $( '#' + newID ); + } else { + optionValue = id; + row = $( inlineEditKey.what + id ); + } + + row.hide().fadeIn(); + } else { + $( '#edit-' + id + ' .inline-edit-save .error' ).html( r ).show(); + } + } else { + $( '#edit-' + id + ' .inline-edit-save .error' ).html( inlineEditL10n.error ).show(); + } + } + ); + return false; + }, + + revert: function() { + var id = $( '#security-keys-section table.widefat tr.inline-editor' ).attr( 'id' ); + + if ( id ) { + $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' ); + $( '#' + id ).siblings( 'tr.hidden' ).addBack().remove(); + id = id.replace( /\w+\-/, '' ); + $( this.what + id ).show(); + } + + return false; + }, + + getId: function( o ) { + var id = 'TR' === o.tagName ? o.id : $( o ).parents( 'tr' ).attr( 'id' ); + return id.replace( /\w+\-/, '' ); + } + }; + + $( document ).ready( function() { + inlineEditKey.init(); + } ); + +} )( jQuery ); diff --git a/wp-content/plugins/two-factor/providers/js/fido-u2f-admin.js b/wp-content/plugins/two-factor/providers/js/fido-u2f-admin.js new file mode 100644 index 0000000000000000000000000000000000000000..66bb0238a880ff324d1fd20f7ebf46d58e80acb5 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/fido-u2f-admin.js @@ -0,0 +1,48 @@ +/* global u2f, u2fL10n */ +( function( $ ) { + var $button = $( '#register_security_key' ); + var $statusNotice = $( '#security-keys-section .security-key-status' ); + var u2fSupported = ( window.u2f && 'register' in window.u2f ); + + if ( ! u2fSupported ) { + $statusNotice.text( u2fL10n.text.u2f_not_supported ); + } + + $button.click( function() { + var registerRequest; + + if ( $( this ).prop( 'disabled' ) ) { + return false; + } + + $( this ).prop( 'disabled', true ); + $( '.register-security-key .spinner' ).addClass( 'is-active' ); + $statusNotice.text( '' ); + + registerRequest = { + version: u2fL10n.register.request.version, + challenge: u2fL10n.register.request.challenge + }; + + window.u2f.register( u2fL10n.register.request.appId, [ registerRequest ], u2fL10n.register.sigs, function( data ) { + $( '.register-security-key .spinner' ).removeClass( 'is-active' ); + $button.prop( 'disabled', false ); + + if ( data.errorCode ) { + if ( u2fL10n.text.error_codes[ data.errorCode ] ) { + $statusNotice.text( u2fL10n.text.error_codes[ data.errorCode ] ); + } else { + $statusNotice.text( u2fL10n.text.error_codes[ u2fL10n.text.error ] ); + } + + return false; + } + + $( '#do_new_security_key' ).val( 'true' ); + $( '#u2f_response' ).val( JSON.stringify( data ) ); + + // See: http://stackoverflow.com/questions/833032/submit-is-not-a-function-error-in-javascript + $( '<form>' )[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 new file mode 100644 index 0000000000000000000000000000000000000000..0fe4cb1ab5f2a4fa012de47b3561b2fbdcf5aea3 --- /dev/null +++ b/wp-content/plugins/two-factor/providers/js/fido-u2f-login.js @@ -0,0 +1,16 @@ +/* global u2f, u2fL10n */ +( 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/readme.md b/wp-content/plugins/two-factor/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..97bf5210a377c398fda13c3705eb8596b7df7fca --- /dev/null +++ b/wp-content/plugins/two-factor/readme.md @@ -0,0 +1,51 @@ +<!-- DO NOT EDIT THIS FILE; it is auto-generated from readme.txt --> +# Two-Factor + + +Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), email and backup verification codes. + +**Contributors:** [georgestephanis](https://profiles.wordpress.org/georgestephanis), [valendesigns](https://profiles.wordpress.org/valendesigns), [stevenkword](https://profiles.wordpress.org/stevenkword), [extendwings](https://profiles.wordpress.org/extendwings), [sgrant](https://profiles.wordpress.org/sgrant), [aaroncampbell](https://profiles.wordpress.org/aaroncampbell), [johnbillion](https://profiles.wordpress.org/johnbillion), [stevegrunwell](https://profiles.wordpress.org/stevegrunwell), [netweb](https://profiles.wordpress.org/netweb), [kasparsd](https://profiles.wordpress.org/kasparsd) +**Tags:** [two factor](https://wordpress.org/plugins/tags/two-factor), [two step](https://wordpress.org/plugins/tags/two-step), [authentication](https://wordpress.org/plugins/tags/authentication), [login](https://wordpress.org/plugins/tags/login), [totp](https://wordpress.org/plugins/tags/totp), [fido u2f](https://wordpress.org/plugins/tags/fido-u2f), [u2f](https://wordpress.org/plugins/tags/u2f), [email](https://wordpress.org/plugins/tags/email), [backup codes](https://wordpress.org/plugins/tags/backup-codes), [2fa](https://wordpress.org/plugins/tags/2fa), [yubikey](https://wordpress.org/plugins/tags/yubikey) +**Requires at least:** 4.3 +**Tested up to:** 5.2 +**Stable tag:** trunk (master) + +[](https://travis-ci.org/georgestephanis/two-factor) [](https://coveralls.io/github/georgestephanis/two-factor) [](http://gruntjs.com) + +## 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: + +- Email codes +- Time Based One-Time Passwords (TOTP) +- FIDO Universal 2nd Factor (U2F) +- Backup Codes +- Dummy Method (only for testing purposes) + +For more history, see [this post](https://stephanis.info/2013/08/14/two-cents-on-two-factor/). + +## Screenshots ## + +### Two-factor options under User Profile. + + + +### U2F Security Keys section under User Profile. + + + +## Get Involved ## + +Development happens [on GitHub](https://github.com/georgestephanis/two-factor/). Join the `#core-passwords` channel [on WordPress Slack](http://wordpress.slack.com) ([sign up here](http://chat.wordpress.org)). + +Here is how to get started: + + $ git clone https://github.com/georgestephanis/two-factor.git + $ npm install + +Then open [a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) with the suggested changes. + +## Changelog ## + +See the [release history](https://github.com/georgestephanis/two-factor/releases). + diff --git a/wp-content/plugins/two-factor/readme.txt b/wp-content/plugins/two-factor/readme.txt new file mode 100644 index 0000000000000000000000000000000000000000..6baa4caa8561a961a584c41e93beb1f3339ed590 --- /dev/null +++ b/wp-content/plugins/two-factor/readme.txt @@ -0,0 +1,42 @@ +=== Two-Factor === +Contributors: georgestephanis, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, kasparsd +Tags: two factor, two step, authentication, login, totp, fido u2f, u2f, email, backup codes, 2fa, yubikey +Requires at least: 4.3 +Tested up to: 5.2 +Stable tag: trunk + +Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), 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: + +- Email codes +- Time Based One-Time Passwords (TOTP) +- FIDO Universal 2nd Factor (U2F) +- Backup Codes +- Dummy Method (only for testing purposes) + +For more history, see [this post](https://stephanis.info/2013/08/14/two-cents-on-two-factor/). + + +== Screenshots == + +1. Two-factor options under User Profile. +2. U2F Security Keys section under User Profile. + + +== Get Involved == + +Development happens [on GitHub](https://github.com/georgestephanis/two-factor/). Join the `#core-passwords` channel [on WordPress Slack](http://wordpress.slack.com) ([sign up here](http://chat.wordpress.org)). + +Here is how to get started: + + $ git clone https://github.com/georgestephanis/two-factor.git + $ npm install + +Then open [a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) with the suggested changes. + +== Changelog == + +See the [release history](https://github.com/georgestephanis/two-factor/releases). diff --git a/wp-content/plugins/two-factor/two-factor.php b/wp-content/plugins/two-factor/two-factor.php new file mode 100644 index 0000000000000000000000000000000000000000..636e29cd7ebc37abe555cbe40cbd8b15ad1b9d2c --- /dev/null +++ b/wp-content/plugins/two-factor/two-factor.php @@ -0,0 +1,28 @@ +<?php +/** + * Plugin Name: Two Factor + * Plugin URI: https://wordpress.org/plugins/two-factor/ + * Description: A prototype extensible core to enable Two-Factor Authentication. + * Author: Plugin Contributors + * Version: 0.4.7 + * Author URI: https://github.com/georgestephanis/two-factor/graphs/contributors + * Network: True + * Text Domain: two-factor + */ + +/** + * Shortcut constant to the path of this file. + */ +define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) ); + +/** + * Include the base class here, so that other plugins can also extend it. + */ +require_once( TWO_FACTOR_DIR . 'providers/class.two-factor-provider.php' ); + +/** + * Include the core that handles the common bits. + */ +require_once( TWO_FACTOR_DIR . 'class.two-factor-core.php' ); + +Two_Factor_Core::add_hooks(); diff --git a/wp-content/plugins/two-factor/user-edit.css b/wp-content/plugins/two-factor/user-edit.css new file mode 100644 index 0000000000000000000000000000000000000000..9572fb6fc28f04c98b2715e0cc33400584602c8d --- /dev/null +++ b/wp-content/plugins/two-factor/user-edit.css @@ -0,0 +1,37 @@ + +.two-factor-methods-table { + background-color: #fff; + border: 1px solid #e5e5e5; + border-spacing: 0; +} + +.two-factor-methods-table thead, +.two-factor-methods-table tfoot { + background: #fff; +} + +.two-factor-methods-table thead th { + padding: 0.5em; +} + +.two-factor-methods-table .col-primary, +.two-factor-methods-table .col-enabled { + width: 5%; +} + +.two-factor-methods-table .col-name { + width: 90%; +} + +.two-factor-methods-table tbody th { + text-align: center; +} + +.two-factor-methods-table tbody th, +.two-factor-methods-table tbody td { + vertical-align: top; +} + +.two-factor-methods-table tbody tr:nth-child(odd) { + background-color: #f9f9f9; +} \ No newline at end of file