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