0d94b90e3c0e481e1e00b51eeecfd84c7a47b695
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Crypto / PasswordHashing / Md5PasswordHash.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 MD5 salted hashing based on PHP's
23 * crypt() function.
24 *
25 * MD5 salted hashing with PHP's crypt() should be available
26 * on most of the systems.
27 */
28 class Md5PasswordHash implements PasswordHashInterface
29 {
30 /**
31 * Prefix for the password hash.
32 */
33 protected const PREFIX = '$1$';
34
35 /**
36 * Method checks if a given plaintext password is correct by comparing it with
37 * a given salted hashed password.
38 *
39 * @param string $plainPW plain-text password to compare with salted hash
40 * @param string $saltedHashPW salted hash to compare plain-text password with
41 * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
42 */
43 public function checkPassword(string $plainPW, string $saltedHashPW): bool
44 {
45 $isCorrect = false;
46 if ($this->isValidSalt($saltedHashPW)) {
47 $isCorrect = \password_verify($plainPW, $saltedHashPW);
48 }
49 return $isCorrect;
50 }
51
52 /**
53 * Returns whether all prerequisites for the hashing methods are matched
54 *
55 * @return bool Method available
56 */
57 public function isAvailable(): bool
58 {
59 return (bool)CRYPT_MD5;
60 }
61
62 /**
63 * Method creates a salted hash for a given plaintext password
64 *
65 * @param string $password plaintext password to create a salted hash from
66 * @return string Salted hashed password
67 */
68 public function getHashedPassword(string $password)
69 {
70 $saltedPW = null;
71 if (!empty($password)) {
72 if (empty($salt) || !$this->isValidSalt($salt)) {
73 $salt = $this->getGeneratedSalt();
74 }
75 $saltedPW = crypt($password, $this->applySettingsToSalt($salt));
76 }
77 return $saltedPW;
78 }
79
80 /**
81 * Checks whether a user's hashed password needs to be replaced with a new hash.
82 *
83 * This is typically called during the login process when the plain text
84 * password is available. A new hash is needed when the desired iteration
85 * count has changed through a change in the variable $hashCount or HASH_COUNT.
86 *
87 * @param string $passString Salted hash to check if it needs an update
88 * @return bool TRUE if salted hash needs an update, otherwise FALSE
89 */
90 public function isHashUpdateNeeded(string $passString): bool
91 {
92 return false;
93 }
94
95 /**
96 * Method determines if a given string is a valid salted hashed password.
97 *
98 * @param string $saltedPW String to check
99 * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
100 */
101 public function isValidSaltedPW(string $saltedPW): bool
102 {
103 $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
104 if ($isValid) {
105 $isValid = $this->isValidSalt($saltedPW);
106 }
107 return $isValid;
108 }
109
110 /**
111 * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
112 *
113 * Proper use of salts may defeat a number of attacks, including:
114 * - The ability to try candidate passwords against multiple hashes at once.
115 * - The ability to use pre-hashed lists of candidate passwords.
116 * - The ability to determine whether two users have the same (or different)
117 * password without actually having to guess one of the passwords.
118 *
119 * @return string A character string containing settings and a random salt
120 */
121 protected function getGeneratedSalt(): string
122 {
123 $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
124 return $this->base64Encode($randomBytes, 6);
125 }
126
127 /**
128 * Method applies settings (prefix, suffix) to a salt.
129 *
130 * @param string $salt A salt to apply setting to
131 * @return string Salt with setting
132 */
133 protected function applySettingsToSalt(string $salt): string
134 {
135 $saltWithSettings = $salt;
136 $reqLenBase64 = $this->getLengthBase64FromBytes(6);
137 // Salt without setting
138 if (strlen($salt) == $reqLenBase64) {
139 $saltWithSettings = self::PREFIX . $salt . '$';
140 }
141 return $saltWithSettings;
142 }
143
144 /**
145 * Returns a string for mapping an int to the corresponding base 64 character.
146 *
147 * @return string String for mapping an int to the corresponding base 64 character
148 */
149 protected function getItoa64(): string
150 {
151 return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
152 }
153
154 /**
155 * Method determines if a given string is a valid salt
156 *
157 * @param string $salt String to check
158 * @return bool TRUE if it's valid salt, otherwise FALSE
159 */
160 protected function isValidSalt(string $salt): bool
161 {
162 $isValid = ($skip = false);
163 $reqLenBase64 = $this->getLengthBase64FromBytes(6);
164 if (strlen($salt) >= $reqLenBase64) {
165 // Salt with prefixed setting
166 if (!strncmp('$', $salt, 1)) {
167 if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
168 $isValid = true;
169 $salt = substr($salt, strlen(self::PREFIX));
170 } else {
171 $skip = true;
172 }
173 }
174 // Checking base64 characters
175 if (!$skip && strlen($salt) >= $reqLenBase64) {
176 if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
177 $isValid = true;
178 }
179 }
180 }
181 return $isValid;
182 }
183
184 /**
185 * Encodes bytes into printable base 64 using the *nix standard from crypt().
186 *
187 * @param string $input The string containing bytes to encode.
188 * @param int $count The number of characters (bytes) to encode.
189 * @return string Encoded string
190 */
191 protected function base64Encode(string $input, int $count): string
192 {
193 $output = '';
194 $i = 0;
195 $itoa64 = $this->getItoa64();
196 do {
197 $value = ord($input[$i++]);
198 $output .= $itoa64[$value & 63];
199 if ($i < $count) {
200 $value |= ord($input[$i]) << 8;
201 }
202 $output .= $itoa64[$value >> 6 & 63];
203 if ($i++ >= $count) {
204 break;
205 }
206 if ($i < $count) {
207 $value |= ord($input[$i]) << 16;
208 }
209 $output .= $itoa64[$value >> 12 & 63];
210 if ($i++ >= $count) {
211 break;
212 }
213 $output .= $itoa64[$value >> 18 & 63];
214 } while ($i < $count);
215 return $output;
216 }
217
218 /**
219 * Method determines required length of base64 characters for a given
220 * length of a byte string.
221 *
222 * @param int $byteLength Length of bytes to calculate in base64 chars
223 * @return int Required length of base64 characters
224 */
225 protected function getLengthBase64FromBytes(int $byteLength): int
226 {
227 // Calculates bytes in bits in base64
228 return (int)ceil($byteLength * 8 / 6);
229 }
230 }