36016d0d9eed6abafa598384b6ad3451bdf1e496
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Crypto / PasswordHashing / PhpassPasswordHash.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 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 PhpassPasswordHash implements PasswordHashInterface
33 {
34 /**
35 * Prefix for the password hash.
36 */
37 protected const PREFIX = '$P$';
38
39 /**
40 * @var array The default log2 number of iterations for password stretching.
41 */
42 protected $options = [
43 'hash_count' => 14
44 ];
45
46 /**
47 * Constructor sets options if given
48 *
49 * @param array $options
50 */
51 public function __construct(array $options = [])
52 {
53 $newOptions = $this->options;
54 if (isset($options['hash_count'])) {
55 if ((int)$options['hash_count'] < 7 || (int)$options['hash_count'] > 24) {
56 throw new \InvalidArgumentException(
57 'hash_count must not be lower than 7 or bigger than 24',
58 1533940454
59 );
60 }
61 $newOptions['hash_count'] = (int)$options['hash_count'];
62 }
63 $this->options = $newOptions;
64 }
65
66 /**
67 * Method checks if a given plaintext password is correct by comparing it with
68 * a given salted hashed password.
69 *
70 * @param string $plainPW Plain-text password to compare with salted hash
71 * @param string $saltedHashPW Salted hash to compare plain-text password with
72 * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
73 */
74 public function checkPassword(string $plainPW, string $saltedHashPW): bool
75 {
76 $hash = $this->cryptPassword($plainPW, $saltedHashPW);
77 return $hash && hash_equals($hash, $saltedHashPW);
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 true;
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|null 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 = $this->cryptPassword($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 HASH_COUNT.
114 *
115 * @param string $passString Salted hash to check if it needs an update
116 * @return bool TRUE if salted hash needs an update, otherwise FALSE
117 */
118 public function isHashUpdateNeeded(string $passString): bool
119 {
120 // Check whether this was an updated password.
121 if (strncmp($passString, '$P$', 3) || strlen($passString) != 34) {
122 return true;
123 }
124 // Check whether the iteration count used differs from the standard number.
125 return $this->getCountLog2($passString) < $this->options['hash_count'];
126 }
127
128 /**
129 * Method determines if a given string is a valid salted hashed password.
130 *
131 * @param string $saltedPW String to check
132 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
133 */
134 public function isValidSaltedPW(string $saltedPW): bool
135 {
136 $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
137 if ($isValid) {
138 $isValid = $this->isValidSalt($saltedPW);
139 }
140 return $isValid;
141 }
142
143 /**
144 * Method applies settings (prefix, hash count) to a salt.
145 *
146 * @param string $salt A salt to apply setting to
147 * @return string Salt with setting
148 */
149 protected function applySettingsToSalt(string $salt): string
150 {
151 $saltWithSettings = $salt;
152 $reqLenBase64 = $this->getLengthBase64FromBytes(6);
153 // Salt without setting
154 if (strlen($salt) == $reqLenBase64) {
155 // We encode the final log2 iteration count in base 64.
156 $itoa64 = $this->getItoa64();
157 $saltWithSettings = self::PREFIX . $itoa64[$this->options['hash_count']];
158 $saltWithSettings .= $salt;
159 }
160 return $saltWithSettings;
161 }
162
163 /**
164 * Hashes a password using a secure stretched hash.
165 *
166 * By using a salt and repeated hashing the password is "stretched". Its
167 * security is increased because it becomes much more computationally costly
168 * for an attacker to try to break the hash by brute-force computation of the
169 * hashes of a large number of plain-text words or strings to find a match.
170 *
171 * @param string $password Plain-text password to hash
172 * @param string $setting An existing hash or the output of getGeneratedSalt()
173 * @return mixed A string containing the hashed password (and salt)
174 */
175 protected function cryptPassword(string $password, string $setting)
176 {
177 $saltedPW = null;
178 $reqLenBase64 = $this->getLengthBase64FromBytes(6);
179 // Retrieving settings with salt
180 $setting = substr($setting, 0, strlen(self::PREFIX) + 1 + $reqLenBase64);
181 $count_log2 = $this->getCountLog2($setting);
182 // Hashes may be imported from elsewhere, so we allow != HASH_COUNT
183 if ($count_log2 >= 7 && $count_log2 <= 24) {
184 $salt = substr($setting, strlen(self::PREFIX) + 1, $reqLenBase64);
185 // We must use md5() or sha1() here since they are the only cryptographic
186 // primitives always available in PHP 5. To implement our own low-level
187 // cryptographic function in PHP would result in much worse performance and
188 // consequently in lower iteration counts and hashes that are quicker to crack
189 // (by non-PHP code).
190 $count = 1 << $count_log2;
191 $hash = md5($salt . $password, true);
192 do {
193 $hash = md5($hash . $password, true);
194 } while (--$count);
195 $saltedPW = $setting . $this->base64Encode($hash, 16);
196 // base64Encode() of a 16 byte MD5 will always be 22 characters.
197 return strlen($saltedPW) == 34 ? $saltedPW : false;
198 }
199 return $saltedPW;
200 }
201
202 /**
203 * Parses the log2 iteration count from a stored hash or setting string.
204 *
205 * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
206 * @return int Used hashcount for given hash string
207 */
208 protected function getCountLog2(string $setting): int
209 {
210 return strpos($this->getItoa64(), $setting[strlen(self::PREFIX)]);
211 }
212
213 /**
214 * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
215 *
216 * Proper use of salts may defeat a number of attacks, including:
217 * - The ability to try candidate passwords against multiple hashes at once.
218 * - The ability to use pre-hashed lists of candidate passwords.
219 * - The ability to determine whether two users have the same (or different)
220 * password without actually having to guess one of the passwords.
221 *
222 * @return string A character string containing settings and a random salt
223 */
224 protected function getGeneratedSalt(): string
225 {
226 $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
227 return $this->base64Encode($randomBytes, 6);
228 }
229
230 /**
231 * Returns a string for mapping an int to the corresponding base 64 character.
232 *
233 * @return string String for mapping an int to the corresponding base 64 character
234 */
235 protected function getItoa64(): string
236 {
237 return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
238 }
239
240 /**
241 * Method determines if a given string is a valid salt.
242 *
243 * @param string $salt String to check
244 * @return bool TRUE if it's valid salt, otherwise FALSE
245 */
246 protected function isValidSalt(string $salt): bool
247 {
248 $isValid = ($skip = false);
249 $reqLenBase64 = $this->getLengthBase64FromBytes(6);
250 if (strlen($salt) >= $reqLenBase64) {
251 // Salt with prefixed setting
252 if (!strncmp('$', $salt, 1)) {
253 if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
254 $isValid = true;
255 $salt = substr($salt, strrpos($salt, '$') + 2);
256 } else {
257 $skip = true;
258 }
259 }
260 // Checking base64 characters
261 if (!$skip && strlen($salt) >= $reqLenBase64) {
262 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
263 $isValid = true;
264 }
265 }
266 }
267 return $isValid;
268 }
269
270 /**
271 * Encodes bytes into printable base 64 using the *nix standard from crypt().
272 *
273 * @param string $input The string containing bytes to encode.
274 * @param int $count The number of characters (bytes) to encode.
275 * @return string Encoded string
276 */
277 protected function base64Encode(string $input, int $count): string
278 {
279 $output = '';
280 $i = 0;
281 $itoa64 = $this->getItoa64();
282 do {
283 $value = ord($input[$i++]);
284 $output .= $itoa64[$value & 63];
285 if ($i < $count) {
286 $value |= ord($input[$i]) << 8;
287 }
288 $output .= $itoa64[$value >> 6 & 63];
289 if ($i++ >= $count) {
290 break;
291 }
292 if ($i < $count) {
293 $value |= ord($input[$i]) << 16;
294 }
295 $output .= $itoa64[$value >> 12 & 63];
296 if ($i++ >= $count) {
297 break;
298 }
299 $output .= $itoa64[$value >> 18 & 63];
300 } while ($i < $count);
301 return $output;
302 }
303
304 /**
305 * Method determines required length of base64 characters for a given
306 * length of a byte string.
307 *
308 * @param int $byteLength Length of bytes to calculate in base64 chars
309 * @return int Required length of base64 characters
310 */
311 protected function getLengthBase64FromBytes(int $byteLength): int
312 {
313 // Calculates bytes in bits in base64
314 return (int)ceil($byteLength * 8 / 6);
315 }
316 }