Added feature #16360: [saltedpasswords] Provide a task for bulk update of passwords...
authorChristian Kuhn <lolli@schwarzbu.ch>
Tue, 16 Nov 2010 14:31:17 +0000 (14:31 +0000)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 16 Nov 2010 14:31:17 +0000 (14:31 +0000)
git-svn-id: https://svn.typo3.org/TYPO3v4/Core/trunk@9401 709f56b5-9817-0410-a4d7-c38de5d9e867

ChangeLog
typo3/sysext/saltedpasswords/classes/salts/class.tx_saltedpasswords_salts_factory.php
typo3/sysext/saltedpasswords/classes/tasks/class.tx_saltedpasswords_tasks_bulkupdate.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/ext_autoload.php
typo3/sysext/saltedpasswords/ext_localconf.php
typo3/sysext/saltedpasswords/locallang.xml

index bf0f25d..3080a0d 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2010-11-16  Christian Kuhn  <lolli@schwarzbu.ch>
+
+       * Added feature #16360: [saltedpasswords] Provide a task for bulk update of passwords for use with saltedpasswords (Thanks to Marcus Krause)
+
 2010-11-16  Steffen Gebert  <steffen@steffen-gebert.de>
 
        * Follow-up to #16403: Fixed inclusion of JS files in template.php (Thanks to Steffen Ritter)
index 6b8b839..04ef9ca 100644 (file)
@@ -95,7 +95,7 @@ class tx_saltedpasswords_salts_factory {
         * @param       string          $saltedHash
         * @return      boolean         TRUE, if salting hashing method has been found, otherwise FALSE
         */
-       static protected function determineSaltingHashingMethod($saltedHash) {
+       static public function determineSaltingHashingMethod($saltedHash) {
                $methodFound = FALSE;
                $defaultMethods = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods'];
                foreach($defaultMethods as $method) {
diff --git a/typo3/sysext/saltedpasswords/classes/tasks/class.tx_saltedpasswords_tasks_bulkupdate.php b/typo3/sysext/saltedpasswords/classes/tasks/class.tx_saltedpasswords_tasks_bulkupdate.php
new file mode 100644 (file)
index 0000000..643b949
--- /dev/null
@@ -0,0 +1,244 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2010 Christian Kuhn <lolli@schwarzbu.ch>
+*              Marcus Krause <marcus#exp2010@t3sec.info>
+*
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+
+/**
+ * Update plaintext and hashed passwords of existing users to salted passwords.
+ *
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
+ * @author Marcus Krause <marcus#exp2010@t3sec.info>
+ * @package TYPO3
+ * @subpackage saltedpasswords
+ */
+class tx_saltedpasswords_Tasks_BulkUpdate extends tx_scheduler_Task {
+       /**
+        * @var boolean Whether or not the task is allowed to deactivate itself after processing all existing user records.
+        * @TODO: This could be set with an additional field later on.
+        *              The idea is to not disable the task after all initial users where handled.
+        *              This could be handy for example if new users are imported regularily from some external source.
+        */
+       protected $canDeactivateSelf = TRUE;
+
+       /**
+        * Converting a password to a salted hash takes some milliseconds (~100ms on an entry system in 2010).
+        * If all users are updated in one run, the task might run a long time if a lot of users must be handled.
+        * Therefore only a small number of frontend and backend users are processed.
+        * If saltedpasswords is enabled for both frontend and backend 2 * numberOfRecords will be handled.
+        *
+        * @var integer Number of records
+        * @TODO: This could be set with an additional field later on
+        */
+       protected $numberOfRecords = 42; // 23 is too low ;)
+
+       /**
+        * @var integer Pointer to last handled frontend and backend user row
+        */
+       protected $userRecordPointer = array();
+
+       /**
+        * Constructor initializes user record pointer
+        *
+        * @return void
+        */
+       public function __construct() {
+               parent::__construct();
+
+               $this->userRecordPointer = array(
+                       'FE' => 0,
+                       'BE' => 0,
+               );
+       }
+
+       /**
+        * Execute task
+        *
+        * @return void
+        */
+       public function execute() {
+               $processedAllRecords = TRUE;
+
+                       // For frontend and backend
+               foreach ($this->userRecordPointer as $mode => $pointer) {
+                               // If saltedpasswords is active for frontend / backend
+                       if (tx_saltedpasswords_div::isUsageEnabled($mode)) {
+                               $usersToUpdate = $this->findUsersToUpdate($mode);
+                               $numberOfRows = count($usersToUpdate);
+                               if ($numberOfRows > 0) {
+                                       $processedAllRecords = FALSE;
+                                       $this->incrementUserRecordPointer($mode, $numberOfRows);
+                                       $this->convertPasswords($mode, $usersToUpdate);
+                               }
+                       }
+               }
+
+                       // Determine if task should disable itself
+               if ($this->canDeactivateSelf && $processedAllRecords) {
+                       $this->deactivateSelf();
+               }
+
+                       // Use save() of parent class tx_scheduler_Task to persist
+                       // changed task variables: $this->userRecordPointer and $this->disabled
+               $this->save();
+
+               return(TRUE);
+       }
+
+       /**
+        * Find next set of frontend or backend users to update.
+        *
+        * @param string 'FE' for frontend, 'BE' for backend user records
+        * @return array Rows with uid and password
+        */
+       protected function findUsersToUpdate($mode) {
+               $usersToUpdate = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
+                       'uid, password',
+                       strtolower($mode) . '_users',
+                       '1 = 1',  // retrieve and update all records (also disabled/deleted) for security reasons
+                       '',
+                       'uid ASC',
+                       $this->userRecordPointer[$mode] . ', ' . $this->numberOfRecords
+               );
+
+               return $usersToUpdate;
+       }
+
+       /**
+        * Iterate over given user records and update password if needed.
+        *
+        * @param string 'FE' for frontend, 'BE' for backend user records
+        * @param array with user uids and passwords
+        * @return void
+        */
+       protected function convertPasswords($mode, $users) {
+               $updateUsers = array();
+               foreach ($users as $user) {
+                               // If a password is already a salted hash it must not be updated
+                       if ($this->isSaltedHash($user['password'])) {
+                               continue;
+                       }
+
+                       $updateUsers[] = $user;
+               }
+
+               if (count($updateUsers) > 0) {
+                       $this->updatePasswords($mode, $updateUsers);
+               }
+       }
+
+       /**
+        * Update password and persist salted hash.
+        *
+        * @param string 'FE' for frontend, 'BE' for backend user records
+        * @param array with user uids and passwords
+        * @return void
+        */
+       protected function updatePasswords($mode, $users) {
+                       // Get a default saltedpasswords instance
+               $saltedpasswordsInstance = tx_saltedpasswords_salts_factory::getSaltingInstance(NULL, $mode);
+               
+               foreach ($users as $user) {
+                       $newPassword = $saltedpasswordsInstance->getHashedPassword($user['password']);
+
+                               // If a given password is a md5 hash (usually default be_users without saltedpasswords activated),
+                               // result of getHasedPassword() is a salted hashed md5 hash.
+                               // We prefix those with 'M', saltedpasswords will then update this password
+                               // to a usual salted hash upon first login of the user.
+                       if ($this->isMd5Password($user['password'])) {
+                               $newPassword = 'M' . $newPassword;
+                       }
+
+                               // Persist updated password
+                       $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
+                               strtolower($mode) . '_users',
+                               'uid = ' . $user['uid'],
+                               array(
+                                       'password' => $newPassword
+                               )
+                       );
+               }
+       }
+
+       /**
+        * Passwords prefixed with M or C might be salted passwords:
+        *      M means: originally a md5 hash before it was salted (eg. default be_users).
+        *      C means: originally a cleartext password with lower hash looping count generated by t3sec_saltedpw.
+        * Both M and C will be updated to usual salted hashes on first login of user.
+        *
+        * If a password does not start with M or C determine if a password is already a usual salted hash.
+        *
+        * @param string Password
+        * @return boolean True if password is a salted hash
+        */
+       protected function isSaltedHash($password) {
+               $isSaltedHash = FALSE;
+               if (strlen($password) > 2 && (t3lib_div::isFirstPartOfStr($password, 'C$') || t3lib_div::isFirstPartOfStr($password, 'M$'))) {
+                               // Cut off M or C and test if we have a salted hash
+                       $isSaltedHash = tx_saltedpasswords_salts_factory::determineSaltingHashingMethod(substr($password, 1));
+               }
+
+                       // Test if given password is a already a usual salted hash
+               if (!$isSaltedHash) {
+                       $isSaltedHash = tx_saltedpasswords_salts_factory::determineSaltingHashingMethod($password);
+               }
+
+               return $isSaltedHash;
+       }
+
+       /**
+        * Check if a given password is a md5 hash, the default for be_user records before saltedpasswords.
+        *
+        * @return boolean TRUE if password is md5
+        */
+       protected function isMd5Password($password) {
+               return (bool) preg_match('/[0-9abcdef]{32,32}/i', $password);
+       }
+
+       /**
+        * Increment current user record counter by number of handled rows.
+        *
+        * @param string 'FE' for frontend, 'BE' for backend user records
+        * @param integer Number of handled rows
+        * @return void
+        */
+       protected function incrementUserRecordPointer($mode, $number) {
+               $this->userRecordPointer[$mode] += $number;
+       }
+
+       /**
+        * Deactivate this task instance.
+        * Uses setDisabled() method of parent class tx_scheduler_Task.
+        *
+        * @return void
+        */
+       protected function deactivateSelf() {
+               $this->setDisabled(TRUE);
+       }
+} // End of class
+
+if (defined('TYPO3_MODE') && $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/saltedpasswords/classes/tasks/class.tx_saltedpasswords_tasks_bulkupdate.php']) {
+       include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/saltedpasswords/classes/tasks/class.tx_saltedpasswords_tasks_bulkupdate.php']);
+}
+
+?>
index 5f60acd..3b7eb78 100644 (file)
@@ -15,6 +15,7 @@ return array(
        'tx_saltedpasswords_eval' => $extensionPath . 'classes/eval/class.tx_saltedpasswords_eval.php',
        'tx_saltedpasswords_eval_be' => $extensionPath . 'classes/eval/class.tx_saltedpasswords_eval_be.php',
        'tx_saltedpasswords_eval_fe' => $extensionPath . 'classes/eval/class.tx_saltedpasswords_eval_fe.php',
+       'tx_saltedpasswords_tasks_bulkupdate' => $extensionPath . 'classes/tasks/class.tx_saltedpasswords_tasks_bulkupdate.php',
        'tx_saltedpasswords_sv1' => $extensionPath . 'sv1/class.tx_saltedpasswords_sv1.php',
        'tx_saltedpasswords_div_testcase' => $extensionPath . 'tests/tx_saltedpasswords_div_testcase.php',
        'tx_saltedpasswords_salts_blowfish_testcase' => $extensionPath . 'tests/tx_saltedpasswords_salts_blowfish_testcase.php',
index b65d273..f939413 100644 (file)
@@ -39,4 +39,13 @@ t3lib_extMgm::addService(
 
 // Use popup window to refresh login instead of the AJAX relogin:
 $TYPO3_CONF_VARS['BE']['showRefreshLoginPopup'] = 1;
-?>
\ No newline at end of file
+
+       // Register bulk update task
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']['tx_saltedpasswords_Tasks_BulkUpdate'] = array(
+       'extension' => $_EXTKEY,
+       'title' => 'LLL:EXT:' . $_EXTKEY . '/locallang.xml:ext.saltedpasswords.tasks.bulkupdate.name',
+       'description' => 'LLL:EXT:' . $_EXTKEY . '/locallang.xml:ext.saltedpasswords.tasks.bulkupdate.description',
+       'additionalFields' => '',
+);
+
+?>
index 3e34bf2..2a67721 100644 (file)
@@ -9,6 +9,8 @@
                        <label index="ext.saltedpasswords.title.tx_saltedpasswords_salts_phpass">Portable PHP password hashing (phpass)</label>
                        <label index="ext.saltedpasswords.title.tx_saltedpasswords_salts_md5">MD5 salted hashing (secure)</label>
                        <label index="ext.saltedpasswords.title.tx_saltedpasswords_salts_blowfish">Blowfish salted hashing (advanced)</label>
+                       <label index="ext.saltedpasswords.tasks.bulkupdate.name">Convert user passwords to salted hashes</label>
+                       <label index="ext.saltedpasswords.tasks.bulkupdate.description">Update all frontend and backend user passwords to salted hashes. This task deactivates itself when completed.</label>
                </languageKey>
        </data>
-</T3locallang>
\ No newline at end of file
+</T3locallang>