1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 05:50:55 +01:00

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
This commit is contained in:
epriestley 2014-02-18 11:03:56 -08:00
parent 5778627e41
commit 580bcd0d2b
6 changed files with 137 additions and 6 deletions

View file

@ -1244,6 +1244,7 @@ phutil_register_library_map(array(
'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php', 'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php', 'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php',
'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php', 'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php',
'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php',
'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
'PhabricatorBotBaseStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php', 'PhabricatorBotBaseStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php',
'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
@ -3915,6 +3916,7 @@ phutil_register_library_map(array(
'PhabricatorBarePageExample' => 'PhabricatorUIExample', 'PhabricatorBarePageExample' => 'PhabricatorUIExample',
'PhabricatorBarePageView' => 'AphrontPageView', 'PhabricatorBarePageView' => 'AphrontPageView',
'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation', 'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation',
'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBot' => 'PhabricatorDaemon',
'PhabricatorBotBaseStreamingProtocolAdapter' => 'PhabricatorBaseProtocolAdapter', 'PhabricatorBotBaseStreamingProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotChannel' => 'PhabricatorBotTarget',

View file

@ -264,6 +264,19 @@ final class PhabricatorAuthProviderPassword
if ($user->comparePassword($envelope)) { if ($user->comparePassword($envelope)) {
$account = $this->loadOrCreateAccount($user->getPHID()); $account = $this->loadOrCreateAccount($user->getPHID());
$log_user = $user; $log_user = $user;
// If the user's password is stored using a less-than-optimal
// hash, upgrade them to the strongest available hash.
$hash_envelope = new PhutilOpaqueEnvelope(
$user->getPasswordHash());
if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
$user->setPassword($envelope);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$user->save();
unset($unguarded);
}
} }
} }
} }

View file

@ -112,6 +112,15 @@ final class PhabricatorSettingsPanelPassword
} }
} }
$hash_envelope = new PhutilOpaqueEnvelope($user->getPasswordHash());
if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
$best_hash = PhabricatorPasswordHasher::getBestHasher();
$errors[] = pht(
'The strength of your stored password hash can be upgraded. '.
'To upgrade, either: log out and log in using your password; or '.
'change your password.');
}
$len_caption = null; $len_caption = null;
if ($min_len) { if ($min_len) {
$len_caption = pht('Minimum password length: %d characters.', $min_len); $len_caption = pht('Minimum password length: %d characters.', $min_len);
@ -146,7 +155,36 @@ final class PhabricatorSettingsPanelPassword
$form $form
->appendChild( ->appendChild(
id(new AphrontFormSubmitControl()) id(new AphrontFormSubmitControl())
->setValue(pht('Save'))); ->setValue(pht('Change Password')));
if (!strlen($user->getPasswordHash())) {
$current_name = pht('None');
} else {
try {
$current_hasher = PhabricatorPasswordHasher::getHasherForHash(
new PhutilOpaqueEnvelope($user->getPasswordHash()));
$current_name = $current_hasher->getHumanReadableName();
} catch (Exception $ex) {
$current_name = pht('Unknown');
}
}
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Current Algorithm'))
->setValue($current_name));
try {
$best_hasher = PhabricatorPasswordHasher::getBestHasher();
$best_name = $best_hasher->getHumanReadableName();
} catch (Exception $ex) {
$best_name = pht('Unknown');
}
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Best Available Algorithm'))
->setValue($best_name));
$form_box = id(new PHUIObjectBoxView()) $form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Change Password')) ->setHeaderText(pht('Change Password'))

View file

@ -0,0 +1,56 @@
<?php
final class PhabricatorBcryptPasswordHasher
extends PhabricatorPasswordHasher {
public function getHumanReadableName() {
return pht('bcrypt');
}
public function getHashName() {
return 'bcrypt';
}
public function getHashLength() {
return 60;
}
public function canHashPasswords() {
return function_exists('password_hash');
}
public function getInstallInstructions() {
return pht('Upgrade to PHP 5.5.0 or newer.');
}
public function getStrength() {
return 2.0;
}
public function getHumanReadableStrength() {
return pht("Good");
}
protected function getPasswordHash(PhutilOpaqueEnvelope $envelope) {
$raw_input = $envelope->openEnvelope();
// NOTE: The default cost is "10", but my laptop can do a hash of cost
// "12" in about 300ms. Since server hardware is often virtualized or old,
// just split the difference.
$options = array(
'cost' => 11,
);
$raw_hash = password_hash($raw_input, CRYPT_BLOWFISH, $options);
return new PhutilOpaqueEnvelope($raw_hash);
}
protected function verifyPassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $hash) {
return password_verify($password->openEnvelope(), $hash->openEnvelope());
}
}

View file

@ -12,7 +12,7 @@ final class PhabricatorIteratedMD5PasswordHasher
} }
public function getHashLength() { public function getHashLength() {
return 40; return 32;
} }
public function canHashPasswords() { public function canHashPasswords() {

View file

@ -108,6 +108,28 @@ abstract class PhabricatorPasswordHasher extends Phobject {
abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope); abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);
/**
* Verify that a password matches a hash.
*
* The default implementation checks for equality; if a hasher embeds salt in
* hashes it should override this method and perform a salt-aware comparison.
*
* @param PhutilOpaqueEnvelope Password to compare.
* @param PhutilOpaqueEnvelope Bare password hash.
* @return bool True if the passwords match.
* @task hasher
*/
protected function verifyPassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $hash) {
$actual_hash = $this->getPasswordHash($password)->openEnvelope();
$expect_hash = $hash->openEnvelope();
return ($actual_hash === $expect_hash);
}
/* -( Using Hashers )------------------------------------------------------ */ /* -( Using Hashers )------------------------------------------------------ */
@ -236,7 +258,7 @@ abstract class PhabricatorPasswordHasher extends Phobject {
*/ */
public static function getBestHasher() { public static function getBestHasher() {
$hashers = self::getAllUsableHashers(); $hashers = self::getAllUsableHashers();
msort($hashers, 'getStrength'); $hashers = msort($hashers, 'getStrength');
$hasher = last($hashers); $hasher = last($hashers);
if (!$hasher) { if (!$hasher) {
@ -292,7 +314,7 @@ abstract class PhabricatorPasswordHasher extends Phobject {
* the hash strength. * the hash strength.
* @task hashing * @task hashing
*/ */
public static function canHashBeUpgraded(PhutilOpaqueEnvelope $hash) { public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
$current_hasher = self::getHasherForHash($hash); $current_hasher = self::getHasherForHash($hash);
$best_hasher = self::getBestHasher(); $best_hasher = self::getBestHasher();
@ -328,9 +350,9 @@ abstract class PhabricatorPasswordHasher extends Phobject {
PhutilOpaqueEnvelope $hash) { PhutilOpaqueEnvelope $hash) {
$hasher = self::getHasherForHash($hash); $hasher = self::getHasherForHash($hash);
$password_hash = $hasher->getPasswordHashForStorage($password); $parts = self::parseHashFromStorage($hash);
return ($password_hash->openEnvelope() == $hash->openEnvelope()); return $hasher->verifyPassword($password, $parts['hash']);
} }
} }