[TASK] Use hash_equals for timing-safe comparison of hash-values
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / Classes / Salt / PhpassSalt.php
1 <?php
2 namespace TYPO3\CMS\Saltedpasswords\Salt;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Crypto\Random;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Class that implements PHPass salted hashing based on Drupal's
22 * modified Openwall implementation.
23 *
24 * Derived from Drupal CMS
25 * original license: GNU General Public License (GPL)
26 *
27 * PHPass should work on every system.
28 * @see http://drupal.org/node/29706/
29 * @see http://www.openwall.com/phpass/
30 */
31 class PhpassSalt extends AbstractSalt implements SaltInterface
32 {
33 /**
34 * Keeps a string for mapping an int to the corresponding
35 * base 64 character.
36 */
37 const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
38
39 /**
40 * The default log2 number of iterations for password stretching.
41 */
42 const HASH_COUNT = 14;
43
44 /**
45 * The default maximum allowed log2 number of iterations for
46 * password stretching.
47 */
48 const MAX_HASH_COUNT = 24;
49
50 /**
51 * The default minimum allowed log2 number of iterations for
52 * password stretching.
53 */
54 const MIN_HASH_COUNT = 7;
55
56 /**
57 * Keeps log2 number
58 * of iterations for password stretching.
59 *
60 * @var int
61 */
62 protected static $hashCount;
63
64 /**
65 * Keeps maximum allowed log2 number
66 * of iterations for password stretching.
67 *
68 * @var int
69 */
70 protected static $maxHashCount;
71
72 /**
73 * Keeps minimum allowed log2 number
74 * of iterations for password stretching.
75 *
76 * @var int
77 */
78 protected static $minHashCount;
79
80 /**
81 * Keeps length of a PHPass salt in bytes.
82 *
83 * @var int
84 */
85 protected static $saltLengthPhpass = 6;
86
87 /**
88 * Setting string to indicate type of hashing method (PHPass).
89 *
90 * @var string
91 */
92 protected static $settingPhpass = '$P$';
93
94 /**
95 * Method applies settings (prefix, hash count) to a salt.
96 *
97 * Overwrites {@link Md5Salt::applySettingsToSalt()}
98 * with Blowfish specifics.
99 *
100 * @param string $salt A salt to apply setting to
101 * @return string Salt with setting
102 */
103 protected function applySettingsToSalt($salt)
104 {
105 $saltWithSettings = $salt;
106 $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
107 // Salt without setting
108 if (strlen($salt) == $reqLenBase64) {
109 // We encode the final log2 iteration count in base 64.
110 $itoa64 = $this->getItoa64();
111 $saltWithSettings = $this->getSetting() . $itoa64[$this->getHashCount()];
112 $saltWithSettings .= $salt;
113 }
114 return $saltWithSettings;
115 }
116
117 /**
118 * Method checks if a given plaintext password is correct by comparing it with
119 * a given salted hashed password.
120 *
121 * @param string $plainPW Plain-text password to compare with salted hash
122 * @param string $saltedHashPW Salted hash to compare plain-text password with
123 * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
124 */
125 public function checkPassword($plainPW, $saltedHashPW)
126 {
127 $hash = $this->cryptPassword($plainPW, $saltedHashPW);
128 return $hash && hash_equals($hash, $saltedHashPW);
129 }
130
131 /**
132 * Returns whether all prerequisites for the hashing methods are matched
133 *
134 * @return bool Method available
135 */
136 public function isAvailable()
137 {
138 return true;
139 }
140
141 /**
142 * Hashes a password using a secure stretched hash.
143 *
144 * By using a salt and repeated hashing the password is "stretched". Its
145 * security is increased because it becomes much more computationally costly
146 * for an attacker to try to break the hash by brute-force computation of the
147 * hashes of a large number of plain-text words or strings to find a match.
148 *
149 * @param string $password Plain-text password to hash
150 * @param string $setting An existing hash or the output of getGeneratedSalt()
151 * @return mixed A string containing the hashed password (and salt)
152 */
153 protected function cryptPassword($password, $setting)
154 {
155 $saltedPW = null;
156 $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
157 // Retrieving settings with salt
158 $setting = substr($setting, 0, strlen($this->getSetting()) + 1 + $reqLenBase64);
159 $count_log2 = $this->getCountLog2($setting);
160 // Hashes may be imported from elsewhere, so we allow != HASH_COUNT
161 if ($count_log2 >= $this->getMinHashCount() && $count_log2 <= $this->getMaxHashCount()) {
162 $salt = substr($setting, strlen($this->getSetting()) + 1, $reqLenBase64);
163 // We must use md5() or sha1() here since they are the only cryptographic
164 // primitives always available in PHP 5. To implement our own low-level
165 // cryptographic function in PHP would result in much worse performance and
166 // consequently in lower iteration counts and hashes that are quicker to crack
167 // (by non-PHP code).
168 $count = 1 << $count_log2;
169 $hash = md5($salt . $password, true);
170 do {
171 $hash = md5($hash . $password, true);
172 } while (--$count);
173 $saltedPW = $setting . $this->base64Encode($hash, 16);
174 // base64Encode() of a 16 byte MD5 will always be 22 characters.
175 return strlen($saltedPW) == 34 ? $saltedPW : false;
176 }
177 return $saltedPW;
178 }
179
180 /**
181 * Parses the log2 iteration count from a stored hash or setting string.
182 *
183 * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
184 * @return int Used hashcount for given hash string
185 */
186 protected function getCountLog2($setting)
187 {
188 return strpos($this->getItoa64(), $setting[strlen($this->getSetting())]);
189 }
190
191 /**
192 * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
193 *
194 * Proper use of salts may defeat a number of attacks, including:
195 * - The ability to try candidate passwords against multiple hashes at once.
196 * - The ability to use pre-hashed lists of candidate passwords.
197 * - The ability to determine whether two users have the same (or different)
198 * password without actually having to guess one of the passwords.
199 *
200 * @return string A character string containing settings and a random salt
201 */
202 protected function getGeneratedSalt()
203 {
204 $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes($this->getSaltLength());
205 return $this->base64Encode($randomBytes, $this->getSaltLength());
206 }
207
208 /**
209 * Method returns log2 number of iterations for password stretching.
210 *
211 * @return int log2 number of iterations for password stretching
212 * @see HASH_COUNT
213 * @see $hashCount
214 * @see setHashCount()
215 */
216 public function getHashCount()
217 {
218 return isset(self::$hashCount) ? self::$hashCount : self::HASH_COUNT;
219 }
220
221 /**
222 * Method creates a salted hash for a given plaintext password
223 *
224 * @param string $password Plaintext password to create a salted hash from
225 * @param string $salt Optional custom salt with setting to use
226 * @return string salted hashed password
227 */
228 public function getHashedPassword($password, $salt = null)
229 {
230 $saltedPW = null;
231 if (!empty($password)) {
232 if (empty($salt) || !$this->isValidSalt($salt)) {
233 $salt = $this->getGeneratedSalt();
234 }
235 $saltedPW = $this->cryptPassword($password, $this->applySettingsToSalt($salt));
236 }
237 return $saltedPW;
238 }
239
240 /**
241 * Returns a string for mapping an int to the corresponding base 64 character.
242 *
243 * @return string String for mapping an int to the corresponding base 64 character
244 */
245 protected function getItoa64()
246 {
247 return self::ITOA64;
248 }
249
250 /**
251 * Method returns maximum allowed log2 number of iterations for password stretching.
252 *
253 * @return int Maximum allowed log2 number of iterations for password stretching
254 * @see MAX_HASH_COUNT
255 * @see $maxHashCount
256 * @see setMaxHashCount()
257 */
258 public function getMaxHashCount()
259 {
260 return isset(self::$maxHashCount) ? self::$maxHashCount : self::MAX_HASH_COUNT;
261 }
262
263 /**
264 * Method returns minimum allowed log2 number of iterations for password stretching.
265 *
266 * @return int Minimum allowed log2 number of iterations for password stretching
267 * @see MIN_HASH_COUNT
268 * @see $minHashCount
269 * @see setMinHashCount()
270 */
271 public function getMinHashCount()
272 {
273 return isset(self::$minHashCount) ? self::$minHashCount : self::MIN_HASH_COUNT;
274 }
275
276 /**
277 * Returns length of a Blowfish salt in bytes.
278 *
279 * @return int Length of a Blowfish salt in bytes
280 */
281 public function getSaltLength()
282 {
283 return self::$saltLengthPhpass;
284 }
285
286 /**
287 * Returns setting string of PHPass salted hashes.
288 *
289 * @return string Setting string of PHPass salted hashes
290 */
291 public function getSetting()
292 {
293 return self::$settingPhpass;
294 }
295
296 /**
297 * Checks whether a user's hashed password needs to be replaced with a new hash.
298 *
299 * This is typically called during the login process when the plain text
300 * password is available. A new hash is needed when the desired iteration
301 * count has changed through a change in the variable $hashCount or
302 * HASH_COUNT or if the user's password hash was generated in an bulk update
303 * with class ext_update.
304 *
305 * @param string $passString Salted hash to check if it needs an update
306 * @return bool TRUE if salted hash needs an update, otherwise FALSE
307 */
308 public function isHashUpdateNeeded($passString)
309 {
310 // Check whether this was an updated password.
311 if (strncmp($passString, '$P$', 3) || strlen($passString) != 34) {
312 return true;
313 }
314 // Check whether the iteration count used differs from the standard number.
315 return $this->getCountLog2($passString) < $this->getHashCount();
316 }
317
318 /**
319 * Method determines if a given string is a valid salt.
320 *
321 * @param string $salt String to check
322 * @return bool TRUE if it's valid salt, otherwise FALSE
323 */
324 public function isValidSalt($salt)
325 {
326 $isValid = ($skip = false);
327 $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
328 if (strlen($salt) >= $reqLenBase64) {
329 // Salt with prefixed setting
330 if (!strncmp('$', $salt, 1)) {
331 if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
332 $isValid = true;
333 $salt = substr($salt, strrpos($salt, '$') + 2);
334 } else {
335 $skip = true;
336 }
337 }
338 // Checking base64 characters
339 if (!$skip && strlen($salt) >= $reqLenBase64) {
340 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
341 $isValid = true;
342 }
343 }
344 }
345 return $isValid;
346 }
347
348 /**
349 * Method determines if a given string is a valid salted hashed password.
350 *
351 * @param string $saltedPW String to check
352 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
353 */
354 public function isValidSaltedPW($saltedPW)
355 {
356 $isValid = !strncmp($this->getSetting(), $saltedPW, strlen($this->getSetting()));
357 if ($isValid) {
358 $isValid = $this->isValidSalt($saltedPW);
359 }
360 return $isValid;
361 }
362
363 /**
364 * Method sets log2 number of iterations for password stretching.
365 *
366 * @param int $hashCount log2 number of iterations for password stretching to set
367 * @see HASH_COUNT
368 * @see $hashCount
369 * @see getHashCount()
370 */
371 public function setHashCount($hashCount = null)
372 {
373 self::$hashCount = !is_null($hashCount) && is_int($hashCount) && $hashCount >= $this->getMinHashCount() && $hashCount <= $this->getMaxHashCount() ? $hashCount : self::HASH_COUNT;
374 }
375
376 /**
377 * Method sets maximum allowed log2 number of iterations for password stretching.
378 *
379 * @param int $maxHashCount Maximum allowed log2 number of iterations for password stretching to set
380 * @see MAX_HASH_COUNT
381 * @see $maxHashCount
382 * @see getMaxHashCount()
383 */
384 public function setMaxHashCount($maxHashCount = null)
385 {
386 self::$maxHashCount = !is_null($maxHashCount) && is_int($maxHashCount) ? $maxHashCount : self::MAX_HASH_COUNT;
387 }
388
389 /**
390 * Method sets minimum allowed log2 number of iterations for password stretching.
391 *
392 * @param int $minHashCount Minimum allowed log2 number of iterations for password stretching to set
393 * @see MIN_HASH_COUNT
394 * @see $minHashCount
395 * @see getMinHashCount()
396 */
397 public function setMinHashCount($minHashCount = null)
398 {
399 self::$minHashCount = !is_null($minHashCount) && is_int($minHashCount) ? $minHashCount : self::MIN_HASH_COUNT;
400 }
401 }