Fixed bug #16352: [saltedpasswords] Login with bulk updated passwords from t3sec_salt...
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / sv1 / class.tx_saltedpasswords_sv1.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) Marcus Krause (marcus#exp2009@t3sec.info)
6 * (c) Steffen Ritter (info@rs-websystems.de)
7 * All rights reserved
8 *
9 * This script is part of the TYPO3 project. The TYPO3 project is
10 * free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * The GNU General Public License can be found at
16 * http://www.gnu.org/copyleft/gpl.html.
17 * A copy is found in the textfile GPL.txt and important notices to the license
18 * from the author is found in LICENSE.txt distributed with these scripts.
19 *
20 *
21 * This script is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * This copyright notice MUST APPEAR in all copies of the script!
27 ***************************************************************/
28 /**
29 * Contains authentication service class for salted hashed passwords.
30 *
31 * $Id$
32 */
33
34
35 /**
36 * Class implements salted-password hashes authentication service.
37 *
38 * @author Marcus Krause <marcus#exp2009@t3sec.info>
39 * @author Steffen Ritter <info@rs-websystems.de>
40 *
41 * @since 2009-06-14
42 * @package TYPO3
43 * @subpackage tx_saltedpasswords
44 */
45 class tx_saltedpasswords_sv1 extends tx_sv_authbase {
46 /**
47 * Keeps class name.
48 *
49 * @var string
50 */
51 public $prefixId = 'tx_saltedpasswords_sv1';
52
53 /**
54 * Keeps path to this script relative to the extension directory.
55 *
56 * @var string
57 */
58 public $scriptRelPath = 'sv1/class.tx_saltedpasswords_sv1.php';
59
60 /**
61 * Keeps extension key.
62 *
63 * @var string
64 */
65 public $extKey = 'saltedpasswords';
66
67 /**
68 * Keeps extension configuration.
69 *
70 * @var mixed
71 */
72 protected $extConf;
73
74 /**
75 * An instance of the salted hashing method.
76 * This member is set in the getSaltingInstance() function.
77 *
78 * @var tx_saltedpasswords_abstract_salts
79 */
80 protected $objInstanceSaltedPW = NULL;
81
82 /**
83 * Indicates whether the salted password authentication has failed.
84 *
85 * Prevents authentication bypass. See vulnerability report:
86 * { @link http://bugs.typo3.org/view.php?id=13372 }
87 *
88 * @var boolean
89 */
90 protected $authenticationFailed = FALSE;
91
92 /**
93 * Checks if service is available. In case of this service we check that
94 * following prerequesties are fulfilled:
95 * - loginSecurityLevel of according TYPO3_MODE is set to normal
96 *
97 * @return boolean TRUE if service is available
98 */
99 public function init() {
100 $available = FALSE;
101
102 if (tx_saltedpasswords_div::isUsageEnabled()) {
103 $available = TRUE;
104 $this->extConf = tx_saltedpasswords_div::returnExtConf();
105 }
106
107 return $available ? parent::init() : FALSE;
108 }
109
110 /**
111 * Checks the login data with the user record data for builtin login method.
112 *
113 * @param array user data array
114 * @param array login data array
115 * @param string login security level (optional)
116 * @return boolean TRUE if login data matched
117 */
118 function compareUident(array $user, array $loginData, $security_level = 'normal') {
119 $validPasswd = FALSE;
120
121 // could be merged; still here to clarify
122 if (!strcmp(TYPO3_MODE, 'BE')) {
123 $password = $loginData['uident_text'];
124 } else if (!strcmp(TYPO3_MODE, 'FE')) {
125 $password = $loginData['uident_text'];
126 }
127
128 // determine method used for given salted hashed password
129 $this->objInstanceSaltedPW = tx_saltedpasswords_salts_factory::getSaltingInstance($user['password']);
130
131 // existing record is in format of Salted Hash password
132 if (is_object($this->objInstanceSaltedPW)) {
133 $validPasswd = $this->objInstanceSaltedPW->checkPassword($password,$user['password']);
134
135 // record is in format of Salted Hash password but authentication failed
136 // skip further authentication methods
137 if (!$validPasswd) {
138 $this->authenticationFailed = TRUE;
139 }
140
141 $defaultHashingClassName = tx_saltedpasswords_div::getDefaultSaltingHashingMethod();
142 $skip = FALSE;
143
144 // test for wrong salted hashing method
145 if ($validPasswd && !(get_class($this->objInstanceSaltedPW) == $defaultHashingClassName) || (is_subclass_of($this->objInstanceSaltedPW, $defaultHashingClassName))) {
146 // instanciate default method class
147 $this->objInstanceSaltedPW = tx_saltedpasswords_salts_factory::getSaltingInstance(NULL);
148 $this->updatePassword(
149 intval($user['uid']),
150 array('password' => $this->objInstanceSaltedPW->getHashedPassword($password))
151 );
152 }
153
154 if ($validPasswd && !$skip && $this->objInstanceSaltedPW->isHashUpdateNeeded($user['password'])) {
155 $this->updatePassword(
156 intval($user['uid']),
157 array('password' => $this->objInstanceSaltedPW->getHashedPassword($password))
158 );
159 }
160 // we process also clear-text, md5 and passwords updated by Portable PHP password hashing framework
161 } else if (!intval($this->extConf['forceSalted'])) {
162
163 // stored password is in deprecated salted hashing method
164 if (t3lib_div::inList('C$,M$', substr($user['password'], 0, 2))) {
165
166 // instanciate default method class
167 $this->objInstanceSaltedPW = tx_saltedpasswords_salts_factory::getSaltingInstance(substr($user['password'], 1));
168
169 // md5
170 if (!strcmp(substr($user['password'], 0, 1), 'M')) {
171 $validPasswd = $this->objInstanceSaltedPW->checkPassword(md5($password), substr($user['password'], 1));
172 } else {
173 $validPasswd = $this->objInstanceSaltedPW->checkPassword($password, substr($user['password'], 1));
174 }
175
176 // skip further authentication methods
177 if (!$validPasswd) {
178 $this->authenticationFailed = TRUE;
179 }
180
181 // password is stored as md5
182 } else if (preg_match('/[0-9abcdef]{32,32}/', $user['password'])) {
183 $validPasswd = (!strcmp(md5($password), $user['password']) ? TRUE : FALSE);
184
185 // skip further authentication methods
186 if (!$validPasswd) {
187 $this->authenticationFailed = TRUE;
188 }
189
190 // password is stored plain or unrecognized format
191 } else {
192 $validPasswd = (!strcmp($password, $user['password']) ? TRUE : FALSE);
193 }
194 // should we store the new format value in DB?
195 if ($validPasswd && intval($this->extConf['updatePasswd'])) {
196 // instanciate default method class
197 $this->objInstanceSaltedPW = tx_saltedpasswords_salts_factory::getSaltingInstance(NULL);
198 $this->updatePassword(
199 intval($user['uid']),
200 array('password' => $this->objInstanceSaltedPW->getHashedPassword($password))
201 );
202 }
203 }
204
205 return $validPasswd;
206 }
207
208 /**
209 * Method adds a further authUser method.
210 *
211 * Will return one of following authentication status codes:
212 * - 0 - authentication failure
213 * - 100 - just go on. User is not authenticated but there is still no reason to stop
214 * - 200 - the service was able to authenticate the user
215 *
216 * @param array Array containing FE user data of the logged user.
217 * @return integer authentication statuscode, one of 0,100 and 200
218 */
219 public function authUser(array $user) {
220 $OK = 100;
221 $validPasswd = FALSE;
222
223 if ($this->pObj->security_level == 'rsa' && t3lib_extMgm::isLoaded('rsaauth')) {
224 require_once(t3lib_extMgm::extPath('rsaauth') . 'sv1/backends/class.tx_rsaauth_backendfactory.php');
225 require_once(t3lib_extMgm::extPath('rsaauth') . 'sv1/storage/class.tx_rsaauth_storagefactory.php');
226
227 $backend = tx_rsaauth_backendfactory::getBackend();
228 $storage = tx_rsaauth_storagefactory::getStorage();
229 // Preprocess the password
230 $password = $this->login['uident'];
231 $key = $storage->get();
232 if ($key != NULL && substr($password, 0, 4) == 'rsa:') {
233 // Decode password and pass to parent
234 $decryptedPassword = $backend->decrypt($key, substr($password, 4));
235 $this->login['uident_text'] = $decryptedPassword;
236 }
237 }
238
239 if ($this->login['uident'] && $this->login['uname']) {
240 if (!empty($this->login['uident_text'])) {
241 $validPasswd = $this->compareUident(
242 $user,
243 $this->login
244 );
245 }
246
247 if (!$validPasswd && (intval($this->extConf['onlyAuthService']) || $this->authenticationFailed)) {
248 // Failed login attempt (wrong password) - no delegation to further services
249 $this->writeLog(
250 TYPO3_MODE . ' Authentication failed - wrong password for username \'%s\'',
251 $this->login['uname']
252 );
253 $OK = 0;
254 } else if(!$validPasswd) {
255 // Failed login attempt (wrong password)
256 $this->writeLog(
257 "Login-attempt from %s, username '%s', password not accepted!",
258 $this->authInfo['REMOTE_ADDR'], $this->login['uname']
259 );
260 } else if ($validPasswd && $user['lockToDomain'] && strcasecmp($user['lockToDomain'], $this->authInfo['HTTP_HOST'])) {
261 // Lock domain didn't match, so error:
262 $this->writeLog(
263 "Login-attempt from %s, username '%s', locked domain '%s' did not match '%s'!",
264 $this->authInfo['REMOTE_ADDR'], $this->login['uname'], $user['lockToDomain'], $this->authInfo['HTTP_HOST']
265 );
266 $OK = 0;
267 } else if ($validPasswd) {
268 $this->writeLog(
269 TYPO3_MODE . ' Authentication successful for username \'%s\'',
270 $this->login['uname']
271 );
272 $OK = 200;
273 }
274 }
275
276 return $OK;
277 }
278
279 /**
280 * Method updates a FE/BE user record - in this case a new password string will be set.
281 *
282 * @param integer $uid: uid of user record that will be updated
283 * @param mixed $updateFields: Field values as key=>value pairs to be updated in database
284 * @return void
285 */
286 protected function updatePassword($uid, $updateFields) {
287 if (TYPO3_MODE === 'BE') {
288 $GLOBALS['TYPO3_DB']->exec_UPDATEquery( 'be_users', sprintf('uid = %u', $uid), $updateFields);
289 } else {
290 $GLOBALS['TYPO3_DB']->exec_UPDATEquery( 'fe_users', sprintf('uid = %u', $uid), $updateFields);
291 }
292
293 t3lib_div::devLog(sprintf('Automatic password update for %s user with uid %u', TYPO3_MODE, $uid), $this->extKey, 1);
294 }
295
296 /**
297 * Writes log message. Destination log depends on the current system mode.
298 * For FE the function writes to the admin panel log. For BE messages are
299 * sent to the system log. If developer log is enabled, messages are also
300 * sent there.
301 *
302 * This function accepts variable number of arguments and can format
303 * parameters. The syntax is the same as for sprintf()
304 *
305 * @param string $message: Message to output
306 * @return void
307 * @see sprintf()
308 * @see t3lib::divLog()
309 * @see t3lib_div::sysLog()
310 * @see t3lib_timeTrack::setTSlogMessage()
311 */
312 function writeLog($message) {
313 if (func_num_args() > 1) {
314 $params = func_get_args();
315 array_shift($params);
316 $message = vsprintf($message, $params);
317 }
318
319 if (TYPO3_MODE === 'BE') {
320 t3lib_div::sysLog($message, $this->extKey, 1);
321 } else {
322 $GLOBALS['TT']->setTSlogMessage($message);
323 }
324
325 if (TYPO3_DLOG) {
326 t3lib_div::devLog($message, $this->extKey, 1);
327 }
328 }
329 }
330
331
332 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/saltedpasswords/sv1/class.tx_saltedpasswords_sv1.php']) {
333 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/saltedpasswords/sv1/class.tx_saltedpasswords_sv1.php']);
334 }
335 ?>