Changeset 59828
- Timestamp:
- 02/17/2025 11:22:33 AM (2 months ago)
- Location:
- trunk
- Files:
-
- 1 added
- 12 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/includes/upgrade.php
r59754 r59828 981 981 * @ignore 982 982 * @since 1.2.0 983 * @since 6.8.0 User passwords are no longer hashed with md5. 983 984 * 984 985 * @global wpdb $wpdb WordPress database abstraction object. … … 993 994 $newname = sanitize_title( $user->user_nickname ); 994 995 $wpdb->update( $wpdb->users, array( 'user_nicename' => $newname ), array( 'ID' => $user->ID ) ); 995 }996 }997 998 $users = $wpdb->get_results( "SELECT ID, user_pass from $wpdb->users" );999 foreach ( $users as $row ) {1000 if ( ! preg_match( '/^[A-Fa-f0-9]{32}$/', $row->user_pass ) ) {1001 $wpdb->update( $wpdb->users, array( 'user_pass' => md5( $row->user_pass ) ), array( 'ID' => $row->ID ) );1002 996 } 1003 997 } -
trunk/src/wp-includes/class-wp-application-passwords.php
r59754 r59828 61 61 * @since 5.6.0 62 62 * @since 5.7.0 Returns WP_Error if application name already exists. 63 * @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass. 63 64 * 64 65 * @param int $user_id User ID. … … 96 97 97 98 $new_password = wp_generate_password( static::PW_LENGTH, false ); 98 $hashed_password = wp_hash_password( $new_password );99 $hashed_password = self::hash_password( $new_password ); 99 100 100 101 $new_item = array( … … 125 126 * 126 127 * @since 5.6.0 128 * @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass. 127 129 * 128 130 * @param int $user_id The user ID. … … 250 252 * 251 253 * @since 5.6.0 254 * @since 6.8.0 The actual password should now be hashed using wp_fast_hash(). 252 255 * 253 256 * @param int $user_id User ID. … … 297 300 * 298 301 * @since 5.6.0 302 * @since 6.8.0 The password is now hashed using wp_fast_hash() instead of phpass. 303 * Existing passwords may still be hashed using phpass. 299 304 * 300 305 * @param int $user_id The user ID. … … 468 473 return trim( chunk_split( $raw_password, 4, ' ' ) ); 469 474 } 475 476 /** 477 * Hashes a plaintext application password. 478 * 479 * @since 6.8.0 480 * 481 * @param string $password Plaintext password. 482 * @return string Hashed password. 483 */ 484 public static function hash_password( 485 #[\SensitiveParameter] 486 string $password 487 ): string { 488 return wp_fast_hash( $password ); 489 } 490 491 /** 492 * Checks a plaintext application password against a hashed password. 493 * 494 * @since 6.8.0 495 * 496 * @param string $password Plaintext password. 497 * @param string $hash Hash of the password to check against. 498 * @return bool Whether the password matches the hashed password. 499 */ 500 public static function check_password( 501 #[\SensitiveParameter] 502 string $password, 503 string $hash 504 ): bool { 505 return wp_verify_fast_hash( $password, $hash ); 506 } 470 507 } -
trunk/src/wp-includes/class-wp-recovery-mode-key-service.php
r58975 r59828 38 38 * 39 39 * @since 5.2.0 40 * 41 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. 40 * @since 6.8.0 The stored key is now hashed using wp_fast_hash() instead of phpass. 42 41 * 43 42 * @param string $token A token generated by {@see generate_recovery_mode_token()}. … … 45 44 */ 46 45 public function generate_and_store_recovery_mode_key( $token ) { 47 48 global $wp_hasher;49 50 46 $key = wp_generate_password( 22, false ); 51 52 if ( empty( $wp_hasher ) ) {53 require_once ABSPATH . WPINC . '/class-phpass.php';54 $wp_hasher = new PasswordHash( 8, true );55 }56 57 $hashed = $wp_hasher->HashPassword( $key );58 47 59 48 $records = $this->get_keys(); 60 49 61 50 $records[ $token ] = array( 62 'hashed_key' => $hashed,51 'hashed_key' => wp_fast_hash( $key ), 63 52 'created_at' => time(), 64 53 ); … … 86 75 * @since 5.2.0 87 76 * 88 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.89 *90 77 * @param string $token The token used when generating the given key. 91 * @param string $key The unhashedkey.78 * @param string $key The plain text key. 92 79 * @param int $ttl Time in seconds for the key to be valid for. 93 80 * @return true|WP_Error True on success, error object on failure. 94 81 */ 95 82 public function validate_recovery_mode_key( $token, $key, $ttl ) { 96 global $wp_hasher;97 98 83 $records = $this->get_keys(); 99 84 … … 110 95 } 111 96 112 if ( empty( $wp_hasher ) ) { 113 require_once ABSPATH . WPINC . '/class-phpass.php'; 114 $wp_hasher = new PasswordHash( 8, true ); 115 } 116 117 if ( ! $wp_hasher->CheckPassword( $key, $record['hashed_key'] ) ) { 97 if ( ! wp_verify_fast_hash( $key, $record['hashed_key'] ) ) { 118 98 return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) ); 119 99 } … … 170 150 * 171 151 * @since 5.2.0 152 * @since 6.8.0 Each key is now hashed using wp_fast_hash() instead of phpass. 153 * Existing keys may still be hashed using phpass. 172 154 * 173 * @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key' 174 * and 'created_at'. 155 * @return array { 156 * Associative array of token => data pairs, where the data is an associative 157 * array of information about the key. 158 * 159 * @type array ...$0 { 160 * Information about the key. 161 * 162 * @type string $hashed_key The hashed value of the key. 163 * @type int $created_at The timestamp when the key was created. 164 * } 165 * } 175 166 */ 176 167 private function get_keys() { … … 182 173 * 183 174 * @since 5.2.0 175 * @since 6.8.0 Each key should now be hashed using wp_fast_hash() instead of phpass. 184 176 * 185 * @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key' 186 * and 'created_at'. 177 * @param array $keys { 178 * Associative array of token => data pairs, where the data is an associative 179 * array of information about the key. 180 * 181 * @type array ...$0 { 182 * Information about the key. 183 * 184 * @type string $hashed_key The hashed value of the key. 185 * @type int $created_at The timestamp when the key was created. 186 * } 187 * } 187 188 * @return bool True on success, false on failure. 188 189 */ -
trunk/src/wp-includes/class-wp-user-request.php
r54133 r59828 93 93 * 94 94 * @since 4.9.6 95 * @since 6.8.0 The key is now hashed using wp_fast_hash() instead of phpass. 96 * 95 97 * @var string 96 98 */ -
trunk/src/wp-includes/class-wp-user.php
r57644 r59828 12 12 * 13 13 * @since 2.0.0 14 * @since 6.8.0 The `user_pass` property is now hashed using bcrypt instead of phpass. 14 15 * 15 16 * @property string $nickname -
trunk/src/wp-includes/functions.php
r59790 r59828 9115 9115 return in_array( $mime_type, $heic_mime_types, true ); 9116 9116 } 9117 9118 /** 9119 * Returns a cryptographically secure hash of a message using a fast generic hash function. 9120 * 9121 * Use the wp_verify_fast_hash() function to verify the hash. 9122 * 9123 * This function does not salt the value prior to being hashed, therefore input to this function must originate from 9124 * a random generator with sufficiently high entropy, preferably greater than 128 bits. This function is used internally 9125 * in WordPress to hash security keys and application passwords which are generated with high entropy. 9126 * 9127 * Important: 9128 * 9129 * - This function must not be used for hashing user-generated passwords. Use wp_hash_password() for that. 9130 * - This function must not be used for hashing other low-entropy input. Use wp_hash() for that. 9131 * 9132 * The BLAKE2b algorithm is used by Sodium to hash the message. 9133 * 9134 * @since 6.8.0 9135 * 9136 * @throws TypeError Thrown by Sodium if the message is not a string. 9137 * 9138 * @param string $message The message to hash. 9139 * @return string The hash of the message. 9140 */ 9141 function wp_fast_hash( 9142 #[\SensitiveParameter] 9143 string $message 9144 ): string { 9145 return '$generic$' . sodium_bin2hex( sodium_crypto_generichash( $message ) ); 9146 } 9147 9148 /** 9149 * Checks whether a plaintext message matches the hashed value. Used to verify values hashed via wp_fast_hash(). 9150 * 9151 * The function uses Sodium to hash the message and compare it to the hashed value. If the hash is not a generic hash, 9152 * the hash is treated as a phpass portable hash in order to provide backward compatibility for application passwords 9153 * which were hashed using phpass prior to WordPress 6.8.0. 9154 * 9155 * @since 6.8.0 9156 * 9157 * @throws TypeError Thrown by Sodium if the message is not a string. 9158 * 9159 * @param string $message The plaintext message. 9160 * @param string $hash Hash of the message to check against. 9161 * @return bool Whether the message matches the hashed message. 9162 */ 9163 function wp_verify_fast_hash( 9164 #[\SensitiveParameter] 9165 string $message, 9166 string $hash 9167 ): bool { 9168 if ( ! str_starts_with( $hash, '$generic$' ) ) { 9169 // Back-compat for old phpass hashes. 9170 require_once ABSPATH . WPINC . '/class-phpass.php'; 9171 return ( new PasswordHash( 8, true ) )->CheckPassword( $message, $hash ); 9172 } 9173 9174 return hash_equals( $hash, wp_fast_hash( $message ) ); 9175 } -
trunk/src/wp-includes/pluggable.php
r59803 r59828 694 694 * @param string $cookie Optional. If used, will validate contents instead of cookie's. 695 695 * @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'. 696 * Note: This does *not* default to 'auth' like other cookie functions. 696 697 * @return int|false User ID if valid cookie, false if invalid. 697 698 */ … … 769 770 } 770 771 771 $pass_frag = substr( $user->user_pass, 8, 4 ); 772 if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) { 773 // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords. 774 $pass_frag = substr( $user->user_pass, 8, 4 ); 775 } else { 776 // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes. 777 $pass_frag = substr( $user->user_pass, -4 ); 778 } 772 779 773 780 $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); … … 870 877 } 871 878 872 $pass_frag = substr( $user->user_pass, 8, 4 ); 879 if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) { 880 // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords. 881 $pass_frag = substr( $user->user_pass, 8, 4 ); 882 } else { 883 // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes. 884 $pass_frag = substr( $user->user_pass, -4 ); 885 } 873 886 874 887 $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); … … 2626 2639 * 2627 2640 * @since 2.5.0 2628 * 2629 * @global PasswordHash $wp_hasher PHPass object. 2641 * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass. 2642 * 2643 * @global PasswordHash $wp_hasher phpass object. 2630 2644 * 2631 2645 * @param string $password Plain text user password to hash. … … 2638 2652 global $wp_hasher; 2639 2653 2640 if ( empty( $wp_hasher ) ) { 2641 require_once ABSPATH . WPINC . '/class-phpass.php'; 2642 // By default, use the portable hash from phpass. 2643 $wp_hasher = new PasswordHash( 8, true ); 2644 } 2645 2646 return $wp_hasher->HashPassword( trim( $password ) ); 2654 if ( ! empty( $wp_hasher ) ) { 2655 return $wp_hasher->HashPassword( trim( $password ) ); 2656 } 2657 2658 if ( strlen( $password ) > 4096 ) { 2659 return '*'; 2660 } 2661 2662 /** 2663 * Filters the hashing algorithm to use in the password_hash() and password_needs_rehash() functions. 2664 * 2665 * The default is the value of the `PASSWORD_BCRYPT` constant which means bcrypt is used. 2666 * 2667 * **Important:** The only password hashing algorithm that is guaranteed to be available across PHP 2668 * installations is bcrypt. If you use any other algorithm you must make sure that it is available on 2669 * the server. The `password_algos()` function can be used to check which hashing algorithms are available. 2670 * 2671 * The hashing options can be controlled via the {@see 'wp_hash_password_options'} filter. 2672 * 2673 * Other available constants include: 2674 * 2675 * - `PASSWORD_ARGON2I` 2676 * - `PASSWORD_ARGON2ID` 2677 * - `PASSWORD_DEFAULT` 2678 * 2679 * @since 6.8.0 2680 * 2681 * @param string $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant. 2682 */ 2683 $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT ); 2684 2685 /** 2686 * Filters the options passed to the password_hash() and password_needs_rehash() functions. 2687 * 2688 * The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'} 2689 * filter. You must ensure that the options are appropriate for the algorithm in use. 2690 * 2691 * @since 6.8.0 2692 * 2693 * @param array $options Array of options to pass to the password hashing functions. 2694 * By default this is an empty array which means the default 2695 * options will be used. 2696 * @param string $algorithm The hashing algorithm in use. 2697 */ 2698 $options = apply_filters( 'wp_hash_password_options', array(), $algorithm ); 2699 2700 // Algorithms other than bcrypt don't need to use pre-hashing. 2701 if ( PASSWORD_BCRYPT !== $algorithm ) { 2702 return password_hash( $password, $algorithm, $options ); 2703 } 2704 2705 // Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation. 2706 $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) ); 2707 2708 // Add a prefix to facilitate distinguishing vanilla bcrypt hashes. 2709 return '$wp' . password_hash( $password_to_hash, $algorithm, $options ); 2647 2710 } 2648 2711 endif; … … 2652 2715 * Checks a plaintext password against a hashed password. 2653 2716 * 2654 * Maintains compatibility between old version and the new cookie authentication 2655 * protocol using PHPass library. The $hash parameter is the encrypted password 2656 * and the function compares the plain text password when encrypted similarly 2657 * against the already encrypted password to see if they match. 2717 * Note that this function may be used to check a value that is not a user password. 2718 * A plugin may use this function to check a password of a different type, and there 2719 * may not always be a user ID associated with the password. 2658 2720 * 2659 2721 * For integration with other applications, this function can be overwritten to … … 2661 2723 * 2662 2724 * @since 2.5.0 2663 * 2664 * @global PasswordHash $wp_hasher PHPass object used for checking the password 2665 * against the $hash + $password. 2666 * @uses PasswordHash::CheckPassword 2667 * 2668 * @param string $password Plaintext user's password. 2669 * @param string $hash Hash of the user's password to check against. 2670 * @param string|int $user_id Optional. User ID. 2725 * @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A 2726 * password that wasn't hashed with bcrypt will be checked with phpass. 2727 * Passwords hashed with md5 are no longer supported. 2728 * 2729 * @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying 2730 * passwords that were hashed with phpass. 2731 * 2732 * @param string $password Plaintext password. 2733 * @param string $hash Hash of the password to check against. 2734 * @param string|int $user_id Optional. ID of a user associated with the password. 2671 2735 * @return bool False, if the $password does not match the hashed password. 2672 2736 */ … … 2679 2743 global $wp_hasher; 2680 2744 2681 // If the hash is still md5... 2745 $check = false; 2746 2747 // If the hash is still md5 or otherwise truncated then invalidate it. 2682 2748 if ( strlen( $hash ) <= 32 ) { 2683 $check = hash_equals( $hash, md5( $password ) );2684 if ( $check && $user_id ) {2685 // Rehash using new hash.2686 wp_set_password( $password, $user_id );2687 $hash = wp_hash_password( $password );2688 }2689 2690 2749 /** 2691 * Filters whether the plaintext password matches the encrypted password.2750 * Filters whether the plaintext password matches the hashed password. 2692 2751 * 2693 2752 * @since 2.5.0 2753 * @since 6.8.0 Passwords are now hashed with bcrypt by default. 2754 * Old passwords may still be hashed with phpass. 2694 2755 * 2695 2756 * @param bool $check Whether the passwords match. 2696 2757 * @param string $password The plaintext password. 2697 2758 * @param string $hash The hashed password. 2698 * @param string|int $user_id User ID. Can be empty. 2759 * @param string|int $user_id Optional ID of a user associated with the password. 2760 * Can be empty. 2699 2761 */ 2700 2762 return apply_filters( 'check_password', $check, $password, $hash, $user_id ); 2701 2763 } 2702 2764 2703 /* 2704 * If the stored hash is longer than an MD5, 2705 * presume the new style phpass portable hash. 2706 */ 2707 if ( empty( $wp_hasher ) ) { 2765 if ( ! empty( $wp_hasher ) ) { 2766 // Check the password using the overridden hasher. 2767 $check = $wp_hasher->CheckPassword( $password, $hash ); 2768 } elseif ( strlen( $password ) > 4096 ) { 2769 $check = false; 2770 } elseif ( str_starts_with( $hash, '$wp' ) ) { 2771 // Check the password using the current prefixed hash. 2772 $password_to_verify = base64_encode( hash_hmac( 'sha384', $password, 'wp-sha384', true ) ); 2773 $check = password_verify( $password_to_verify, substr( $hash, 3 ) ); 2774 } elseif ( str_starts_with( $hash, '$P$' ) ) { 2775 // Check the password using phpass. 2708 2776 require_once ABSPATH . WPINC . '/class-phpass.php'; 2709 // By default, use the portable hash from phpass.2710 $wp_hasher = new PasswordHash( 8, true );2711 }2712 2713 $check = $wp_hasher->CheckPassword( $password, $hash );2777 $check = ( new PasswordHash( 8, true ) )->CheckPassword( $password, $hash ); 2778 } else { 2779 // Check the password using compat support for any non-prefixed hash. 2780 $check = password_verify( $password, $hash ); 2781 } 2714 2782 2715 2783 /** This filter is documented in wp-includes/pluggable.php */ 2716 2784 return apply_filters( 'check_password', $check, $password, $hash, $user_id ); 2785 } 2786 endif; 2787 2788 if ( ! function_exists( 'wp_password_needs_rehash' ) ) : 2789 /** 2790 * Checks whether a password hash needs to be rehashed. 2791 * 2792 * Passwords are hashed with bcrypt using the default cost. A password hashed in a prior version 2793 * of WordPress may still be hashed with phpass and will need to be rehashed. If the default cost 2794 * or algorithm is changed in PHP or WordPress then a password hashed in a previous version will 2795 * need to be rehashed. 2796 * 2797 * Note that, just like wp_check_password(), this function may be used to check a value that is 2798 * not a user password. A plugin may use this function to check a password of a different type, 2799 * and there may not always be a user ID associated with the password. 2800 * 2801 * @since 6.8.0 2802 * 2803 * @global PasswordHash $wp_hasher phpass object. 2804 * 2805 * @param string $hash Hash of a password to check. 2806 * @param string|int $user_id Optional. ID of a user associated with the password. 2807 * @return bool Whether the hash needs to be rehashed. 2808 */ 2809 function wp_password_needs_rehash( $hash, $user_id = '' ) { 2810 global $wp_hasher; 2811 2812 if ( ! empty( $wp_hasher ) ) { 2813 return false; 2814 } 2815 2816 /** This filter is documented in wp-includes/pluggable.php */ 2817 $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT ); 2818 2819 /** This filter is documented in wp-includes/pluggable.php */ 2820 $options = apply_filters( 'wp_hash_password_options', array(), $algorithm ); 2821 2822 $prefixed = str_starts_with( $hash, '$wp' ); 2823 2824 if ( ( PASSWORD_BCRYPT === $algorithm ) && ! $prefixed ) { 2825 // If bcrypt is in use and the hash is not prefixed then it needs to be rehashed. 2826 $needs_rehash = true; 2827 } else { 2828 // Otherwise check the hash minus its prefix if necessary. 2829 $hash_to_check = $prefixed ? substr( $hash, 3 ) : $hash; 2830 $needs_rehash = password_needs_rehash( $hash_to_check, $algorithm, $options ); 2831 } 2832 2833 /** 2834 * Filters whether the password hash needs to be rehashed. 2835 * 2836 * @since 6.8.0 2837 * 2838 * @param bool $needs_rehash Whether the password hash needs to be rehashed. 2839 * @param string $hash The password hash. 2840 * @param string|int $user_id Optional. ID of a user associated with the password. 2841 */ 2842 return apply_filters( 'password_needs_rehash', $needs_rehash, $hash, $user_id ); 2717 2843 } 2718 2844 endif; … … 2866 2992 * 2867 2993 * @since 2.5.0 2994 * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass. 2868 2995 * 2869 2996 * @global wpdb $wpdb WordPress database abstraction object. -
trunk/src/wp-includes/user.php
r59817 r59828 206 206 } 207 207 208 if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { 208 $valid = wp_check_password( $password, $user->user_pass, $user->ID ); 209 210 if ( ! $valid ) { 209 211 return new WP_Error( 210 212 'incorrect_password', … … 220 222 } 221 223 224 if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) { 225 wp_set_password( $password, $user->ID ); 226 } 227 222 228 return $user; 223 229 } … … 283 289 } 284 290 285 if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { 291 $valid = wp_check_password( $password, $user->user_pass, $user->ID ); 292 293 if ( ! $valid ) { 286 294 return new WP_Error( 287 295 'incorrect_password', … … 295 303 '</a>' 296 304 ); 305 } 306 307 if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) { 308 wp_set_password( $password, $user->ID ); 297 309 } 298 310 … … 446 458 447 459 foreach ( $hashed_passwords as $key => $item ) { 448 if ( ! wp_check_password( $password, $item['password'], $user->ID) ) {460 if ( ! WP_Application_Passwords::check_password( $password, $item['password'] ) ) { 449 461 continue; 450 462 } … … 2432 2444 * @since 4.9.0 2433 2445 * @since 5.8.0 The `$userdata` parameter was added. 2446 * @since 6.8.0 The user's password is now hashed using bcrypt instead of phpass. 2434 2447 * 2435 2448 * @param array $data { … … 2979 2992 * @since 4.4.0 2980 2993 * 2981 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.2982 *2983 2994 * @param WP_User $user User to retrieve password reset key for. 2984 2995 * @return string|WP_Error Password reset key on success. WP_Error on error. 2985 2996 */ 2986 2997 function get_password_reset_key( $user ) { 2987 global $wp_hasher;2988 2989 2998 if ( ! ( $user instanceof WP_User ) ) { 2990 2999 return new WP_Error( 'invalidcombo', __( '<strong>Error:</strong> There is no account with that username or email address.' ) ); … … 3032 3041 do_action( 'retrieve_password_key', $user->user_login, $key ); 3033 3042 3034 // Now insert the key, hashed, into the DB. 3035 if ( empty( $wp_hasher ) ) { 3036 require_once ABSPATH . WPINC . '/class-phpass.php'; 3037 $wp_hasher = new PasswordHash( 8, true ); 3038 } 3039 3040 $hashed = time() . ':' . $wp_hasher->HashPassword( $key ); 3043 $hashed = time() . ':' . wp_fast_hash( $key ); 3041 3044 3042 3045 $key_saved = wp_update_user( … … 3064 3067 * @since 3.1.0 3065 3068 * 3066 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. 3067 * 3068 * @param string $key Hash to validate sending user's password. 3069 * @param string $key The password reset key. 3069 3070 * @param string $login The user login. 3070 3071 * @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys. … … 3075 3076 $login 3076 3077 ) { 3077 global $wp_hasher;3078 3079 3078 $key = preg_replace( '/[^a-z0-9]/i', '', $key ); 3080 3079 … … 3091 3090 if ( ! $user ) { 3092 3091 return new WP_Error( 'invalid_key', __( 'Invalid key.' ) ); 3093 }3094 3095 if ( empty( $wp_hasher ) ) {3096 require_once ABSPATH . WPINC . '/class-phpass.php';3097 $wp_hasher = new PasswordHash( 8, true );3098 3092 } 3099 3093 … … 3119 3113 } 3120 3114 3121 $hash_is_correct = $wp_hasher->CheckPassword( $key, $pass_key );3115 $hash_is_correct = wp_verify_fast_hash( $key, $pass_key ); 3122 3116 3123 3117 if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) { … … 3134 3128 /** 3135 3129 * Filters the return value of check_password_reset_key() when an 3136 * old-style key is used.3130 * old-style key or an expired key is used. 3137 3131 * 3138 3132 * @since 3.7.0 Previously plain-text keys were stored in the database. … … 3155 3149 * @since 5.7.0 Added `$user_login` parameter. 3156 3150 * 3157 * @global wpdb $wpdb WordPress database abstraction object. 3158 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. 3151 * @global wpdb $wpdb WordPress database abstraction object. 3159 3152 * 3160 3153 * @param string $user_login Optional. Username to send a password retrieval email for. … … 4937 4930 * @since 4.9.6 4938 4931 * 4939 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.4940 *4941 4932 * @param int $request_id Request ID. 4942 4933 * @return string Confirmation key. 4943 4934 */ 4944 4935 function wp_generate_user_request_key( $request_id ) { 4945 global $wp_hasher;4946 4947 4936 // Generate something random for a confirmation key. 4948 4937 $key = wp_generate_password( 20, false ); 4949 4938 4950 // Return the key, hashed. 4951 if ( empty( $wp_hasher ) ) { 4952 require_once ABSPATH . WPINC . '/class-phpass.php'; 4953 $wp_hasher = new PasswordHash( 8, true ); 4954 } 4955 4939 // Save the key, hashed. 4956 4940 wp_update_post( 4957 4941 array( 4958 4942 'ID' => $request_id, 4959 4943 'post_status' => 'request-pending', 4960 'post_password' => $wp_hasher->HashPassword( $key ),4944 'post_password' => wp_fast_hash( $key ), 4961 4945 ) 4962 4946 ); … … 4969 4953 * 4970 4954 * @since 4.9.6 4971 *4972 * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.4973 4955 * 4974 4956 * @param string $request_id ID of the request being confirmed. … … 4981 4963 $key 4982 4964 ) { 4983 global $wp_hasher;4984 4985 4965 $request_id = absint( $request_id ); 4986 4966 $request = wp_get_user_request( $request_id ); … … 5000 4980 } 5001 4981 5002 if ( empty( $wp_hasher ) ) {5003 require_once ABSPATH . WPINC . '/class-phpass.php';5004 $wp_hasher = new PasswordHash( 8, true );5005 }5006 5007 4982 /** 5008 4983 * Filters the expiration time of confirm keys. … … 5015 4990 $expiration_time = $key_request_time + $expiration_duration; 5016 4991 5017 if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) {4992 if ( ! wp_verify_fast_hash( $key, $saved_key ) ) { 5018 4993 return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) ); 5019 4994 } -
trunk/tests/phpunit/includes/bootstrap.php
r59769 r59828 330 330 require __DIR__ . '/class-wp-rest-test-configurable-controller.php'; 331 331 require __DIR__ . '/class-wp-fake-block-type.php'; 332 require __DIR__ . '/class-wp-fake-hasher.php'; 332 333 require __DIR__ . '/class-wp-sitemaps-test-provider.php'; 333 334 require __DIR__ . '/class-wp-sitemaps-empty-test-provider.php'; -
trunk/tests/phpunit/tests/auth.php
r59595 r59828 11 11 const USER_PASS = 'password'; 12 12 13 /** 14 * @var WP_User 15 */ 13 16 protected $user; 14 17 … … 17 20 */ 18 21 protected static $_user; 22 23 /** 24 * @var int 25 */ 19 26 protected static $user_id; 27 28 /** 29 * @var PasswordHash 30 */ 20 31 protected static $wp_hasher; 32 33 protected static $bcrypt_length_limit = 72; 34 35 protected static $phpass_length_limit = 4096; 36 37 protected static $password_length_limit = 4096; 21 38 22 39 /** … … 90 107 $cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'foo' ); 91 108 $this->assertFalse( wp_validate_auth_cookie( $cookie, 'bar' ) ); 109 } 110 111 /** 112 * @ticket 21022 113 */ 114 public function test_auth_cookie_generated_with_phpass_hash_remains_valid() { 115 self::set_user_password_with_phpass( 'password', self::$user_id ); 116 117 $auth_cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' ); 118 119 $this->assertSame( self::$user_id, wp_validate_auth_cookie( $auth_cookie, 'auth' ) ); 120 } 121 122 /** 123 * @ticket 21022 124 */ 125 public function test_auth_cookie_generated_with_plain_bcrypt_hash_remains_valid() { 126 self::set_user_password_with_plain_bcrypt( 'password', self::$user_id ); 127 128 $auth_cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' ); 129 130 $this->assertSame( self::$user_id, wp_validate_auth_cookie( $auth_cookie, 'auth' ) ); 92 131 } 93 132 … … 107 146 $authed_user = wp_authenticate( $this->user->user_login, $password_to_test ); 108 147 148 $this->assertNotWPError( $authed_user ); 109 149 $this->assertInstanceOf( 'WP_User', $authed_user ); 110 150 $this->assertSame( $this->user->ID, $authed_user->ID ); … … 158 198 $password = "pass with vertical tab o_O\x0B"; 159 199 $this->assertTrue( wp_check_password( 'pass with vertical tab o_O', wp_hash_password( $password ) ) ); 200 } 201 202 /** 203 * @ticket 21022 204 */ 205 public function test_wp_check_password_supports_phpass_hash() { 206 $password = 'password'; 207 $hash = self::$wp_hasher->HashPassword( $password ); 208 $this->assertTrue( wp_check_password( $password, $hash ) ); 209 $this->assertSame( 1, did_filter( 'check_password' ) ); 210 } 211 212 /** 213 * Ensure wp_check_password() remains compatible with an increase to the default bcrypt cost. 214 * 215 * The test verifies this by reducing the cost used to generate the hash, therefore mimicing a hash 216 * which was generated prior to the default cost being increased. 217 * 218 * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 . 219 * 220 * @ticket 21022 221 */ 222 public function test_wp_check_password_supports_hash_with_increased_bcrypt_cost() { 223 $password = 'password'; 224 225 // Reducing the cost mimics an increase to the default cost. 226 add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 227 $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); 228 remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 229 230 $this->assertTrue( wp_check_password( $password, $hash ) ); 231 $this->assertSame( 1, did_filter( 'check_password' ) ); 232 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 233 } 234 235 /** 236 * Ensure wp_check_password() remains compatible with a reduction of the default bcrypt cost. 237 * 238 * The test verifies this by increasing the cost used to generate the hash, therefore mimicing a hash 239 * which was generated prior to the default cost being reduced. 240 * 241 * A reduction of the cost is unlikely to occur but is fully supported. 242 * 243 * @ticket 21022 244 */ 245 public function test_wp_check_password_supports_hash_with_reduced_bcrypt_cost() { 246 $password = 'password'; 247 248 // Increasing the cost mimics a reduction of the default cost. 249 add_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); 250 $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); 251 remove_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); 252 253 $this->assertTrue( wp_check_password( $password, $hash ) ); 254 $this->assertSame( 1, did_filter( 'check_password' ) ); 255 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 256 } 257 258 /** 259 * @ticket 21022 260 */ 261 public function test_wp_check_password_supports_wp_hash_with_default_bcrypt_cost() { 262 $password = 'password'; 263 264 $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); 265 266 $this->assertTrue( wp_check_password( $password, $hash ) ); 267 $this->assertSame( 1, did_filter( 'check_password' ) ); 268 $this->assertFalse( wp_password_needs_rehash( $hash ) ); 269 } 270 271 /** 272 * @ticket 21022 273 */ 274 public function test_wp_check_password_supports_plain_bcrypt_hash_with_default_bcrypt_cost() { 275 $password = 'password'; 276 277 $hash = password_hash( $password, PASSWORD_BCRYPT ); 278 279 $this->assertTrue( wp_check_password( $password, $hash ) ); 280 $this->assertSame( 1, did_filter( 'check_password' ) ); 281 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 282 } 283 284 /** 285 * Ensure wp_check_password() is compatible with Argon2i hashes. 286 * 287 * @ticket 21022 288 */ 289 public function test_wp_check_password_supports_argon2i_hash() { 290 if ( ! defined( 'PASSWORD_ARGON2I' ) ) { 291 $this->fail( 'Argon2i is not supported.' ); 292 } 293 294 $password = 'password'; 295 $hash = password_hash( trim( $password ), PASSWORD_ARGON2I ); 296 $this->assertTrue( wp_check_password( $password, $hash ) ); 297 $this->assertSame( 1, did_filter( 'check_password' ) ); 298 } 299 300 /** 301 * Ensure wp_check_password() is compatible with Argon2id hashes. 302 * 303 * @requires PHP >= 7.3 304 * 305 * @ticket 21022 306 */ 307 public function test_wp_check_password_supports_argon2id_hash() { 308 if ( ! defined( 'PASSWORD_ARGON2ID' ) ) { 309 $this->fail( 'Argon2id is not supported.' ); 310 } 311 312 $password = 'password'; 313 $hash = password_hash( trim( $password ), PASSWORD_ARGON2ID ); 314 $this->assertTrue( wp_check_password( $password, $hash ) ); 315 $this->assertSame( 1, did_filter( 'check_password' ) ); 316 } 317 318 /** 319 * @ticket 21022 320 */ 321 public function test_wp_check_password_does_not_support_md5_hashes() { 322 $password = 'password'; 323 $hash = md5( $password ); 324 $this->assertFalse( wp_check_password( $password, $hash ) ); 325 $this->assertSame( 1, did_filter( 'check_password' ) ); 326 } 327 328 /** 329 * @ticket 21022 330 */ 331 public function test_wp_check_password_does_not_support_plain_text() { 332 $password = 'password'; 333 $hash = $password; 334 $this->assertFalse( wp_check_password( $password, $hash ) ); 335 $this->assertSame( 1, did_filter( 'check_password' ) ); 336 } 337 338 /** 339 * @ticket 21022 340 * 341 * @dataProvider data_empty_values 342 * @param mixed $value 343 */ 344 public function test_wp_check_password_does_not_support_empty_hash( $value ) { 345 $password = 'password'; 346 $hash = $value; 347 $this->assertFalse( wp_check_password( $password, $hash ) ); 348 $this->assertSame( 1, did_filter( 'check_password' ) ); 349 } 350 351 /** 352 * @ticket 21022 353 * 354 * @dataProvider data_empty_values 355 * @param mixed $value 356 */ 357 public function test_wp_check_password_does_not_support_empty_password( $value ) { 358 $password = $value; 359 $hash = $value; 360 $this->assertFalse( wp_check_password( $password, $hash ) ); 361 $this->assertSame( 1, did_filter( 'check_password' ) ); 362 } 363 364 public function data_empty_values() { 365 return array( 366 // Integer zero: 367 array( 0 ), 368 // String zero: 369 array( '0' ), 370 // Zero-length string: 371 array( '' ), 372 // Null byte character: 373 array( "\0" ), 374 // Asterisk values: 375 array( '*' ), 376 array( '*0' ), 377 array( '*1' ), 378 ); 160 379 } 161 380 … … 236 455 } 237 456 238 public function test_password_length_limit() { 239 $limit = str_repeat( 'a', 4096 ); 240 457 /** 458 * @ticket 21022 459 */ 460 public function test_password_is_hashed_with_bcrypt() { 461 $password = 'password'; 462 463 // Set the user password. 464 wp_set_password( $password, self::$user_id ); 465 466 // Ensure the password is hashed with bcrypt. 467 $this->assertStringStartsWith( '$wp$2y$', get_userdata( self::$user_id )->user_pass ); 468 469 // Authenticate. 470 $user = wp_authenticate( $this->user->user_login, $password ); 471 472 // Verify correct password. 473 $this->assertNotWPError( $user ); 474 $this->assertInstanceOf( 'WP_User', $user ); 475 $this->assertSame( self::$user_id, $user->ID ); 476 } 477 478 /** 479 * @ticket 21022 480 */ 481 public function test_invalid_password_at_bcrypt_length_limit_is_rejected() { 482 $limit = str_repeat( 'a', self::$bcrypt_length_limit ); 483 484 // Set the user password to the bcrypt limit. 241 485 wp_set_password( $limit, self::$user_id ); 242 // phpass hashed password.243 $this->assertStringStartsWith( '$P$', $this->user->data->user_pass );244 486 245 487 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 246 488 // Wrong password. 247 $this->assertInstanceOf( 'WP_Error', $user ); 248 489 $this->assertWPError( $user ); 490 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 491 } 492 493 /** 494 * @ticket 21022 495 */ 496 public function test_invalid_password_beyond_bcrypt_length_limit_is_rejected() { 497 $limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 ); 498 499 // Set the user password beyond the bcrypt limit. 500 wp_set_password( $limit, self::$user_id ); 501 502 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 503 // Wrong password. 504 $this->assertWPError( $user ); 505 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 506 } 507 508 /** 509 * @ticket 21022 510 */ 511 public function test_valid_password_at_bcrypt_length_limit_is_accepted() { 512 $limit = str_repeat( 'a', self::$bcrypt_length_limit ); 513 514 // Set the user password to the bcrypt limit. 515 wp_set_password( $limit, self::$user_id ); 516 517 // Authenticate. 249 518 $user = wp_authenticate( $this->user->user_login, $limit ); 519 520 // Correct password. 521 $this->assertNotWPError( $user ); 250 522 $this->assertInstanceOf( 'WP_User', $user ); 251 523 $this->assertSame( self::$user_id, $user->ID ); 252 253 // One char too many. 254 $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); 524 } 525 526 /** 527 * @ticket 21022 528 */ 529 public function test_valid_password_beyond_bcrypt_length_limit_is_accepted() { 530 $limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 ); 531 532 // Set the user password beyond the bcrypt limit. 533 wp_set_password( $limit, self::$user_id ); 534 535 // Authenticate. 536 $user = wp_authenticate( $this->user->user_login, $limit ); 537 538 // Correct password depite its length. 539 $this->assertNotWPError( $user ); 540 $this->assertInstanceOf( 'WP_User', $user ); 541 $this->assertSame( self::$user_id, $user->ID ); 542 } 543 544 /** 545 * A password beyond 72 bytes will be truncated by bcrypt by default and still be accepted. 546 * 547 * This ensures that a truncated password is not accepted by WordPress. 548 * 549 * @ticket 21022 550 */ 551 public function test_long_truncated_password_is_rejected() { 552 $at_limit = str_repeat( 'a', self::$bcrypt_length_limit ); 553 $beyond_limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 ); 554 555 // Set the user password beyond the bcrypt limit. 556 wp_set_password( $beyond_limit, self::$user_id ); 557 558 // Authenticate using a truncated password. 559 $user = wp_authenticate( $this->user->user_login, $at_limit ); 560 561 // Incorrect password. 562 $this->assertWPError( $user ); 563 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 564 } 565 566 /** 567 * @ticket 21022 568 */ 569 public function test_setting_password_beyond_bcrypt_length_limit_is_rejected() { 570 $beyond_limit = str_repeat( 'a', self::$password_length_limit + 1 ); 571 572 // Set the user password beyond the limit. 573 wp_set_password( $beyond_limit, self::$user_id ); 574 575 // Password broken by setting it to be too long. 576 $user = get_user_by( 'id', self::$user_id ); 577 $this->assertSame( '*', $user->data->user_pass ); 578 579 // Password is not accepted. 580 $user = wp_authenticate( $this->user->user_login, $beyond_limit ); 581 $this->assertInstanceOf( 'WP_Error', $user ); 582 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 583 584 // Placeholder is not accepted. 585 $user = wp_authenticate( $this->user->user_login, '*' ); 586 $this->assertInstanceOf( 'WP_Error', $user ); 587 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 588 } 589 590 /** 591 * @see https://core.trac.wordpress.org/changeset/30466 592 */ 593 public function test_invalid_password_at_phpass_length_limit_is_rejected() { 594 $limit = str_repeat( 'a', self::$phpass_length_limit ); 595 596 // Set the user password with the old phpass algorithm. 597 self::set_user_password_with_phpass( $limit, self::$user_id ); 598 599 // Authenticate. 600 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 601 255 602 // Wrong password. 256 603 $this->assertInstanceOf( 'WP_Error', $user ); 257 258 wp_set_password( $limit . 'a', self::$user_id ); 604 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 605 } 606 607 public function test_valid_password_at_phpass_length_limit_is_accepted() { 608 $limit = str_repeat( 'a', self::$phpass_length_limit ); 609 610 // Set the user password with the old phpass algorithm. 611 self::set_user_password_with_phpass( $limit, self::$user_id ); 612 613 // Authenticate. 614 $user = wp_authenticate( $this->user->user_login, $limit ); 615 616 // Correct password. 617 $this->assertNotWPError( $user ); 618 $this->assertInstanceOf( 'WP_User', $user ); 619 $this->assertSame( self::$user_id, $user->ID ); 620 } 621 622 public function test_too_long_password_at_phpass_length_limit_is_rejected() { 623 $limit = str_repeat( 'a', self::$phpass_length_limit ); 624 625 // Set the user password with the old phpass algorithm. 626 self::set_user_password_with_phpass( $limit, self::$user_id ); 627 628 // Authenticate with a password that is one character too long. 629 $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); 630 631 // Wrong password. 632 $this->assertInstanceOf( 'WP_Error', $user ); 633 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 634 } 635 636 public function test_too_long_password_beyond_phpass_length_limit_is_rejected() { 637 // One char too many. 638 $too_long = str_repeat( 'a', self::$phpass_length_limit + 1 ); 639 640 // Set the user password with the old phpass algorithm. 641 self::set_user_password_with_phpass( $too_long, self::$user_id ); 642 259 643 $user = get_user_by( 'id', self::$user_id ); 260 644 // Password broken by setting it to be too long. 261 645 $this->assertSame( '*', $user->data->user_pass ); 262 646 647 // Password is not accepted. 263 648 $user = wp_authenticate( $this->user->user_login, '*' ); 264 649 $this->assertInstanceOf( 'WP_Error', $user ); 265 266 $user = wp_authenticate( $this->user->user_login, '*0' ); 650 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 651 } 652 653 /** 654 * @dataProvider data_empty_values 655 * @param mixed $value 656 */ 657 public function test_empty_password_is_rejected_by_bcrypt( $value ) { 658 // Set the user password. 659 wp_set_password( 'password', self::$user_id ); 660 661 $user = wp_authenticate( $this->user->user_login, $value ); 267 662 $this->assertInstanceOf( 'WP_Error', $user ); 268 269 $user = wp_authenticate( $this->user->user_login, '*1' ); 663 } 664 665 /** 666 * @dataProvider data_empty_values 667 * @param mixed $value 668 */ 669 public function test_empty_password_is_rejected_by_phpass( $value ) { 670 // Set the user password with the old phpass algorithm. 671 self::set_user_password_with_phpass( 'password', self::$user_id ); 672 673 $user = wp_authenticate( $this->user->user_login, $value ); 270 674 $this->assertInstanceOf( 'WP_Error', $user ); 675 } 676 677 public function test_incorrect_password_is_rejected_by_phpass() { 678 // Set the user password with the old phpass algorithm. 679 self::set_user_password_with_phpass( 'password', self::$user_id ); 271 680 272 681 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 682 273 683 // Wrong password. 274 684 $this->assertInstanceOf( 'WP_Error', $user ); 275 276 $user = wp_authenticate( $this->user->user_login, $limit ); 277 // Wrong password. 278 $this->assertInstanceOf( 'WP_Error', $user ); 685 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 686 } 687 688 public function test_too_long_password_is_rejected_by_phpass() { 689 $limit = str_repeat( 'a', self::$phpass_length_limit ); 690 691 // Set the user password with the old phpass algorithm. 692 self::set_user_password_with_phpass( 'password', self::$user_id ); 279 693 280 694 $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); 695 281 696 // Password broken by setting it to be too long. 282 697 $this->assertInstanceOf( 'WP_Error', $user ); 698 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 283 699 } 284 700 … … 307 723 $wpdb->users, 308 724 array( 309 'user_activation_key' => strtotime( '-1 hour' ) . ':' . self::$wp_hasher->HashPassword( $key ),725 'user_activation_key' => strtotime( '-1 hour' ) . ':' . wp_fast_hash( $key ), 310 726 ), 311 727 array( … … 345 761 $wpdb->users, 346 762 array( 347 'user_activation_key' => strtotime( '-48 hours' ) . ':' . self::$wp_hasher->HashPassword( $key ),763 'user_activation_key' => strtotime( '-48 hours' ) . ':' . wp_fast_hash( $key ), 348 764 ), 349 765 array( … … 356 772 $check = check_password_reset_key( $key, $this->user->user_login ); 357 773 $this->assertInstanceOf( 'WP_Error', $check ); 774 $this->assertSame( 'expired_key', $check->get_error_code() ); 358 775 } 359 776 … … 394 811 $check = check_password_reset_key( $key, $this->user->user_login ); 395 812 $this->assertInstanceOf( 'WP_Error', $check ); 813 $this->assertSame( 'expired_key', $check->get_error_code() ); 396 814 397 815 // An empty key with a legacy user_activation_key should be rejected. 398 816 $check = check_password_reset_key( '', $this->user->user_login ); 399 817 $this->assertInstanceOf( 'WP_Error', $check ); 818 $this->assertSame( 'invalid_key', $check->get_error_code() ); 819 } 820 821 /** 822 * @ticket 21022 823 */ 824 public function test_phpass_user_activation_key_is_allowed() { 825 global $wpdb; 826 827 // A legacy user_activation_key is one hashed using phpass between WordPress 4.3 and 6.8.0. 828 829 $key = wp_generate_password( 20, false ); 830 $wpdb->update( 831 $wpdb->users, 832 array( 833 'user_activation_key' => strtotime( '-1 hour' ) . ':' . self::$wp_hasher->HashPassword( $key ), 834 ), 835 array( 836 'ID' => $this->user->ID, 837 ) 838 ); 839 clean_user_cache( $this->user ); 840 841 // A legacy phpass user_activation_key should remain valid. 842 $check = check_password_reset_key( $key, $this->user->user_login ); 843 $this->assertNotWPError( $check ); 844 $this->assertInstanceOf( 'WP_User', $check ); 845 $this->assertSame( $this->user->ID, $check->ID ); 846 847 // An empty key with a legacy user_activation_key should be rejected. 848 $check = check_password_reset_key( '', $this->user->user_login ); 849 $this->assertWPError( $check ); 850 $this->assertSame( 'invalid_key', $check->get_error_code() ); 851 } 852 853 /** 854 * @ticket 21022 855 */ 856 public function test_expired_phpass_user_activation_key_is_rejected() { 857 global $wpdb; 858 859 // A legacy user_activation_key is one hashed using phpass between WordPress 4.3 and 6.8.0. 860 861 $key = wp_generate_password( 20, false ); 862 $wpdb->update( 863 $wpdb->users, 864 array( 865 'user_activation_key' => strtotime( '-48 hours' ) . ':' . self::$wp_hasher->HashPassword( $key ), 866 ), 867 array( 868 'ID' => $this->user->ID, 869 ) 870 ); 871 clean_user_cache( $this->user ); 872 873 // A legacy phpass user_activation_key should still be subject to an expiry check. 874 $check = check_password_reset_key( $key, $this->user->user_login ); 875 $this->assertWPError( $check ); 876 $this->assertSame( 'expired_key', $check->get_error_code() ); 877 878 // An empty key with a legacy user_activation_key should be rejected. 879 $check = check_password_reset_key( '', $this->user->user_login ); 880 $this->assertWPError( $check ); 881 $this->assertSame( 'invalid_key', $check->get_error_code() ); 882 } 883 884 /** 885 * @ticket 21022 886 */ 887 public function test_user_request_key_handling() { 888 $request_id = wp_create_user_request( 'test@example.com', 'remove_personal_data' ); 889 $key = wp_generate_user_request_key( $request_id ); 890 891 // A valid key should be accepted. 892 $check = wp_validate_user_request_key( $request_id, $key ); 893 $this->assertNotWPError( $check ); 894 $this->assertTrue( $check ); 895 896 // An invalid key should rejected. 897 $check = wp_validate_user_request_key( $request_id, 'invalid' ); 898 $this->assertWPError( $check ); 899 $this->assertSame( 'invalid_key', $check->get_error_code() ); 900 901 // An empty key should be rejected. 902 $check = wp_validate_user_request_key( $request_id, '' ); 903 $this->assertWPError( $check ); 904 $this->assertSame( 'missing_key', $check->get_error_code() ); 905 } 906 907 /** 908 * @ticket 21022 909 */ 910 public function test_phpass_user_request_key_is_allowed() { 911 // A legacy user request key is one hashed using phpass between WordPress 4.3 and 6.8.0. 912 913 $request_id = wp_create_user_request( 'test@example.com', 'remove_personal_data' ); 914 $key = wp_generate_password( 20, false ); 915 916 wp_update_post( 917 array( 918 'ID' => $request_id, 919 'post_password' => self::$wp_hasher->HashPassword( $key ), 920 ) 921 ); 922 923 // A legacy phpass key should remain valid. 924 $check = wp_validate_user_request_key( $request_id, $key ); 925 $this->assertNotWPError( $check ); 926 $this->assertTrue( $check ); 927 928 // An empty key with a legacy key should be rejected. 929 $check = wp_validate_user_request_key( $request_id, '' ); 930 $this->assertWPError( $check ); 931 $this->assertSame( 'missing_key', $check->get_error_code() ); 932 } 933 934 /** 935 * The `wp_password_needs_rehash()` function is just a wrapper around `password_needs_rehash()`, but this ensures 936 * that it works as expected. 937 * 938 * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 . 939 * 940 * @ticket 21022 941 */ 942 public function check_password_needs_rehashing() { 943 $password = 'password'; 944 945 // Current password hashing algorithm. 946 $hash = wp_hash_password( $password ); 947 $this->assertFalse( wp_password_needs_rehash( $hash ) ); 948 949 // A future upgrade from a previously lower cost. 950 $default = self::get_default_bcrypt_cost(); 951 $opts = array( 952 // Reducing the cost mimics an increase in the default cost. 953 'cost' => $default - 1, 954 ); 955 $hash = password_hash( $password, PASSWORD_BCRYPT, $opts ); 956 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 957 958 // Previous phpass algorithm. 959 $hash = self::$wp_hasher->HashPassword( $password ); 960 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 961 962 // o_O md5. 963 $hash = md5( $password ); 964 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 400 965 } 401 966 … … 456 1021 $this->assertEmpty( $user->user_activation_key, 'The `user_activation_key` was not empty on the user object returned by `wp_signon()` function.' ); 457 1022 $this->assertEmpty( $activation_key_from_database, 'The `user_activation_key` was not empty in the database.' ); 1023 } 1024 1025 /** 1026 * @ticket 21022 1027 */ 1028 public function test_phpass_application_password_is_accepted() { 1029 add_filter( 'application_password_is_api_request', '__return_true' ); 1030 add_filter( 'wp_is_application_passwords_available', '__return_true' ); 1031 1032 $password = 'password'; 1033 1034 // Set an application password with the old phpass algorithm. 1035 $uuid = self::set_application_password_with_phpass( $password, self::$user_id ); 1036 1037 // Authenticate. 1038 $user = wp_authenticate_application_password( null, self::USER_LOGIN, $password ); 1039 1040 // Verify that the phpass hash for the application password was valid. 1041 $this->assertNotWPError( $user ); 1042 $this->assertInstanceOf( 'WP_User', $user ); 1043 $this->assertSame( self::$user_id, $user->ID ); 1044 } 1045 1046 /** 1047 * @dataProvider data_usernames 1048 * 1049 * @ticket 21022 1050 */ 1051 public function test_phpass_password_is_rehashed_after_successful_user_password_authentication( $username_or_email ) { 1052 $password = 'password'; 1053 1054 // Set the user password with the old phpass algorithm. 1055 self::set_user_password_with_phpass( $password, self::$user_id ); 1056 1057 // Verify that the password needs rehashing. 1058 $hash = get_userdata( self::$user_id )->user_pass; 1059 $this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) ); 1060 1061 // Authenticate. 1062 $user = wp_authenticate( $username_or_email, $password ); 1063 1064 // Verify that the phpass password hash was valid. 1065 $this->assertNotWPError( $user ); 1066 $this->assertInstanceOf( 'WP_User', $user ); 1067 $this->assertSame( self::$user_id, $user->ID ); 1068 1069 // Verify that the password no longer needs rehashing. 1070 $hash = get_userdata( self::$user_id )->user_pass; 1071 $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) ); 1072 1073 // Authenticate a second time to ensure the new hash is valid. 1074 $user = wp_authenticate( $username_or_email, $password ); 1075 1076 // Verify that the bcrypt password hash is valid. 1077 $this->assertNotWPError( $user ); 1078 $this->assertInstanceOf( 'WP_User', $user ); 1079 $this->assertSame( self::$user_id, $user->ID ); 1080 } 1081 1082 /** 1083 * @dataProvider data_usernames 1084 * 1085 * @ticket 21022 1086 */ 1087 public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_user_password_authentication( $username_or_email ) { 1088 $password = 'password'; 1089 1090 // Hash the user password with a lower cost than default to mimic a cost upgrade. 1091 add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 1092 wp_set_password( $password, self::$user_id ); 1093 remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 1094 1095 // Verify that the password needs rehashing. 1096 $hash = get_userdata( self::$user_id )->user_pass; 1097 $this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) ); 1098 1099 // Authenticate. 1100 $user = wp_authenticate( $username_or_email, $password ); 1101 1102 // Verify that the reduced cost password hash was valid. 1103 $this->assertNotWPError( $user ); 1104 $this->assertInstanceOf( 'WP_User', $user ); 1105 $this->assertSame( self::$user_id, $user->ID ); 1106 1107 // Verify that the password has been rehashed with the increased cost. 1108 $hash = get_userdata( self::$user_id )->user_pass; 1109 $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) ); 1110 $this->assertSame( self::get_default_bcrypt_cost(), password_get_info( substr( $hash, 3 ) )['options']['cost'] ); 1111 1112 // Authenticate a second time to ensure the new hash is valid. 1113 $user = wp_authenticate( $username_or_email, $password ); 1114 1115 // Verify that the password hash is valid. 1116 $this->assertNotWPError( $user ); 1117 $this->assertInstanceOf( 'WP_User', $user ); 1118 $this->assertSame( self::$user_id, $user->ID ); 1119 } 1120 1121 public function reduce_hash_cost( array $options ): array { 1122 $options['cost'] = self::get_default_bcrypt_cost() - 1; 1123 return $options; 1124 } 1125 1126 public function increase_hash_cost( array $options ): array { 1127 $options['cost'] = self::get_default_bcrypt_cost() + 1; 1128 return $options; 1129 } 1130 1131 public function data_usernames() { 1132 return array( 1133 array( 1134 self::USER_LOGIN, 1135 ), 1136 array( 1137 self::USER_EMAIL, 1138 ), 1139 ); 1140 } 1141 1142 /** 1143 * @ticket 21022 1144 */ 1145 public function test_password_rehashing_requirement_can_be_filtered() { 1146 $filter_count_before = did_filter( 'password_needs_rehash' ); 1147 1148 wp_password_needs_rehash( '$hash' ); 1149 1150 $this->assertSame( $filter_count_before + 1, did_filter( 'password_needs_rehash' ) ); 1151 } 1152 1153 /** 1154 * @ticket 21022 1155 */ 1156 public function test_password_hashing_algorithm_can_be_filtered() { 1157 $password = 'password'; 1158 1159 $filter_count_before = did_filter( 'wp_hash_password_algorithm' ); 1160 1161 $wp_hash = wp_hash_password( $password ); 1162 1163 wp_check_password( $password, $wp_hash ); 1164 wp_password_needs_rehash( $wp_hash ); 1165 1166 $this->assertSame( $filter_count_before + 2, did_filter( 'wp_hash_password_algorithm' ) ); 1167 } 1168 1169 /** 1170 * @ticket 21022 1171 */ 1172 public function test_password_hashing_options_can_be_filtered() { 1173 $password = 'password'; 1174 1175 add_filter( 1176 'wp_hash_password_options', 1177 static function ( $options ) { 1178 $options['cost'] = 5; 1179 return $options; 1180 } 1181 ); 1182 1183 $filter_count_before = did_filter( 'wp_hash_password_options' ); 1184 1185 $wp_hash = wp_hash_password( $password ); 1186 $valid = wp_check_password( $password, $wp_hash ); 1187 $needs_rehash = wp_password_needs_rehash( $wp_hash ); 1188 $info = password_get_info( substr( $wp_hash, 3 ) ); 1189 $cost = $info['options']['cost']; 1190 1191 $this->assertTrue( $valid ); 1192 $this->assertFalse( $needs_rehash ); 1193 $this->assertSame( $filter_count_before + 2, did_filter( 'wp_hash_password_options' ) ); 1194 $this->assertSame( 5, $cost ); 1195 } 1196 1197 /** 1198 * @ticket 21022 1199 */ 1200 public function test_password_checks_support_wp_hasher_fallback() { 1201 global $wp_hasher; 1202 1203 $filter_count_before = did_filter( 'wp_hash_password_options' ); 1204 1205 $password = 'password'; 1206 1207 // Ensure the global $wp_hasher is set. 1208 $wp_hasher = new WP_Fake_Hasher(); 1209 1210 $hasher_hash = $wp_hasher->HashPassword( $password ); 1211 $wp_hash = wp_hash_password( $password ); 1212 $valid = wp_check_password( $password, $wp_hash ); 1213 $needs_rehash = wp_password_needs_rehash( $wp_hash ); 1214 1215 // Reset the global $wp_hasher. 1216 $wp_hasher = null; 1217 1218 $this->assertSame( $hasher_hash, $wp_hash ); 1219 $this->assertTrue( $valid ); 1220 $this->assertFalse( $needs_rehash ); 1221 $this->assertSame( 1, did_filter( 'check_password' ) ); 1222 $this->assertSame( $filter_count_before, did_filter( 'wp_hash_password_options' ) ); 458 1223 } 459 1224 … … 704 1469 */ 705 1470 public function test_authenticate_application_password_respects_existing_user() { 706 $this->assertSame( self::$_user, wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ) ); 1471 $user = wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ); 1472 $this->assertNotWPError( $user ); 1473 $this->assertSame( self::$_user, $user ); 707 1474 } 708 1475 … … 713 1480 add_filter( 'application_password_is_api_request', '__return_false' ); 714 1481 715 $this->assertNull( wp_authenticate_application_password( null, self::$_user->user_login, 'password' ) ); 1482 $user = wp_authenticate_application_password( null, self::$_user->user_login, 'password' ); 1483 $this->assertNotWPError( $user ); 1484 $this->assertNull( $user ); 716 1485 } 717 1486 … … 806 1575 807 1576 $user = wp_authenticate_application_password( null, self::$_user->user_login, $password ); 1577 $this->assertNotWPError( $user ); 808 1578 $this->assertInstanceOf( WP_User::class, $user ); 809 1579 $this->assertSame( self::$user_id, $user->ID ); … … 820 1590 821 1591 $user = wp_authenticate_application_password( null, self::$_user->user_email, $password ); 1592 $this->assertNotWPError( $user ); 822 1593 $this->assertInstanceOf( WP_User::class, $user ); 823 1594 $this->assertSame( self::$user_id, $user->ID ); … … 834 1605 835 1606 $user = wp_authenticate_application_password( null, self::$_user->user_email, WP_Application_Passwords::chunk_password( $password ) ); 1607 $this->assertNotWPError( $user ); 836 1608 $this->assertInstanceOf( WP_User::class, $user ); 837 1609 $this->assertSame( self::$user_id, $user->ID ); … … 845 1617 846 1618 $authenticated = wp_authenticate_application_password( null, 'idonotexist', 'password' ); 1619 $this->assertNotWPError( $authenticated ); 847 1620 $this->assertNull( $authenticated ); 848 1621 } … … 968 1741 $this->assertSame( $_SERVER['PHP_AUTH_PW'], 'pass:word' ); 969 1742 } 1743 1744 /** 1745 * Test the tests 1746 * 1747 * @covers Tests_Auth::set_user_password_with_phpass 1748 * 1749 * @ticket 21022 1750 */ 1751 public function test_set_user_password_with_phpass() { 1752 // Set the user password with the old phpass algorithm. 1753 self::set_user_password_with_phpass( 'password', self::$user_id ); 1754 1755 // Ensure the password is hashed with phpass. 1756 $hash = get_userdata( self::$user_id )->user_pass; 1757 $this->assertStringStartsWith( '$P$', $hash ); 1758 } 1759 1760 private static function set_user_password_with_phpass( string $password, int $user_id ) { 1761 global $wpdb; 1762 1763 $wpdb->update( 1764 $wpdb->users, 1765 array( 1766 'user_pass' => self::$wp_hasher->HashPassword( $password ), 1767 ), 1768 array( 1769 'ID' => $user_id, 1770 ) 1771 ); 1772 clean_user_cache( $user_id ); 1773 } 1774 1775 1776 /** 1777 * Test the tests 1778 * 1779 * @covers Tests_Auth::set_user_password_with_plain_bcrypt 1780 * 1781 * @ticket 21022 1782 */ 1783 public function test_set_user_password_with_plain_bcrypt() { 1784 // Set the user password with plain bcrypt. 1785 self::set_user_password_with_plain_bcrypt( 'password', self::$user_id ); 1786 1787 // Ensure the password is hashed with bcrypt. 1788 $hash = get_userdata( self::$user_id )->user_pass; 1789 $this->assertStringStartsWith( '$2y$', $hash ); 1790 } 1791 1792 private static function set_user_password_with_plain_bcrypt( string $password, int $user_id ) { 1793 global $wpdb; 1794 1795 $wpdb->update( 1796 $wpdb->users, 1797 array( 1798 'user_pass' => password_hash( 'password', PASSWORD_BCRYPT ), 1799 ), 1800 array( 1801 'ID' => $user_id, 1802 ) 1803 ); 1804 clean_user_cache( $user_id ); 1805 } 1806 1807 /** 1808 * Test the tests 1809 * 1810 * @covers Tests_Auth::set_application_password_with_phpass 1811 * 1812 * @ticket 21022 1813 */ 1814 public function test_set_application_password_with_phpass() { 1815 // Set an application password with the old phpass algorithm. 1816 $uuid = self::set_application_password_with_phpass( 'password', self::$user_id ); 1817 1818 // Ensure the password is hashed with phpass. 1819 $hash = WP_Application_Passwords::get_user_application_password( self::$user_id, $uuid )['password']; 1820 $this->assertStringStartsWith( '$P$', $hash ); 1821 } 1822 1823 private static function set_application_password_with_phpass( string $password, int $user_id ) { 1824 $uuid = wp_generate_uuid4(); 1825 $item = array( 1826 'uuid' => $uuid, 1827 'app_id' => '', 1828 'name' => 'Test', 1829 'password' => self::$wp_hasher->HashPassword( $password ), 1830 'created' => time(), 1831 'last_used' => null, 1832 'last_ip' => null, 1833 ); 1834 1835 $saved = update_user_meta( 1836 $user_id, 1837 WP_Application_Passwords::USERMETA_KEY_APPLICATION_PASSWORDS, 1838 array( $item ) 1839 ); 1840 1841 if ( ! $saved ) { 1842 throw new Exception( 'Could not save application password.' ); 1843 } 1844 1845 update_network_option( get_main_network_id(), WP_Application_Passwords::OPTION_KEY_IN_USE, true ); 1846 1847 return $uuid; 1848 } 1849 1850 private static function get_default_bcrypt_cost(): int { 1851 $hash = password_hash( 'password', PASSWORD_BCRYPT ); 1852 $info = password_get_info( $hash ); 1853 1854 return $info['options']['cost']; 1855 } 970 1856 } -
trunk/tests/phpunit/tests/pluggable/signatures.php
r59578 r59828 215 215 'wp_check_password' => array( 216 216 'password', 217 'hash', 218 'user_id' => '', 219 ), 220 'wp_password_needs_rehash' => array( 217 221 'hash', 218 222 'user_id' => '', -
trunk/tests/phpunit/tests/user/passwordHash.php
r56547 r59828 3 3 /** 4 4 * Tests for the PasswordHash external library. 5 * 6 * PasswordHash is no longer used to hash user passwords or security keys, but it is still used to 7 * hash post passwords and as a fallback to verify old passwords that were hashed by phpass. The 8 * library therefore needs to remain compatible with the latest versions of PHP. 5 9 * 6 10 * @covers PasswordHash
Note: See TracChangeset
for help on using the changeset viewer.