@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
at recaptime-dev/main 285 lines 8.7 kB view raw
1<?php 2 3final class PhabricatorHash extends Phobject { 4 5 const INDEX_DIGEST_LENGTH = 12; 6 const ANCHOR_DIGEST_LENGTH = 12; 7 8 /** 9 * Digest a string using HMAC+SHA1. 10 * 11 * Because a SHA1 collision is now known, this method should be considered 12 * weak. Callers should prefer @{method:digestWithNamedKey}. 13 * 14 * @param string $string Input string. 15 * @param string $key (optional) 16 * @return string 32-byte hexadecimal SHA1+HMAC hash. 17 */ 18 public static function weakDigest($string, $key = null) { 19 if ($key === null) { 20 $key = PhabricatorEnv::getEnvConfig('security.hmac-key'); 21 } 22 23 if (!$key) { 24 throw new Exception( 25 pht( 26 "Set a '%s' in your configuration!", 27 'security.hmac-key')); 28 } 29 30 return hash_hmac('sha1', $string, $key); 31 } 32 33 34 /** 35 * Digest a string for use in, e.g., a MySQL index. This produces a short 36 * (12-byte), case-sensitive alphanumeric string with 72 bits of entropy, 37 * which is generally safe in most contexts (notably, URLs). 38 * 39 * This method emphasizes compactness, and should not be used for security 40 * related hashing (for general purpose hashing, see @{method:digest}). 41 * 42 * @param string|null $string Input string. 43 * @return string 12-byte, case-sensitive, mostly-alphanumeric hash of 44 * the string. 45 */ 46 public static function digestForIndex($string) { 47 if ($string === null) { 48 $string = ''; 49 } 50 $hash = sha1($string, $raw_output = true); 51 52 static $map; 53 if ($map === null) { 54 $map = '0123456789'. 55 'abcdefghij'. 56 'klmnopqrst'. 57 'uvwxyzABCD'. 58 'EFGHIJKLMN'. 59 'OPQRSTUVWX'. 60 'YZ._'; 61 } 62 63 $result = ''; 64 for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) { 65 $result .= $map[(ord($hash[$ii]) & 0x3F)]; 66 } 67 68 return $result; 69 } 70 71 /** 72 * Digest a string for use in HTML page anchors. This is similar to 73 * @{method:digestForIndex} but produces purely alphanumeric output. 74 * 75 * This tries to be mostly compatible with the index digest to limit how 76 * much stuff we're breaking by switching to it. For additional discussion, 77 * see T13045. 78 * 79 * @param string $string Input string. 80 * @return string 12-byte, case-sensitive, purely-alphanumeric hash of 81 * the string. 82 */ 83 public static function digestForAnchor($string) { 84 $hash = sha1($string, $raw_output = true); 85 86 static $map; 87 if ($map === null) { 88 $map = '0123456789'. 89 'abcdefghij'. 90 'klmnopqrst'. 91 'uvwxyzABCD'. 92 'EFGHIJKLMN'. 93 'OPQRSTUVWX'. 94 'YZ'; 95 } 96 97 $result = ''; 98 $accum = 0; 99 $map_size = strlen($map); 100 for ($ii = 0; $ii < self::ANCHOR_DIGEST_LENGTH; $ii++) { 101 $byte = ord($hash[$ii]); 102 $low_bits = ($byte & 0x3F); 103 $accum = ($accum + $byte) % $map_size; 104 105 if ($low_bits < $map_size) { 106 // If an index digest would produce any alphanumeric character, just 107 // use that character. This means that these digests are the same as 108 // digests created with "digestForIndex()" in all positions where the 109 // output character is some character other than "." or "_". 110 $result .= $map[$low_bits]; 111 } else { 112 // If an index digest would produce a non-alphumeric character ("." or 113 // "_"), pick an alphanumeric character instead. We accumulate an 114 // index into the alphanumeric character list to try to preserve 115 // entropy here. We could use this strategy for all bytes instead, 116 // but then these digests would differ from digests created with 117 // "digestForIndex()" in all positions, instead of just a small number 118 // of positions. 119 $result .= $map[$accum]; 120 } 121 } 122 123 return $result; 124 } 125 126 127 public static function digestToRange($string, $min, $max) { 128 if ($min > $max) { 129 throw new Exception(pht('Maximum must be larger than minimum.')); 130 } 131 132 if ($min == $max) { 133 return $min; 134 } 135 136 $hash = sha1($string, $raw_output = true); 137 // Make sure this ends up positive, even on 32-bit machines. 138 $value = head(unpack('L', $hash)) & 0x7FFFFFFF; 139 140 return $min + ($value % (1 + $max - $min)); 141 } 142 143 144 /** 145 * Shorten a string to a maximum byte length in a collision-resistant way 146 * while retaining some degree of human-readability. 147 * 148 * This function converts an input string into a prefix plus a hash. For 149 * example, a very long string beginning with "crabapplepie..." might be 150 * digested to something like "crabapp-N1wM1Nz3U84k". 151 * 152 * This allows the maximum length of identifiers to be fixed while 153 * maintaining a high degree of collision resistance and a moderate degree 154 * of human readability. 155 * 156 * @param string $string The string to shorten. 157 * @param int $length Maximum length of the result. 158 * @return string String shortened in a collision-resistant way. 159 */ 160 public static function digestToLength($string, $length) { 161 // We need at least two more characters than the hash length to fit in a 162 // a 1-character prefix and a separator. 163 $min_length = self::INDEX_DIGEST_LENGTH + 2; 164 if ($length < $min_length) { 165 throw new Exception( 166 pht( 167 'Length parameter in %s must be at least %s, '. 168 'but %s was provided.', 169 'digestToLength()', 170 new PhutilNumber($min_length), 171 new PhutilNumber($length))); 172 } 173 174 // We could conceivably return the string unmodified if it's shorter than 175 // the specified length. Instead, always hash it. This makes the output of 176 // the method more recognizable and consistent (no surprising new behavior 177 // once you hit a string longer than `$length`) and prevents an attacker 178 // who can control the inputs from intentionally using the hashed form 179 // of a string to cause a collision. 180 181 $hash = self::digestForIndex($string); 182 183 $prefix = substr($string, 0, ($length - ($min_length - 1))); 184 185 return $prefix.'-'.$hash; 186 } 187 188 public static function digestWithNamedKey($message, $key_name) { 189 $key_bytes = self::getNamedHMACKey($key_name); 190 return self::digestHMACSHA256($message, $key_bytes); 191 } 192 193 public static function digestHMACSHA256($message, $key) { 194 if (!is_string($message)) { 195 throw new Exception( 196 pht('HMAC-SHA256 can only digest strings.')); 197 } 198 199 if (!is_string($key)) { 200 throw new Exception( 201 pht('HMAC-SHA256 keys must be strings.')); 202 } 203 204 if (!strlen($key)) { 205 throw new Exception( 206 pht('HMAC-SHA256 requires a nonempty key.')); 207 } 208 209 $result = hash_hmac('sha256', $message, $key, $raw_output = false); 210 211 // Although "hash_hmac()" is documented as returning `false` when it fails, 212 // it can also return `null` if you pass an object as the "$message". 213 if ($result === false || $result === null) { 214 throw new Exception( 215 pht('Unable to compute HMAC-SHA256 digest of message.')); 216 } 217 218 return $result; 219 } 220 221 222/* -( HMAC Key Management )------------------------------------------------ */ 223 224 225 private static function getNamedHMACKey($hmac_name) { 226 $cache = PhabricatorCaches::getImmutableCache(); 227 228 $cache_key = "hmac.key({$hmac_name})"; 229 230 $hmac_key = $cache->getKey($cache_key); 231 if (($hmac_key === null) || !strlen($hmac_key)) { 232 $hmac_key = self::readHMACKey($hmac_name); 233 234 if ($hmac_key === null) { 235 $hmac_key = self::newHMACKey($hmac_name); 236 self::writeHMACKey($hmac_name, $hmac_key); 237 } 238 239 $cache->setKey($cache_key, $hmac_key); 240 } 241 242 // The "hex2bin()" function doesn't exist until PHP 5.4.0 so just 243 // implement it inline. 244 $result = ''; 245 for ($ii = 0; $ii < strlen($hmac_key); $ii += 2) { 246 $result .= pack('H*', substr($hmac_key, $ii, 2)); 247 } 248 249 return $result; 250 } 251 252 private static function newHMACKey($hmac_name) { 253 $hmac_key = Filesystem::readRandomBytes(64); 254 return bin2hex($hmac_key); 255 } 256 257 private static function writeHMACKey($hmac_name, $hmac_key) { 258 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 259 260 id(new PhabricatorAuthHMACKey()) 261 ->setKeyName($hmac_name) 262 ->setKeyValue($hmac_key) 263 ->save(); 264 265 unset($unguarded); 266 } 267 268 private static function readHMACKey($hmac_name) { 269 $table = new PhabricatorAuthHMACKey(); 270 $conn = $table->establishConnection('r'); 271 272 $row = queryfx_one( 273 $conn, 274 'SELECT keyValue FROM %T WHERE keyName = %s', 275 $table->getTableName(), 276 $hmac_name); 277 if (!$row) { 278 return null; 279 } 280 281 return $row['keyValue']; 282 } 283 284 285}