@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 420 lines 12 kB view raw
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 */ 14abstract 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 usable 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 $envelope 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 Password to compare. 118 * @param PhutilOpaqueEnvelope $hash 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 phutil_hashes_are_identical($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 $hash 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 $envelope 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 %s, but a ". 173 "maximum length of %s 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<PhabricatorPasswordHasher> Hasher objects. 212 * @task hashing 213 */ 214 public static function getAllHashers() { 215 $objects = id(new PhutilClassMapQuery()) 216 ->setAncestorClass(self::class) 217 ->setUniqueMethod('getHashName') 218 ->execute(); 219 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 238 return $objects; 239 } 240 241 242 /** 243 * Get all usable password hashers. This may include hashers which are 244 * not desirable or advisable. 245 * 246 * @return list<PhabricatorPasswordHasher> Hasher objects. 247 * @task hashing 248 */ 249 public static function getAllUsableHashers() { 250 $hashers = self::getAllHashers(); 251 foreach ($hashers as $key => $hasher) { 252 if (!$hasher->canHashPasswords()) { 253 unset($hashers[$key]); 254 } 255 } 256 return $hashers; 257 } 258 259 260 /** 261 * Get the best (strongest) available hasher. 262 * 263 * @return PhabricatorPasswordHasher Best hasher. 264 * @task hashing 265 */ 266 public static function getBestHasher() { 267 $hashers = self::getAllUsableHashers(); 268 $hashers = msort($hashers, 'getStrength'); 269 270 $hasher = last($hashers); 271 if (!$hasher) { 272 throw new PhabricatorPasswordHasherUnavailableException( 273 pht( 274 'There are no password hashers available which are usable for '. 275 'new passwords.')); 276 } 277 278 return $hasher; 279 } 280 281 282 /** 283 * Get the hasher for a given stored hash. 284 * 285 * @return PhabricatorPasswordHasher Corresponding hasher. 286 * @task hashing 287 */ 288 public static function getHasherForHash(PhutilOpaqueEnvelope $hash) { 289 $info = self::parseHashFromStorage($hash); 290 $name = $info['name']; 291 292 $usable = self::getAllUsableHashers(); 293 if (isset($usable[$name])) { 294 return $usable[$name]; 295 } 296 297 $all = self::getAllHashers(); 298 if (isset($all[$name])) { 299 throw new PhabricatorPasswordHasherUnavailableException( 300 pht( 301 'Attempting to compare a password saved with the "%s" hash. The '. 302 'hasher exists, but is not currently usable. %s', 303 $name, 304 $all[$name]->getInstallInstructions())); 305 } 306 307 throw new PhabricatorPasswordHasherUnavailableException( 308 pht( 309 'Attempting to compare a password saved with the "%s" hash. No such '. 310 'hasher is known.', 311 $name)); 312 } 313 314 315 /** 316 * Test if a password is using an weaker hash than the strongest available 317 * hash. This can be used to prompt users to upgrade, or automatically upgrade 318 * on login. 319 * 320 * @return bool True to indicate that rehashing this password will improve 321 * the hash strength. 322 * @task hashing 323 */ 324 public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) { 325 if (!strlen($hash->openEnvelope())) { 326 throw new Exception( 327 pht('Expected a password hash, received nothing!')); 328 } 329 330 $current_hasher = self::getHasherForHash($hash); 331 $best_hasher = self::getBestHasher(); 332 333 if ($current_hasher->getHashName() != $best_hasher->getHashName()) { 334 // If the algorithm isn't the best one, we can upgrade. 335 return true; 336 } 337 338 $info = self::parseHashFromStorage($hash); 339 if ($current_hasher->canUpgradeInternalHash($info['hash'])) { 340 // If the algorithm provides an internal upgrade, we can also upgrade. 341 return true; 342 } 343 344 // Already on the best algorithm with the best settings. 345 return false; 346 } 347 348 349 /** 350 * Generate a new hash for a password, using the best available hasher. 351 * 352 * @param PhutilOpaqueEnvelope $password Password to hash. 353 * @return PhutilOpaqueEnvelope Hashed password, using best available 354 * hasher. 355 * @task hashing 356 */ 357 public static function generateNewPasswordHash( 358 PhutilOpaqueEnvelope $password) { 359 $hasher = self::getBestHasher(); 360 return $hasher->getPasswordHashForStorage($password); 361 } 362 363 364 /** 365 * Compare a password to a stored hash. 366 * 367 * @param PhutilOpaqueEnvelope $password Password to compare. 368 * @param PhutilOpaqueEnvelope $hash Stored password hash. 369 * @return bool True if the passwords match. 370 * @task hashing 371 */ 372 public static function comparePassword( 373 PhutilOpaqueEnvelope $password, 374 PhutilOpaqueEnvelope $hash) { 375 376 $hasher = self::getHasherForHash($hash); 377 $parts = self::parseHashFromStorage($hash); 378 379 return $hasher->verifyPassword($password, $parts['hash']); 380 } 381 382 383 /** 384 * Get the human-readable algorithm name for a given hash. 385 * 386 * @param PhutilOpaqueEnvelope $hash Storage hash. 387 * @return string Human-readable algorithm name. 388 */ 389 public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) { 390 $raw_hash = $hash->openEnvelope(); 391 if (!strlen($raw_hash)) { 392 return pht('None'); 393 } 394 395 try { 396 $current_hasher = self::getHasherForHash($hash); 397 return $current_hasher->getHumanReadableName(); 398 } catch (Exception $ex) { 399 $info = self::parseHashFromStorage($hash); 400 $name = $info['name']; 401 return pht('Unknown ("%s")', $name); 402 } 403 } 404 405 406 /** 407 * Get the human-readable algorithm name for the best available hash. 408 * 409 * @return string Human-readable name for best hash. 410 */ 411 public static function getBestAlgorithmName() { 412 try { 413 $best_hasher = self::getBestHasher(); 414 return $best_hasher->getHumanReadableName(); 415 } catch (Exception $ex) { 416 return pht('Unknown'); 417 } 418 } 419 420}