<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[59828] trunk: Security: Switch to using bcrypt for hashing user passwords and BLAKE2b for hashing application passwords and security keys.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/59828">59828</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/59828","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>johnbillion</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2025-02-17 11:22:33 +0000 (Mon, 17 Feb 2025)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Security: Switch to using bcrypt for hashing user passwords and BLAKE2b for hashing application passwords and security keys.

Passwords and security keys that were saved in prior versions of WordPress will continue to work. Each user's password will be opportunistically rehashed and resaved when they next subsequently log in using a valid password.

The following new functions have been introduced:

* `wp_password_needs_rehash()`
* `wp_fast_hash()`
* `wp_verify_fast_hash()`

The following new filters have been introduced:

* `password_needs_rehash`
* `wp_hash_password_algorithm`
* `wp_hash_password_options`

Props ayeshrajans, bgermann, dd32, deadduck169, desrosj, haozi, harrym, iandunn, jammycakes, joehoyle, johnbillion, mbijon, mojorob, mslavco, my1xt, nacin, otto42, paragoninitiativeenterprises, paulkevan, rmccue, ryanhellyer, scribu, swalkinshaw, synchro, th23, timothyblynjacobs, tomdxw, westi, xknown.

Additional thanks go to the Roots team, Soatok, Calvin Alkan, and Raphael Ahrens.

Fixes <a href="https://core.trac.wordpress.org/ticket/21022">#21022</a>, <a href="https://core.trac.wordpress.org/ticket/44628">#44628</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesupgradephp">trunk/src/wp-admin/includes/upgrade.php</a></li>
<li><a href="#trunksrcwpincludesclasswpapplicationpasswordsphp">trunk/src/wp-includes/class-wp-application-passwords.php</a></li>
<li><a href="#trunksrcwpincludesclasswprecoverymodekeyservicephp">trunk/src/wp-includes/class-wp-recovery-mode-key-service.php</a></li>
<li><a href="#trunksrcwpincludesclasswpuserrequestphp">trunk/src/wp-includes/class-wp-user-request.php</a></li>
<li><a href="#trunksrcwpincludesclasswpuserphp">trunk/src/wp-includes/class-wp-user.php</a></li>
<li><a href="#trunksrcwpincludesfunctionsphp">trunk/src/wp-includes/functions.php</a></li>
<li><a href="#trunksrcwpincludespluggablephp">trunk/src/wp-includes/pluggable.php</a></li>
<li><a href="#trunksrcwpincludesuserphp">trunk/src/wp-includes/user.php</a></li>
<li><a href="#trunktestsphpunitincludesbootstrapphp">trunk/tests/phpunit/includes/bootstrap.php</a></li>
<li><a href="#trunktestsphpunittestsauthphp">trunk/tests/phpunit/tests/auth.php</a></li>
<li><a href="#trunktestsphpunittestspluggablesignaturesphp">trunk/tests/phpunit/tests/pluggable/signatures.php</a></li>
<li><a href="#trunktestsphpunittestsuserpasswordHashphp">trunk/tests/phpunit/tests/user/passwordHash.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunitincludesclasswpfakehasherphp">trunk/tests/phpunit/includes/class-wp-fake-hasher.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludesupgradephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/upgrade.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/upgrade.php   2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-admin/includes/upgrade.php     2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -980,6 +980,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @ignore
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 1.2.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.8.0 User passwords are no longer hashed with md5.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @global wpdb $wpdb WordPress database abstraction object.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -995,13 +996,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $users = $wpdb->get_results( "SELECT ID, user_pass from $wpdb->users" );
-       foreach ( $users as $row ) {
-               if ( ! preg_match( '/^[A-Fa-f0-9]{32}$/', $row->user_pass ) ) {
-                       $wpdb->update( $wpdb->users, array( 'user_pass' => md5( $row->user_pass ) ), array( 'ID' => $row->ID ) );
-               }
-       }
-
</del><span class="cx" style="display: block; padding: 0 10px">         // Get the GMT offset, we'll use that later on.
</span><span class="cx" style="display: block; padding: 0 10px">        $all_options = get_alloptions_110();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpapplicationpasswordsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-application-passwords.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-application-passwords.php  2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/class-wp-application-passwords.php    2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -60,6 +60,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.6.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.7.0 Returns WP_Error if application name already exists.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int   $user_id  User ID.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $args     {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -95,7 +96,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $new_password    = wp_generate_password( static::PW_LENGTH, false );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $hashed_password = wp_hash_password( $new_password );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $hashed_password = self::hash_password( $new_password );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $new_item = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'uuid'      => wp_generate_uuid4(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -124,6 +125,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires when an application password is created.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 5.6.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 * @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">                  *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param int    $user_id      The user ID.
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param array  $new_item     {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -249,6 +251,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Updates an application password.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.6.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The actual password should now be hashed using wp_fast_hash().
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int    $user_id User ID.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $uuid    The password's UUID.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -296,6 +299,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * Fires when an application password is updated.
</span><span class="cx" style="display: block; padding: 0 10px">                         *
</span><span class="cx" style="display: block; padding: 0 10px">                         * @since 5.6.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         * @since 6.8.0 The password is now hashed using wp_fast_hash() instead of phpass.
+                        *              Existing passwords may still be hashed using phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">                          *
</span><span class="cx" style="display: block; padding: 0 10px">                         * @param int   $user_id The user ID.
</span><span class="cx" style="display: block; padding: 0 10px">                         * @param array $item    {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -467,4 +472,36 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return trim( chunk_split( $raw_password, 4, ' ' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Hashes a plaintext application password.
+        *
+        * @since 6.8.0
+        *
+        * @param string $password Plaintext password.
+        * @return string Hashed password.
+        */
+       public static function hash_password(
+               #[\SensitiveParameter]
+               string $password
+       ): string {
+               return wp_fast_hash( $password );
+       }
+
+       /**
+        * Checks a plaintext application password against a hashed password.
+        *
+        * @since 6.8.0
+        *
+        * @param string $password Plaintext password.
+        * @param string $hash     Hash of the password to check against.
+        * @return bool Whether the password matches the hashed password.
+        */
+       public static function check_password(
+               #[\SensitiveParameter]
+               string $password,
+               string $hash
+       ): bool {
+               return wp_verify_fast_hash( $password, $hash );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpincludesclasswprecoverymodekeyservicephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-recovery-mode-key-service.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-recovery-mode-key-service.php      2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/class-wp-recovery-mode-key-service.php        2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -37,29 +37,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Creates a recovery mode key.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The stored key is now hashed using wp_fast_hash() instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
-        *
</del><span class="cx" style="display: block; padding: 0 10px">          * @param string $token A token generated by {@see generate_recovery_mode_token()}.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string Recovery mode key.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function generate_and_store_recovery_mode_key( $token ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               global $wp_hasher;
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $key = wp_generate_password( 22, false );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( empty( $wp_hasher ) ) {
-                       require_once ABSPATH . WPINC . '/class-phpass.php';
-                       $wp_hasher = new PasswordHash( 8, true );
-               }
-
-               $hashed = $wp_hasher->HashPassword( $key );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $records = $this->get_keys();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $records[ $token ] = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'hashed_key' => $hashed,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'hashed_key' => wp_fast_hash( $key ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'created_at' => time(),
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -85,16 +74,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
-        *
</del><span class="cx" style="display: block; padding: 0 10px">          * @param string $token The token used when generating the given key.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param string $key   The unhashed key.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string $key   The plain text key.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @param int    $ttl   Time in seconds for the key to be valid for.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return true|WP_Error True on success, error object on failure.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function validate_recovery_mode_key( $token, $key, $ttl ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                global $wp_hasher;
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $records = $this->get_keys();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! isset( $records[ $token ] ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -109,12 +94,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( empty( $wp_hasher ) ) {
-                       require_once ABSPATH . WPINC . '/class-phpass.php';
-                       $wp_hasher = new PasswordHash( 8, true );
-               }
-
-               if ( ! $wp_hasher->CheckPassword( $key, $record['hashed_key'] ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! wp_verify_fast_hash( $key, $record['hashed_key'] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -169,9 +149,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Gets the recovery key records.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 Each key is now hashed using wp_fast_hash() instead of phpass.
+        *              Existing keys may still be hashed using phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key'
-        *               and 'created_at'.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return array {
+        *     Associative array of token => data pairs, where the data is an associative
+        *     array of information about the key.
+        *
+        *     @type array ...$0 {
+        *         Information about the key.
+        *
+        *         @type string $hashed_key The hashed value of the key.
+        *         @type int    $created_at The timestamp when the key was created.
+        *     }
+        * }
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        private function get_keys() {
</span><span class="cx" style="display: block; padding: 0 10px">                return (array) get_option( $this->option_name, array() );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -181,9 +172,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Updates the recovery key records.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 Each key should now be hashed using wp_fast_hash() instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key'
-        *                    and 'created_at'.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array $keys {
+        *     Associative array of token => data pairs, where the data is an associative
+        *     array of information about the key.
+        *
+        *     @type array ...$0 {
+        *         Information about the key.
+        *
+        *         @type string $hashed_key The hashed value of the key.
+        *         @type int    $created_at The timestamp when the key was created.
+        *     }
+        * }
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return bool True on success, false on failure.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        private function update_keys( array $keys ) {
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpuserrequestphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-user-request.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-user-request.php   2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/class-wp-user-request.php     2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -92,6 +92,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Key used to confirm this request.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.6
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The key is now hashed using wp_fast_hash() instead of phpass.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @var string
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public $confirm_key = '';
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpuserphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-user.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-user.php   2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/class-wp-user.php     2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -11,6 +11,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Core class used to implement the WP_User object.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.0.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.8.0 The `user_pass` property is now hashed using bcrypt instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @property string $nickname
</span><span class="cx" style="display: block; padding: 0 10px">  * @property string $description
</span></span></pre></div>
<a id="trunksrcwpincludesfunctionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.php       2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/functions.php 2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -9114,3 +9114,62 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return in_array( $mime_type, $heic_mime_types, true );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Returns a cryptographically secure hash of a message using a fast generic hash function.
+ *
+ * Use the wp_verify_fast_hash() function to verify the hash.
+ *
+ * This function does not salt the value prior to being hashed, therefore input to this function must originate from
+ * a random generator with sufficiently high entropy, preferably greater than 128 bits. This function is used internally
+ * in WordPress to hash security keys and application passwords which are generated with high entropy.
+ *
+ * Important:
+ *
+ *  - This function must not be used for hashing user-generated passwords. Use wp_hash_password() for that.
+ *  - This function must not be used for hashing other low-entropy input. Use wp_hash() for that.
+ *
+ * The BLAKE2b algorithm is used by Sodium to hash the message.
+ *
+ * @since 6.8.0
+ *
+ * @throws TypeError Thrown by Sodium if the message is not a string.
+ *
+ * @param string $message The message to hash.
+ * @return string The hash of the message.
+ */
+function wp_fast_hash(
+       #[\SensitiveParameter]
+       string $message
+): string {
+       return '$generic$' . sodium_bin2hex( sodium_crypto_generichash( $message ) );
+}
+
+/**
+ * Checks whether a plaintext message matches the hashed value. Used to verify values hashed via wp_fast_hash().
+ *
+ * The function uses Sodium to hash the message and compare it to the hashed value. If the hash is not a generic hash,
+ * the hash is treated as a phpass portable hash in order to provide backward compatibility for application passwords
+ * which were hashed using phpass prior to WordPress 6.8.0.
+ *
+ * @since 6.8.0
+ *
+ * @throws TypeError Thrown by Sodium if the message is not a string.
+ *
+ * @param string $message The plaintext message.
+ * @param string $hash    Hash of the message to check against.
+ * @return bool Whether the message matches the hashed message.
+ */
+function wp_verify_fast_hash(
+       #[\SensitiveParameter]
+       string $message,
+       string $hash
+): bool {
+       if ( ! str_starts_with( $hash, '$generic$' ) ) {
+               // Back-compat for old phpass hashes.
+               require_once ABSPATH . WPINC . '/class-phpass.php';
+               return ( new PasswordHash( 8, true ) )->CheckPassword( $message, $hash );
+       }
+
+       return hash_equals( $hash, wp_fast_hash( $message ) );
+}
</ins></span></pre></div>
<a id="trunksrcwpincludespluggablephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/pluggable.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/pluggable.php       2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/pluggable.php 2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -693,6 +693,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $cookie Optional. If used, will validate contents instead of cookie's.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *                       Note: This does *not* default to 'auth' like other cookie functions.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return int|false User ID if valid cookie, false if invalid.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -768,7 +769,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $pass_frag = substr( $user->user_pass, 8, 4 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
+                       // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
+                       $pass_frag = substr( $user->user_pass, 8, 4 );
+               } else {
+                       // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
+                       $pass_frag = substr( $user->user_pass, -4 );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -869,7 +876,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $token   = $manager->create( $expiration );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $pass_frag = substr( $user->user_pass, 8, 4 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
+                       // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
+                       $pass_frag = substr( $user->user_pass, 8, 4 );
+               } else {
+                       // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
+                       $pass_frag = substr( $user->user_pass, -4 );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2625,8 +2638,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * instead use the other package password hashing algorithm.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 2.5.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @global PasswordHash $wp_hasher PHPass object.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @global PasswordHash $wp_hasher phpass object.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $password Plain text user password to hash.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string The hash string of the password.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2637,13 +2651,62 @@
</span><span class="cx" style="display: block; padding: 0 10px">        ) {
</span><span class="cx" style="display: block; padding: 0 10px">                global $wp_hasher;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( empty( $wp_hasher ) ) {
-                       require_once ABSPATH . WPINC . '/class-phpass.php';
-                       // By default, use the portable hash from phpass.
-                       $wp_hasher = new PasswordHash( 8, true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! empty( $wp_hasher ) ) {
+                       return $wp_hasher->HashPassword( trim( $password ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $wp_hasher->HashPassword( trim( $password ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( strlen( $password ) > 4096 ) {
+                       return '*';
+               }
+
+               /**
+                * Filters the hashing algorithm to use in the password_hash() and password_needs_rehash() functions.
+                *
+                * The default is the value of the `PASSWORD_BCRYPT` constant which means bcrypt is used.
+                *
+                * **Important:** The only password hashing algorithm that is guaranteed to be available across PHP
+                * installations is bcrypt. If you use any other algorithm you must make sure that it is available on
+                * the server. The `password_algos()` function can be used to check which hashing algorithms are available.
+                *
+                * The hashing options can be controlled via the {@see 'wp_hash_password_options'} filter.
+                *
+                * Other available constants include:
+                *
+                * - `PASSWORD_ARGON2I`
+                * - `PASSWORD_ARGON2ID`
+                * - `PASSWORD_DEFAULT`
+                *
+                * @since 6.8.0
+                *
+                * @param string $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant.
+                */
+               $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
+
+               /**
+                * Filters the options passed to the password_hash() and password_needs_rehash() functions.
+                *
+                * The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'}
+                * filter. You must ensure that the options are appropriate for the algorithm in use.
+                *
+                * @since 6.8.0
+                *
+                * @param array $options    Array of options to pass to the password hashing functions.
+                *                          By default this is an empty array which means the default
+                *                          options will be used.
+                * @param string $algorithm The hashing algorithm in use.
+                */
+               $options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
+
+               // Algorithms other than bcrypt don't need to use pre-hashing.
+               if ( PASSWORD_BCRYPT !== $algorithm ) {
+                       return password_hash( $password, $algorithm, $options );
+               }
+
+               // Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation.
+               $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) );
+
+               // Add a prefix to facilitate distinguishing vanilla bcrypt hashes.
+               return '$wp' . password_hash( $password_to_hash, $algorithm, $options );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> endif;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2651,23 +2714,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Checks a plaintext password against a hashed password.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Maintains compatibility between old version and the new cookie authentication
-        * protocol using PHPass library. The $hash parameter is the encrypted password
-        * and the function compares the plain text password when encrypted similarly
-        * against the already encrypted password to see if they match.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Note that this function may be used to check a value that is not a user password.
+        * A plugin may use this function to check a password of a different type, and there
+        * may not always be a user ID associated with the password.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * For integration with other applications, this function can be overwritten to
</span><span class="cx" style="display: block; padding: 0 10px">         * instead use the other package password hashing algorithm.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 2.5.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A
+        *              password that wasn't hashed with bcrypt will be checked with phpass.
+        *              Passwords hashed with md5 are no longer supported.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @global PasswordHash $wp_hasher PHPass object used for checking the password
-        *                                 against the $hash + $password.
-        * @uses PasswordHash::CheckPassword
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying
+        *                                 passwords that were hashed with phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param string     $password Plaintext user's password.
-        * @param string     $hash     Hash of the user's password to check against.
-        * @param string|int $user_id  Optional. User ID.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string     $password Plaintext password.
+        * @param string     $hash     Hash of the password to check against.
+        * @param string|int $user_id  Optional. ID of a user associated with the password.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return bool False, if the $password does not match the hashed password.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function wp_check_password(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2678,45 +2742,107 @@
</span><span class="cx" style="display: block; padding: 0 10px">        ) {
</span><span class="cx" style="display: block; padding: 0 10px">                global $wp_hasher;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // If the hash is still md5...
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $check = false;
+
+               // If the hash is still md5 or otherwise truncated then invalidate it.
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( strlen( $hash ) <= 32 ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $check = hash_equals( $hash, md5( $password ) );
-                       if ( $check && $user_id ) {
-                               // Rehash using new hash.
-                               wp_set_password( $password, $user_id );
-                               $hash = wp_hash_password( $password );
-                       }
-
</del><span class="cx" style="display: block; padding: 0 10px">                         /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                         * Filters whether the plaintext password matches the encrypted password.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                  * Filters whether the plaintext password matches the hashed password.
</ins><span class="cx" style="display: block; padding: 0 10px">                          *
</span><span class="cx" style="display: block; padding: 0 10px">                         * @since 2.5.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         * @since 6.8.0 Passwords are now hashed with bcrypt by default.
+                        *              Old passwords may still be hashed with phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">                          *
</span><span class="cx" style="display: block; padding: 0 10px">                         * @param bool       $check    Whether the passwords match.
</span><span class="cx" style="display: block; padding: 0 10px">                         * @param string     $password The plaintext password.
</span><span class="cx" style="display: block; padding: 0 10px">                         * @param string     $hash     The hashed password.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                         * @param string|int $user_id  User ID. Can be empty.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                  * @param string|int $user_id  Optional ID of a user associated with the password.
+                        *                             Can be empty.
</ins><span class="cx" style="display: block; padding: 0 10px">                          */
</span><span class="cx" style="display: block; padding: 0 10px">                        return apply_filters( 'check_password', $check, $password, $hash, $user_id );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /*
-                * If the stored hash is longer than an MD5,
-                * presume the new style phpass portable hash.
-                */
-               if ( empty( $wp_hasher ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! empty( $wp_hasher ) ) {
+                       // Check the password using the overridden hasher.
+                       $check = $wp_hasher->CheckPassword( $password, $hash );
+               } elseif ( strlen( $password ) > 4096 ) {
+                       $check = false;
+               } elseif ( str_starts_with( $hash, '$wp' ) ) {
+                       // Check the password using the current prefixed hash.
+                       $password_to_verify = base64_encode( hash_hmac( 'sha384', $password, 'wp-sha384', true ) );
+                       $check              = password_verify( $password_to_verify, substr( $hash, 3 ) );
+               } elseif ( str_starts_with( $hash, '$P$' ) ) {
+                       // Check the password using phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">                         require_once ABSPATH . WPINC . '/class-phpass.php';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // By default, use the portable hash from phpass.
-                       $wp_hasher = new PasswordHash( 8, true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $check = ( new PasswordHash( 8, true ) )->CheckPassword( $password, $hash );
+               } else {
+                       // Check the password using compat support for any non-prefixed hash.
+                       $check = password_verify( $password, $hash );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $check = $wp_hasher->CheckPassword( $password, $hash );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 /** This filter is documented in wp-includes/pluggable.php */
</span><span class="cx" style="display: block; padding: 0 10px">                return apply_filters( 'check_password', $check, $password, $hash, $user_id );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> endif;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+if ( ! function_exists( 'wp_password_needs_rehash' ) ) :
+       /**
+        * Checks whether a password hash needs to be rehashed.
+        *
+        * Passwords are hashed with bcrypt using the default cost. A password hashed in a prior version
+        * of WordPress may still be hashed with phpass and will need to be rehashed. If the default cost
+        * or algorithm is changed in PHP or WordPress then a password hashed in a previous version will
+        * need to be rehashed.
+        *
+        * Note that, just like wp_check_password(), this function may be used to check a value that is
+        * not a user password. A plugin may use this function to check a password of a different type,
+        * and there may not always be a user ID associated with the password.
+        *
+        * @since 6.8.0
+        *
+        * @global PasswordHash $wp_hasher phpass object.
+        *
+        * @param string     $hash    Hash of a password to check.
+        * @param string|int $user_id Optional. ID of a user associated with the password.
+        * @return bool Whether the hash needs to be rehashed.
+        */
+       function wp_password_needs_rehash( $hash, $user_id = '' ) {
+               global $wp_hasher;
+
+               if ( ! empty( $wp_hasher ) ) {
+                       return false;
+               }
+
+               /** This filter is documented in wp-includes/pluggable.php */
+               $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
+
+               /** This filter is documented in wp-includes/pluggable.php */
+               $options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
+
+               $prefixed = str_starts_with( $hash, '$wp' );
+
+               if ( ( PASSWORD_BCRYPT === $algorithm ) && ! $prefixed ) {
+                       // If bcrypt is in use and the hash is not prefixed then it needs to be rehashed.
+                       $needs_rehash = true;
+               } else {
+                       // Otherwise check the hash minus its prefix if necessary.
+                       $hash_to_check = $prefixed ? substr( $hash, 3 ) : $hash;
+                       $needs_rehash  = password_needs_rehash( $hash_to_check, $algorithm, $options );
+               }
+
+               /**
+                * Filters whether the password hash needs to be rehashed.
+                *
+                * @since 6.8.0
+                *
+                * @param bool       $needs_rehash Whether the password hash needs to be rehashed.
+                * @param string     $hash         The password hash.
+                * @param string|int $user_id      Optional. ID of a user associated with the password.
+                */
+               return apply_filters( 'password_needs_rehash', $needs_rehash, $hash, $user_id );
+       }
+endif;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( ! function_exists( 'wp_generate_password' ) ) :
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Generates a random password drawn from the defined set of characters.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2865,6 +2991,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * of password resets if precautions are not taken to ensure it does not execute on every page load.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 2.5.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @global wpdb $wpdb WordPress database abstraction object.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span></span></pre></div>
<a id="trunksrcwpincludesuserphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/user.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/user.php    2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/src/wp-includes/user.php      2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -205,7 +205,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return $user;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $valid = wp_check_password( $password, $user->user_pass, $user->ID );
+
+       if ( ! $valid ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return new WP_Error(
</span><span class="cx" style="display: block; padding: 0 10px">                        'incorrect_password',
</span><span class="cx" style="display: block; padding: 0 10px">                        sprintf(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -219,6 +221,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) {
+               wp_set_password( $password, $user->ID );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         return $user;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -282,7 +288,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return $user;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $valid = wp_check_password( $password, $user->user_pass, $user->ID );
+
+       if ( ! $valid ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return new WP_Error(
</span><span class="cx" style="display: block; padding: 0 10px">                        'incorrect_password',
</span><span class="cx" style="display: block; padding: 0 10px">                        sprintf(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -296,6 +304,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) {
+               wp_set_password( $password, $user->ID );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         return $user;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -445,7 +457,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        foreach ( $hashed_passwords as $key => $item ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! WP_Application_Passwords::check_password( $password, $item['password'] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         continue;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2431,6 +2443,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.8.0 The `$userdata` parameter was added.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.8.0 The user's password is now hashed using bcrypt instead of phpass.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array    $data {
</span><span class="cx" style="display: block; padding: 0 10px">         *     Values and keys for the user.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2978,14 +2991,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 4.4.0
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
- *
</del><span class="cx" style="display: block; padding: 0 10px">  * @param WP_User $user User to retrieve password reset key for.
</span><span class="cx" style="display: block; padding: 0 10px">  * @return string|WP_Error Password reset key on success. WP_Error on error.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function get_password_reset_key( $user ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        global $wp_hasher;
-
</del><span class="cx" style="display: block; padding: 0 10px">         if ( ! ( $user instanceof WP_User ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return new WP_Error( 'invalidcombo', __( '<strong>Error:</strong> There is no account with that username or email address.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3031,14 +3040,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        do_action( 'retrieve_password_key', $user->user_login, $key );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        // Now insert the key, hashed, into the DB.
-       if ( empty( $wp_hasher ) ) {
-               require_once ABSPATH . WPINC . '/class-phpass.php';
-               $wp_hasher = new PasswordHash( 8, true );
-       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $hashed = time() . ':' . wp_fast_hash( $key );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $hashed = time() . ':' . $wp_hasher->HashPassword( $key );
-
</del><span class="cx" style="display: block; padding: 0 10px">         $key_saved = wp_update_user(
</span><span class="cx" style="display: block; padding: 0 10px">                array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'ID'                  => $user->ID,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3063,9 +3066,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.1.0
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
- *
- * @param string $key       Hash to validate sending user's password.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param string $key       The password reset key.
</ins><span class="cx" style="display: block; padding: 0 10px">  * @param string $login     The user login.
</span><span class="cx" style="display: block; padding: 0 10px">  * @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3074,8 +3075,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $key,
</span><span class="cx" style="display: block; padding: 0 10px">        $login
</span><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        global $wp_hasher;
-
</del><span class="cx" style="display: block; padding: 0 10px">         $key = preg_replace( '/[^a-z0-9]/i', '', $key );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( empty( $key ) || ! is_string( $key ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3092,11 +3091,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( empty( $wp_hasher ) ) {
-               require_once ABSPATH . WPINC . '/class-phpass.php';
-               $wp_hasher = new PasswordHash( 8, true );
-       }
-
</del><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Filters the expiration time of password reset keys.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3118,7 +3112,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $hash_is_correct = $wp_hasher->CheckPassword( $key, $pass_key );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $hash_is_correct = wp_verify_fast_hash( $key, $pass_key );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return $user;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3133,7 +3127,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters the return value of check_password_reset_key() when an
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * old-style key is used.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * old-style key or an expired key is used.
</ins><span class="cx" style="display: block; padding: 0 10px">                  *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 3.7.0 Previously plain-text keys were stored in the database.
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 4.3.0 Previously key hashes were stored without an expiration time.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3154,8 +3148,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.5.0
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 5.7.0 Added `$user_login` parameter.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @global wpdb         $wpdb      WordPress database abstraction object.
- * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @global wpdb $wpdb WordPress database abstraction object.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @param string $user_login Optional. Username to send a password retrieval email for.
</span><span class="cx" style="display: block; padding: 0 10px">  *                           Defaults to `$_POST['user_login']` if not set.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4936,28 +4929,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 4.9.6
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
- *
</del><span class="cx" style="display: block; padding: 0 10px">  * @param int $request_id Request ID.
</span><span class="cx" style="display: block; padding: 0 10px">  * @return string Confirmation key.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_generate_user_request_key( $request_id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        global $wp_hasher;
-
</del><span class="cx" style="display: block; padding: 0 10px">         // Generate something random for a confirmation key.
</span><span class="cx" style="display: block; padding: 0 10px">        $key = wp_generate_password( 20, false );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        // Return the key, hashed.
-       if ( empty( $wp_hasher ) ) {
-               require_once ABSPATH . WPINC . '/class-phpass.php';
-               $wp_hasher = new PasswordHash( 8, true );
-       }
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Save the key, hashed.
</ins><span class="cx" style="display: block; padding: 0 10px">         wp_update_post(
</span><span class="cx" style="display: block; padding: 0 10px">                array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'ID'            => $request_id,
</span><span class="cx" style="display: block; padding: 0 10px">                        'post_status'   => 'request-pending',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'post_password' => $wp_hasher->HashPassword( $key ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'post_password' => wp_fast_hash( $key ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 )
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4969,8 +4953,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 4.9.6
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
- *
</del><span class="cx" style="display: block; padding: 0 10px">  * @param string $request_id ID of the request being confirmed.
</span><span class="cx" style="display: block; padding: 0 10px">  * @param string $key        Provided key to validate.
</span><span class="cx" style="display: block; padding: 0 10px">  * @return true|WP_Error True on success, WP_Error on failure.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4980,8 +4962,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        #[\SensitiveParameter]
</span><span class="cx" style="display: block; padding: 0 10px">        $key
</span><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        global $wp_hasher;
-
</del><span class="cx" style="display: block; padding: 0 10px">         $request_id       = absint( $request_id );
</span><span class="cx" style="display: block; padding: 0 10px">        $request          = wp_get_user_request( $request_id );
</span><span class="cx" style="display: block; padding: 0 10px">        $saved_key        = $request->confirm_key;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4999,11 +4979,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return new WP_Error( 'missing_key', __( 'The confirmation key is missing from this personal data request.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( empty( $wp_hasher ) ) {
-               require_once ABSPATH . WPINC . '/class-phpass.php';
-               $wp_hasher = new PasswordHash( 8, true );
-       }
-
</del><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Filters the expiration time of confirm keys.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5014,7 +4989,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
</span><span class="cx" style="display: block; padding: 0 10px">        $expiration_time     = $key_request_time + $expiration_duration;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! wp_verify_fast_hash( $key, $saved_key ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunitincludesbootstrapphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/includes/bootstrap.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/bootstrap.php        2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/tests/phpunit/includes/bootstrap.php  2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -329,6 +329,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require __DIR__ . '/class-wp-rest-test-search-handler.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require __DIR__ . '/class-wp-rest-test-configurable-controller.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require __DIR__ . '/class-wp-fake-block-type.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require __DIR__ . '/class-wp-fake-hasher.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require __DIR__ . '/class-wp-sitemaps-test-provider.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require __DIR__ . '/class-wp-sitemaps-empty-test-provider.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require __DIR__ . '/class-wp-sitemaps-large-test-provider.php';
</span></span></pre></div>
<a id="trunktestsphpunitincludesclasswpfakehasherphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/includes/class-wp-fake-hasher.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/class-wp-fake-hasher.php                             (rev 0)
+++ trunk/tests/phpunit/includes/class-wp-fake-hasher.php       2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,41 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * WP_Fake_Hasher for testing
+ *
+ * @package WordPress
+ * @since 6.8.0
+ */
+
+/**
+ * Test class.
+ *
+ * @since 6.8.0
+ */
+class WP_Fake_Hasher {
+       private $hash = '';
+
+       public function __construct() {
+               $this->hash = str_repeat( 'a', 36 );
+       }
+
+       /**
+        * Hashes a password.
+        *
+        * @param string $password Password to hash.
+        * @return string Hashed password.
+        */
+       public function HashPassword( string $password ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
+               return $this->hash;
+       }
+
+       /**
+        * Checks the password hash.
+        *
+        * @param string $password Password to check.
+        * @param string $hash     Hash to check against.
+        * @return bool Whether the password hash is valid.
+        */
+       public function CheckPassword( string $password, string $hash ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
+               return $hash === $this->hash;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/includes/class-wp-fake-hasher.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsauthphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/auth.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/auth.php        2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/tests/phpunit/tests/auth.php  2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -10,6 +10,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">        const USER_LOGIN = 'password-user';
</span><span class="cx" style="display: block; padding: 0 10px">        const USER_PASS  = 'password';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * @var WP_User
+        */
</ins><span class="cx" style="display: block; padding: 0 10px">         protected $user;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -16,9 +19,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @var WP_User
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected static $_user;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @var int
+        */
</ins><span class="cx" style="display: block; padding: 0 10px">         protected static $user_id;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @var PasswordHash
+        */
</ins><span class="cx" style="display: block; padding: 0 10px">         protected static $wp_hasher;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        protected static $bcrypt_length_limit = 72;
+
+       protected static $phpass_length_limit = 4096;
+
+       protected static $password_length_limit = 4096;
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Action hook.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -92,6 +109,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 21022
+        */
+       public function test_auth_cookie_generated_with_phpass_hash_remains_valid() {
+               self::set_user_password_with_phpass( 'password', self::$user_id );
+
+               $auth_cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' );
+
+               $this->assertSame( self::$user_id, wp_validate_auth_cookie( $auth_cookie, 'auth' ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_auth_cookie_generated_with_plain_bcrypt_hash_remains_valid() {
+               self::set_user_password_with_plain_bcrypt( 'password', self::$user_id );
+
+               $auth_cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' );
+
+               $this->assertSame( self::$user_id, wp_validate_auth_cookie( $auth_cookie, 'auth' ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 23494
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_password_trimming() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -106,6 +145,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_set_password( $password_to_test, $this->user->ID );
</span><span class="cx" style="display: block; padding: 0 10px">                        $authed_user = wp_authenticate( $this->user->user_login, $password_to_test );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $this->assertNotWPError( $authed_user );
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->assertInstanceOf( 'WP_User', $authed_user );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertSame( $this->user->ID, $authed_user->ID );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -160,6 +200,185 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 21022
+        */
+       public function test_wp_check_password_supports_phpass_hash() {
+               $password = 'password';
+               $hash     = self::$wp_hasher->HashPassword( $password );
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       /**
+        * Ensure wp_check_password() remains compatible with an increase to the default bcrypt cost.
+        *
+        * The test verifies this by reducing the cost used to generate the hash, therefore mimicing a hash
+        * which was generated prior to the default cost being increased.
+        *
+        * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 .
+        *
+        * @ticket 21022
+        */
+       public function test_wp_check_password_supports_hash_with_increased_bcrypt_cost() {
+               $password = 'password';
+
+               // Reducing the cost mimics an increase to the default cost.
+               add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) );
+               $hash = wp_hash_password( $password, PASSWORD_BCRYPT );
+               remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) );
+
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+               $this->assertTrue( wp_password_needs_rehash( $hash ) );
+       }
+
+       /**
+        * Ensure wp_check_password() remains compatible with a reduction of the default bcrypt cost.
+        *
+        * The test verifies this by increasing the cost used to generate the hash, therefore mimicing a hash
+        * which was generated prior to the default cost being reduced.
+        *
+        * A reduction of the cost is unlikely to occur but is fully supported.
+        *
+        * @ticket 21022
+        */
+       public function test_wp_check_password_supports_hash_with_reduced_bcrypt_cost() {
+               $password = 'password';
+
+               // Increasing the cost mimics a reduction of the default cost.
+               add_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) );
+               $hash = wp_hash_password( $password, PASSWORD_BCRYPT );
+               remove_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) );
+
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+               $this->assertTrue( wp_password_needs_rehash( $hash ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_wp_check_password_supports_wp_hash_with_default_bcrypt_cost() {
+               $password = 'password';
+
+               $hash = wp_hash_password( $password, PASSWORD_BCRYPT );
+
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+               $this->assertFalse( wp_password_needs_rehash( $hash ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_wp_check_password_supports_plain_bcrypt_hash_with_default_bcrypt_cost() {
+               $password = 'password';
+
+               $hash = password_hash( $password, PASSWORD_BCRYPT );
+
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+               $this->assertTrue( wp_password_needs_rehash( $hash ) );
+       }
+
+       /**
+        * Ensure wp_check_password() is compatible with Argon2i hashes.
+        *
+        * @ticket 21022
+        */
+       public function test_wp_check_password_supports_argon2i_hash() {
+               if ( ! defined( 'PASSWORD_ARGON2I' ) ) {
+                       $this->fail( 'Argon2i is not supported.' );
+               }
+
+               $password = 'password';
+               $hash     = password_hash( trim( $password ), PASSWORD_ARGON2I );
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       /**
+        * Ensure wp_check_password() is compatible with Argon2id hashes.
+        *
+        * @requires PHP >= 7.3
+        *
+        * @ticket 21022
+        */
+       public function test_wp_check_password_supports_argon2id_hash() {
+               if ( ! defined( 'PASSWORD_ARGON2ID' ) ) {
+                       $this->fail( 'Argon2id is not supported.' );
+               }
+
+               $password = 'password';
+               $hash     = password_hash( trim( $password ), PASSWORD_ARGON2ID );
+               $this->assertTrue( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_wp_check_password_does_not_support_md5_hashes() {
+               $password = 'password';
+               $hash     = md5( $password );
+               $this->assertFalse( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_wp_check_password_does_not_support_plain_text() {
+               $password = 'password';
+               $hash     = $password;
+               $this->assertFalse( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       /**
+        * @ticket 21022
+        *
+        * @dataProvider data_empty_values
+        * @param mixed $value
+        */
+       public function test_wp_check_password_does_not_support_empty_hash( $value ) {
+               $password = 'password';
+               $hash     = $value;
+               $this->assertFalse( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       /**
+        * @ticket 21022
+        *
+        * @dataProvider data_empty_values
+        * @param mixed $value
+        */
+       public function test_wp_check_password_does_not_support_empty_password( $value ) {
+               $password = $value;
+               $hash     = $value;
+               $this->assertFalse( wp_check_password( $password, $hash ) );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+       }
+
+       public function data_empty_values() {
+               return array(
+                       // Integer zero:
+                       array( 0 ),
+                       // String zero:
+                       array( '0' ),
+                       // Zero-length string:
+                       array( '' ),
+                       // Null byte character:
+                       array( "\0" ),
+                       // Asterisk values:
+                       array( '*' ),
+                       array( '*0' ),
+                       array( '*1' ),
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 29217
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_wp_verify_nonce_with_empty_arg() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -235,51 +454,248 @@
</span><span class="cx" style="display: block; padding: 0 10px">                unset( $_REQUEST['_wpnonce'] );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function test_password_length_limit() {
-               $limit = str_repeat( 'a', 4096 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+        * @ticket 21022
+        */
+       public function test_password_is_hashed_with_bcrypt() {
+               $password = 'password';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Set the user password.
+               wp_set_password( $password, self::$user_id );
+
+               // Ensure the password is hashed with bcrypt.
+               $this->assertStringStartsWith( '$wp$2y$', get_userdata( self::$user_id )->user_pass );
+
+               // Authenticate.
+               $user = wp_authenticate( $this->user->user_login, $password );
+
+               // Verify correct password.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_invalid_password_at_bcrypt_length_limit_is_rejected() {
+               $limit = str_repeat( 'a', self::$bcrypt_length_limit );
+
+               // Set the user password to the bcrypt limit.
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_set_password( $limit, self::$user_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // phpass hashed password.
-               $this->assertStringStartsWith( '$P$', $this->user->data->user_pass );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' );
</span><span class="cx" style="display: block; padding: 0 10px">                // Wrong password.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertWPError( $user );
+               $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_invalid_password_beyond_bcrypt_length_limit_is_rejected() {
+               $limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 );
+
+               // Set the user password beyond the bcrypt limit.
+               wp_set_password( $limit, self::$user_id );
+
+               $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' );
+               // Wrong password.
+               $this->assertWPError( $user );
+               $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_valid_password_at_bcrypt_length_limit_is_accepted() {
+               $limit = str_repeat( 'a', self::$bcrypt_length_limit );
+
+               // Set the user password to the bcrypt limit.
+               wp_set_password( $limit, self::$user_id );
+
+               // Authenticate.
+               $user = wp_authenticate( $this->user->user_login, $limit );
+
+               // Correct password.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_valid_password_beyond_bcrypt_length_limit_is_accepted() {
+               $limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 );
+
+               // Set the user password beyond the bcrypt limit.
+               wp_set_password( $limit, self::$user_id );
+
+               // Authenticate.
+               $user = wp_authenticate( $this->user->user_login, $limit );
+
+               // Correct password depite its length.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+       }
+
+       /**
+        * A password beyond 72 bytes will be truncated by bcrypt by default and still be accepted.
+        *
+        * This ensures that a truncated password is not accepted by WordPress.
+        *
+        * @ticket 21022
+        */
+       public function test_long_truncated_password_is_rejected() {
+               $at_limit     = str_repeat( 'a', self::$bcrypt_length_limit );
+               $beyond_limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 );
+
+               // Set the user password beyond the bcrypt limit.
+               wp_set_password( $beyond_limit, self::$user_id );
+
+               // Authenticate using a truncated password.
+               $user = wp_authenticate( $this->user->user_login, $at_limit );
+
+               // Incorrect password.
+               $this->assertWPError( $user );
+               $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_setting_password_beyond_bcrypt_length_limit_is_rejected() {
+               $beyond_limit = str_repeat( 'a', self::$password_length_limit + 1 );
+
+               // Set the user password beyond the limit.
+               wp_set_password( $beyond_limit, self::$user_id );
+
+               // Password broken by setting it to be too long.
+               $user = get_user_by( 'id', self::$user_id );
+               $this->assertSame( '*', $user->data->user_pass );
+
+               // Password is not accepted.
+               $user = wp_authenticate( $this->user->user_login, $beyond_limit );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'incorrect_password', $user->get_error_code() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Placeholder is not accepted.
+               $user = wp_authenticate( $this->user->user_login, '*' );
+               $this->assertInstanceOf( 'WP_Error', $user );
+               $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
+
+       /**
+        * @see https://core.trac.wordpress.org/changeset/30466
+        */
+       public function test_invalid_password_at_phpass_length_limit_is_rejected() {
+               $limit = str_repeat( 'a', self::$phpass_length_limit );
+
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( $limit, self::$user_id );
+
+               // Authenticate.
+               $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' );
+
+               // Wrong password.
+               $this->assertInstanceOf( 'WP_Error', $user );
+               $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
+
+       public function test_valid_password_at_phpass_length_limit_is_accepted() {
+               $limit = str_repeat( 'a', self::$phpass_length_limit );
+
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( $limit, self::$user_id );
+
+               // Authenticate.
</ins><span class="cx" style="display: block; padding: 0 10px">                 $user = wp_authenticate( $this->user->user_login, $limit );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               // Correct password.
+               $this->assertNotWPError( $user );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( 'WP_User', $user );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( self::$user_id, $user->ID );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // One char too many.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_too_long_password_at_phpass_length_limit_is_rejected() {
+               $limit = str_repeat( 'a', self::$phpass_length_limit );
+
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( $limit, self::$user_id );
+
+               // Authenticate with a password that is one character too long.
</ins><span class="cx" style="display: block; padding: 0 10px">                 $user = wp_authenticate( $this->user->user_login, $limit . 'a' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Wrong password.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_password( $limit . 'a', self::$user_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_too_long_password_beyond_phpass_length_limit_is_rejected() {
+               // One char too many.
+               $too_long = str_repeat( 'a', self::$phpass_length_limit + 1 );
+
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( $too_long, self::$user_id );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $user = get_user_by( 'id', self::$user_id );
</span><span class="cx" style="display: block; padding: 0 10px">                // Password broken by setting it to be too long.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( '*', $user->data->user_pass );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Password is not accepted.
</ins><span class="cx" style="display: block; padding: 0 10px">                 $user = wp_authenticate( $this->user->user_login, '*' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $user = wp_authenticate( $this->user->user_login, '*0' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+        * @dataProvider data_empty_values
+        * @param mixed $value
+        */
+       public function test_empty_password_is_rejected_by_bcrypt( $value ) {
+               // Set the user password.
+               wp_set_password( 'password', self::$user_id );
+
+               $user = wp_authenticate( $this->user->user_login, $value );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $user = wp_authenticate( $this->user->user_login, '*1' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+        * @dataProvider data_empty_values
+        * @param mixed $value
+        */
+       public function test_empty_password_is_rejected_by_phpass( $value ) {
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( 'password', self::$user_id );
+
+               $user = wp_authenticate( $this->user->user_login, $value );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function test_incorrect_password_is_rejected_by_phpass() {
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( 'password', self::$user_id );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Wrong password.
-               $this->assertInstanceOf( 'WP_Error', $user );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $user = wp_authenticate( $this->user->user_login, $limit );
</del><span class="cx" style="display: block; padding: 0 10px">                 // Wrong password.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'incorrect_password', $user->get_error_code() );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function test_too_long_password_is_rejected_by_phpass() {
+               $limit = str_repeat( 'a', self::$phpass_length_limit );
+
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( 'password', self::$user_id );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $user = wp_authenticate( $this->user->user_login, $limit . 'a' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Password broken by setting it to be too long.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $user );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'incorrect_password', $user->get_error_code() );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -306,7 +722,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $wpdb->update(
</span><span class="cx" style="display: block; padding: 0 10px">                        $wpdb->users,
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'user_activation_key' => strtotime( '-1 hour' ) . ':' . self::$wp_hasher->HashPassword( $key ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'user_activation_key' => strtotime( '-1 hour' ) . ':' . wp_fast_hash( $key ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'ID' => $this->user->ID,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -344,7 +760,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $wpdb->update(
</span><span class="cx" style="display: block; padding: 0 10px">                        $wpdb->users,
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'user_activation_key' => strtotime( '-48 hours' ) . ':' . self::$wp_hasher->HashPassword( $key ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'user_activation_key' => strtotime( '-48 hours' ) . ':' . wp_fast_hash( $key ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'ID' => $this->user->ID,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -355,6 +771,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // An expired but otherwise valid key should be rejected.
</span><span class="cx" style="display: block; padding: 0 10px">                $check = check_password_reset_key( $key, $this->user->user_login );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $check );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'expired_key', $check->get_error_code() );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -393,13 +810,161 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // A legacy user_activation_key should not be accepted.
</span><span class="cx" style="display: block; padding: 0 10px">                $check = check_password_reset_key( $key, $this->user->user_login );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $check );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'expired_key', $check->get_error_code() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // An empty key with a legacy user_activation_key should be rejected.
</span><span class="cx" style="display: block; padding: 0 10px">                $check = check_password_reset_key( '', $this->user->user_login );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInstanceOf( 'WP_Error', $check );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertSame( 'invalid_key', $check->get_error_code() );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 21022
+        */
+       public function test_phpass_user_activation_key_is_allowed() {
+               global $wpdb;
+
+               // A legacy user_activation_key is one hashed using phpass between WordPress 4.3 and 6.8.0.
+
+               $key = wp_generate_password( 20, false );
+               $wpdb->update(
+                       $wpdb->users,
+                       array(
+                               'user_activation_key' => strtotime( '-1 hour' ) . ':' . self::$wp_hasher->HashPassword( $key ),
+                       ),
+                       array(
+                               'ID' => $this->user->ID,
+                       )
+               );
+               clean_user_cache( $this->user );
+
+               // A legacy phpass user_activation_key should remain valid.
+               $check = check_password_reset_key( $key, $this->user->user_login );
+               $this->assertNotWPError( $check );
+               $this->assertInstanceOf( 'WP_User', $check );
+               $this->assertSame( $this->user->ID, $check->ID );
+
+               // An empty key with a legacy user_activation_key should be rejected.
+               $check = check_password_reset_key( '', $this->user->user_login );
+               $this->assertWPError( $check );
+               $this->assertSame( 'invalid_key', $check->get_error_code() );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_expired_phpass_user_activation_key_is_rejected() {
+               global $wpdb;
+
+               // A legacy user_activation_key is one hashed using phpass between WordPress 4.3 and 6.8.0.
+
+               $key = wp_generate_password( 20, false );
+               $wpdb->update(
+                       $wpdb->users,
+                       array(
+                               'user_activation_key' => strtotime( '-48 hours' ) . ':' . self::$wp_hasher->HashPassword( $key ),
+                       ),
+                       array(
+                               'ID' => $this->user->ID,
+                       )
+               );
+               clean_user_cache( $this->user );
+
+               // A legacy phpass user_activation_key should still be subject to an expiry check.
+               $check = check_password_reset_key( $key, $this->user->user_login );
+               $this->assertWPError( $check );
+               $this->assertSame( 'expired_key', $check->get_error_code() );
+
+               // An empty key with a legacy user_activation_key should be rejected.
+               $check = check_password_reset_key( '', $this->user->user_login );
+               $this->assertWPError( $check );
+               $this->assertSame( 'invalid_key', $check->get_error_code() );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_user_request_key_handling() {
+               $request_id = wp_create_user_request( 'test@example.com', 'remove_personal_data' );
+               $key        = wp_generate_user_request_key( $request_id );
+
+               // A valid key should be accepted.
+               $check = wp_validate_user_request_key( $request_id, $key );
+               $this->assertNotWPError( $check );
+               $this->assertTrue( $check );
+
+               // An invalid key should rejected.
+               $check = wp_validate_user_request_key( $request_id, 'invalid' );
+               $this->assertWPError( $check );
+               $this->assertSame( 'invalid_key', $check->get_error_code() );
+
+               // An empty key should be rejected.
+               $check = wp_validate_user_request_key( $request_id, '' );
+               $this->assertWPError( $check );
+               $this->assertSame( 'missing_key', $check->get_error_code() );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_phpass_user_request_key_is_allowed() {
+               // A legacy user request key is one hashed using phpass between WordPress 4.3 and 6.8.0.
+
+               $request_id = wp_create_user_request( 'test@example.com', 'remove_personal_data' );
+               $key        = wp_generate_password( 20, false );
+
+               wp_update_post(
+                       array(
+                               'ID'            => $request_id,
+                               'post_password' => self::$wp_hasher->HashPassword( $key ),
+                       )
+               );
+
+               // A legacy phpass key should remain valid.
+               $check = wp_validate_user_request_key( $request_id, $key );
+               $this->assertNotWPError( $check );
+               $this->assertTrue( $check );
+
+               // An empty key with a legacy key should be rejected.
+               $check = wp_validate_user_request_key( $request_id, '' );
+               $this->assertWPError( $check );
+               $this->assertSame( 'missing_key', $check->get_error_code() );
+       }
+
+       /**
+        * The `wp_password_needs_rehash()` function is just a wrapper around `password_needs_rehash()`, but this ensures
+        * that it works as expected.
+        *
+        * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 .
+        *
+        * @ticket 21022
+        */
+       public function check_password_needs_rehashing() {
+               $password = 'password';
+
+               // Current password hashing algorithm.
+               $hash = wp_hash_password( $password );
+               $this->assertFalse( wp_password_needs_rehash( $hash ) );
+
+               // A future upgrade from a previously lower cost.
+               $default = self::get_default_bcrypt_cost();
+               $opts    = array(
+                       // Reducing the cost mimics an increase in the default cost.
+                       'cost' => $default - 1,
+               );
+               $hash    = password_hash( $password, PASSWORD_BCRYPT, $opts );
+               $this->assertTrue( wp_password_needs_rehash( $hash ) );
+
+               // Previous phpass algorithm.
+               $hash = self::$wp_hasher->HashPassword( $password );
+               $this->assertTrue( wp_password_needs_rehash( $hash ) );
+
+               // o_O md5.
+               $hash = md5( $password );
+               $this->assertTrue( wp_password_needs_rehash( $hash ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 32429
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 24783
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -458,6 +1023,206 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 21022
+        */
+       public function test_phpass_application_password_is_accepted() {
+               add_filter( 'application_password_is_api_request', '__return_true' );
+               add_filter( 'wp_is_application_passwords_available', '__return_true' );
+
+               $password = 'password';
+
+               // Set an application password with the old phpass algorithm.
+               $uuid = self::set_application_password_with_phpass( $password, self::$user_id );
+
+               // Authenticate.
+               $user = wp_authenticate_application_password( null, self::USER_LOGIN, $password );
+
+               // Verify that the phpass hash for the application password was valid.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+       }
+
+       /**
+        * @dataProvider data_usernames
+        *
+        * @ticket 21022
+        */
+       public function test_phpass_password_is_rehashed_after_successful_user_password_authentication( $username_or_email ) {
+               $password = 'password';
+
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( $password, self::$user_id );
+
+               // Verify that the password needs rehashing.
+               $hash = get_userdata( self::$user_id )->user_pass;
+               $this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) );
+
+               // Authenticate.
+               $user = wp_authenticate( $username_or_email, $password );
+
+               // Verify that the phpass password hash was valid.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+
+               // Verify that the password no longer needs rehashing.
+               $hash = get_userdata( self::$user_id )->user_pass;
+               $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) );
+
+               // Authenticate a second time to ensure the new hash is valid.
+               $user = wp_authenticate( $username_or_email, $password );
+
+               // Verify that the bcrypt password hash is valid.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+       }
+
+       /**
+        * @dataProvider data_usernames
+        *
+        * @ticket 21022
+        */
+       public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_user_password_authentication( $username_or_email ) {
+               $password = 'password';
+
+               // Hash the user password with a lower cost than default to mimic a cost upgrade.
+               add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) );
+               wp_set_password( $password, self::$user_id );
+               remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) );
+
+               // Verify that the password needs rehashing.
+               $hash = get_userdata( self::$user_id )->user_pass;
+               $this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) );
+
+               // Authenticate.
+               $user = wp_authenticate( $username_or_email, $password );
+
+               // Verify that the reduced cost password hash was valid.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+
+               // Verify that the password has been rehashed with the increased cost.
+               $hash = get_userdata( self::$user_id )->user_pass;
+               $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) );
+               $this->assertSame( self::get_default_bcrypt_cost(), password_get_info( substr( $hash, 3 ) )['options']['cost'] );
+
+               // Authenticate a second time to ensure the new hash is valid.
+               $user = wp_authenticate( $username_or_email, $password );
+
+               // Verify that the password hash is valid.
+               $this->assertNotWPError( $user );
+               $this->assertInstanceOf( 'WP_User', $user );
+               $this->assertSame( self::$user_id, $user->ID );
+       }
+
+       public function reduce_hash_cost( array $options ): array {
+               $options['cost'] = self::get_default_bcrypt_cost() - 1;
+               return $options;
+       }
+
+       public function increase_hash_cost( array $options ): array {
+               $options['cost'] = self::get_default_bcrypt_cost() + 1;
+               return $options;
+       }
+
+       public function data_usernames() {
+               return array(
+                       array(
+                               self::USER_LOGIN,
+                       ),
+                       array(
+                               self::USER_EMAIL,
+                       ),
+               );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_password_rehashing_requirement_can_be_filtered() {
+               $filter_count_before = did_filter( 'password_needs_rehash' );
+
+               wp_password_needs_rehash( '$hash' );
+
+               $this->assertSame( $filter_count_before + 1, did_filter( 'password_needs_rehash' ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_password_hashing_algorithm_can_be_filtered() {
+               $password = 'password';
+
+               $filter_count_before = did_filter( 'wp_hash_password_algorithm' );
+
+               $wp_hash = wp_hash_password( $password );
+
+               wp_check_password( $password, $wp_hash );
+               wp_password_needs_rehash( $wp_hash );
+
+               $this->assertSame( $filter_count_before + 2, did_filter( 'wp_hash_password_algorithm' ) );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_password_hashing_options_can_be_filtered() {
+               $password = 'password';
+
+               add_filter(
+                       'wp_hash_password_options',
+                       static function ( $options ) {
+                               $options['cost'] = 5;
+                               return $options;
+                       }
+               );
+
+               $filter_count_before = did_filter( 'wp_hash_password_options' );
+
+               $wp_hash      = wp_hash_password( $password );
+               $valid        = wp_check_password( $password, $wp_hash );
+               $needs_rehash = wp_password_needs_rehash( $wp_hash );
+               $info         = password_get_info( substr( $wp_hash, 3 ) );
+               $cost         = $info['options']['cost'];
+
+               $this->assertTrue( $valid );
+               $this->assertFalse( $needs_rehash );
+               $this->assertSame( $filter_count_before + 2, did_filter( 'wp_hash_password_options' ) );
+               $this->assertSame( 5, $cost );
+       }
+
+       /**
+        * @ticket 21022
+        */
+       public function test_password_checks_support_wp_hasher_fallback() {
+               global $wp_hasher;
+
+               $filter_count_before = did_filter( 'wp_hash_password_options' );
+
+               $password = 'password';
+
+               // Ensure the global $wp_hasher is set.
+               $wp_hasher = new WP_Fake_Hasher();
+
+               $hasher_hash  = $wp_hasher->HashPassword( $password );
+               $wp_hash      = wp_hash_password( $password );
+               $valid        = wp_check_password( $password, $wp_hash );
+               $needs_rehash = wp_password_needs_rehash( $wp_hash );
+
+               // Reset the global $wp_hasher.
+               $wp_hasher = null;
+
+               $this->assertSame( $hasher_hash, $wp_hash );
+               $this->assertTrue( $valid );
+               $this->assertFalse( $needs_rehash );
+               $this->assertSame( 1, did_filter( 'check_password' ) );
+               $this->assertSame( $filter_count_before, did_filter( 'wp_hash_password_options' ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Ensure users can log in using both their username and their email address.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 9568
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -703,7 +1468,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 42790
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_authenticate_application_password_respects_existing_user() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertSame( self::$_user, wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $user = wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' );
+               $this->assertNotWPError( $user );
+               $this->assertSame( self::$_user, $user );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -712,7 +1479,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_authenticate_application_password_is_rejected_if_not_api_request() {
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'application_password_is_api_request', '__return_false' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertNull( wp_authenticate_application_password( null, self::$_user->user_login, 'password' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $user = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
+               $this->assertNotWPError( $user );
+               $this->assertNull( $user );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -805,6 +1574,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $user = wp_authenticate_application_password( null, self::$_user->user_login, $password );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertNotWPError( $user );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( WP_User::class, $user );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( self::$user_id, $user->ID );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -819,6 +1589,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $user = wp_authenticate_application_password( null, self::$_user->user_email, $password );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertNotWPError( $user );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( WP_User::class, $user );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( self::$user_id, $user->ID );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -833,6 +1604,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $user = wp_authenticate_application_password( null, self::$_user->user_email, WP_Application_Passwords::chunk_password( $password ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertNotWPError( $user );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( WP_User::class, $user );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( self::$user_id, $user->ID );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -844,6 +1616,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                delete_site_option( 'using_application_passwords' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $authenticated = wp_authenticate_application_password( null, 'idonotexist', 'password' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertNotWPError( $authenticated );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertNull( $authenticated );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -967,4 +1740,117 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( $_SERVER['PHP_AUTH_USER'], 'username' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( $_SERVER['PHP_AUTH_PW'], 'pass:word' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Test the tests
+        *
+        * @covers Tests_Auth::set_user_password_with_phpass
+        *
+        * @ticket 21022
+        */
+       public function test_set_user_password_with_phpass() {
+               // Set the user password with the old phpass algorithm.
+               self::set_user_password_with_phpass( 'password', self::$user_id );
+
+               // Ensure the password is hashed with phpass.
+               $hash = get_userdata( self::$user_id )->user_pass;
+               $this->assertStringStartsWith( '$P$', $hash );
+       }
+
+       private static function set_user_password_with_phpass( string $password, int $user_id ) {
+               global $wpdb;
+
+               $wpdb->update(
+                       $wpdb->users,
+                       array(
+                               'user_pass' => self::$wp_hasher->HashPassword( $password ),
+                       ),
+                       array(
+                               'ID' => $user_id,
+                       )
+               );
+               clean_user_cache( $user_id );
+       }
+
+
+       /**
+        * Test the tests
+        *
+        * @covers Tests_Auth::set_user_password_with_plain_bcrypt
+        *
+        * @ticket 21022
+        */
+       public function test_set_user_password_with_plain_bcrypt() {
+               // Set the user password with plain bcrypt.
+               self::set_user_password_with_plain_bcrypt( 'password', self::$user_id );
+
+               // Ensure the password is hashed with bcrypt.
+               $hash = get_userdata( self::$user_id )->user_pass;
+               $this->assertStringStartsWith( '$2y$', $hash );
+       }
+
+       private static function set_user_password_with_plain_bcrypt( string $password, int $user_id ) {
+               global $wpdb;
+
+               $wpdb->update(
+                       $wpdb->users,
+                       array(
+                               'user_pass' => password_hash( 'password', PASSWORD_BCRYPT ),
+                       ),
+                       array(
+                               'ID' => $user_id,
+                       )
+               );
+               clean_user_cache( $user_id );
+       }
+
+       /**
+        * Test the tests
+        *
+        * @covers Tests_Auth::set_application_password_with_phpass
+        *
+        * @ticket 21022
+        */
+       public function test_set_application_password_with_phpass() {
+               // Set an application password with the old phpass algorithm.
+               $uuid = self::set_application_password_with_phpass( 'password', self::$user_id );
+
+               // Ensure the password is hashed with phpass.
+               $hash = WP_Application_Passwords::get_user_application_password( self::$user_id, $uuid )['password'];
+               $this->assertStringStartsWith( '$P$', $hash );
+       }
+
+       private static function set_application_password_with_phpass( string $password, int $user_id ) {
+               $uuid = wp_generate_uuid4();
+               $item = array(
+                       'uuid'      => $uuid,
+                       'app_id'    => '',
+                       'name'      => 'Test',
+                       'password'  => self::$wp_hasher->HashPassword( $password ),
+                       'created'   => time(),
+                       'last_used' => null,
+                       'last_ip'   => null,
+               );
+
+               $saved = update_user_meta(
+                       $user_id,
+                       WP_Application_Passwords::USERMETA_KEY_APPLICATION_PASSWORDS,
+                       array( $item )
+               );
+
+               if ( ! $saved ) {
+                       throw new Exception( 'Could not save application password.' );
+               }
+
+               update_network_option( get_main_network_id(), WP_Application_Passwords::OPTION_KEY_IN_USE, true );
+
+               return $uuid;
+       }
+
+       private static function get_default_bcrypt_cost(): int {
+               $hash = password_hash( 'password', PASSWORD_BCRYPT );
+               $info = password_get_info( $hash );
+
+               return $info['options']['cost'];
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestspluggablesignaturesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/pluggable/signatures.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/pluggable/signatures.php        2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/tests/phpunit/tests/pluggable/signatures.php  2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -217,6 +217,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'hash',
</span><span class="cx" style="display: block; padding: 0 10px">                                'user_id' => '',
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'wp_password_needs_rehash'        => array(
+                               'hash',
+                               'user_id' => '',
+                       ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'wp_generate_password'            => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'length'              => 12,
</span><span class="cx" style="display: block; padding: 0 10px">                                'special_chars'       => true,
</span></span></pre></div>
<a id="trunktestsphpunittestsuserpasswordHashphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/user/passwordHash.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/user/passwordHash.php   2025-02-16 18:33:58 UTC (rev 59827)
+++ trunk/tests/phpunit/tests/user/passwordHash.php     2025-02-17 11:22:33 UTC (rev 59828)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3,6 +3,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Tests for the PasswordHash external library.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * PasswordHash is no longer used to hash user passwords or security keys, but it is still used to
+ * hash post passwords and as a fallback to verify old passwords that were hashed by phpass. The
+ * library therefore needs to remain compatible with the latest versions of PHP.
+ *
</ins><span class="cx" style="display: block; padding: 0 10px">  * @covers PasswordHash
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class Tests_User_PasswordHash extends WP_UnitTestCase {
</span></span></pre>
</div>
</div>

</body>
</html>