From 33e3e350b8d0d7862883856a9d7d1f221a891782 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 13:50:43 +0200
Subject: [PATCH 1/8] Add TPhpass::encode64()

---
 .../password/algorithm/TPhpass.class.php      | 54 ++++++++++---------
 1 file changed, 29 insertions(+), 25 deletions(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
index 87a3ca8d797..a816c16bf5a 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
@@ -48,41 +48,45 @@ private function hashPhpass(string $password, string $settings): string
         } while (--$count);
 
         $output = \mb_substr($settings, 0, 12, '8bit');
-        $hash_encode64 = static function ($input, $count, &$itoa64) {
-            $output = '';
-            $i = 0;
+        $output .= $this->encode64($hash, 16);
 
-            do {
-                $value = \ord($input[$i++]);
-                $output .= $itoa64[$value & 0x3f];
+        return $output;
+    }
 
-                if ($i < $count) {
-                    $value |= \ord($input[$i]) << 8;
-                }
+    /**
+     * Encodes $count characters from $input with PHPASS' custom base64 encoder.
+     */
+    private function encode64(string $input, int $count): string
+    {
+        $output = '';
+        $i = 0;
 
-                $output .= $itoa64[($value >> 6) & 0x3f];
+        do {
+            $value = \ord($input[$i++]);
+            $output .= $this->itoa64[$value & 0x3f];
 
-                if ($i++ >= $count) {
-                    break;
-                }
+            if ($i < $count) {
+                $value |= \ord($input[$i]) << 8;
+            }
 
-                if ($i < $count) {
-                    $value |= \ord($input[$i]) << 16;
-                }
+            $output .= $this->itoa64[($value >> 6) & 0x3f];
 
-                $output .= $itoa64[($value >> 12) & 0x3f];
+            if ($i++ >= $count) {
+                break;
+            }
 
-                if ($i++ >= $count) {
-                    break;
-                }
+            if ($i < $count) {
+                $value |= \ord($input[$i]) << 16;
+            }
 
-                $output .= $itoa64[($value >> 18) & 0x3f];
-            } while ($i < $count);
+            $output .= $this->itoa64[($value >> 12) & 0x3f];
 
-            return $output;
-        };
+            if ($i++ >= $count) {
+                break;
+            }
 
-        $output .= $hash_encode64($hash, 16, $this->itoa64);
+            $output .= $this->itoa64[($value >> 18) & 0x3f];
+        } while ($i < $count);
 
         return $output;
     }

From 4513f1a0ed0f5a604aaf49c0b5029e04d9c32b06 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 13:54:50 +0200
Subject: [PATCH 2/8] Delegate base64 encoding for Drupal8 to TPhpass

---
 .../password/algorithm/Drupal8.class.php      | 38 +------------------
 1 file changed, 2 insertions(+), 36 deletions(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
index 24c441da897..18963d99924 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
@@ -16,7 +16,7 @@
  */
 final class Drupal8 implements IPasswordAlgorithm
 {
-    private $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    use TPhpass;
 
     /**
      * Returns the hashed password, with the given settings.
@@ -49,41 +49,7 @@ private function hashDrupal(string $password, string $settings): string
         } while (--$count);
 
         $output = \mb_substr($settings, 0, 12, '8bit');
-        $hash_encode64 = static function ($input, $count, &$itoa64) {
-            $output = '';
-            $i = 0;
-
-            do {
-                $value = \ord($input[$i++]);
-                $output .= $itoa64[$value & 0x3f];
-
-                if ($i < $count) {
-                    $value |= \ord($input[$i]) << 8;
-                }
-
-                $output .= $itoa64[($value >> 6) & 0x3f];
-
-                if ($i++ >= $count) {
-                    break;
-                }
-
-                if ($i < $count) {
-                    $value |= \ord($input[$i]) << 16;
-                }
-
-                $output .= $itoa64[($value >> 12) & 0x3f];
-
-                if ($i++ >= $count) {
-                    break;
-                }
-
-                $output .= $itoa64[($value >> 18) & 0x3f];
-            } while ($i < $count);
-
-            return $output;
-        };
-
-        $output .= $hash_encode64($hash, 64, $this->itoa64);
+        $output .= $this->encode64($hash, 64);
 
         return \mb_substr($output, 0, 55, '8bit');
     }

From 3b73f079ef1a269a09eeb00cc7e3a1111926000d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 13:52:48 +0200
Subject: [PATCH 3/8] =?UTF-8?q?Use=20`\hash('md5',=20=E2=80=A6)`=20instead?=
 =?UTF-8?q?=20of=20`\md5()`=20in=20TPhpass?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This makes it easier to parameterize the implementation.
---
 .../user/authentication/password/algorithm/TPhpass.class.php  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
index a816c16bf5a..d7c71682357 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
@@ -42,9 +42,9 @@ private function hashPhpass(string $password, string $settings): string
             return $output;
         }
 
-        $hash = \md5($salt . $password, true);
+        $hash = \hash('md5', $salt . $password, true);
         do {
-            $hash = \md5($hash . $password, true);
+            $hash = \hash('md5', $hash . $password, true);
         } while (--$count);
 
         $output = \mb_substr($settings, 0, 12, '8bit');

From 077d6f5d3d5ef08dce1ebe82202eae396e3d5d90 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 14:04:02 +0200
Subject: [PATCH 4/8] Delegate to TPhpass in Drupal8

---
 .../password/algorithm/Drupal8.class.php      | 28 +------------------
 .../password/algorithm/TPhpass.class.php      | 21 +++++++++++---
 2 files changed, 18 insertions(+), 31 deletions(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
index 18963d99924..f7195c0decd 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
@@ -23,33 +23,7 @@ final class Drupal8 implements IPasswordAlgorithm
      */
     private function hashDrupal(string $password, string $settings): string
     {
-        $output = '*';
-
-        // Check for correct hash
-        if (\mb_substr($settings, 0, 3, '8bit') !== '$S$') {
-            return $output;
-        }
-
-        $count_log2 = \mb_strpos($this->itoa64, $settings[3], 0, '8bit');
-
-        if ($count_log2 < 7 || $count_log2 > 30) {
-            return $output;
-        }
-
-        $count = 1 << $count_log2;
-        $salt = \mb_substr($settings, 4, 8, '8bit');
-
-        if (\mb_strlen($salt, '8bit') != 8) {
-            return $output;
-        }
-
-        $hash = \hash('sha512', $salt . $password, true);
-        do {
-            $hash = \hash('sha512', $hash . $password, true);
-        } while (--$count);
-
-        $output = \mb_substr($settings, 0, 12, '8bit');
-        $output .= $this->encode64($hash, 64);
+        $output = $this->hashPhpass($password, $settings);
 
         return \mb_substr($output, 0, 55, '8bit');
     }
diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
index d7c71682357..64016dee78c 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
@@ -25,10 +25,23 @@ private function hashPhpass(string $password, string $settings): string
         $output = '*';
 
         // Check for correct hash
-        if (\mb_substr($settings, 0, 3, '8bit') !== '$H$' && \mb_substr($settings, 0, 3, '8bit') !== '$P$') {
+        if ($settings[0] !== '$' || $settings[2] !== '$') {
             return $output;
         }
 
+        $variant = $settings[1];
+        switch ($variant) {
+            case 'H':
+            case 'P':
+                $algo = 'md5';
+                break;
+            case 'S':
+                $algo = 'sha512';
+                break;
+            default:
+                return $output;
+        }
+
         $count_log2 = \mb_strpos($this->itoa64, $settings[3], 0, '8bit');
 
         if ($count_log2 < 7 || $count_log2 > 30) {
@@ -42,13 +55,13 @@ private function hashPhpass(string $password, string $settings): string
             return $output;
         }
 
-        $hash = \hash('md5', $salt . $password, true);
+        $hash = \hash($algo, $salt . $password, true);
         do {
-            $hash = \hash('md5', $hash . $password, true);
+            $hash = \hash($algo, $hash . $password, true);
         } while (--$count);
 
         $output = \mb_substr($settings, 0, 12, '8bit');
-        $output .= $this->encode64($hash, 16);
+        $output .= $this->encode64($hash, \mb_strlen($hash, '8bit'));
 
         return $output;
     }

From 634a03df0aef7b16187b5cd73194595bf90ea6f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 14:08:12 +0200
Subject: [PATCH 5/8] Move the Drupal8 costs into a class constant

---
 .../user/authentication/password/algorithm/Drupal8.class.php | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
index f7195c0decd..1963e69fea2 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
@@ -18,6 +18,8 @@ final class Drupal8 implements IPasswordAlgorithm
 {
     use TPhpass;
 
+    private const COSTS = 15;
+
     /**
      * Returns the hashed password, with the given settings.
      */
@@ -45,7 +47,8 @@ public function verify(string $password, string $hash): bool
      */
     public function hash(string $password): string
     {
-        $settings = '$S$D';
+        $settings = '$S$';
+        $settings .= $this->itoa64[self::COSTS];
         $settings .= Hex::encode(\random_bytes(4));
 
         return $this->hashDrupal($password, $settings) . ':';

From e7406a1263ed71a5038c8a01f232deaa37a7a79e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 14:14:03 +0200
Subject: [PATCH 6/8] Implement Drupal8::needsRehash()

---
 .../password/algorithm/Drupal8.class.php         | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
index 1963e69fea2..37037d355ca 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Drupal8.class.php
@@ -47,11 +47,9 @@ public function verify(string $password, string $hash): bool
      */
     public function hash(string $password): string
     {
-        $settings = '$S$';
-        $settings .= $this->itoa64[self::COSTS];
-        $settings .= Hex::encode(\random_bytes(4));
+        $salt = Hex::encode(\random_bytes(4));
 
-        return $this->hashDrupal($password, $settings) . ':';
+        return $this->hashDrupal($password, $this->getSettings() . $salt) . ':';
     }
 
     /**
@@ -59,6 +57,14 @@ public function hash(string $password): string
      */
     public function needsRehash(string $hash): bool
     {
-        return false;
+        return !\str_starts_with($hash, $this->getSettings());
+    }
+
+    /**
+     * Returns the settings prefix with the algorithm identifier and costs.
+     */
+    private function getSettings(): string
+    {
+        return '$S$' . $this->itoa64[self::COSTS];
     }
 }

From 0510bc056b60e39027f366ad1e76a9adfea48cf6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 14:16:41 +0200
Subject: [PATCH 7/8] Explicitly implement `Phpass::hash()` and
 `Phpass::needsRehash()`

They don't really belong into the `TPhpass` trait.
---
 .../password/algorithm/Phpass.class.php       | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpass.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpass.class.php
index 575a3e07eac..9ed648f762d 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpass.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpass.class.php
@@ -2,6 +2,7 @@
 
 namespace wcf\system\user\authentication\password\algorithm;
 
+use ParagonIE\ConstantTime\Hex;
 use wcf\system\user\authentication\password\IPasswordAlgorithm;
 
 /**
@@ -16,4 +17,32 @@
 final class Phpass implements IPasswordAlgorithm
 {
     use TPhpass;
+
+    private const COSTS = 10;
+
+    /**
+     * @inheritDoc
+     */
+    public function hash(string $password): string
+    {
+        $salt = Hex::encode(\random_bytes(4));
+
+        return $this->hashPhpass($password, $this->getSettings() . $salt) . ':';
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function needsRehash(string $hash): bool
+    {
+        return !\str_starts_with($hash, $this->getSettings());
+    }
+
+    /**
+     * Returns the settings prefix with the algorithm identifier and costs.
+     */
+    private function getSettings(): string
+    {
+        return '$H$' . $this->itoa64[self::COSTS];
+    }
 }

From 01eec81cc26c39aea3fae05bd445a27be131374a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <duesterhus@woltlab.com>
Date: Thu, 28 Apr 2022 14:20:47 +0200
Subject: [PATCH 8/8] Deprecate TPhpass::hash() and TPhpass::needsRehash()

---
 .../user/authentication/password/algorithm/Phpbb3.class.php   | 2 --
 .../user/authentication/password/algorithm/TPhpass.class.php  | 4 ++--
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpbb3.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpbb3.class.php
index eef8ac37e27..149f555144b 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpbb3.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/Phpbb3.class.php
@@ -18,8 +18,6 @@ final class Phpbb3 implements IPasswordAlgorithm
 {
     use TPhpass {
         verify as phpassVerify;
-
-        hash as phpassHash;
     }
 
     public function verify(string $password, string $hash): bool
diff --git a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
index 64016dee78c..c684b752648 100644
--- a/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
+++ b/wcfsetup/install/files/lib/system/user/authentication/password/algorithm/TPhpass.class.php
@@ -121,7 +121,7 @@ public function verify(string $password, string $hash): bool
     }
 
     /**
-     * @inheritDoc
+     * @deprecated 5.5 Use Phpass::hash() instead.
      */
     public function hash(string $password): string
     {
@@ -132,7 +132,7 @@ public function hash(string $password): string
     }
 
     /**
-     * @inheritDoc
+     * @deprecated 5.5 Use Phpass::needsRehash() instead.
      */
     public function needsRehash(string $hash): bool
     {