[!!!][TASK] Remove sysext:sv, move files to sysext:core
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / Classes / SaltedPasswordService.php
1 <?php
2 namespace TYPO3\CMS\Saltedpasswords;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
17 use TYPO3\CMS\Core\Database\ConnectionPool;
18 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Class implements salted-password hashes authentication service.
23 * Contains authentication service class for salted hashed passwords.
24 */
25 class SaltedPasswordService extends AbstractAuthenticationService
26 {
27 /**
28 * Keeps class name.
29 *
30 * @var string
31 */
32 public $prefixId = 'tx_saltedpasswords_sv1';
33
34 /**
35 * Keeps extension key.
36 *
37 * @var string
38 */
39 public $extKey = 'saltedpasswords';
40
41 /**
42 * Keeps extension configuration.
43 *
44 * @var mixed
45 */
46 protected $extConf;
47
48 /**
49 * An instance of the salted hashing method.
50 * This member is set in the getSaltingInstance() function.
51 *
52 * @var \TYPO3\CMS\Saltedpasswords\Salt\AbstractSalt
53 */
54 protected $objInstanceSaltedPW = null;
55
56 /**
57 * Indicates whether the salted password authentication has failed.
58 *
59 * Prevents authentication bypass. See vulnerability report:
60 * { @link http://forge.typo3.org/issues/22030 }
61 *
62 * @var bool
63 */
64 protected $authenticationFailed = false;
65
66 /**
67 * Checks if service is available. In case of this service we check that
68 * following prerequesties are fulfilled:
69 * - loginSecurityLevel of according TYPO3_MODE is set to normal
70 *
71 * @return bool TRUE if service is available
72 */
73 public function init()
74 {
75 $available = false;
76 $mode = TYPO3_MODE;
77 if ($this->info['requestedServiceSubType'] === 'authUserBE') {
78 $mode = 'BE';
79 } elseif ($this->info['requestedServiceSubType'] === 'authUserFE') {
80 $mode = 'FE';
81 }
82 if (\TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility::isUsageEnabled($mode)) {
83 $available = true;
84 $this->extConf = \TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility::returnExtConf();
85 }
86 return $available ? parent::init() : false;
87 }
88
89 /**
90 * Checks the login data with the user record data for builtin login method.
91 *
92 * @param array $user User data array
93 * @param array $loginData Login data array
94 * @param string $passwordCompareStrategy Password compare strategy
95 * @return bool TRUE if login data matched
96 */
97 public function compareUident(array $user, array $loginData, $passwordCompareStrategy = '')
98 {
99 $validPasswd = false;
100 $password = $loginData['uident_text'];
101 // Determine method used for given salted hashed password
102 $this->objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance($user['password']);
103 // Existing record is in format of Salted Hash password
104 if (is_object($this->objInstanceSaltedPW)) {
105 $validPasswd = $this->objInstanceSaltedPW->checkPassword($password, $user['password']);
106 // Record is in format of Salted Hash password but authentication failed
107 // skip further authentication methods
108 if (!$validPasswd) {
109 $this->authenticationFailed = true;
110 }
111 $defaultHashingClassName = \TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility::getDefaultSaltingHashingMethod();
112 $skip = false;
113 // Test for wrong salted hashing method
114 if ($validPasswd && !(get_class($this->objInstanceSaltedPW) == $defaultHashingClassName) || is_subclass_of($this->objInstanceSaltedPW, $defaultHashingClassName)) {
115 // Instantiate default method class
116 $this->objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(null);
117 $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
118 }
119 if ($validPasswd && !$skip && $this->objInstanceSaltedPW->isHashUpdateNeeded($user['password'])) {
120 $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
121 }
122 } elseif (!(int)$this->extConf['forceSalted']) {
123 // Stored password is in deprecated salted hashing method
124 $hashingMethod = substr($user['password'], 0, 2);
125 if ($hashingMethod === 'C$' || $hashingMethod === 'M$') {
126 // Instantiate default method class
127 $this->objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(substr($user['password'], 1));
128 // md5
129 if ($hashingMethod === 'M$') {
130 $validPasswd = $this->objInstanceSaltedPW->checkPassword(md5($password), substr($user['password'], 1));
131 } else {
132 $validPasswd = $this->objInstanceSaltedPW->checkPassword($password, substr($user['password'], 1));
133 }
134 // Skip further authentication methods
135 if (!$validPasswd) {
136 $this->authenticationFailed = true;
137 }
138 } elseif (preg_match('/[0-9abcdef]{32,32}/', $user['password'])) {
139 $validPasswd = \hash_equals(md5($password), (string)$user['password']);
140 // Skip further authentication methods
141 if (!$validPasswd) {
142 $this->authenticationFailed = true;
143 }
144 } else {
145 $validPasswd = (string)$password !== '' && \hash_equals((string)$user['password'], (string)$password);
146 }
147 // Should we store the new format value in DB?
148 if ($validPasswd && (int)$this->extConf['updatePasswd']) {
149 // Instantiate default method class
150 $this->objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(null);
151 $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
152 }
153 }
154 return $validPasswd;
155 }
156
157 /**
158 * Method adds a further authUser method.
159 *
160 * Will return one of following authentication status codes:
161 * - 0 - authentication failure
162 * - 100 - just go on. User is not authenticated but there is still no reason to stop
163 * - 200 - the service was able to authenticate the user
164 *
165 * @param array Array containing FE user data of the logged user.
166 * @return int Authentication statuscode, one of 0,100 and 200
167 */
168 public function authUser(array $user)
169 {
170 $OK = 100;
171 // The salted password service can only work correctly, if a non empty username along with a non empty password is provided.
172 // Otherwise a different service is allowed to check for other login credentials
173 if ((string)$this->login['uident_text'] !== '' && (string)$this->login['uname'] !== '') {
174 $validPasswd = $this->compareUident($user, $this->login);
175 if (!$validPasswd) {
176 // Failed login attempt (wrong password)
177 $errorMessage = 'Login-attempt from %s (%s), username \'%s\', password not accepted!';
178 // No delegation to further services
179 if ((int)$this->extConf['onlyAuthService'] || $this->authenticationFailed) {
180 $this->writeLogMessage(TYPO3_MODE . ' Authentication failed - wrong password for username \'%s\'', $this->login['uname']);
181 $OK = 0;
182 } else {
183 $this->writeLogMessage($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $this->login['uname']);
184 }
185 $this->writelog(255, 3, 3, 1, $errorMessage, [
186 $this->authInfo['REMOTE_ADDR'],
187 $this->authInfo['REMOTE_HOST'],
188 $this->login['uname']
189 ]);
190 GeneralUtility::sysLog(sprintf($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $this->login['uname']), 'core', GeneralUtility::SYSLOG_SEVERITY_INFO);
191 } elseif ($validPasswd && $user['lockToDomain'] && strcasecmp($user['lockToDomain'], $this->authInfo['HTTP_HOST'])) {
192 // Lock domain didn't match, so error:
193 $errorMessage = 'Login-attempt from %s (%s), username \'%s\', locked domain \'%s\' did not match \'%s\'!';
194 $this->writeLogMessage($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $this->login['uname'], $user['lockToDomain'], $this->authInfo['HTTP_HOST']);
195 $this->writelog(255, 3, 3, 1, $errorMessage, [
196 $this->authInfo['REMOTE_ADDR'],
197 $this->authInfo['REMOTE_HOST'],
198 $user[$this->db_user['username_column']],
199 $user['lockToDomain'],
200 $this->authInfo['HTTP_HOST']
201 ]);
202 GeneralUtility::sysLog(sprintf($errorMessage, $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST'], $user[$this->db_user['username_column']], $user['lockToDomain'], $this->authInfo['HTTP_HOST']), 'core', GeneralUtility::SYSLOG_SEVERITY_INFO);
203 $OK = 0;
204 } elseif ($validPasswd) {
205 $this->writeLogMessage(TYPO3_MODE . ' Authentication successful for username \'%s\'', $this->login['uname']);
206 $OK = 200;
207 }
208 }
209 return $OK;
210 }
211
212 /**
213 * Method updates a FE/BE user record - in this case a new password string will be set.
214 *
215 * @param int $uid uid of user record that will be updated
216 * @param mixed $updateFields Field values as key=>value pairs to be updated in database
217 */
218 protected function updatePassword($uid, $updateFields)
219 {
220 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
221 ->getConnectionForTable($this->pObj->user_table);
222
223 $connection->update(
224 $this->pObj->user_table,
225 $updateFields,
226 ['uid' => (int)$uid]
227 );
228
229 GeneralUtility::devLog(
230 sprintf('Automatic password update for user record in %s with uid %u', $this->pObj->user_table, $uid),
231 $this->extKey,
232 1
233 );
234 }
235
236 /**
237 * Writes log message. Destination log depends on the current system mode.
238 * For FE the function writes to the admin panel log. For BE messages are
239 * sent to the system log. If developer log is enabled, messages are also
240 * sent there.
241 *
242 * This function accepts variable number of arguments and can format
243 * parameters. The syntax is the same as for sprintf()
244 *
245 * @param string $message Message to output
246 * @param array<int, mixed> $params
247 * @see \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog()
248 */
249 public function writeLogMessage($message, ...$params)
250 {
251 if (!empty($params)) {
252 $message = vsprintf($message, $params);
253 }
254 if (TYPO3_MODE === 'BE') {
255 GeneralUtility::sysLog($message, $this->extKey, GeneralUtility::SYSLOG_SEVERITY_NOTICE);
256 } else {
257 /** @var TimeTracker $timeTracker */
258 $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
259 $timeTracker->setTSlogMessage($message);
260 }
261 GeneralUtility::devLog($message, $this->extKey, GeneralUtility::SYSLOG_SEVERITY_NOTICE);
262 }
263 }