[TASK] Use null coalescing operator where possible
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / Classes / Salt / BlowfishSalt.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 /**
19 * Class that implements Blowfish salted hashing based on PHP's
20 * crypt() function.
21 *
22 * Warning: Blowfish salted hashing with PHP's crypt() is not available
23 * on every system.
24 */
25 class BlowfishSalt extends Md5Salt
26 {
27 /**
28 * The default log2 number of iterations for password stretching.
29 */
30 const HASH_COUNT = 7;
31
32 /**
33 * The default maximum allowed log2 number of iterations for
34 * password stretching.
35 */
36 const MAX_HASH_COUNT = 17;
37
38 /**
39 * The default minimum allowed log2 number of iterations for
40 * password stretching.
41 */
42 const MIN_HASH_COUNT = 4;
43
44 /**
45 * Keeps log2 number
46 * of iterations for password stretching.
47 *
48 * @var int
49 */
50 protected static $hashCount;
51
52 /**
53 * Keeps maximum allowed log2 number
54 * of iterations for password stretching.
55 *
56 * @var int
57 */
58 protected static $maxHashCount;
59
60 /**
61 * Keeps minimum allowed log2 number
62 * of iterations for password stretching.
63 *
64 * @var int
65 */
66 protected static $minHashCount;
67
68 /**
69 * Keeps length of a Blowfish salt in bytes.
70 *
71 * @var int
72 */
73 protected static $saltLengthBlowfish = 16;
74
75 /**
76 * Setting string to indicate type of hashing method (blowfish).
77 *
78 * @var string
79 */
80 protected static $settingBlowfish = '$2a$';
81
82 /**
83 * Method applies settings (prefix, hash count) to a salt.
84 *
85 * Overwrites {@link Md5Salt::applySettingsToSalt()}
86 * with Blowfish specifics.
87 *
88 * @param string $salt A salt to apply setting to
89 * @return string Salt with setting
90 */
91 protected function applySettingsToSalt(string $salt): string
92 {
93 $saltWithSettings = $salt;
94 $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
95 // salt without setting
96 if (strlen($salt) == $reqLenBase64) {
97 $saltWithSettings = $this->getSetting() . sprintf('%02u', $this->getHashCount()) . '$' . $salt;
98 }
99 return $saltWithSettings;
100 }
101
102 /**
103 * Parses the log2 iteration count from a stored hash or setting string.
104 *
105 * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
106 * @return int Used hashcount for given hash string
107 */
108 protected function getCountLog2(string $setting): int
109 {
110 $countLog2 = null;
111 $setting = substr($setting, strlen($this->getSetting()));
112 $firstSplitPos = strpos($setting, '$');
113 // Hashcount existing
114 if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
115 $countLog2 = (int)substr($setting, 0, $firstSplitPos);
116 }
117 return $countLog2;
118 }
119
120 /**
121 * Method returns log2 number of iterations for password stretching.
122 *
123 * @return int log2 number of iterations for password stretching
124 * @see HASH_COUNT
125 * @see $hashCount
126 * @see setHashCount()
127 */
128 public function getHashCount(): int
129 {
130 return self::$hashCount ?? self::HASH_COUNT;
131 }
132
133 /**
134 * Method returns maximum allowed log2 number of iterations for password stretching.
135 *
136 * @return int Maximum allowed log2 number of iterations for password stretching
137 * @see MAX_HASH_COUNT
138 * @see $maxHashCount
139 * @see setMaxHashCount()
140 */
141 public function getMaxHashCount(): int
142 {
143 return self::$maxHashCount ?? self::MAX_HASH_COUNT;
144 }
145
146 /**
147 * Returns whether all prerequisites for the hashing methods are matched
148 *
149 * @return bool Method available
150 */
151 public function isAvailable(): bool
152 {
153 return (bool)CRYPT_BLOWFISH;
154 }
155
156 /**
157 * Method returns minimum allowed log2 number of iterations for password stretching.
158 *
159 * @return int Minimum allowed log2 number of iterations for password stretching
160 * @see MIN_HASH_COUNT
161 * @see $minHashCount
162 * @see setMinHashCount()
163 */
164 public function getMinHashCount(): int
165 {
166 return self::$minHashCount ?? self::MIN_HASH_COUNT;
167 }
168
169 /**
170 * Returns length of a Blowfish salt in bytes.
171 *
172 * Overwrites {@link Md5Salt::getSaltLength()}
173 * with Blowfish specifics.
174 *
175 * @return int Length of a Blowfish salt in bytes
176 */
177 public function getSaltLength(): int
178 {
179 return self::$saltLengthBlowfish;
180 }
181
182 /**
183 * Returns setting string of Blowfish salted hashes.
184 *
185 * Overwrites {@link Md5Salt::getSetting()}
186 * with Blowfish specifics.
187 *
188 * @return string Setting string of Blowfish salted hashes
189 */
190 public function getSetting(): string
191 {
192 return self::$settingBlowfish;
193 }
194
195 /**
196 * Checks whether a user's hashed password needs to be replaced with a new hash.
197 *
198 * This is typically called during the login process when the plain text
199 * password is available. A new hash is needed when the desired iteration
200 * count has changed through a change in the variable $hashCount or
201 * HASH_COUNT.
202 *
203 * @param string $saltedPW Salted hash to check if it needs an update
204 * @return bool TRUE if salted hash needs an update, otherwise FALSE
205 */
206 public function isHashUpdateNeeded(string $saltedPW): bool
207 {
208 // Check whether this was an updated password.
209 if (strncmp($saltedPW, '$2', 2) || !$this->isValidSalt($saltedPW)) {
210 return true;
211 }
212 // Check whether the iteration count used differs from the standard number.
213 $countLog2 = $this->getCountLog2($saltedPW);
214 return !is_null($countLog2) && $countLog2 < $this->getHashCount();
215 }
216
217 /**
218 * Method determines if a given string is a valid salt.
219 *
220 * Overwrites {@link Md5Salt::isValidSalt()} with
221 * Blowfish specifics.
222 *
223 * @param string $salt String to check
224 * @return bool TRUE if it's valid salt, otherwise FALSE
225 */
226 public function isValidSalt(string $salt): bool
227 {
228 $isValid = ($skip = false);
229 $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
230 if (strlen($salt) >= $reqLenBase64) {
231 // Salt with prefixed setting
232 if (!strncmp('$', $salt, 1)) {
233 if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
234 $isValid = true;
235 $salt = substr($salt, strrpos($salt, '$') + 1);
236 } else {
237 $skip = true;
238 }
239 }
240 // Checking base64 characters
241 if (!$skip && strlen($salt) >= $reqLenBase64) {
242 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
243 $isValid = true;
244 }
245 }
246 }
247 return $isValid;
248 }
249
250 /**
251 * Method determines if a given string is a valid salted hashed password.
252 *
253 * @param string $saltedPW String to check
254 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
255 */
256 public function isValidSaltedPW(string $saltedPW): bool
257 {
258 $isValid = !strncmp($this->getSetting(), $saltedPW, strlen($this->getSetting()));
259 if ($isValid) {
260 $isValid = $this->isValidSalt($saltedPW);
261 }
262 return $isValid;
263 }
264
265 /**
266 * Method sets log2 number of iterations for password stretching.
267 *
268 * @param int $hashCount log2 number of iterations for password stretching to set
269 * @see HASH_COUNT
270 * @see $hashCount
271 * @see getHashCount()
272 */
273 public function setHashCount(int $hashCount = null)
274 {
275 self::$hashCount = !is_null($hashCount) && $hashCount >= $this->getMinHashCount() && $hashCount <= $this->getMaxHashCount() ? $hashCount : self::HASH_COUNT;
276 }
277
278 /**
279 * Method sets maximum allowed log2 number of iterations for password stretching.
280 *
281 * @param int $maxHashCount Maximum allowed log2 number of iterations for password stretching to set
282 * @see MAX_HASH_COUNT
283 * @see $maxHashCount
284 * @see getMaxHashCount()
285 */
286 public function setMaxHashCount(int $maxHashCount = null)
287 {
288 self::$maxHashCount = $maxHashCount ?? self::MAX_HASH_COUNT;
289 }
290
291 /**
292 * Method sets minimum allowed log2 number of iterations for password stretching.
293 *
294 * @param int $minHashCount Minimum allowed log2 number of iterations for password stretching to set
295 * @see MIN_HASH_COUNT
296 * @see $minHashCount
297 * @see getMinHashCount()
298 */
299 public function setMinHashCount(int $minHashCount = null)
300 {
301 self::$minHashCount = $minHashCount ?? self::MIN_HASH_COUNT;
302 }
303 }