Path: blob/master/src/infrastructure/util/password/PhabricatorPasswordHasher.php
12241 views
<?php12/**3* Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",4* "scrypt", etc.5*6* Hashers define suitability and strength, and the system automatically7* chooses the strongest available hasher and can prompt users to upgrade as8* soon as a stronger hasher is available.9*10* @task hasher Implementing a Hasher11* @task hashing Using Hashers12*/13abstract class PhabricatorPasswordHasher extends Phobject {1415const MAXIMUM_STORAGE_SIZE = 128;161718/* -( Implementing a Hasher )---------------------------------------------- */192021/**22* Return a human-readable description of this hasher, like "Iterated MD5".23*24* @return string Human readable hash name.25* @task hasher26*/27abstract public function getHumanReadableName();282930/**31* Return a short, unique, key identifying this hasher, like "md5" or32* "bcrypt". This identifier should not be translated.33*34* @return string Short, unique hash name.35* @task hasher36*/37abstract public function getHashName();383940/**41* Return the maximum byte length of hashes produced by this hasher. This is42* used to prevent storage overflows.43*44* @return int Maximum number of bytes in hashes this class produces.45* @task hasher46*/47abstract public function getHashLength();484950/**51* Return `true` to indicate that any required extensions or dependencies52* are available, and this hasher is able to perform hashing.53*54* @return bool True if this hasher can execute.55* @task hasher56*/57abstract public function canHashPasswords();585960/**61* Return a human-readable string describing why this hasher is unable62* to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".63*64* @return string Human-readable description of how to enable this hasher.65* @task hasher66*/67abstract public function getInstallInstructions();686970/**71* Return an indicator of this hasher's strength. When choosing to hash72* new passwords, the strongest available hasher which is usable for new73* passwords will be used, and the presence of a stronger hasher will74* prompt users to update their hashes.75*76* Generally, this method should return a larger number than hashers it is77* preferable to, but a smaller number than hashers which are better than it78* is. This number does not need to correspond directly with the actual hash79* strength.80*81* @return float Strength of this hasher.82* @task hasher83*/84abstract public function getStrength();858687/**88* Return a short human-readable indicator of this hasher's strength, like89* "Weak", "Okay", or "Good".90*91* This is only used to help administrators make decisions about92* configuration.93*94* @return string Short human-readable description of hash strength.95* @task hasher96*/97abstract public function getHumanReadableStrength();9899100/**101* Produce a password hash.102*103* @param PhutilOpaqueEnvelope Text to be hashed.104* @return PhutilOpaqueEnvelope Hashed text.105* @task hasher106*/107abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);108109110/**111* Verify that a password matches a hash.112*113* The default implementation checks for equality; if a hasher embeds salt in114* hashes it should override this method and perform a salt-aware comparison.115*116* @param PhutilOpaqueEnvelope Password to compare.117* @param PhutilOpaqueEnvelope Bare password hash.118* @return bool True if the passwords match.119* @task hasher120*/121protected function verifyPassword(122PhutilOpaqueEnvelope $password,123PhutilOpaqueEnvelope $hash) {124125$actual_hash = $this->getPasswordHash($password)->openEnvelope();126$expect_hash = $hash->openEnvelope();127128return phutil_hashes_are_identical($actual_hash, $expect_hash);129}130131132/**133* Check if an existing hash created by this algorithm is upgradeable.134*135* The default implementation returns `false`. However, hash algorithms which136* have (for example) an internal cost function may be able to upgrade an137* existing hash to a stronger one with a higher cost.138*139* @param PhutilOpaqueEnvelope Bare hash.140* @return bool True if the hash can be upgraded without141* changing the algorithm (for example, to a142* higher cost).143* @task hasher144*/145protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {146return false;147}148149150/* -( Using Hashers )------------------------------------------------------ */151152153/**154* Get the hash of a password for storage.155*156* @param PhutilOpaqueEnvelope Password text.157* @return PhutilOpaqueEnvelope Hashed text.158* @task hashing159*/160final public function getPasswordHashForStorage(161PhutilOpaqueEnvelope $envelope) {162163$name = $this->getHashName();164$hash = $this->getPasswordHash($envelope);165166$actual_len = strlen($hash->openEnvelope());167$expect_len = $this->getHashLength();168if ($actual_len > $expect_len) {169throw new Exception(170pht(171"Password hash '%s' produced a hash of length %d, but a ".172"maximum length of %d was expected.",173$name,174new PhutilNumber($actual_len),175new PhutilNumber($expect_len)));176}177178return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());179}180181182/**183* Parse a storage hash into its components, like the hash type and hash184* data.185*186* @return map Dictionary of information about the hash.187* @task hashing188*/189private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {190$raw_hash = $hash->openEnvelope();191if (strpos($raw_hash, ':') === false) {192throw new Exception(193pht(194'Malformed password hash, expected "name:hash".'));195}196197list($name, $hash) = explode(':', $raw_hash);198199return array(200'name' => $name,201'hash' => new PhutilOpaqueEnvelope($hash),202);203}204205206/**207* Get all available password hashers. This may include hashers which can not208* actually be used (for example, a required extension is missing).209*210* @return list<PhabricatorPasswordHasher> Hasher objects.211* @task hashing212*/213public static function getAllHashers() {214$objects = id(new PhutilClassMapQuery())215->setAncestorClass(__CLASS__)216->setUniqueMethod('getHashName')217->execute();218219foreach ($objects as $object) {220$name = $object->getHashName();221222$potential_length = strlen($name) + $object->getHashLength() + 1;223$maximum_length = self::MAXIMUM_STORAGE_SIZE;224225if ($potential_length > $maximum_length) {226throw new Exception(227pht(228'Hasher "%s" may produce hashes which are too long to fit in '.229'storage. %d characters are available, but its hashes may be '.230'up to %d characters in length.',231$name,232$maximum_length,233$potential_length));234}235}236237return $objects;238}239240241/**242* Get all usable password hashers. This may include hashers which are243* not desirable or advisable.244*245* @return list<PhabricatorPasswordHasher> Hasher objects.246* @task hashing247*/248public static function getAllUsableHashers() {249$hashers = self::getAllHashers();250foreach ($hashers as $key => $hasher) {251if (!$hasher->canHashPasswords()) {252unset($hashers[$key]);253}254}255return $hashers;256}257258259/**260* Get the best (strongest) available hasher.261*262* @return PhabricatorPasswordHasher Best hasher.263* @task hashing264*/265public static function getBestHasher() {266$hashers = self::getAllUsableHashers();267$hashers = msort($hashers, 'getStrength');268269$hasher = last($hashers);270if (!$hasher) {271throw new PhabricatorPasswordHasherUnavailableException(272pht(273'There are no password hashers available which are usable for '.274'new passwords.'));275}276277return $hasher;278}279280281/**282* Get the hasher for a given stored hash.283*284* @return PhabricatorPasswordHasher Corresponding hasher.285* @task hashing286*/287public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {288$info = self::parseHashFromStorage($hash);289$name = $info['name'];290291$usable = self::getAllUsableHashers();292if (isset($usable[$name])) {293return $usable[$name];294}295296$all = self::getAllHashers();297if (isset($all[$name])) {298throw new PhabricatorPasswordHasherUnavailableException(299pht(300'Attempting to compare a password saved with the "%s" hash. The '.301'hasher exists, but is not currently usable. %s',302$name,303$all[$name]->getInstallInstructions()));304}305306throw new PhabricatorPasswordHasherUnavailableException(307pht(308'Attempting to compare a password saved with the "%s" hash. No such '.309'hasher is known.',310$name));311}312313314/**315* Test if a password is using an weaker hash than the strongest available316* hash. This can be used to prompt users to upgrade, or automatically upgrade317* on login.318*319* @return bool True to indicate that rehashing this password will improve320* the hash strength.321* @task hashing322*/323public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {324if (!strlen($hash->openEnvelope())) {325throw new Exception(326pht('Expected a password hash, received nothing!'));327}328329$current_hasher = self::getHasherForHash($hash);330$best_hasher = self::getBestHasher();331332if ($current_hasher->getHashName() != $best_hasher->getHashName()) {333// If the algorithm isn't the best one, we can upgrade.334return true;335}336337$info = self::parseHashFromStorage($hash);338if ($current_hasher->canUpgradeInternalHash($info['hash'])) {339// If the algorithm provides an internal upgrade, we can also upgrade.340return true;341}342343// Already on the best algorithm with the best settings.344return false;345}346347348/**349* Generate a new hash for a password, using the best available hasher.350*351* @param PhutilOpaqueEnvelope Password to hash.352* @return PhutilOpaqueEnvelope Hashed password, using best available353* hasher.354* @task hashing355*/356public static function generateNewPasswordHash(357PhutilOpaqueEnvelope $password) {358$hasher = self::getBestHasher();359return $hasher->getPasswordHashForStorage($password);360}361362363/**364* Compare a password to a stored hash.365*366* @param PhutilOpaqueEnvelope Password to compare.367* @param PhutilOpaqueEnvelope Stored password hash.368* @return bool True if the passwords match.369* @task hashing370*/371public static function comparePassword(372PhutilOpaqueEnvelope $password,373PhutilOpaqueEnvelope $hash) {374375$hasher = self::getHasherForHash($hash);376$parts = self::parseHashFromStorage($hash);377378return $hasher->verifyPassword($password, $parts['hash']);379}380381382/**383* Get the human-readable algorithm name for a given hash.384*385* @param PhutilOpaqueEnvelope Storage hash.386* @return string Human-readable algorithm name.387*/388public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) {389$raw_hash = $hash->openEnvelope();390if (!strlen($raw_hash)) {391return pht('None');392}393394try {395$current_hasher = self::getHasherForHash($hash);396return $current_hasher->getHumanReadableName();397} catch (Exception $ex) {398$info = self::parseHashFromStorage($hash);399$name = $info['name'];400return pht('Unknown ("%s")', $name);401}402}403404405/**406* Get the human-readable algorithm name for the best available hash.407*408* @return string Human-readable name for best hash.409*/410public static function getBestAlgorithmName() {411try {412$best_hasher = self::getBestHasher();413return $best_hasher->getHumanReadableName();414} catch (Exception $ex) {415return pht('Unknown');416}417}418419}420421422