@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement bcrypt hasher, transparent login upgrade, and explicit upgrade for passwords

Summary:
Ref T4443.

- Add a `password_hash()`-based bcrypt hasher if `password_hash()` is available.
- When a user logs in using a password, upgrade their password to the strongest available hash format.
- On the password settings page:
- Warn the user if their password uses any algorithm other than the strongest one.
- Show the algorithm the password uses.
- Show the best available algorithm.

Test Plan: As an md5 user, viewed password settings page and saw a warning. Logged out. Logged in, got upgraded, no more warning. Changed password, verified database rehash. Logged out, logged in.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4443

Differential Revision: https://secure.phabricator.com/D8270

+137 -6
+2
src/__phutil_library_map__.php
··· 1244 1244 'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php', 1245 1245 'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php', 1246 1246 'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php', 1247 + 'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php', 1247 1248 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 1248 1249 'PhabricatorBotBaseStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php', 1249 1250 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', ··· 3915 3916 'PhabricatorBarePageExample' => 'PhabricatorUIExample', 3916 3917 'PhabricatorBarePageView' => 'AphrontPageView', 3917 3918 'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation', 3919 + 'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher', 3918 3920 'PhabricatorBot' => 'PhabricatorDaemon', 3919 3921 'PhabricatorBotBaseStreamingProtocolAdapter' => 'PhabricatorBaseProtocolAdapter', 3920 3922 'PhabricatorBotChannel' => 'PhabricatorBotTarget',
+13
src/applications/auth/provider/PhabricatorAuthProviderPassword.php
··· 264 264 if ($user->comparePassword($envelope)) { 265 265 $account = $this->loadOrCreateAccount($user->getPHID()); 266 266 $log_user = $user; 267 + 268 + // If the user's password is stored using a less-than-optimal 269 + // hash, upgrade them to the strongest available hash. 270 + 271 + $hash_envelope = new PhutilOpaqueEnvelope( 272 + $user->getPasswordHash()); 273 + if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { 274 + $user->setPassword($envelope); 275 + 276 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 277 + $user->save(); 278 + unset($unguarded); 279 + } 267 280 } 268 281 } 269 282 }
+39 -1
src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
··· 112 112 } 113 113 } 114 114 115 + $hash_envelope = new PhutilOpaqueEnvelope($user->getPasswordHash()); 116 + if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { 117 + $best_hash = PhabricatorPasswordHasher::getBestHasher(); 118 + $errors[] = pht( 119 + 'The strength of your stored password hash can be upgraded. '. 120 + 'To upgrade, either: log out and log in using your password; or '. 121 + 'change your password.'); 122 + } 123 + 115 124 $len_caption = null; 116 125 if ($min_len) { 117 126 $len_caption = pht('Minimum password length: %d characters.', $min_len); ··· 146 155 $form 147 156 ->appendChild( 148 157 id(new AphrontFormSubmitControl()) 149 - ->setValue(pht('Save'))); 158 + ->setValue(pht('Change Password'))); 159 + 160 + if (!strlen($user->getPasswordHash())) { 161 + $current_name = pht('None'); 162 + } else { 163 + try { 164 + $current_hasher = PhabricatorPasswordHasher::getHasherForHash( 165 + new PhutilOpaqueEnvelope($user->getPasswordHash())); 166 + $current_name = $current_hasher->getHumanReadableName(); 167 + } catch (Exception $ex) { 168 + $current_name = pht('Unknown'); 169 + } 170 + } 171 + 172 + $form->appendChild( 173 + id(new AphrontFormStaticControl()) 174 + ->setLabel(pht('Current Algorithm')) 175 + ->setValue($current_name)); 176 + 177 + try { 178 + $best_hasher = PhabricatorPasswordHasher::getBestHasher(); 179 + $best_name = $best_hasher->getHumanReadableName(); 180 + } catch (Exception $ex) { 181 + $best_name = pht('Unknown'); 182 + } 183 + 184 + $form->appendChild( 185 + id(new AphrontFormStaticControl()) 186 + ->setLabel(pht('Best Available Algorithm')) 187 + ->setValue($best_name)); 150 188 151 189 $form_box = id(new PHUIObjectBoxView()) 152 190 ->setHeaderText(pht('Change Password'))
+56
src/infrastructure/util/password/PhabricatorBcryptPasswordHasher.php
··· 1 + <?php 2 + 3 + final class PhabricatorBcryptPasswordHasher 4 + extends PhabricatorPasswordHasher { 5 + 6 + public function getHumanReadableName() { 7 + return pht('bcrypt'); 8 + } 9 + 10 + public function getHashName() { 11 + return 'bcrypt'; 12 + } 13 + 14 + public function getHashLength() { 15 + return 60; 16 + } 17 + 18 + public function canHashPasswords() { 19 + return function_exists('password_hash'); 20 + } 21 + 22 + public function getInstallInstructions() { 23 + return pht('Upgrade to PHP 5.5.0 or newer.'); 24 + } 25 + 26 + public function getStrength() { 27 + return 2.0; 28 + } 29 + 30 + public function getHumanReadableStrength() { 31 + return pht("Good"); 32 + } 33 + 34 + protected function getPasswordHash(PhutilOpaqueEnvelope $envelope) { 35 + $raw_input = $envelope->openEnvelope(); 36 + 37 + // NOTE: The default cost is "10", but my laptop can do a hash of cost 38 + // "12" in about 300ms. Since server hardware is often virtualized or old, 39 + // just split the difference. 40 + 41 + $options = array( 42 + 'cost' => 11, 43 + ); 44 + 45 + $raw_hash = password_hash($raw_input, CRYPT_BLOWFISH, $options); 46 + 47 + return new PhutilOpaqueEnvelope($raw_hash); 48 + } 49 + 50 + protected function verifyPassword( 51 + PhutilOpaqueEnvelope $password, 52 + PhutilOpaqueEnvelope $hash) { 53 + return password_verify($password->openEnvelope(), $hash->openEnvelope()); 54 + } 55 + 56 + }
+1 -1
src/infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php
··· 12 12 } 13 13 14 14 public function getHashLength() { 15 - return 40; 15 + return 32; 16 16 } 17 17 18 18 public function canHashPasswords() {
+26 -4
src/infrastructure/util/password/PhabricatorPasswordHasher.php
··· 108 108 abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope); 109 109 110 110 111 + /** 112 + * Verify that a password matches a hash. 113 + * 114 + * The default implementation checks for equality; if a hasher embeds salt in 115 + * hashes it should override this method and perform a salt-aware comparison. 116 + * 117 + * @param PhutilOpaqueEnvelope Password to compare. 118 + * @param PhutilOpaqueEnvelope Bare password hash. 119 + * @return bool True if the passwords match. 120 + * @task hasher 121 + */ 122 + protected function verifyPassword( 123 + PhutilOpaqueEnvelope $password, 124 + PhutilOpaqueEnvelope $hash) { 125 + 126 + $actual_hash = $this->getPasswordHash($password)->openEnvelope(); 127 + $expect_hash = $hash->openEnvelope(); 128 + 129 + return ($actual_hash === $expect_hash); 130 + } 131 + 132 + 111 133 /* -( Using Hashers )------------------------------------------------------ */ 112 134 113 135 ··· 236 258 */ 237 259 public static function getBestHasher() { 238 260 $hashers = self::getAllUsableHashers(); 239 - msort($hashers, 'getStrength'); 261 + $hashers = msort($hashers, 'getStrength'); 240 262 241 263 $hasher = last($hashers); 242 264 if (!$hasher) { ··· 292 314 * the hash strength. 293 315 * @task hashing 294 316 */ 295 - public static function canHashBeUpgraded(PhutilOpaqueEnvelope $hash) { 317 + public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) { 296 318 $current_hasher = self::getHasherForHash($hash); 297 319 $best_hasher = self::getBestHasher(); 298 320 ··· 328 350 PhutilOpaqueEnvelope $hash) { 329 351 330 352 $hasher = self::getHasherForHash($hash); 331 - $password_hash = $hasher->getPasswordHashForStorage($password); 353 + $parts = self::parseHashFromStorage($hash); 332 354 333 - return ($password_hash->openEnvelope() == $hash->openEnvelope()); 355 + return $hasher->verifyPassword($password, $parts['hash']); 334 356 } 335 357 336 358 }