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 {