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