[BUGFIX] Remove unused if in PasswordHashing
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Crypto / PasswordHashing / BlowfishPasswordHash.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
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 Blowfish salted hashing based on PHP's
23 * crypt() function.
24 *
25 * Warning: Blowfish salted hashing with PHP's crypt() is not available
26 * on every system.
27 */
28 class BlowfishPasswordHash implements PasswordHashInterface
29 {
30 /**
31 * Prefix for the password hash.
32 */
33 protected const PREFIX = '$2a$';
34
35 /**
36 * @var array The default log2 number of iterations for password stretching.
37 */
38 protected $options = [
39 'hash_count' => 7
40 ];
41
42 /**
43 * Constructor sets options if given
44 *
45 * @param array $options
46 * @throws \InvalidArgumentException
47 */
48 public function __construct(array $options = [])
49 {
50 $newOptions = $this->options;
51 if (isset($options['hash_count'])) {
52 if ((int)$options['hash_count'] < 4 || (int)$options['hash_count'] > 17) {
53 throw new \InvalidArgumentException(
54 'hash_count must not be lower than 4 or bigger than 17',
55 1533903545
56 );
57 }
58 $newOptions['hash_count'] = (int)$options['hash_count'];
59 }
60 $this->options = $newOptions;
61 }
62
63 /**
64 * Method checks if a given plaintext password is correct by comparing it with
65 * a given salted hashed password.
66 *
67 * @param string $plainPW plain-text password to compare with salted hash
68 * @param string $saltedHashPW salted hash to compare plain-text password with
69 * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
70 */
71 public function checkPassword(string $plainPW, string $saltedHashPW): bool
72 {
73 $isCorrect = false;
74 if ($this->isValidSalt($saltedHashPW)) {
75 $isCorrect = \password_verify($plainPW, $saltedHashPW);
76 }
77 return $isCorrect;
78 }
79
80 /**
81 * Returns whether all prerequisites for the hashing methods are matched
82 *
83 * @return bool Method available
84 */
85 public function isAvailable(): bool
86 {
87 return (bool)CRYPT_BLOWFISH;
88 }
89
90 /**
91 * Method creates a salted hash for a given plaintext password
92 *
93 * @param string $password plaintext password to create a salted hash from
94 * @return string Salted hashed password
95 */
96 public function getHashedPassword(string $password)
97 {
98 $saltedPW = null;
99 if (!empty($password)) {
100 $salt = $this->getGeneratedSalt();
101 $saltedPW = crypt($password, $this->applySettingsToSalt($salt));
102 }
103 return $saltedPW;
104 }
105
106 /**
107 * Checks whether a user's hashed password needs to be replaced with a new hash.
108 *
109 * This is typically called during the login process when the plain text
110 * password is available. A new hash is needed when the desired iteration
111 * count has changed through a change in the variable $hashCount or
112 * HASH_COUNT.
113 *
114 * @param string $saltedPW Salted hash to check if it needs an update
115 * @return bool TRUE if salted hash needs an update, otherwise FALSE
116 */
117 public function isHashUpdateNeeded(string $saltedPW): bool
118 {
119 // Check whether the iteration count used differs from the standard number.
120 $countLog2 = $this->getCountLog2($saltedPW);
121 return $countLog2 !== null && $countLog2 < $this->options['hash_count'];
122 }
123
124 /**
125 * Method determines if a given string is a valid salted hashed password.
126 *
127 * @param string $saltedPW String to check
128 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
129 */
130 public function isValidSaltedPW(string $saltedPW): bool
131 {
132 $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
133 if ($isValid) {
134 $isValid = $this->isValidSalt($saltedPW);
135 }
136 return $isValid;
137 }
138
139 /**
140 * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
141 *
142 * Proper use of salts may defeat a number of attacks, including:
143 * - The ability to try candidate passwords against multiple hashes at once.
144 * - The ability to use pre-hashed lists of candidate passwords.
145 * - The ability to determine whether two users have the same (or different)
146 * password without actually having to guess one of the passwords.
147 *
148 * @return string A character string containing settings and a random salt
149 */
150 protected function getGeneratedSalt(): string
151 {
152 $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
153 return $this->base64Encode($randomBytes, 16);
154 }
155
156 /**
157 * Method applies settings (prefix, hash count) to a salt.
158 *
159 * @param string $salt A salt to apply setting to
160 * @return string Salt with setting
161 */
162 protected function applySettingsToSalt(string $salt): string
163 {
164 $saltWithSettings = $salt;
165 $reqLenBase64 = $this->getLengthBase64FromBytes(16);
166 // salt without setting
167 if (strlen($salt) == $reqLenBase64) {
168 $saltWithSettings = self::PREFIX . sprintf('%02u', $this->options['hash_count']) . '$' . $salt;
169 }
170 return $saltWithSettings;
171 }
172
173 /**
174 * Parses the log2 iteration count from a stored hash or setting string.
175 *
176 * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
177 * @return int Used hashcount for given hash string
178 */
179 protected function getCountLog2(string $setting): int
180 {
181 $countLog2 = null;
182 $setting = substr($setting, strlen(self::PREFIX));
183 $firstSplitPos = strpos($setting, '$');
184 // Hashcount existing
185 if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
186 $countLog2 = (int)substr($setting, 0, $firstSplitPos);
187 }
188 return $countLog2;
189 }
190
191 /**
192 * Returns a string for mapping an int to the corresponding base 64 character.
193 *
194 * @return string String for mapping an int to the corresponding base 64 character
195 */
196 protected function getItoa64(): string
197 {
198 return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
199 }
200
201 /**
202 * Method determines if a given string is a valid salt.
203 *
204 * @param string $salt String to check
205 * @return bool TRUE if it's valid salt, otherwise FALSE
206 */
207 protected function isValidSalt(string $salt): bool
208 {
209 $isValid = ($skip = false);
210 $reqLenBase64 = $this->getLengthBase64FromBytes(16);
211 if (strlen($salt) >= $reqLenBase64) {
212 // Salt with prefixed setting
213 if (!strncmp('$', $salt, 1)) {
214 if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
215 $isValid = true;
216 $salt = substr($salt, strrpos($salt, '$') + 1);
217 } else {
218 $skip = true;
219 }
220 }
221 // Checking base64 characters
222 if (!$skip && strlen($salt) >= $reqLenBase64) {
223 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
224 $isValid = true;
225 }
226 }
227 }
228 return $isValid;
229 }
230
231 /**
232 * Encodes bytes into printable base 64 using the *nix standard from crypt().
233 *
234 * @param string $input The string containing bytes to encode.
235 * @param int $count The number of characters (bytes) to encode.
236 * @return string Encoded string
237 */
238 protected function base64Encode(string $input, int $count): string
239 {
240 $output = '';
241 $i = 0;
242 $itoa64 = $this->getItoa64();
243 do {
244 $value = ord($input[$i++]);
245 $output .= $itoa64[$value & 63];
246 if ($i < $count) {
247 $value |= ord($input[$i]) << 8;
248 }
249 $output .= $itoa64[$value >> 6 & 63];
250 if ($i++ >= $count) {
251 break;
252 }
253 if ($i < $count) {
254 $value |= ord($input[$i]) << 16;
255 }
256 $output .= $itoa64[$value >> 12 & 63];
257 if ($i++ >= $count) {
258 break;
259 }
260 $output .= $itoa64[$value >> 18 & 63];
261 } while ($i < $count);
262 return $output;
263 }
264
265 /**
266 * Method determines required length of base64 characters for a given
267 * length of a byte string.
268 *
269 * @param int $byteLength Length of bytes to calculate in base64 chars
270 * @return int Required length of base64 characters
271 */
272 protected function getLengthBase64FromBytes(int $byteLength): int
273 {
274 // Calculates bytes in bits in base64
275 return (int)ceil($byteLength * 8 / 6);
276 }
277 }