[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |