Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/util/password/PhabricatorPasswordHasher.php
12241 views
1
<?php
2
3
/**
4
* Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",
5
* "scrypt", etc.
6
*
7
* Hashers define suitability and strength, and the system automatically
8
* chooses the strongest available hasher and can prompt users to upgrade as
9
* soon as a stronger hasher is available.
10
*
11
* @task hasher Implementing a Hasher
12
* @task hashing Using Hashers
13
*/
14
abstract class PhabricatorPasswordHasher extends Phobject {
15
16
const MAXIMUM_STORAGE_SIZE = 128;
17
18
19
/* -( Implementing a Hasher )---------------------------------------------- */
20
21
22
/**
23
* Return a human-readable description of this hasher, like "Iterated MD5".
24
*
25
* @return string Human readable hash name.
26
* @task hasher
27
*/
28
abstract public function getHumanReadableName();
29
30
31
/**
32
* Return a short, unique, key identifying this hasher, like "md5" or
33
* "bcrypt". This identifier should not be translated.
34
*
35
* @return string Short, unique hash name.
36
* @task hasher
37
*/
38
abstract public function getHashName();
39
40
41
/**
42
* Return the maximum byte length of hashes produced by this hasher. This is
43
* used to prevent storage overflows.
44
*
45
* @return int Maximum number of bytes in hashes this class produces.
46
* @task hasher
47
*/
48
abstract public function getHashLength();
49
50
51
/**
52
* Return `true` to indicate that any required extensions or dependencies
53
* are available, and this hasher is able to perform hashing.
54
*
55
* @return bool True if this hasher can execute.
56
* @task hasher
57
*/
58
abstract public function canHashPasswords();
59
60
61
/**
62
* Return a human-readable string describing why this hasher is unable
63
* to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".
64
*
65
* @return string Human-readable description of how to enable this hasher.
66
* @task hasher
67
*/
68
abstract public function getInstallInstructions();
69
70
71
/**
72
* Return an indicator of this hasher's strength. When choosing to hash
73
* new passwords, the strongest available hasher which is usable for new
74
* passwords will be used, and the presence of a stronger hasher will
75
* prompt users to update their hashes.
76
*
77
* Generally, this method should return a larger number than hashers it is
78
* preferable to, but a smaller number than hashers which are better than it
79
* is. This number does not need to correspond directly with the actual hash
80
* strength.
81
*
82
* @return float Strength of this hasher.
83
* @task hasher
84
*/
85
abstract public function getStrength();
86
87
88
/**
89
* Return a short human-readable indicator of this hasher's strength, like
90
* "Weak", "Okay", or "Good".
91
*
92
* This is only used to help administrators make decisions about
93
* configuration.
94
*
95
* @return string Short human-readable description of hash strength.
96
* @task hasher
97
*/
98
abstract public function getHumanReadableStrength();
99
100
101
/**
102
* Produce a password hash.
103
*
104
* @param PhutilOpaqueEnvelope Text to be hashed.
105
* @return PhutilOpaqueEnvelope Hashed text.
106
* @task hasher
107
*/
108
abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);
109
110
111
/**
112
* Verify that a password matches a hash.
113
*
114
* The default implementation checks for equality; if a hasher embeds salt in
115
* hashes it should override this method and perform a salt-aware comparison.
116
*
117
* @param PhutilOpaqueEnvelope Password to compare.
118
* @param PhutilOpaqueEnvelope Bare password hash.
119
* @return bool True if the passwords match.
120
* @task hasher
121
*/
122
protected function verifyPassword(
123
PhutilOpaqueEnvelope $password,
124
PhutilOpaqueEnvelope $hash) {
125
126
$actual_hash = $this->getPasswordHash($password)->openEnvelope();
127
$expect_hash = $hash->openEnvelope();
128
129
return phutil_hashes_are_identical($actual_hash, $expect_hash);
130
}
131
132
133
/**
134
* Check if an existing hash created by this algorithm is upgradeable.
135
*
136
* The default implementation returns `false`. However, hash algorithms which
137
* have (for example) an internal cost function may be able to upgrade an
138
* existing hash to a stronger one with a higher cost.
139
*
140
* @param PhutilOpaqueEnvelope Bare hash.
141
* @return bool True if the hash can be upgraded without
142
* changing the algorithm (for example, to a
143
* higher cost).
144
* @task hasher
145
*/
146
protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
147
return false;
148
}
149
150
151
/* -( Using Hashers )------------------------------------------------------ */
152
153
154
/**
155
* Get the hash of a password for storage.
156
*
157
* @param PhutilOpaqueEnvelope Password text.
158
* @return PhutilOpaqueEnvelope Hashed text.
159
* @task hashing
160
*/
161
final public function getPasswordHashForStorage(
162
PhutilOpaqueEnvelope $envelope) {
163
164
$name = $this->getHashName();
165
$hash = $this->getPasswordHash($envelope);
166
167
$actual_len = strlen($hash->openEnvelope());
168
$expect_len = $this->getHashLength();
169
if ($actual_len > $expect_len) {
170
throw new Exception(
171
pht(
172
"Password hash '%s' produced a hash of length %d, but a ".
173
"maximum length of %d was expected.",
174
$name,
175
new PhutilNumber($actual_len),
176
new PhutilNumber($expect_len)));
177
}
178
179
return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());
180
}
181
182
183
/**
184
* Parse a storage hash into its components, like the hash type and hash
185
* data.
186
*
187
* @return map Dictionary of information about the hash.
188
* @task hashing
189
*/
190
private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {
191
$raw_hash = $hash->openEnvelope();
192
if (strpos($raw_hash, ':') === false) {
193
throw new Exception(
194
pht(
195
'Malformed password hash, expected "name:hash".'));
196
}
197
198
list($name, $hash) = explode(':', $raw_hash);
199
200
return array(
201
'name' => $name,
202
'hash' => new PhutilOpaqueEnvelope($hash),
203
);
204
}
205
206
207
/**
208
* Get all available password hashers. This may include hashers which can not
209
* actually be used (for example, a required extension is missing).
210
*
211
* @return list<PhabricatorPasswordHasher> Hasher objects.
212
* @task hashing
213
*/
214
public static function getAllHashers() {
215
$objects = id(new PhutilClassMapQuery())
216
->setAncestorClass(__CLASS__)
217
->setUniqueMethod('getHashName')
218
->execute();
219
220
foreach ($objects as $object) {
221
$name = $object->getHashName();
222
223
$potential_length = strlen($name) + $object->getHashLength() + 1;
224
$maximum_length = self::MAXIMUM_STORAGE_SIZE;
225
226
if ($potential_length > $maximum_length) {
227
throw new Exception(
228
pht(
229
'Hasher "%s" may produce hashes which are too long to fit in '.
230
'storage. %d characters are available, but its hashes may be '.
231
'up to %d characters in length.',
232
$name,
233
$maximum_length,
234
$potential_length));
235
}
236
}
237
238
return $objects;
239
}
240
241
242
/**
243
* Get all usable password hashers. This may include hashers which are
244
* not desirable or advisable.
245
*
246
* @return list<PhabricatorPasswordHasher> Hasher objects.
247
* @task hashing
248
*/
249
public static function getAllUsableHashers() {
250
$hashers = self::getAllHashers();
251
foreach ($hashers as $key => $hasher) {
252
if (!$hasher->canHashPasswords()) {
253
unset($hashers[$key]);
254
}
255
}
256
return $hashers;
257
}
258
259
260
/**
261
* Get the best (strongest) available hasher.
262
*
263
* @return PhabricatorPasswordHasher Best hasher.
264
* @task hashing
265
*/
266
public static function getBestHasher() {
267
$hashers = self::getAllUsableHashers();
268
$hashers = msort($hashers, 'getStrength');
269
270
$hasher = last($hashers);
271
if (!$hasher) {
272
throw new PhabricatorPasswordHasherUnavailableException(
273
pht(
274
'There are no password hashers available which are usable for '.
275
'new passwords.'));
276
}
277
278
return $hasher;
279
}
280
281
282
/**
283
* Get the hasher for a given stored hash.
284
*
285
* @return PhabricatorPasswordHasher Corresponding hasher.
286
* @task hashing
287
*/
288
public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {
289
$info = self::parseHashFromStorage($hash);
290
$name = $info['name'];
291
292
$usable = self::getAllUsableHashers();
293
if (isset($usable[$name])) {
294
return $usable[$name];
295
}
296
297
$all = self::getAllHashers();
298
if (isset($all[$name])) {
299
throw new PhabricatorPasswordHasherUnavailableException(
300
pht(
301
'Attempting to compare a password saved with the "%s" hash. The '.
302
'hasher exists, but is not currently usable. %s',
303
$name,
304
$all[$name]->getInstallInstructions()));
305
}
306
307
throw new PhabricatorPasswordHasherUnavailableException(
308
pht(
309
'Attempting to compare a password saved with the "%s" hash. No such '.
310
'hasher is known.',
311
$name));
312
}
313
314
315
/**
316
* Test if a password is using an weaker hash than the strongest available
317
* hash. This can be used to prompt users to upgrade, or automatically upgrade
318
* on login.
319
*
320
* @return bool True to indicate that rehashing this password will improve
321
* the hash strength.
322
* @task hashing
323
*/
324
public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
325
if (!strlen($hash->openEnvelope())) {
326
throw new Exception(
327
pht('Expected a password hash, received nothing!'));
328
}
329
330
$current_hasher = self::getHasherForHash($hash);
331
$best_hasher = self::getBestHasher();
332
333
if ($current_hasher->getHashName() != $best_hasher->getHashName()) {
334
// If the algorithm isn't the best one, we can upgrade.
335
return true;
336
}
337
338
$info = self::parseHashFromStorage($hash);
339
if ($current_hasher->canUpgradeInternalHash($info['hash'])) {
340
// If the algorithm provides an internal upgrade, we can also upgrade.
341
return true;
342
}
343
344
// Already on the best algorithm with the best settings.
345
return false;
346
}
347
348
349
/**
350
* Generate a new hash for a password, using the best available hasher.
351
*
352
* @param PhutilOpaqueEnvelope Password to hash.
353
* @return PhutilOpaqueEnvelope Hashed password, using best available
354
* hasher.
355
* @task hashing
356
*/
357
public static function generateNewPasswordHash(
358
PhutilOpaqueEnvelope $password) {
359
$hasher = self::getBestHasher();
360
return $hasher->getPasswordHashForStorage($password);
361
}
362
363
364
/**
365
* Compare a password to a stored hash.
366
*
367
* @param PhutilOpaqueEnvelope Password to compare.
368
* @param PhutilOpaqueEnvelope Stored password hash.
369
* @return bool True if the passwords match.
370
* @task hashing
371
*/
372
public static function comparePassword(
373
PhutilOpaqueEnvelope $password,
374
PhutilOpaqueEnvelope $hash) {
375
376
$hasher = self::getHasherForHash($hash);
377
$parts = self::parseHashFromStorage($hash);
378
379
return $hasher->verifyPassword($password, $parts['hash']);
380
}
381
382
383
/**
384
* Get the human-readable algorithm name for a given hash.
385
*
386
* @param PhutilOpaqueEnvelope Storage hash.
387
* @return string Human-readable algorithm name.
388
*/
389
public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) {
390
$raw_hash = $hash->openEnvelope();
391
if (!strlen($raw_hash)) {
392
return pht('None');
393
}
394
395
try {
396
$current_hasher = self::getHasherForHash($hash);
397
return $current_hasher->getHumanReadableName();
398
} catch (Exception $ex) {
399
$info = self::parseHashFromStorage($hash);
400
$name = $info['name'];
401
return pht('Unknown ("%s")', $name);
402
}
403
}
404
405
406
/**
407
* Get the human-readable algorithm name for the best available hash.
408
*
409
* @return string Human-readable name for best hash.
410
*/
411
public static function getBestAlgorithmName() {
412
try {
413
$best_hasher = self::getBestHasher();
414
return $best_hasher->getHumanReadableName();
415
} catch (Exception $ex) {
416
return pht('Unknown');
417
}
418
}
419
420
}
421
422