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

Support HMAC+SHA256 with automatic key generation and management

Summary:
Ref T12509. This adds support for HMAC+SHA256 (instead of HMAC+SHA1). Although HMAC+SHA1 is not currently broken in any sense, SHA1 has a well-known collision and it's good to look at moving away from HMAC+SHA1.

The new mechanism also automatically generates and stores HMAC keys.

Currently, HMAC keys largely use a per-install constant defined in `security.hmac-key`. In theory this can be changed, but in practice essentially no install changes it.

We generally (in fact, always, I think?) don't use HMAC digests in a way where it matters that this key is well-known, but it's slightly better if this key is unique per class of use cases. Principally, if use cases have unique HMAC keys they are generally less vulnerable to precomputation attacks where an attacker might generate a large number of HMAC hashes of well-known values and use them in a nefarious way. The actual threat here is probably close to nonexistent, but we can harden against it without much extra effort.

Beyond that, this isn't something users should really have to think about or bother configuring.

Test Plan:
- Added unit tests.
- Used `bin/files integrity` to verify, strip, and recompute hashes.
- Tampered with a generated HMAC key, verified it invalidated hashes.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12509

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

+171 -4
+8
resources/sql/autopatches/20170406.hmac.01.keystore.sql
··· 1 + CREATE TABLE {$NAMESPACE}_auth.auth_hmackey ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + keyName VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, 4 + keyValue VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, 5 + dateCreated INT UNSIGNED NOT NULL, 6 + dateModified INT UNSIGNED NOT NULL, 7 + UNIQUE KEY `key_name` (keyName) 8 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+4
src/__phutil_library_map__.php
··· 1920 1920 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 1921 1921 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 1922 1922 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 1923 + 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', 1923 1924 'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php', 1924 1925 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 1925 1926 'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php', ··· 2864 2865 'PhabricatorGuideModule' => 'applications/guides/module/PhabricatorGuideModule.php', 2865 2866 'PhabricatorGuideModuleController' => 'applications/guides/controller/PhabricatorGuideModuleController.php', 2866 2867 'PhabricatorGuideQuickStartModule' => 'applications/guides/module/PhabricatorGuideQuickStartModule.php', 2868 + 'PhabricatorHMACTestCase' => 'infrastructure/util/__tests__/PhabricatorHMACTestCase.php', 2867 2869 'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php', 2868 2870 'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php', 2869 2871 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', ··· 6900 6902 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 6901 6903 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 6902 6904 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 6905 + 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', 6903 6906 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', 6904 6907 'PhabricatorAuthHighSecurityToken' => 'Phobject', 6905 6908 'PhabricatorAuthInvite' => array( ··· 7998 8001 'PhabricatorGuideModule' => 'Phobject', 7999 8002 'PhabricatorGuideModuleController' => 'PhabricatorGuideController', 8000 8003 'PhabricatorGuideQuickStartModule' => 'PhabricatorGuideModule', 8004 + 'PhabricatorHMACTestCase' => 'PhabricatorTestCase', 8001 8005 'PhabricatorHTTPParameterTypeTableView' => 'AphrontView', 8002 8006 'PhabricatorHandleList' => array( 8003 8007 'Phobject',
+24
src/applications/auth/storage/PhabricatorAuthHMACKey.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthHMACKey 4 + extends PhabricatorAuthDAO { 5 + 6 + protected $keyName; 7 + protected $keyValue; 8 + 9 + protected function getConfiguration() { 10 + return array( 11 + self::CONFIG_COLUMN_SCHEMA => array( 12 + 'keyName' => 'text64', 13 + 'keyValue' => 'text128', 14 + ), 15 + self::CONFIG_KEY_SCHEMA => array( 16 + 'key_name' => array( 17 + 'columns' => array('keyName'), 18 + 'unique' => true, 19 + ), 20 + ), 21 + ) + parent::getConfiguration(); 22 + } 23 + 24 + }
+2 -1
src/applications/config/option/PhabricatorSecurityConfigOptions.php
··· 109 109 'Default key for HMAC digests where the key is not important '. 110 110 '(i.e., the hash itself is secret). You can change this if you '. 111 111 'want (to any other string), but doing so will break existing '. 112 - 'sessions and CSRF tokens.')), 112 + 'sessions and CSRF tokens. This option is deprecated. Newer '. 113 + 'code automatically manages HMAC keys.')), 113 114 $this->newOption('security.require-https', 'bool', false) 114 115 ->setLocked(true) 115 116 ->setSummary(
+6 -2
src/applications/files/engine/PhabricatorFileStorageEngine.php
··· 16 16 */ 17 17 abstract class PhabricatorFileStorageEngine extends Phobject { 18 18 19 + const HMAC_INTEGRITY = 'file.integrity'; 20 + 19 21 /** 20 22 * Construct a new storage engine. 21 23 * ··· 367 369 $data, 368 370 PhabricatorFileStorageFormat $format) { 369 371 370 - $data_hash = PhabricatorHash::digest($data); 372 + $hmac_name = self::HMAC_INTEGRITY; 373 + 374 + $data_hash = PhabricatorHash::digestWithNamedKey($data, $hmac_name); 371 375 $format_hash = $format->newFormatIntegrityHash(); 372 376 373 377 $full_hash = "{$data_hash}/{$format_hash}"; 374 378 375 - return PhabricatorHash::digest($full_hash); 379 + return PhabricatorHash::digestWithNamedKey($full_hash, $hmac_name); 376 380 } 377 381 378 382 }
+3 -1
src/applications/files/format/PhabricatorFileAES256StorageFormat.php
··· 67 67 68 68 $input = self::FORMATKEY.'/iv:'.$iv_envelope->openEnvelope(); 69 69 70 - return PhabricatorHash::digest($input); 70 + return PhabricatorHash::digestWithNamedKey( 71 + $input, 72 + PhabricatorFileStorageEngine::HMAC_INTEGRITY); 71 73 } 72 74 73 75 public function newStorageProperties() {
+84
src/infrastructure/util/PhabricatorHash.php
··· 139 139 return $prefix.'-'.$hash; 140 140 } 141 141 142 + public static function digestWithNamedKey($message, $key_name) { 143 + $key_bytes = self::getNamedHMACKey($key_name); 144 + return self::digestHMACSHA256($message, $key_bytes); 145 + } 146 + 147 + public static function digestHMACSHA256($message, $key) { 148 + if (!strlen($key)) { 149 + throw new Exception( 150 + pht('HMAC-SHA256 requires a nonempty key.')); 151 + } 152 + 153 + $result = hash_hmac('sha256', $message, $key, $raw_output = false); 154 + 155 + if ($result === false) { 156 + throw new Exception( 157 + pht('Unable to compute HMAC-SHA256 digest of message.')); 158 + } 159 + 160 + return $result; 161 + } 162 + 163 + 164 + /* -( HMAC Key Management )------------------------------------------------ */ 165 + 166 + 167 + private static function getNamedHMACKey($hmac_name) { 168 + $cache = PhabricatorCaches::getImmutableCache(); 169 + 170 + $cache_key = "hmac.key({$hmac_name})"; 171 + 172 + $hmac_key = $cache->getKey($cache_key); 173 + if (!strlen($hmac_key)) { 174 + $hmac_key = self::readHMACKey($hmac_name); 175 + 176 + if ($hmac_key === null) { 177 + $hmac_key = self::newHMACKey($hmac_name); 178 + self::writeHMACKey($hmac_name, $hmac_key); 179 + } 180 + 181 + $cache->setKey($cache_key, $hmac_key); 182 + } 183 + 184 + // The "hex2bin()" function doesn't exist until PHP 5.4.0 so just 185 + // implement it inline. 186 + $result = ''; 187 + for ($ii = 0; $ii < strlen($hmac_key); $ii += 2) { 188 + $result .= pack('H*', substr($hmac_key, $ii, 2)); 189 + } 190 + 191 + return $result; 192 + } 193 + 194 + private static function newHMACKey($hmac_name) { 195 + $hmac_key = Filesystem::readRandomBytes(64); 196 + return bin2hex($hmac_key); 197 + } 198 + 199 + private static function writeHMACKey($hmac_name, $hmac_key) { 200 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 201 + 202 + id(new PhabricatorAuthHMACKey()) 203 + ->setKeyName($hmac_name) 204 + ->setKeyValue($hmac_key) 205 + ->save(); 206 + 207 + unset($unguarded); 208 + } 209 + 210 + private static function readHMACKey($hmac_name) { 211 + $table = new PhabricatorAuthHMACKey(); 212 + $conn = $table->establishConnection('r'); 213 + 214 + $row = queryfx_one( 215 + $conn, 216 + 'SELECT keyValue FROM %T WHERE keyName = %s', 217 + $table->getTableName(), 218 + $hmac_name); 219 + if (!$row) { 220 + return null; 221 + } 222 + 223 + return $row['keyValue']; 224 + } 225 + 142 226 143 227 }
+40
src/infrastructure/util/__tests__/PhabricatorHMACTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorHMACTestCase extends PhabricatorTestCase { 4 + 5 + protected function getPhabricatorTestCaseConfiguration() { 6 + return array( 7 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, 8 + ); 9 + } 10 + 11 + public function testHMACKeyGeneration() { 12 + $input = 'quack'; 13 + 14 + $hash_1 = PhabricatorHash::digestWithNamedKey($input, 'test'); 15 + $hash_2 = PhabricatorHash::digestWithNamedKey($input, 'test'); 16 + 17 + $this->assertEqual($hash_1, $hash_2); 18 + } 19 + 20 + public function testSHA256Hashing() { 21 + $input = 'quack'; 22 + $key = 'duck'; 23 + $expect = 24 + '5274473dc34fc61bd7a6a5ff258e6505'. 25 + '4b26644fb7a272d74f276ab677361b9a'; 26 + 27 + $hash = PhabricatorHash::digestHMACSHA256($input, $key); 28 + $this->assertEqual($expect, $hash); 29 + 30 + $input = 'The quick brown fox jumps over the lazy dog'; 31 + $key = 'key'; 32 + $expect = 33 + 'f7bc83f430538424b13298e6aa6fb143'. 34 + 'ef4d59a14946175997479dbc2d1a3cd8'; 35 + 36 + $hash = PhabricatorHash::digestHMACSHA256($input, $key); 37 + $this->assertEqual($expect, $hash); 38 + } 39 + 40 + }