[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/util/password/ -> PhabricatorPasswordHasher.php (source)

   1  <?php
   2  
   3  /**
   4   * Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",
   5   * "scrypt", etc.
   6   *
   7   * Hashers define suitability and strength, and the system automatically
   8   * chooses the strongest available hasher and can prompt users to upgrade as
   9   * soon as a stronger hasher is available.
  10   *
  11   * @task hasher   Implementing a Hasher
  12   * @task hashing  Using Hashers
  13   */
  14  abstract class PhabricatorPasswordHasher extends Phobject {
  15  
  16    const MAXIMUM_STORAGE_SIZE = 128;
  17  
  18  
  19  /* -(  Implementing a Hasher  )---------------------------------------------- */
  20  
  21  
  22    /**
  23     * Return a human-readable description of this hasher, like "Iterated MD5".
  24     *
  25     * @return string Human readable hash name.
  26     * @task hasher
  27     */
  28    abstract public function getHumanReadableName();
  29  
  30  
  31    /**
  32     * Return a short, unique, key identifying this hasher, like "md5" or
  33     * "bcrypt". This identifier should not be translated.
  34     *
  35     * @return string Short, unique hash name.
  36     * @task hasher
  37     */
  38    abstract public function getHashName();
  39  
  40  
  41    /**
  42     * Return the maximum byte length of hashes produced by this hasher. This is
  43     * used to prevent storage overflows.
  44     *
  45     * @return int  Maximum number of bytes in hashes this class produces.
  46     * @task hasher
  47     */
  48    abstract public function getHashLength();
  49  
  50  
  51    /**
  52     * Return `true` to indicate that any required extensions or dependencies
  53     * are available, and this hasher is able to perform hashing.
  54     *
  55     * @return bool True if this hasher can execute.
  56     * @task hasher
  57     */
  58    abstract public function canHashPasswords();
  59  
  60  
  61    /**
  62     * Return a human-readable string describing why this hasher is unable
  63     * to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".
  64     *
  65     * @return string Human-readable description of how to enable this hasher.
  66     * @task hasher
  67     */
  68    abstract public function getInstallInstructions();
  69  
  70  
  71    /**
  72     * Return an indicator of this hasher's strength. When choosing to hash
  73     * new passwords, the strongest available hasher which is usuable for new
  74     * passwords will be used, and the presence of a stronger hasher will
  75     * prompt users to update their hashes.
  76     *
  77     * Generally, this method should return a larger number than hashers it is
  78     * preferable to, but a smaller number than hashers which are better than it
  79     * is. This number does not need to correspond directly with the actual hash
  80     * strength.
  81     *
  82     * @return float  Strength of this hasher.
  83     * @task hasher
  84     */
  85    abstract public function getStrength();
  86  
  87  
  88    /**
  89     * Return a short human-readable indicator of this hasher's strength, like
  90     * "Weak", "Okay", or "Good".
  91     *
  92     * This is only used to help administrators make decisions about
  93     * configuration.
  94     *
  95     * @return string Short human-readable description of hash strength.
  96     * @task hasher
  97     */
  98    abstract public function getHumanReadableStrength();
  99  
 100  
 101    /**
 102     * Produce a password hash.
 103     *
 104     * @param   PhutilOpaqueEnvelope  Text to be hashed.
 105     * @return  PhutilOpaqueEnvelope  Hashed text.
 106     * @task hasher
 107     */
 108    abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);
 109  
 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  
 133    /**
 134     * Check if an existing hash created by this algorithm is upgradeable.
 135     *
 136     * The default implementation returns `false`. However, hash algorithms which
 137     * have (for example) an internal cost function may be able to upgrade an
 138     * existing hash to a stronger one with a higher cost.
 139     *
 140     * @param PhutilOpaqueEnvelope  Bare hash.
 141     * @return bool                 True if the hash can be upgraded without
 142     *                              changing the algorithm (for example, to a
 143     *                              higher cost).
 144     * @task hasher
 145     */
 146    protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
 147      return false;
 148    }
 149  
 150  
 151  /* -(  Using Hashers  )------------------------------------------------------ */
 152  
 153  
 154    /**
 155     * Get the hash of a password for storage.
 156     *
 157     * @param   PhutilOpaqueEnvelope  Password text.
 158     * @return  PhutilOpaqueEnvelope  Hashed text.
 159     * @task hashing
 160     */
 161    final public function getPasswordHashForStorage(
 162      PhutilOpaqueEnvelope $envelope) {
 163  
 164      $name = $this->getHashName();
 165      $hash = $this->getPasswordHash($envelope);
 166  
 167      $actual_len = strlen($hash->openEnvelope());
 168      $expect_len = $this->getHashLength();
 169      if ($actual_len > $expect_len) {
 170        throw new Exception(
 171          pht(
 172            "Password hash '%s' produced a hash of length %d, but a ".
 173            "maximum length of %d was expected.",
 174            $name,
 175            new PhutilNumber($actual_len),
 176            new PhutilNumber($expect_len)));
 177      }
 178  
 179      return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());
 180    }
 181  
 182  
 183    /**
 184     * Parse a storage hash into its components, like the hash type and hash
 185     * data.
 186     *
 187     * @return map  Dictionary of information about the hash.
 188     * @task hashing
 189     */
 190    private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {
 191      $raw_hash = $hash->openEnvelope();
 192      if (strpos($raw_hash, ':') === false) {
 193        throw new Exception(
 194          pht(
 195            'Malformed password hash, expected "name:hash".'));
 196      }
 197  
 198      list($name, $hash) = explode(':', $raw_hash);
 199  
 200      return array(
 201        'name' => $name,
 202        'hash' => new PhutilOpaqueEnvelope($hash),
 203      );
 204    }
 205  
 206  
 207    /**
 208     * Get all available password hashers. This may include hashers which can not
 209     * actually be used (for example, a required extension is missing).
 210     *
 211     * @return list<PhabicatorPasswordHasher> Hasher objects.
 212     * @task hashing
 213     */
 214    public static function getAllHashers() {
 215      $objects = id(new PhutilSymbolLoader())
 216        ->setAncestorClass('PhabricatorPasswordHasher')
 217        ->loadObjects();
 218  
 219      $map = array();
 220      foreach ($objects as $object) {
 221        $name = $object->getHashName();
 222  
 223        $potential_length = strlen($name) + $object->getHashLength() + 1;
 224        $maximum_length = self::MAXIMUM_STORAGE_SIZE;
 225  
 226        if ($potential_length > $maximum_length) {
 227          throw new Exception(
 228            pht(
 229              'Hasher "%s" may produce hashes which are too long to fit in '.
 230              'storage. %d characters are available, but its hashes may be '.
 231              'up to %d characters in length.',
 232              $name,
 233              $maximum_length,
 234              $potential_length));
 235        }
 236  
 237        if (isset($map[$name])) {
 238          throw new Exception(
 239            pht(
 240              'Two hashers use the same hash name ("%s"), "%s" and "%s". Each '.
 241              'hasher must have a unique name.',
 242              $name,
 243              get_class($object),
 244              get_class($map[$name])));
 245        }
 246        $map[$name] = $object;
 247      }
 248  
 249      return $map;
 250    }
 251  
 252  
 253    /**
 254     * Get all usable password hashers. This may include hashers which are
 255     * not desirable or advisable.
 256     *
 257     * @return list<PhabicatorPasswordHasher> Hasher objects.
 258     * @task hashing
 259     */
 260    public static function getAllUsableHashers() {
 261      $hashers = self::getAllHashers();
 262      foreach ($hashers as $key => $hasher) {
 263        if (!$hasher->canHashPasswords()) {
 264          unset($hashers[$key]);
 265        }
 266      }
 267      return $hashers;
 268    }
 269  
 270  
 271    /**
 272     * Get the best (strongest) available hasher.
 273     *
 274     * @return PhabicatorPasswordHasher Best hasher.
 275     * @task hashing
 276     */
 277    public static function getBestHasher() {
 278      $hashers = self::getAllUsableHashers();
 279      $hashers = msort($hashers, 'getStrength');
 280  
 281      $hasher = last($hashers);
 282      if (!$hasher) {
 283        throw new PhabricatorPasswordHasherUnavailableException(
 284          pht(
 285            'There are no password hashers available which are usable for '.
 286            'new passwords.'));
 287      }
 288  
 289      return $hasher;
 290    }
 291  
 292  
 293    /**
 294     * Get the hashser for a given stored hash.
 295     *
 296     * @return PhabicatorPasswordHasher Corresponding hasher.
 297     * @task hashing
 298     */
 299    public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {
 300      $info = self::parseHashFromStorage($hash);
 301      $name = $info['name'];
 302  
 303      $usable = self::getAllUsableHashers();
 304      if (isset($usable[$name])) {
 305        return $usable[$name];
 306      }
 307  
 308      $all = self::getAllHashers();
 309      if (isset($all[$name])) {
 310        throw new PhabricatorPasswordHasherUnavailableException(
 311          pht(
 312            'Attempting to compare a password saved with the "%s" hash. The '.
 313            'hasher exists, but is not currently usable. %s',
 314            $name,
 315            $all[$name]->getInstallInstructions()));
 316      }
 317  
 318      throw new PhabricatorPasswordHasherUnavailableException(
 319        pht(
 320          'Attempting to compare a password saved with the "%s" hash. No such '.
 321          'hasher is known to Phabricator.',
 322          $name));
 323    }
 324  
 325  
 326    /**
 327     * Test if a password is using an weaker hash than the strongest available
 328     * hash. This can be used to prompt users to upgrade, or automatically upgrade
 329     * on login.
 330     *
 331     * @return bool True to indicate that rehashing this password will improve
 332     *              the hash strength.
 333     * @task hashing
 334     */
 335    public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
 336      if (!strlen($hash->openEnvelope())) {
 337        throw new Exception(
 338          pht('Expected a password hash, received nothing!'));
 339      }
 340  
 341      $current_hasher = self::getHasherForHash($hash);
 342      $best_hasher = self::getBestHasher();
 343  
 344      if ($current_hasher->getHashName() != $best_hasher->getHashName()) {
 345        // If the algorithm isn't the best one, we can upgrade.
 346        return true;
 347      }
 348  
 349      $info = self::parseHashFromStorage($hash);
 350      if ($current_hasher->canUpgradeInternalHash($info['hash'])) {
 351        // If the algorithm provides an internal upgrade, we can also upgrade.
 352        return true;
 353      }
 354  
 355      // Already on the best algorithm with the best settings.
 356      return false;
 357    }
 358  
 359  
 360    /**
 361     * Generate a new hash for a password, using the best available hasher.
 362     *
 363     * @param   PhutilOpaqueEnvelope  Password to hash.
 364     * @return  PhutilOpaqueEnvelope  Hashed password, using best available
 365     *                                hasher.
 366     * @task hashing
 367     */
 368    public static function generateNewPasswordHash(
 369      PhutilOpaqueEnvelope $password) {
 370      $hasher = self::getBestHasher();
 371      return $hasher->getPasswordHashForStorage($password);
 372    }
 373  
 374  
 375    /**
 376     * Compare a password to a stored hash.
 377     *
 378     * @param   PhutilOpaqueEnvelope  Password to compare.
 379     * @param   PhutilOpaqueEnvelope  Stored password hash.
 380     * @return  bool                  True if the passwords match.
 381     * @task hashing
 382     */
 383    public static function comparePassword(
 384      PhutilOpaqueEnvelope $password,
 385      PhutilOpaqueEnvelope $hash) {
 386  
 387      $hasher = self::getHasherForHash($hash);
 388      $parts = self::parseHashFromStorage($hash);
 389  
 390      return $hasher->verifyPassword($password, $parts['hash']);
 391    }
 392  
 393  
 394    /**
 395     * Get the human-readable algorithm name for a given hash.
 396     *
 397     * @param   PhutilOpaqueEnvelope  Storage hash.
 398     * @return  string                Human-readable algorithm name.
 399     */
 400    public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) {
 401      $raw_hash = $hash->openEnvelope();
 402      if (!strlen($raw_hash)) {
 403        return pht('None');
 404      }
 405  
 406      try {
 407        $current_hasher = PhabricatorPasswordHasher::getHasherForHash($hash);
 408        return $current_hasher->getHumanReadableName();
 409      } catch (Exception $ex) {
 410        $info = self::parseHashFromStorage($hash);
 411        $name = $info['name'];
 412        return pht('Unknown ("%s")', $name);
 413      }
 414    }
 415  
 416  
 417    /**
 418     * Get the human-readable algorithm name for the best available hash.
 419     *
 420     * @return  string                Human-readable name for best hash.
 421     */
 422    public static function getBestAlgorithmName() {
 423      try {
 424        $best_hasher = PhabricatorPasswordHasher::getBestHasher();
 425        return $best_hasher->getHumanReadableName();
 426      } catch (Exception $ex) {
 427        return pht('Unknown');
 428      }
 429    }
 430  
 431  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1