[TASK] Use hash_equals for timing-safe comparison of hash-values
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / Classes / Salt / Pbkdf2Salt.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 PBKDF2 salted hashing based on PHP's
22 * hash_pbkdf2() function.
23 */
24 class Pbkdf2Salt extends AbstractSalt implements SaltInterface
25 {
26 /**
27 * Keeps a string for mapping an int to the corresponding
28 * base 64 character.
29 */
30 const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
31
32 /**
33 * The default number of iterations for password stretching.
34 */
35 const HASH_COUNT = 25000;
36
37 /**
38 * The default maximum allowed number of iterations for password stretching.
39 */
40 const MAX_HASH_COUNT = 10000000;
41
42 /**
43 * The default minimum allowed number of iterations for password stretching.
44 */
45 const MIN_HASH_COUNT = 1000;
46
47 /**
48 * Keeps number of iterations for password stretching.
49 *
50 * @var int
51 */
52 protected static $hashCount;
53
54 /**
55 * Keeps maximum allowed number of iterations for password stretching.
56 *
57 * @var int
58 */
59 protected static $maxHashCount;
60
61 /**
62 * Keeps minimum allowed number of iterations for password stretching.
63 *
64 * @var int
65 */
66 protected static $minHashCount;
67
68 /**
69 * Keeps length of a PBKDF2 salt in bytes.
70 *
71 * @var int
72 */
73 protected static $saltLengthPbkdf2 = 16;
74
75 /**
76 * Setting string to indicate type of hashing method (PBKDF2).
77 *
78 * @var string
79 */
80 protected static $settingPbkdf2 = '$pbkdf2-sha256$';
81
82 /**
83 * Method applies settings (prefix, hash count) to a salt.
84 *
85 * Overwrites {@link Md5Salt::applySettingsToSalt()}
86 * with PBKDF2 specifics.
87 *
88 * @param string $salt A salt to apply setting to
89 * @return string Salt with setting
90 */
91 protected function applySettingsToSalt($salt)
92 {
93 $saltWithSettings = $salt;
94 // salt without setting
95 if (strlen($salt) === $this->getSaltLength()) {
96 $saltWithSettings = $this->getSetting() . sprintf('%02u', $this->getHashCount()) . '$' . $this->base64Encode($salt, $this->getSaltLength());
97 }
98 return $saltWithSettings;
99 }
100
101 /**
102 * Method checks if a given plaintext password is correct by comparing it with
103 * a given salted hashed password.
104 *
105 * @param string $plainPW plain-text password to compare with salted hash
106 * @param string $saltedHashPW salted hash to compare plain-text password with
107 * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
108 */
109 public function checkPassword($plainPW, $saltedHashPW)
110 {
111 return $this->isValidSalt($saltedHashPW) && hash_equals($this->getHashedPassword($plainPW, $saltedHashPW), $saltedHashPW);
112 }
113
114 /**
115 * Parses the log2 iteration count from a stored hash or setting string.
116 *
117 * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
118 * @return int|null Used hashcount for given hash string
119 */
120 protected function getIterationCount($setting)
121 {
122 $iterationCount = null;
123 $setting = substr($setting, strlen($this->getSetting()));
124 $firstSplitPos = strpos($setting, '$');
125 // Hashcount existing
126 if ($firstSplitPos !== false
127 && $firstSplitPos <= strlen((string)$this->getMaxHashCount())
128 && is_numeric(substr($setting, 0, $firstSplitPos))
129 ) {
130 $iterationCount = (int)substr($setting, 0, $firstSplitPos);
131 }
132 return $iterationCount;
133 }
134
135 /**
136 * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
137 *
138 * Proper use of salts may defeat a number of attacks, including:
139 * - The ability to try candidate passwords against multiple hashes at once.
140 * - The ability to use pre-hashed lists of candidate passwords.
141 * - The ability to determine whether two users have the same (or different)
142 * password without actually having to guess one of the passwords.
143 *
144 * @return string A character string containing settings and a random salt
145 */
146 protected function getGeneratedSalt()
147 {
148 return GeneralUtility::makeInstance(Random::class)->generateRandomBytes($this->getSaltLength());
149 }
150
151 /**
152 * Parses the salt out of a salt string including settings. If the salt does not include settings
153 * it is returned unmodified.
154 *
155 * @param string $salt
156 * @return string
157 */
158 protected function getStoredSalt($salt)
159 {
160 if (!strncmp('$', $salt, 1)) {
161 if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
162 $saltParts = GeneralUtility::trimExplode('$', $salt, 4);
163 $salt = $saltParts[2];
164 }
165 }
166 return $this->base64Decode($salt);
167 }
168
169 /**
170 * Returns a string for mapping an int to the corresponding base 64 character.
171 *
172 * @return string String for mapping an int to the corresponding base 64 character
173 */
174 protected function getItoa64()
175 {
176 return self::ITOA64;
177 }
178
179 /**
180 * Method creates a salted hash for a given plaintext password
181 *
182 * @param string $password plaintext password to create a salted hash from
183 * @param string $salt Optional custom salt with setting to use
184 * @return string|null Salted hashed password
185 */
186 public function getHashedPassword($password, $salt = null)
187 {
188 $saltedPW = null;
189 if ($password !== '') {
190 if (empty($salt) || !$this->isValidSalt($salt)) {
191 $salt = $this->getGeneratedSalt();
192 } else {
193 $this->setHashCount($this->getIterationCount($salt));
194 $salt = $this->getStoredSalt($salt);
195 }
196 $hash = hash_pbkdf2('sha256', $password, $salt, $this->getHashCount(), 0, true);
197 $saltedPW = $this->applySettingsToSalt($salt) . '$' . $this->base64Encode($hash, strlen($hash));
198 }
199 return $saltedPW;
200 }
201
202 /**
203 * Method returns number of iterations for password stretching.
204 *
205 * @return int number of iterations for password stretching
206 * @see HASH_COUNT
207 * @see $hashCount
208 * @see setHashCount()
209 */
210 public function getHashCount()
211 {
212 return isset(self::$hashCount) ? self::$hashCount : self::HASH_COUNT;
213 }
214
215 /**
216 * Method returns maximum allowed number of iterations for password stretching.
217 *
218 * @return int Maximum allowed number of iterations for password stretching
219 * @see MAX_HASH_COUNT
220 * @see $maxHashCount
221 * @see setMaxHashCount()
222 */
223 public function getMaxHashCount()
224 {
225 return isset(self::$maxHashCount) ? self::$maxHashCount : self::MAX_HASH_COUNT;
226 }
227
228 /**
229 * Returns whether all prerequisites for the hashing methods are matched
230 *
231 * @return bool Method available
232 */
233 public function isAvailable()
234 {
235 return function_exists('hash_pbkdf2');
236 }
237
238 /**
239 * Method returns minimum allowed number of iterations for password stretching.
240 *
241 * @return int Minimum allowed number of iterations for password stretching
242 * @see MIN_HASH_COUNT
243 * @see $minHashCount
244 * @see setMinHashCount()
245 */
246 public function getMinHashCount()
247 {
248 return isset(self::$minHashCount) ? self::$minHashCount : self::MIN_HASH_COUNT;
249 }
250
251 /**
252 * Returns length of a PBKDF2 salt in bytes.
253 *
254 * Overwrites {@link Md5Salt::getSaltLength()}
255 * with PBKDF2 specifics.
256 *
257 * @return int Length of a PBKDF2 salt in bytes
258 */
259 public function getSaltLength()
260 {
261 return self::$saltLengthPbkdf2;
262 }
263
264 /**
265 * Returns setting string of PBKDF2 salted hashes.
266 *
267 * Overwrites {@link Md5Salt::getSetting()}
268 * with PBKDF2 specifics.
269 *
270 * @return string Setting string of PBKDF2 salted hashes
271 */
272 public function getSetting()
273 {
274 return self::$settingPbkdf2;
275 }
276
277 /**
278 * Checks whether a user's hashed password needs to be replaced with a new hash.
279 *
280 * This is typically called during the login process when the plain text
281 * password is available. A new hash is needed when the desired iteration
282 * count has changed through a change in the variable $hashCount or
283 * HASH_COUNT.
284 *
285 * @param string $saltedPW Salted hash to check if it needs an update
286 * @return bool TRUE if salted hash needs an update, otherwise FALSE
287 */
288 public function isHashUpdateNeeded($saltedPW)
289 {
290 // Check whether this was an updated password.
291 if (strncmp($saltedPW, $this->getSetting(), strlen($this->getSetting())) || !$this->isValidSalt($saltedPW)) {
292 return true;
293 }
294 // Check whether the iteration count used differs from the standard number.
295 $iterationCount = $this->getIterationCount($saltedPW);
296 return !is_null($iterationCount) && $iterationCount < $this->getHashCount();
297 }
298
299 /**
300 * Method determines if a given string is a valid salt.
301 *
302 * Overwrites {@link Md5Salt::isValidSalt()} with
303 * PBKDF2 specifics.
304 *
305 * @param string $salt String to check
306 * @return bool TRUE if it's valid salt, otherwise FALSE
307 */
308 public function isValidSalt($salt)
309 {
310 $isValid = ($skip = false);
311 $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
312 if (strlen($salt) >= $reqLenBase64) {
313 // Salt with prefixed setting
314 if (!strncmp('$', $salt, 1)) {
315 if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
316 $isValid = true;
317 $salt = substr($salt, strrpos($salt, '$') + 1);
318 } else {
319 $skip = true;
320 }
321 }
322 // Checking base64 characters
323 if (!$skip && strlen($salt) >= $reqLenBase64) {
324 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
325 $isValid = true;
326 }
327 }
328 }
329 return $isValid;
330 }
331
332 /**
333 * Method determines if a given string is a valid salted hashed password.
334 *
335 * @param string $saltedPW String to check
336 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
337 */
338 public function isValidSaltedPW($saltedPW)
339 {
340 $isValid = !strncmp($this->getSetting(), $saltedPW, strlen($this->getSetting()));
341 if ($isValid) {
342 $isValid = $this->isValidSalt($saltedPW);
343 }
344 return $isValid;
345 }
346
347 /**
348 * Method sets number of iterations for password stretching.
349 *
350 * @param int $hashCount number of iterations for password stretching to set
351 * @see HASH_COUNT
352 * @see $hashCount
353 * @see getHashCount()
354 */
355 public function setHashCount($hashCount = null)
356 {
357 self::$hashCount = !is_null($hashCount) && is_int($hashCount) && $hashCount >= $this->getMinHashCount() && $hashCount <= $this->getMaxHashCount() ? $hashCount : self::HASH_COUNT;
358 }
359
360 /**
361 * Method sets maximum allowed number of iterations for password stretching.
362 *
363 * @param int $maxHashCount Maximum allowed number of iterations for password stretching to set
364 * @see MAX_HASH_COUNT
365 * @see $maxHashCount
366 * @see getMaxHashCount()
367 */
368 public function setMaxHashCount($maxHashCount = null)
369 {
370 self::$maxHashCount = !is_null($maxHashCount) && is_int($maxHashCount) ? $maxHashCount : self::MAX_HASH_COUNT;
371 }
372
373 /**
374 * Method sets minimum allowed number of iterations for password stretching.
375 *
376 * @param int $minHashCount Minimum allowed number of iterations for password stretching to set
377 * @see MIN_HASH_COUNT
378 * @see $minHashCount
379 * @see getMinHashCount()
380 */
381 public function setMinHashCount($minHashCount = null)
382 {
383 self::$minHashCount = !is_null($minHashCount) && is_int($minHashCount) ? $minHashCount : self::MIN_HASH_COUNT;
384 }
385
386 /**
387 * Adapted version of base64_encoding for compatibility with python passlib. The output of this function is
388 * is identical to base64_encode, except that it uses . instead of +, and omits trailing padding = and whitepsace.
389 *
390 * @param string $input The string containing bytes to encode.
391 * @param int $count The number of characters (bytes) to encode.
392 * @return string Encoded string
393 */
394 public function base64Encode($input, $count)
395 {
396 $input = substr($input, 0, $count);
397 return rtrim(str_replace('+', '.', base64_encode($input)), " =\r\n\t\0\x0B");
398 }
399
400 /**
401 * Adapted version of base64_encoding for compatibility with python passlib. The output of this function is
402 * is identical to base64_encode, except that it uses . instead of +, and omits trailing padding = and whitepsace.
403 *
404 * @param string $value
405 * @return string
406 */
407 public function base64Decode($value)
408 {
409 return base64_decode(str_replace('.', '+', $value));
410 }
411 }