Page MenuHomePhorge

No OneTemporary

diff --git a/src/infrastructure/util/password/PhabricatorBcryptPasswordHasher.php b/src/infrastructure/util/password/PhabricatorBcryptPasswordHasher.php
index 11e5a9389d..fc2bf7a523 100644
--- a/src/infrastructure/util/password/PhabricatorBcryptPasswordHasher.php
+++ b/src/infrastructure/util/password/PhabricatorBcryptPasswordHasher.php
@@ -1,56 +1,75 @@
<?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,
+ 'cost' => $this->getBcryptCost(),
);
$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());
}
+ protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
+ $info = password_get_info($hash->openEnvelope());
+
+ // NOTE: If the costs don't match -- even if the new cost is lower than
+ // the old cost -- count this as an upgrade. This allows costs to be
+ // adjusted down and hashing to be migrated toward the new cost if costs
+ // are ever configured too high for some reason.
+
+ $cost = idx($info['options'], 'cost');
+ if ($cost != $this->getBcryptCost()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function getBcryptCost() {
+ // 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.
+ return 11;
+ }
+
}
diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
index 12a341289f..0dcdb0e7c8 100644
--- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php
+++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
@@ -1,358 +1,388 @@
<?php
/**
* Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",
* "scrypt", etc.
*
* Hashers define suitability and strength, and the system automatically
* chooses the strongest available hasher and can prompt users to upgrade as
* soon as a stronger hasher is available.
*
* @task hasher Implementing a Hasher
* @task hashing Using Hashers
*/
abstract class PhabricatorPasswordHasher extends Phobject {
const MAXIMUM_STORAGE_SIZE = 128;
/* -( Implementing a Hasher )---------------------------------------------- */
/**
* Return a human-readable description of this hasher, like "Iterated MD5".
*
* @return string Human readable hash name.
* @task hasher
*/
abstract public function getHumanReadableName();
/**
* Return a short, unique, key identifying this hasher, like "md5" or
* "bcrypt". This identifier should not be translated.
*
* @return string Short, unique hash name.
* @task hasher
*/
abstract public function getHashName();
/**
* Return the maximum byte length of hashes produced by this hasher. This is
* used to prevent storage overflows.
*
* @return int Maximum number of bytes in hashes this class produces.
* @task hasher
*/
abstract public function getHashLength();
/**
* Return `true` to indicate that any required extensions or dependencies
* are available, and this hasher is able to perform hashing.
*
* @return bool True if this hasher can execute.
* @task hasher
*/
abstract public function canHashPasswords();
/**
* Return a human-readable string describing why this hasher is unable
* to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".
*
* @return string Human-readable description of how to enable this hasher.
* @task hasher
*/
abstract public function getInstallInstructions();
/**
* Return an indicator of this hasher's strength. When choosing to hash
* new passwords, the strongest available hasher which is usuable for new
* passwords will be used, and the presence of a stronger hasher will
* prompt users to update their hashes.
*
* Generally, this method should return a larger number than hashers it is
* preferable to, but a smaller number than hashers which are better than it
* is. This number does not need to correspond directly with the actual hash
* strength.
*
* @return float Strength of this hasher.
* @task hasher
*/
abstract public function getStrength();
/**
* Return a short human-readable indicator of this hasher's strength, like
* "Weak", "Okay", or "Good".
*
* This is only used to help administrators make decisions about
* configuration.
*
* @return string Short human-readable description of hash strength.
* @task hasher
*/
abstract public function getHumanReadableStrength();
/**
* Produce a password hash.
*
* @param PhutilOpaqueEnvelope Text to be hashed.
* @return PhutilOpaqueEnvelope Hashed text.
* @task hasher
*/
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);
}
+ /**
+ * Check if an existing hash created by this algorithm is upgradeable.
+ *
+ * The default implementation returns `false`. However, hash algorithms which
+ * have (for example) an internal cost function may be able to upgrade an
+ * existing hash to a stronger one with a higher cost.
+ *
+ * @param PhutilOpaqueEnvelope Bare hash.
+ * @return bool True if the hash can be upgraded without
+ * changing the algorithm (for example, to a
+ * higher cost).
+ * @task hasher
+ */
+ protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
+ return false;
+ }
+
+
/* -( Using Hashers )------------------------------------------------------ */
/**
* Get the hash of a password for storage.
*
* @param PhutilOpaqueEnvelope Password text.
* @return PhutilOpaqueEnvelope Hashed text.
* @task hashing
*/
final public function getPasswordHashForStorage(
PhutilOpaqueEnvelope $envelope) {
$name = $this->getHashName();
$hash = $this->getPasswordHash($envelope);
$actual_len = strlen($hash->openEnvelope());
$expect_len = $this->getHashLength();
if ($actual_len > $expect_len) {
throw new Exception(
pht(
"Password hash '%s' produced a hash of length %d, but a ".
"maximum length of %d was expected.",
$name,
new PhutilNumber($actual_len),
new PhutilNumber($expect_len)));
}
return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());
}
/**
* Parse a storage hash into its components, like the hash type and hash
* data.
*
* @return map Dictionary of information about the hash.
* @task hashing
*/
private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {
$raw_hash = $hash->openEnvelope();
if (strpos($raw_hash, ':') === false) {
throw new Exception(
pht(
'Malformed password hash, expected "name:hash".'));
}
list($name, $hash) = explode(':', $raw_hash);
return array(
'name' => $name,
'hash' => new PhutilOpaqueEnvelope($hash),
);
}
/**
* Get all available password hashers. This may include hashers which can not
* actually be used (for example, a required extension is missing).
*
* @return list<PhabicatorPasswordHasher> Hasher objects.
* @task hashing
*/
public static function getAllHashers() {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPasswordHasher')
->loadObjects();
$map = array();
foreach ($objects as $object) {
$name = $object->getHashName();
$potential_length = strlen($name) + $object->getHashLength() + 1;
$maximum_length = self::MAXIMUM_STORAGE_SIZE;
if ($potential_length > $maximum_length) {
throw new Exception(
pht(
'Hasher "%s" may produce hashes which are too long to fit in '.
'storage. %d characters are available, but its hashes may be '.
'up to %d characters in length.',
$name,
$maximum_length,
$potential_length));
}
if (isset($map[$name])) {
throw new Exception(
pht(
'Two hashers use the same hash name ("%s"), "%s" and "%s". Each '.
'hasher must have a unique name.',
$name,
get_class($object),
get_class($map[$name])));
}
$map[$name] = $object;
}
return $map;
}
/**
* Get all usable password hashers. This may include hashers which are
* not desirable or advisable.
*
* @return list<PhabicatorPasswordHasher> Hasher objects.
* @task hashing
*/
public static function getAllUsableHashers() {
$hashers = self::getAllHashers();
foreach ($hashers as $key => $hasher) {
if (!$hasher->canHashPasswords()) {
unset($hashers[$key]);
}
}
return $hashers;
}
/**
* Get the best (strongest) available hasher.
*
* @return PhabicatorPasswordHasher Best hasher.
* @task hashing
*/
public static function getBestHasher() {
$hashers = self::getAllUsableHashers();
$hashers = msort($hashers, 'getStrength');
$hasher = last($hashers);
if (!$hasher) {
throw new PhabricatorPasswordHasherUnavailableException(
pht(
'There are no password hashers available which are usable for '.
'new passwords.'));
}
return $hasher;
}
/**
* Get the hashser for a given stored hash.
*
* @return PhabicatorPasswordHasher Corresponding hasher.
* @task hashing
*/
public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {
$info = self::parseHashFromStorage($hash);
$name = $info['name'];
$usable = self::getAllUsableHashers();
if (isset($usable[$name])) {
return $usable[$name];
}
$all = self::getAllHashers();
if (isset($all[$name])) {
throw new PhabricatorPasswordHasherUnavailableException(
pht(
'Attempting to compare a password saved with the "%s" hash. The '.
'hasher exists, but is not currently usable. %s',
$name,
$all[$name]->getInstallInstructions()));
}
throw new PhabricatorPasswordHasherUnavailableException(
pht(
'Attempting to compare a password saved with the "%s" hash. No such '.
'hasher is known to Phabricator.',
$name));
}
/**
* Test if a password is using an weaker hash than the strongest available
* hash. This can be used to prompt users to upgrade, or automatically upgrade
* on login.
*
* @return bool True to indicate that rehashing this password will improve
* the hash strength.
* @task hashing
*/
public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
$current_hasher = self::getHasherForHash($hash);
$best_hasher = self::getBestHasher();
- return ($current_hasher->getHashName() != $best_hasher->getHashName());
+ if ($current_hasher->getHashName() != $best_hasher->getHashName()) {
+ // If the algorithm isn't the best one, we can upgrade.
+ return true;
+ }
+
+ $info = self::parseHashFromStorage($hash);
+ if ($current_hasher->canUpgradeInternalHash($info['hash'])) {
+ // If the algorithm provides an internal upgrade, we can also upgrade.
+ return true;
+ }
+
+ // Already on the best algorithm with the best settings.
+ return false;
}
/**
* Generate a new hash for a password, using the best available hasher.
*
* @param PhutilOpaqueEnvelope Password to hash.
* @return PhutilOpaqueEnvelope Hashed password, using best available
* hasher.
* @task hashing
*/
public static function generateNewPasswordHash(
PhutilOpaqueEnvelope $password) {
$hasher = self::getBestHasher();
return $hasher->getPasswordHashForStorage($password);
}
/**
* Compare a password to a stored hash.
*
* @param PhutilOpaqueEnvelope Password to compare.
* @param PhutilOpaqueEnvelope Stored password hash.
* @return bool True if the passwords match.
* @task hashing
*/
public static function comparePassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $hash) {
$hasher = self::getHasherForHash($hash);
$parts = self::parseHashFromStorage($hash);
return $hasher->verifyPassword($password, $parts['hash']);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 16:18 (2 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126405
Default Alt Text
(14 KB)

Event Timeline