@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.

Prepare the new AuthPassword infrastructure for storing account passwords

Summary:
Ref T13043. In D18898 I moved VCS passwords to the new shared infrastructure.

Before account passwords can move, we need to make two changes:

- For legacy reasons, VCS passwords and Account passwords have different "digest" algorithms. Both are more complicated than they should be, but we can't easily fix it without breaking existing passwords. Add a `PasswordHashInterface` so that objects which can have passwords hashes can implement custom digest logic for each password type.
- Account passwords have a dedicated external salt (`PhabricatorUser->passwordSalt`). This is a generally reasonable thing to support (since not all hashers are self-salting) and we need to keep it around so existing passwords still work. Add salt support to `AuthPassword` and make it generate/regenerate when passwords are updated.

Then add a nice story about password digestion.

Test Plan: Ran migrations. Used an existing VCS password; changed VCS password. Tried to use a revoked password. Unit tests still pass. Grepped for callers to legacy `PhabricatorHash::digestPassword()`, found none.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13043

Differential Revision: https://secure.phabricator.com/D18900

+105 -28
+2
resources/sql/autopatches/20180121.auth.02.passsalt.sql
··· 1 + ALTER TABLE {$NAMESPACE}_auth.auth_password 2 + ADD passwordSalt VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT};
+2
src/__phutil_library_map__.php
··· 3494 3494 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 3495 3495 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php', 3496 3496 'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php', 3497 + 'PhabricatorPasswordHashInterface' => 'applications/auth/password/PhabricatorPasswordHashInterface.php', 3497 3498 'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php', 3498 3499 'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php', 3499 3500 'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php', ··· 9992 9993 'PhabricatorFulltextInterface', 9993 9994 'PhabricatorFerretInterface', 9994 9995 'PhabricatorConduitResultInterface', 9996 + 'PhabricatorPasswordHashInterface', 9995 9997 ), 9996 9998 'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType', 9997 9999 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
+1 -1
src/applications/auth/engine/PhabricatorAuthPasswordEngine.php
··· 27 27 return $this->contentSource; 28 28 } 29 29 30 - public function setObject($object) { 30 + public function setObject(PhabricatorPasswordHashInterface $object) { 31 31 $this->object = $object; 32 32 return $this; 33 33 }
+9
src/applications/auth/password/PhabricatorPasswordHashInterface.php
··· 1 + <?php 2 + 3 + interface PhabricatorPasswordHashInterface { 4 + 5 + public function newPasswordDigest( 6 + PhutilOpaqueEnvelope $envelope, 7 + PhabricatorAuthPassword $password); 8 + 9 + }
+27 -8
src/applications/auth/storage/PhabricatorAuthPassword.php
··· 10 10 protected $objectPHID; 11 11 protected $passwordType; 12 12 protected $passwordHash; 13 + protected $passwordSalt; 13 14 protected $isRevoked; 14 15 15 16 private $object = self::ATTACHABLE; ··· 19 20 const PASSWORD_TYPE_TEST = 'test'; 20 21 21 22 public static function initializeNewPassword( 22 - PhabricatorUser $object, 23 + PhabricatorPasswordHashInterface $object, 23 24 $type) { 24 25 25 26 return id(new self()) ··· 35 36 self::CONFIG_COLUMN_SCHEMA => array( 36 37 'passwordType' => 'text64', 37 38 'passwordHash' => 'text128', 39 + 'passwordSalt' => 'text64', 38 40 'isRevoked' => 'bool', 39 41 ), 40 42 self::CONFIG_KEY_SCHEMA => array( ··· 70 72 71 73 public function upgradePasswordHasher( 72 74 PhutilOpaqueEnvelope $envelope, 73 - PhabricatorUser $object) { 75 + PhabricatorPasswordHashInterface $object) { 74 76 75 77 // Before we make changes, double check that this is really the correct 76 78 // password. It could be really bad if we "upgraded" a password and changed ··· 88 90 89 91 public function setPassword( 90 92 PhutilOpaqueEnvelope $password, 91 - PhabricatorUser $object) { 93 + PhabricatorPasswordHashInterface $object) { 92 94 93 95 $hasher = PhabricatorPasswordHasher::getBestHasher(); 94 96 return $this->setPasswordWithHasher($password, $object, $hasher); ··· 96 98 97 99 public function setPasswordWithHasher( 98 100 PhutilOpaqueEnvelope $password, 99 - PhabricatorUser $object, 101 + PhabricatorPasswordHashInterface $object, 100 102 PhabricatorPasswordHasher $hasher) { 101 103 104 + if (!strlen($password->openEnvelope())) { 105 + throw new Exception( 106 + pht('Attempting to set an empty password!')); 107 + } 108 + 109 + // Generate (or regenerate) the salt first. 110 + $new_salt = Filesystem::readRandomCharacters(64); 111 + $this->setPasswordSalt($new_salt); 112 + 102 113 $digest = $this->digestPassword($password, $object); 103 114 $hash = $hasher->getPasswordHashForStorage($digest); 104 115 $raw_hash = $hash->openEnvelope(); ··· 108 119 109 120 public function comparePassword( 110 121 PhutilOpaqueEnvelope $password, 111 - PhabricatorUser $object) { 122 + PhabricatorPasswordHashInterface $object) { 112 123 113 124 $digest = $this->digestPassword($password, $object); 114 125 $hash = $this->newPasswordEnvelope(); ··· 122 133 123 134 private function digestPassword( 124 135 PhutilOpaqueEnvelope $password, 125 - PhabricatorUser $object) { 136 + PhabricatorPasswordHashInterface $object) { 126 137 127 138 $object_phid = $object->getPHID(); 128 139 ··· 135 146 $object->getPHID())); 136 147 } 137 148 138 - $raw_input = PhabricatorHash::digestPassword($password, $object_phid); 149 + $digest = $object->newPasswordDigest($password, $this); 150 + 151 + if (!($digest instanceof PhutilOpaqueEnvelope)) { 152 + throw new Exception( 153 + pht( 154 + 'Failed to digest password: object ("%s") did not return an '. 155 + 'opaque envelope with a password digest.', 156 + $object->getPHID())); 157 + } 139 158 140 - return new PhutilOpaqueEnvelope($raw_input); 159 + return $digest; 141 160 } 142 161 143 162
+64 -1
src/applications/people/storage/PhabricatorUser.php
··· 20 20 PhabricatorApplicationTransactionInterface, 21 21 PhabricatorFulltextInterface, 22 22 PhabricatorFerretInterface, 23 - PhabricatorConduitResultInterface { 23 + PhabricatorConduitResultInterface, 24 + PhabricatorPasswordHashInterface { 24 25 25 26 const SESSION_TABLE = 'phabricator_session'; 26 27 const NAMETOKEN_TABLE = 'user_nametoken'; ··· 1619 1620 1620 1621 return $variables[$variable_key]; 1621 1622 } 1623 + 1624 + /* -( PhabricatorPasswordHashInterface )----------------------------------- */ 1625 + 1626 + 1627 + public function newPasswordDigest( 1628 + PhutilOpaqueEnvelope $envelope, 1629 + PhabricatorAuthPassword $password) { 1630 + 1631 + // Before passwords are hashed, they are digested. The goal of digestion 1632 + // is twofold: to reduce the length of very long passwords to something 1633 + // reasonable; and to salt the password in case the best available hasher 1634 + // does not include salt automatically. 1635 + 1636 + // Users may choose arbitrarily long passwords, and attackers may try to 1637 + // attack the system by probing it with very long passwords. When large 1638 + // inputs are passed to hashers -- which are intentionally slow -- it 1639 + // can result in unacceptably long runtimes. The classic attack here is 1640 + // to try to log in with a 64MB password and see if that locks up the 1641 + // machine for the next century. By digesting passwords to a standard 1642 + // length first, the length of the raw input does not impact the runtime 1643 + // of the hashing algorithm. 1644 + 1645 + // Some hashers like bcrypt are self-salting, while other hashers are not. 1646 + // Applying salt while digesting passwords ensures that hashes are salted 1647 + // whether we ultimately select a self-salting hasher or not. 1648 + 1649 + // For legacy compatibility reasons, the VCS and Account password digest 1650 + // algorithms are significantly more complicated than necessary to achieve 1651 + // these goals. This is because they once used a different hashing and 1652 + // salting process. When we upgraded to the modern modular hasher 1653 + // infrastructure, we just bolted it onto the end of the existing pipelines 1654 + // so that upgrading didn't break all users' credentials. 1655 + 1656 + // New implementations can (and, generally, should) safely select the 1657 + // simple HMAC SHA256 digest at the bottom of the function, which does 1658 + // everything that a digest callback should without any needless legacy 1659 + // baggage on top. 1660 + 1661 + switch ($password->getPasswordType()) { 1662 + case PhabricatorAuthPassword::PASSWORD_TYPE_VCS: 1663 + // VCS passwords use an iterated HMAC SHA1 as a digest algorithm. They 1664 + // originally used this as a hasher, but it became a digest alorithm 1665 + // once hashing was upgraded to include bcrypt. 1666 + $digest = $envelope->openEnvelope(); 1667 + $salt = $this->getPHID(); 1668 + for ($ii = 0; $ii < 1000; $ii++) { 1669 + $digest = PhabricatorHash::weakDigest($digest, $salt); 1670 + } 1671 + return new PhutilOpaqueEnvelope($digest); 1672 + } 1673 + 1674 + // For passwords which do not have some crazy legacy reason to use some 1675 + // other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies 1676 + // the digest requirements and is simple. 1677 + 1678 + $digest = PhabricatorHash::digestHMACSHA256( 1679 + $envelope->openEnvelope(), 1680 + $password->getPasswordSalt()); 1681 + 1682 + return new PhutilOpaqueEnvelope($digest); 1683 + } 1684 + 1622 1685 1623 1686 }
-18
src/infrastructure/util/PhabricatorHash.php
··· 30 30 31 31 32 32 /** 33 - * Digest a string into a password hash. This is similar to @{method:digest}, 34 - * but requires a salt and iterates the hash to increase cost. 35 - */ 36 - public static function digestPassword(PhutilOpaqueEnvelope $envelope, $salt) { 37 - $result = $envelope->openEnvelope(); 38 - if (!$result) { 39 - throw new Exception(pht('Trying to digest empty password!')); 40 - } 41 - 42 - for ($ii = 0; $ii < 1000; $ii++) { 43 - $result = self::weakDigest($result, $salt); 44 - } 45 - 46 - return $result; 47 - } 48 - 49 - 50 - /** 51 33 * Digest a string for use in, e.g., a MySQL index. This produces a short 52 34 * (12-byte), case-sensitive alphanumeric string with 72 bits of entropy, 53 35 * which is generally safe in most contexts (notably, URLs).