Path: blob/master/src/infrastructure/util/PhabricatorHash.php
12241 views
<?php12final class PhabricatorHash extends Phobject {34const INDEX_DIGEST_LENGTH = 12;5const ANCHOR_DIGEST_LENGTH = 12;67/**8* Digest a string using HMAC+SHA1.9*10* Because a SHA1 collision is now known, this method should be considered11* weak. Callers should prefer @{method:digestWithNamedKey}.12*13* @param string Input string.14* @return string 32-byte hexadecimal SHA1+HMAC hash.15*/16public static function weakDigest($string, $key = null) {17if ($key === null) {18$key = PhabricatorEnv::getEnvConfig('security.hmac-key');19}2021if (!$key) {22throw new Exception(23pht(24"Set a '%s' in your configuration!",25'security.hmac-key'));26}2728return hash_hmac('sha1', $string, $key);29}303132/**33* Digest a string for use in, e.g., a MySQL index. This produces a short34* (12-byte), case-sensitive alphanumeric string with 72 bits of entropy,35* which is generally safe in most contexts (notably, URLs).36*37* This method emphasizes compactness, and should not be used for security38* related hashing (for general purpose hashing, see @{method:digest}).39*40* @param string Input string.41* @return string 12-byte, case-sensitive, mostly-alphanumeric hash of42* the string.43*/44public static function digestForIndex($string) {45$hash = sha1($string, $raw_output = true);4647static $map;48if ($map === null) {49$map = '0123456789'.50'abcdefghij'.51'klmnopqrst'.52'uvwxyzABCD'.53'EFGHIJKLMN'.54'OPQRSTUVWX'.55'YZ._';56}5758$result = '';59for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) {60$result .= $map[(ord($hash[$ii]) & 0x3F)];61}6263return $result;64}6566/**67* Digest a string for use in HTML page anchors. This is similar to68* @{method:digestForIndex} but produces purely alphanumeric output.69*70* This tries to be mostly compatible with the index digest to limit how71* much stuff we're breaking by switching to it. For additional discussion,72* see T13045.73*74* @param string Input string.75* @return string 12-byte, case-sensitive, purely-alphanumeric hash of76* the string.77*/78public static function digestForAnchor($string) {79$hash = sha1($string, $raw_output = true);8081static $map;82if ($map === null) {83$map = '0123456789'.84'abcdefghij'.85'klmnopqrst'.86'uvwxyzABCD'.87'EFGHIJKLMN'.88'OPQRSTUVWX'.89'YZ';90}9192$result = '';93$accum = 0;94$map_size = strlen($map);95for ($ii = 0; $ii < self::ANCHOR_DIGEST_LENGTH; $ii++) {96$byte = ord($hash[$ii]);97$low_bits = ($byte & 0x3F);98$accum = ($accum + $byte) % $map_size;99100if ($low_bits < $map_size) {101// If an index digest would produce any alphanumeric character, just102// use that character. This means that these digests are the same as103// digests created with "digestForIndex()" in all positions where the104// output character is some character other than "." or "_".105$result .= $map[$low_bits];106} else {107// If an index digest would produce a non-alphumeric character ("." or108// "_"), pick an alphanumeric character instead. We accumulate an109// index into the alphanumeric character list to try to preserve110// entropy here. We could use this strategy for all bytes instead,111// but then these digests would differ from digests created with112// "digestForIndex()" in all positions, instead of just a small number113// of positions.114$result .= $map[$accum];115}116}117118return $result;119}120121122public static function digestToRange($string, $min, $max) {123if ($min > $max) {124throw new Exception(pht('Maximum must be larger than minimum.'));125}126127if ($min == $max) {128return $min;129}130131$hash = sha1($string, $raw_output = true);132// Make sure this ends up positive, even on 32-bit machines.133$value = head(unpack('L', $hash)) & 0x7FFFFFFF;134135return $min + ($value % (1 + $max - $min));136}137138139/**140* Shorten a string to a maximum byte length in a collision-resistant way141* while retaining some degree of human-readability.142*143* This function converts an input string into a prefix plus a hash. For144* example, a very long string beginning with "crabapplepie..." might be145* digested to something like "crabapp-N1wM1Nz3U84k".146*147* This allows the maximum length of identifiers to be fixed while148* maintaining a high degree of collision resistance and a moderate degree149* of human readability.150*151* @param string The string to shorten.152* @param int Maximum length of the result.153* @return string String shortened in a collision-resistant way.154*/155public static function digestToLength($string, $length) {156// We need at least two more characters than the hash length to fit in a157// a 1-character prefix and a separator.158$min_length = self::INDEX_DIGEST_LENGTH + 2;159if ($length < $min_length) {160throw new Exception(161pht(162'Length parameter in %s must be at least %s, '.163'but %s was provided.',164'digestToLength()',165new PhutilNumber($min_length),166new PhutilNumber($length)));167}168169// We could conceivably return the string unmodified if it's shorter than170// the specified length. Instead, always hash it. This makes the output of171// the method more recognizable and consistent (no surprising new behavior172// once you hit a string longer than `$length`) and prevents an attacker173// who can control the inputs from intentionally using the hashed form174// of a string to cause a collision.175176$hash = self::digestForIndex($string);177178$prefix = substr($string, 0, ($length - ($min_length - 1)));179180return $prefix.'-'.$hash;181}182183public static function digestWithNamedKey($message, $key_name) {184$key_bytes = self::getNamedHMACKey($key_name);185return self::digestHMACSHA256($message, $key_bytes);186}187188public static function digestHMACSHA256($message, $key) {189if (!is_string($message)) {190throw new Exception(191pht('HMAC-SHA256 can only digest strings.'));192}193194if (!is_string($key)) {195throw new Exception(196pht('HMAC-SHA256 keys must be strings.'));197}198199if (!strlen($key)) {200throw new Exception(201pht('HMAC-SHA256 requires a nonempty key.'));202}203204$result = hash_hmac('sha256', $message, $key, $raw_output = false);205206// Although "hash_hmac()" is documented as returning `false` when it fails,207// it can also return `null` if you pass an object as the "$message".208if ($result === false || $result === null) {209throw new Exception(210pht('Unable to compute HMAC-SHA256 digest of message.'));211}212213return $result;214}215216217/* -( HMAC Key Management )------------------------------------------------ */218219220private static function getNamedHMACKey($hmac_name) {221$cache = PhabricatorCaches::getImmutableCache();222223$cache_key = "hmac.key({$hmac_name})";224225$hmac_key = $cache->getKey($cache_key);226if (($hmac_key === null) || !strlen($hmac_key)) {227$hmac_key = self::readHMACKey($hmac_name);228229if ($hmac_key === null) {230$hmac_key = self::newHMACKey($hmac_name);231self::writeHMACKey($hmac_name, $hmac_key);232}233234$cache->setKey($cache_key, $hmac_key);235}236237// The "hex2bin()" function doesn't exist until PHP 5.4.0 so just238// implement it inline.239$result = '';240for ($ii = 0; $ii < strlen($hmac_key); $ii += 2) {241$result .= pack('H*', substr($hmac_key, $ii, 2));242}243244return $result;245}246247private static function newHMACKey($hmac_name) {248$hmac_key = Filesystem::readRandomBytes(64);249return bin2hex($hmac_key);250}251252private static function writeHMACKey($hmac_name, $hmac_key) {253$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();254255id(new PhabricatorAuthHMACKey())256->setKeyName($hmac_name)257->setKeyValue($hmac_key)258->save();259260unset($unguarded);261}262263private static function readHMACKey($hmac_name) {264$table = new PhabricatorAuthHMACKey();265$conn = $table->establishConnection('r');266267$row = queryfx_one(268$conn,269'SELECT keyValue FROM %T WHERE keyName = %s',270$table->getTableName(),271$hmac_name);272if (!$row) {273return null;274}275276return $row['keyValue'];277}278279280}281282283