5cd3c08460597d8a984c1db51eed4d4876e36075
[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 if (empty($salt) || !$this->isValidSalt($salt)) {
101 $salt = $this->getGeneratedSalt();
102 }
103 $saltedPW = crypt($password, $this->applySettingsToSalt($salt));
104 }
105 return $saltedPW;
106 }
107
108 /**
109 * Checks whether a user's hashed password needs to be replaced with a new hash.
110 *
111 * This is typically called during the login process when the plain text
112 * password is available. A new hash is needed when the desired iteration
113 * count has changed through a change in the variable $hashCount or
114 * HASH_COUNT.
115 *
116 * @param string $saltedPW Salted hash to check if it needs an update
117 * @return bool TRUE if salted hash needs an update, otherwise FALSE
118 */
119 public function isHashUpdateNeeded(string $saltedPW): bool
120 {
121 // Check whether the iteration count used differs from the standard number.
122 $countLog2 = $this->getCountLog2($saltedPW);
123 return $countLog2 !== null && $countLog2 < $this->options['hash_count'];
124 }
125
126 /**
127 * Method determines if a given string is a valid salted hashed password.
128 *
129 * @param string $saltedPW String to check
130 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
131 */
132 public function isValidSaltedPW(string $saltedPW): bool
133 {
134 $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
135 if ($isValid) {
136 $isValid = $this->isValidSalt($saltedPW);
137 }
138 return $isValid;
139 }
140
141 /**
142 * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
143 *
144 * Proper use of salts may defeat a number of attacks, including:
145 * - The ability to try candidate passwords against multiple hashes at once.
146 * - The ability to use pre-hashed lists of candidate passwords.
147 * - The ability to determine whether two users have the same (or different)
148 * password without actually having to guess one of the passwords.
149 *
150 * @return string A character string containing settings and a random salt
151 */
152 protected function getGeneratedSalt(): string
153 {
154 $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
155 return $this->base64Encode($randomBytes, 16);
156 }
157
158 /**
159 * Method applies settings (prefix, hash count) to a salt.
160 *
161 * @param string $salt A salt to apply setting to
162 * @return string Salt with setting
163 */
164 protected function applySettingsToSalt(string $salt): string
165 {
166 $saltWithSettings = $salt;
167 $reqLenBase64 = $this->getLengthBase64FromBytes(16);
168 // salt without setting
169 if (strlen($salt) == $reqLenBase64) {
170 $saltWithSettings = self::PREFIX . sprintf('%02u', $this->options['hash_count']) . '$' . $salt;
171 }
172 return $saltWithSettings;
173 }
174
175 /**
176 * Parses the log2 iteration count from a stored hash or setting string.
177 *
178 * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
179 * @return int Used hashcount for given hash string
180 */
181 protected function getCountLog2(string $setting): int
182 {
183 $countLog2 = null;
184 $setting = substr($setting, strlen(self::PREFIX));
185 $firstSplitPos = strpos($setting, '$');
186 // Hashcount existing
187 if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
188 $countLog2 = (int)substr($setting, 0, $firstSplitPos);
189 }
190 return $countLog2;
191 }
192
193 /**
194 * Returns a string for mapping an int to the corresponding base 64 character.
195 *
196 * @return string String for mapping an int to the corresponding base 64 character
197 */
198 protected function getItoa64(): string
199 {
200 return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
201 }
202
203 /**
204 * Method determines if a given string is a valid salt.
205 *
206 * @param string $salt String to check
207 * @return bool TRUE if it's valid salt, otherwise FALSE
208 */
209 protected function isValidSalt(string $salt): bool
210 {
211 $isValid = ($skip = false);
212 $reqLenBase64 = $this->getLengthBase64FromBytes(16);
213 if (strlen($salt) >= $reqLenBase64) {
214 // Salt with prefixed setting
215 if (!strncmp('$', $salt, 1)) {
216 if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
217 $isValid = true;
218 $salt = substr($salt, strrpos($salt, '$') + 1);
219 } else {
220 $skip = true;
221 }
222 }
223 // Checking base64 characters
224 if (!$skip && strlen($salt) >= $reqLenBase64) {
225 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
226 $isValid = true;
227 }
228 }
229 }
230 return $isValid;
231 }
232
233 /**
234 * Encodes bytes into printable base 64 using the *nix standard from crypt().
235 *
236 * @param string $input The string containing bytes to encode.
237 * @param int $count The number of characters (bytes) to encode.
238 * @return string Encoded string
239 */
240 protected function base64Encode(string $input, int $count): string
241 {
242 $output = '';
243 $i = 0;
244 $itoa64 = $this->getItoa64();
245 do {
246 $value = ord($input[$i++]);
247 $output .= $itoa64[$value & 63];
248 if ($i < $count) {
249 $value |= ord($input[$i]) << 8;
250 }
251 $output .= $itoa64[$value >> 6 & 63];
252 if ($i++ >= $count) {
253 break;
254 }
255 if ($i < $count) {
256 $value |= ord($input[$i]) << 16;
257 }
258 $output .= $itoa64[$value >> 12 & 63];
259 if ($i++ >= $count) {
260 break;
261 }
262 $output .= $itoa64[$value >> 18 & 63];
263 } while ($i < $count);
264 return $output;
265 }
266
267 /**
268 * Method determines required length of base64 characters for a given
269 * length of a byte string.
270 *
271 * @param int $byteLength Length of bytes to calculate in base64 chars
272 * @return int Required length of base64 characters
273 */
274 protected function getLengthBase64FromBytes(int $byteLength): int
275 {
276 // Calculates bytes in bits in base64
277 return (int)ceil($byteLength * 8 / 6);
278 }
279 }