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