configurations are $wgDeletedDirectory and $wgHashedUploadDirectory.
* The deprecated $wgUseCommaCount variable has been removed.
* $wgEnableSorbs and $wgSorbsUrl have been removed.
+* The UserCryptPassword and UserComparePassword hooks are no longer called. Any extensions
+ using them must be updated to use the Password Hashing API.
=== New features in 1.24 ===
* Added a new hook, "WhatLinksHereProps", to allow extensions to annotate
no longer be displayed in the sidebar when $wgInterwikiMagic is true.
* New special page, MyLanguages, to redirect users to subpages with localised
versions of a page. (Integrated from Extension:Translate)
+* MediaWiki now supports multiple password types, including bcrypt and PBKDF2.
+ The default type can be changed with $wgPasswordDefault and the type
+ configurations can be changed with $wgPasswordConfig.
=== Bug fixes in 1.24 ===
* (bug 49116) Footer copyright notice is now always displayed in user language
$oldid: ID of the talk page revision being viewed (0 means the most recent one)
'UserComparePasswords': Called when checking passwords, return false to
-override the default password checks.
+override the default password checks. REMOVED since 1.23, use Password class instead.
&$hash: String of the password hash (from the database)
&$password: String of the plaintext password the user entered
&$userId: Integer of the user's ID or Boolean false if the user ID was not
$template: SimpleTemplate instance for the form
'UserCryptPassword': Called when hashing a password, return false to implement
-your own hashing method.
+your own hashing method. REMOVED since 1.23, use Password class instead.
&$password: String of the plaintext password to encrypt
&$salt: String of the password salt or Boolean false if no salt is provided
&$wgPasswordSalt: Boolean of whether the salt is used in the default hashing
'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php',
'StripState' => 'includes/parser/StripState.php',
+ # includes/password
+ 'BcryptPassword' => 'includes/password/BcryptPassword.php',
+ 'InvalidPassword' => 'includes/password/InvalidPassword.php',
+ 'LayeredParameterizedPassword' => 'includes/password/LayeredParameterizedPassword.php',
+ 'MWSaltedPassword' => 'includes/password/MWSaltedPassword.php',
+ 'MWOldPassword' => 'includes/password/MWOldPassword.php',
+ 'ParameterizedPassword' => 'includes/password/ParameterizedPassword.php',
+ 'Password' => 'includes/password/Password.php',
+ 'PasswordFactory' => 'includes/password/PasswordFactory.php',
+ 'Pbkdf2Password' => 'includes/password/Pbkdf2Password.php',
+ 'EncryptedPassword' => 'includes/password/EncryptedPassword.php',
+
# includes/profiler
'Profiler' => 'includes/profiler/Profiler.php',
'ProfilerMwprof' => 'includes/profiler/ProfilerMwprof.php',
/**
* For compatibility with old installations set to false
+ * @deprecated since 1.24 will be removed in future
*/
$wgPasswordSalt = true;
*/
$wgInvalidPasswordReset = true;
+/*
+ * Default password type to use when hashing user passwords
+ *
+ * @since 1.24
+ */
+$wgPasswordDefault = 'B';
+
+/**
+ * Configuration for built-in password types. Maps the password type
+ * to an array of options. The 'class' option is the Password class to
+ * use. All other options are class-dependent.
+ *
+ * An advanced example:
+ * @code
+ * $wgPasswordConfig['bcrypt-peppered'] = array(
+ * 'class' => 'EncryptedPassword',
+ * 'underlying' => 'bcrypt',
+ * 'secrets' => array(),
+ * 'cipher' => MCRYPT_RIJNDAEL_256,
+ * 'mode' => MCRYPT_MODE_CBC,
+ * 'cost' => 5,
+ * );
+ * @endcode
+ *
+ * @since 1.24
+ */
+$wgPasswordConfig = array(
+ 'A' => array(
+ 'class' => 'MWOldPassword',
+ ),
+ 'B' => array(
+ 'class' => 'MWSaltedPassword',
+ ),
+ 'pbkdf2-legacyA' => array(
+ 'class' => 'LayeredParameterizedPassword',
+ 'types' => array(
+ 'A',
+ 'pbkdf2',
+ ),
+ ),
+ 'pbkdf2-legacyB' => array(
+ 'class' => 'LayeredParameterizedPassword',
+ 'types' => array(
+ 'B',
+ 'pbkdf2',
+ ),
+ ),
+ 'bcrypt' => array(
+ 'class' => 'BcryptPassword',
+ 'cost' => 9,
+ ),
+ 'pbkdf2' => array(
+ 'class' => 'Pbkdf2Password',
+ 'algo' => 'sha256',
+ 'cost' => '10000',
+ 'length' => '128',
+ ),
+);
+
/**
* Whether to allow password resets ("enter some identifying data, and we'll send an email
* with a temporary password you can use to get back into the account") identified by
* Int Serialized record version.
* @ingroup Constants
*/
-define( 'MW_USER_VERSION', 9 );
+define( 'MW_USER_VERSION', 10 );
/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
*/
const MAX_WATCHED_ITEMS_CACHE = 100;
+ /**
+ * @var PasswordFactory Lazily loaded factory object for passwords
+ */
+ private static $mPasswordFactory = null;
+
/**
* Array of Strings List of member variables which are saved to the
* shared cache (memcached). Any operation which changes the
'mId',
'mName',
'mRealName',
- 'mPassword',
- 'mNewpassword',
- 'mNewpassTime',
'mEmail',
'mTouched',
'mToken',
'mEmailAuthenticated',
'mEmailToken',
'mEmailTokenExpires',
- 'mPasswordExpires',
'mRegistration',
'mEditCount',
// user_groups table
public function loadDefaults( $name = false ) {
wfProfileIn( __METHOD__ );
+ $passwordFactory = self::getPasswordFactory();
+
$this->mId = 0;
$this->mName = $name;
$this->mRealName = '';
- $this->mPassword = $this->mNewpassword = '';
+ $this->mPassword = $passwordFactory->newFromCiphertext( null );
+ $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
$this->mNewpassTime = null;
$this->mEmail = '';
$this->mOptionOverrides = null;
*/
public function loadFromRow( $row, $data = null ) {
$all = true;
+ $passwordFactory = self::getPasswordFactory();
$this->mGroups = null; // deferred
}
if ( isset( $row->user_password ) ) {
- $this->mPassword = $row->user_password;
- $this->mNewpassword = $row->user_newpassword;
+ // Check for *really* old password hashes that don't even have a type
+ // The old hash format was just an md5 hex hash, with no type information
+ if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
+ $row->user_password = ":A:{$this->mId}:{$row->user_password}";
+ }
+
+ try {
+ $this->mPassword = $passwordFactory->newFromCiphertext( $row->user_password );
+ } catch ( PasswordError $e ) {
+ wfDebug( 'Invalid password hash found in database.' );
+ $this->mPassword = $passwordFactory->newFromCiphertext( null );
+ }
+
+ try {
+ $this->mNewpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+ } catch ( PasswordError $e ) {
+ wfDebug( 'Invalid password hash found in database.' );
+ $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
+ }
+
$this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
+ $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
+ }
+
+ if ( isset( $row->user_email ) ) {
$this->mEmail = $row->user_email;
$this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
$this->mToken = $row->user_token;
$this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
$this->mEmailToken = $row->user_email_token;
$this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
- $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
$this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
} else {
$all = false;
}
}
+ /**
+ * Load the user's password hashes from the database
+ *
+ * This is usually called in a scenario where the actual User object was
+ * loaded from the cache, and then password comparison needs to be performed.
+ * Password hashes are not stored in memcached.
+ *
+ * @since 1.24
+ */
+ private function loadPasswords() {
+ if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) {
+ $this->loadFromRow( wfGetDB( DB_MASTER )->selectRow(
+ 'user',
+ array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
+ array( 'user_id' => $this->getId() ),
+ __METHOD__
+ ) );
+ }
+ }
+
/**
* Add the user to the group if he/she meets given criteria.
*
*
* Called implicitly from invalidateCache() and saveSettings().
*/
- private function clearSharedCache() {
+ public function clearSharedCache() {
$this->load();
if ( $this->mId ) {
global $wgMemc;
* through the web interface.
*/
public function setInternalPassword( $str ) {
- $this->load();
$this->setToken();
+ $passwordFactory = self::getPasswordFactory();
if ( $str === null ) {
- // Save an invalid hash...
- $this->mPassword = '';
+ $this->mPassword = $passwordFactory->newFromCiphertext( null );
} else {
- $this->mPassword = self::crypt( $str );
+ $this->mPassword = $passwordFactory->newFromPlaintext( $str );
}
- $this->mNewpassword = '';
+
+ $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
$this->mNewpassTime = null;
}
$this->mNewpassword = '';
$this->mNewpassTime = null;
} else {
- $this->mNewpassword = self::crypt( $str );
+ $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str );
if ( $throttle ) {
$this->mNewpassTime = wfTimestampNow();
}
global $wgAuth;
$this->load();
+ $this->loadPasswords();
if ( wfReadOnly() ) {
return;
}
$this->mTouched = self::newTouchedTimestamp();
if ( !$wgAuth->allowSetLocalPassword() ) {
- $this->mPassword = '';
+ $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
}
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'user',
array( /* SET */
'user_name' => $this->mName,
- 'user_password' => $this->mPassword,
- 'user_newpassword' => $this->mNewpassword,
+ 'user_password' => $this->mPassword->toString(),
+ 'user_newpassword' => $this->mNewpassword->toString(),
'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
'user_real_name' => $this->mRealName,
'user_email' => $this->mEmail,
public static function createNew( $name, $params = array() ) {
$user = new User;
$user->load();
+ $user->loadPasswords();
$user->setToken(); // init token
if ( isset( $params['options'] ) ) {
$user->mOptions = $params['options'] + (array)$user->mOptions;
$fields = array(
'user_id' => $seqVal,
'user_name' => $name,
- 'user_password' => $user->mPassword,
- 'user_newpassword' => $user->mNewpassword,
+ 'user_password' => $user->mPassword->toString(),
+ 'user_newpassword' => $user->mNewpassword->toString(),
'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
'user_email' => $user->mEmail,
'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
*/
public function addToDatabase() {
$this->load();
+ $this->loadPasswords();
if ( !$this->mToken ) {
$this->setToken(); // init token
}
array(
'user_id' => $seqVal,
'user_name' => $this->mName,
- 'user_password' => $this->mPassword,
- 'user_newpassword' => $this->mNewpassword,
+ 'user_password' => $this->mPassword->toString(),
+ 'user_newpassword' => $this->mNewpassword->toString(),
'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
'user_email' => $this->mEmail,
'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
*/
public function checkPassword( $password ) {
global $wgAuth, $wgLegacyEncoding;
- $this->load();
+ $this->loadPasswords();
// Certain authentication plugins do NOT want to save
// domain passwords in a mysql database, so we should
// Auth plugin doesn't allow local authentication for this user name
return false;
}
- if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
- return true;
- } elseif ( $wgLegacyEncoding ) {
- // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
- // Check for this with iconv
- $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
- if ( $cp1252Password != $password
- && self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId )
- ) {
- return true;
+
+ $passwordFactory = self::getPasswordFactory();
+ if ( !$this->mPassword->equals( $password ) ) {
+ if ( $wgLegacyEncoding ) {
+ // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+ // Check for this with iconv
+ $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
+ if ( $cp1252Password === $password || !$this->mPassword->equals( $cp1252Password ) ) {
+ return false;
+ }
+ } else {
+ return false;
}
}
- return false;
+
+ if ( $passwordFactory->needsUpdate( $this->mPassword ) ) {
+ $this->mPassword = $passwordFactory->newFromPlaintext( $password );
+ $this->saveSettings();
+ }
+
+ return true;
}
/**
global $wgNewPasswordExpiry;
$this->load();
- if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
+ if ( $this->mNewpassword->equals( $plaintext ) ) {
if ( is_null( $this->mNewpassTime ) ) {
return true;
}
return $msg->isBlank() ? $right : $msg->text();
}
- /**
- * Make an old-style password hash
- *
- * @param string $password Plain-text password
- * @param string $userId User ID
- * @return string Password hash
- */
- public static function oldCrypt( $password, $userId ) {
- global $wgPasswordSalt;
- if ( $wgPasswordSalt ) {
- return md5( $userId . '-' . md5( $password ) );
- } else {
- return md5( $password );
- }
- }
-
/**
* Make a new-style password hash
*
* @param bool|string $salt Optional salt, may be random or the user ID.
* If unspecified or false, will generate one automatically
* @return string Password hash
+ * @deprecated since 1.23, use Password class
*/
public static function crypt( $password, $salt = false ) {
- global $wgPasswordSalt;
-
- $hash = '';
- if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
- return $hash;
- }
-
- if ( $wgPasswordSalt ) {
- if ( $salt === false ) {
- $salt = MWCryptRand::generateHex( 8 );
- }
- return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
- } else {
- return ':A:' . md5( $password );
- }
+ wfDeprecated( __METHOD__, '1.23' );
+ $hash = self::getPasswordFactory()->newFromPlaintext( $password );
+ return $hash->toString();
}
/**
* @param string|bool $userId User ID for old-style password salt
*
* @return bool
+ * @deprecated since 1.23, use Password class
*/
public static function comparePasswords( $hash, $password, $userId = false ) {
- $type = substr( $hash, 0, 3 );
-
- $result = false;
- if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
- return $result;
+ wfDeprecated( __METHOD__, '1.23' );
+
+ // Check for *really* old password hashes that don't even have a type
+ // The old hash format was just an md5 hex hash, with no type information
+ if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
+ global $wgPasswordSalt;
+ if ( $wgPasswordSalt ) {
+ $password = ":B:{$userId}:{$hash}";
+ } else {
+ $password = ":A:{$hash}";
+ }
}
- if ( $type == ':A:' ) {
- // Unsalted
- return md5( $password ) === substr( $hash, 3 );
- } elseif ( $type == ':B:' ) {
- // Salted
- list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
- return md5( $salt . '-' . md5( $password ) ) === $realHash;
- } else {
- // Old-style
- return self::oldCrypt( $password, $userId ) === $hash;
- }
+ $hash = self::getPasswordFactory()->newFromCiphertext( $hash );
+ return $hash->equals( $password );
}
/**
$dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
}
+ /**
+ * Lazily instantiate and return a factory object for making passwords
+ *
+ * @return PasswordFactory
+ */
+ public static function getPasswordFactory() {
+ if ( self::$mPasswordFactory === null ) {
+ self::$mPasswordFactory = new PasswordFactory();
+ self::$mPasswordFactory->init( RequestContext::getMain()->getConfig() );
+ }
+
+ return self::$mPasswordFactory;
+ }
+
/**
* Provide an array of HTML5 attributes to put on an input element
* intended for the user to enter a new password. This may include
'user_id',
'user_name',
'user_real_name',
- 'user_password',
- 'user_newpassword',
- 'user_newpass_time',
'user_email',
'user_touched',
'user_token',
'user_email_authenticated',
'user_email_token',
'user_email_token_expires',
- 'user_password_expires',
'user_registration',
'user_editcount',
);
--- /dev/null
+<?php
+/**
+ * Implements the BcryptPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * A Bcrypt-hashed password
+ *
+ * This is a computationally complex password hash for use in modern applications.
+ * The number of rounds can be configured by $wgPasswordCost.
+ *
+ * @since 1.24
+ */
+class BcryptPassword extends ParameterizedPassword {
+ protected function getDefaultParams() {
+ return array(
+ 'rounds' => $this->config['cost'],
+ );
+ }
+
+ protected function getDelimiter() {
+ return '$';
+ }
+
+ protected function parseHash( $hash ) {
+ parent::parseHash( $hash );
+
+ $this->params['rounds'] = (int)$this->params['rounds'];
+ }
+
+ /**
+ * @param string $password Password to encrypt
+ *
+ * @throws PasswordError If bcrypt has an unknown error
+ * @throws MWException If bcrypt is not supported by PHP
+ */
+ public function crypt( $password ) {
+ if ( !defined( 'CRYPT_BLOWFISH' ) ) {
+ throw new MWException( 'Bcrypt is not supported.' );
+ }
+
+ // Either use existing hash or make a new salt
+ // Bcrypt expects 22 characters of base64-encoded salt
+ // Note: bcrypt does not use MIME base64. It uses its own base64 without any '=' padding.
+ // It expects a 128 bit salt, so it will ignore anything after the first 128 bits
+ if ( !isset( $this->args[0] ) ) {
+ $this->args[] = substr(
+ // Replace + with ., because bcrypt uses a non-MIME base64 format
+ strtr(
+ // Random base64 encoded string
+ base64_encode( MWCryptRand::generate( 16, true ) ),
+ '+', '.'
+ ),
+ 0, 22
+ );
+ }
+
+ $hash = crypt( $password,
+ sprintf( '$2y$%02d$%s', (int)$this->params['rounds'], $this->args[0] ) );
+
+ if ( !is_string( $hash ) || strlen( $hash ) <= 13 ) {
+ throw new PasswordError( 'Error when hashing password.' );
+ }
+
+ // Strip the $2y$
+ $parts = explode( $this->getDelimiter(), substr( $hash, 4 ) );
+ $this->params['rounds'] = (int)$parts[0];
+ $this->args[0] = substr( $parts[1], 0, 22 );
+ $this->hash = substr( $parts[1], 22 );
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the EncryptedPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Helper class for passwords that use another password hash underneath it
+ * and encrypts that hash with a configured secret.
+ *
+ * @since 1.24
+ */
+class EncryptedPassword extends ParameterizedPassword {
+ protected function getDelimiter() {
+ return ':';
+ }
+
+ protected function getDefaultParams() {
+ return array(
+ 'cipher' => $this->config['cipher'],
+ 'secret' => count( $this->config['secrets'] ) - 1
+ );
+ }
+
+ public function crypt( $password ) {
+ $secret = $this->config['secrets'][$this->params['secret']];
+
+ if ( $this->hash ) {
+ $underlyingPassword = $this->factory->newFromCiphertext( openssl_decrypt(
+ base64_decode( $this->hash ), $this->params['cipher'],
+ $secret, 0, base64_decode( $this->args[0] )
+ ) );
+ } else {
+ $underlyingPassword = $this->factory->newFromType( $this->config['underlying'], $this->config );
+ }
+
+ $underlyingPassword->crypt( $password );
+ $iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true );
+
+ $this->hash = openssl_encrypt(
+ $underlyingPassword->toString(), $this->params['cipher'], $secret, 0, $iv );
+ $this->args = array( base64_encode( $iv ) );
+ }
+
+ /**
+ * Updates the underlying hash by encrypting it with the newest secret.
+ *
+ * @throws MWException If the configuration is not valid
+ * @return bool True if the password was updated
+ */
+ public function update() {
+ if ( count( $this->args ) != 2 || $this->params == $this->getDefaultParams() ) {
+ // Hash does not need updating
+ return false;
+ }
+
+ // Decrypt the underlying hash
+ $underlyingHash = openssl_decrypt(
+ base64_decode( $this->args[1] ),
+ $this->params['cipher'],
+ $this->config['secrets'][$this->params['secret']],
+ 0,
+ base64_decode( $this->args[0] )
+ );
+
+ // Reset the params
+ $this->params = $this->getDefaultParams();
+
+ // Check the key size with the new params
+ $iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true );
+ $this->hash = base64_encode( openssl_encrypt(
+ $underlyingHash,
+ $this->params['cipher'],
+ $this->config['secrets'][$this->params['secret']],
+ 0,
+ $iv
+ ) );
+ $this->args = array( base64_encode( $iv ) );
+
+ return true;
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the InvalidPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Represents an invalid password hash. It is represented as the empty string (i.e.,
+ * a password hash with no type).
+ *
+ * No two invalid passwords are equal. Comparing anything to an invalid password will
+ * return false.
+ *
+ * @since 1.24
+ */
+class InvalidPassword extends Password {
+ public function crypt( $plaintext ) {
+ }
+
+ public function toString() {
+ return '';
+ }
+
+ public function equals( $other ) {
+ return false;
+ }
+
+ public function needsUpdate() {
+ return false;
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the LayeredParameterizedPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * This password hash type layers one or more parameterized password types
+ * on top of each other.
+ *
+ * The underlying types must be parameterized. This wrapping type accumulates
+ * all the parameters and arguments from each hash and then passes the hash of
+ * the last layer as the password for the next layer.
+ *
+ * @since 1.24
+ */
+class LayeredParameterizedPassword extends ParameterizedPassword {
+ protected function getDelimiter() {
+ return '!';
+ }
+
+ protected function getDefaultParams() {
+ $params = array();
+
+ foreach ( $this->config['types'] as $type ) {
+ $passObj = $this->factory->newFromType( $type );
+
+ if ( !$passObj instanceof ParameterizedPassword ) {
+ throw new MWException( 'Underlying type must be a parameterized password.' );
+ } elseif ( $passObj->getDelimiter() === $this->getDelimiter() ) {
+ throw new MWException( 'Underlying type cannot use same delimiter as encapsulating type.' );
+ }
+
+ $params[] = implode( $passObj->getDelimiter(), $passObj->getDefaultParams() );
+ }
+
+ return $params;
+ }
+
+ public function crypt( $password ) {
+ $lastHash = $password;
+ foreach ( $this->config['types'] as $i => $type ) {
+ // Construct pseudo-hash based on params and arguments
+ /** @var ParameterizedPassword $passObj */
+ $passObj = $this->factory->newFromType( $type );
+
+ $params = '';
+ $args = '';
+ if ( $this->params[$i] !== '' ) {
+ $params = $this->params[$i] . $passObj->getDelimiter();
+ }
+ if ( isset( $this->args[$i] ) && $this->args[$i] !== '' ) {
+ $args = $this->args[$i] . $passObj->getDelimiter();
+ }
+ $existingHash = ":$type:" . $params . $args . $this->hash;
+
+ // Hash the last hash with the next type in the layer
+ $passObj = $this->factory->newFromCiphertext( $existingHash );
+ $passObj->crypt( $lastHash );
+
+ // Move over the params and args
+ $this->params[$i] = implode( $passObj->getDelimiter(), $passObj->params );
+ $this->args[$i] = implode( $passObj->getDelimiter(), $passObj->args );
+ $lastHash = $passObj->hash;
+ }
+
+ $this->hash = $lastHash;
+ }
+
+ /**
+ * Finish the hashing of a partially hashed layered hash
+ *
+ * Given a password hash that is hashed using the first layer of this object's
+ * configuration, perform the remaining layers of password hashing in order to
+ * get an updated hash with all the layers.
+ *
+ * @param ParameterizedPassword $passObj Password hash of the first layer
+ *
+ * @throws MWException If the first parameter is not of the correct type
+ */
+ public function partialCrypt( ParameterizedPassword $passObj ) {
+ $type = $passObj->config['type'];
+ if ( $type !== $this->config['types'][0] ) {
+ throw new MWException( 'Only a hash in the first layer can be finished.' );
+ }
+
+ // Gather info from the existing hash
+ $this->params[0] = implode( $passObj->getDelimiter(), $passObj->params );
+ $this->args[0] = implode( $passObj->getDelimiter(), $passObj->args );
+ $lastHash = $passObj->hash;
+
+ // Layer the remaining types
+ foreach ( $this->config['types'] as $i => $type ) {
+ if ( $i == 0 ) {
+ continue;
+ };
+
+ // Construct pseudo-hash based on params and arguments
+ /** @var ParameterizedPassword $passObj */
+ $passObj = $this->factory->newFromType( $type );
+
+ $params = '';
+ $args = '';
+ if ( $this->params[$i] !== '' ) {
+ $params = $this->params[$i] . $passObj->getDelimiter();
+ }
+ if ( isset( $this->args[$i] ) && $this->args[$i] !== '' ) {
+ $args = $this->args[$i] . $passObj->getDelimiter();
+ }
+ $existingHash = ":$type:" . $params . $args . $this->hash;
+
+ // Hash the last hash with the next type in the layer
+ $passObj = $this->factory->newFromCiphertext( $existingHash );
+ $passObj->crypt( $lastHash );
+
+ // Move over the params and args
+ $this->params[$i] = implode( $passObj->getDelimiter(), $passObj->params );
+ $this->args[$i] = implode( $passObj->getDelimiter(), $passObj->args );
+ $lastHash = $passObj->hash;
+ }
+
+ $this->hash = $lastHash;
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the MWOldPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * The old style of MediaWiki password hashing. It involves
+ * running MD5 on the password.
+ *
+ * @since 1.24
+ */
+class MWOldPassword extends ParameterizedPassword {
+ protected function getDefaultParams() {
+ return array();
+ }
+
+ protected function getDelimiter() {
+ return ':';
+ }
+
+ public function crypt( $plaintext ) {
+ global $wgPasswordSalt;
+
+ if ( $wgPasswordSalt && count( $this->args ) == 1 ) {
+ $this->hash = md5( $this->args[0] . '-' . md5( $plaintext ) );
+ } else {
+ $this->args = array();
+ $this->hash = md5( $plaintext );
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the BcryptPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * The old style of MediaWiki password hashing, with a salt. It involves
+ * running MD5 on the password, and then running MD5 on the salt concatenated
+ * with the first hash.
+ *
+ * @since 1.24
+ */
+class MWSaltedPassword extends ParameterizedPassword {
+ protected function getDefaultParams() {
+ return array();
+ }
+
+ protected function getDelimiter() {
+ return ':';
+ }
+
+ public function crypt( $plaintext ) {
+ if ( count( $this->args ) == 0 ) {
+ $this->args[] = MWCryptRand::generateHex( 8 );
+ }
+
+ $this->hash = md5( $this->args[0] . '-' . md5( $plaintext ) );
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the ParameterizedPassword class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Helper class for password hash types that have a delimited set of parameters
+ * inside of the hash.
+ *
+ * All passwords are in the form of :<TYPE>:... as explained in the main Password
+ * class. This class is for hashes in the form of :<TYPE>:<PARAM1>:<PARAM2>:... where
+ * <PARAM1>, <PARAM2>, etc. are parameters that determine how the password was hashed.
+ * Of course, the internal delimiter (which is : by convention and default), can be
+ * changed by overriding the ParameterizedPassword::getDelimiter() function.
+ *
+ * This class requires overriding an additional function: ParameterizedPassword::getDefaultParams().
+ * See the function description for more details on the implementation.
+ *
+ * @since 1.24
+ */
+abstract class ParameterizedPassword extends Password {
+ /**
+ * Named parameters that have default values for this password type
+ * @var array
+ */
+ protected $params = array();
+
+ /**
+ * Extra arguments that were found in the hash. This may or may not make
+ * the hash invalid.
+ * @var array
+ */
+ protected $args = array();
+
+ protected function parseHash( $hash ) {
+ parent::parseHash( $hash );
+
+ if ( $hash === null ) {
+ $this->params = $this->getDefaultParams();
+ return;
+ }
+
+ $parts = explode( $this->getDelimiter(), $hash );
+ $paramKeys = array_keys( $this->getDefaultParams() );
+
+ if ( count( $parts ) < count( $paramKeys ) ) {
+ throw new PasswordError( 'Hash is missing required parameters.' );
+ }
+
+ if ( $paramKeys ) {
+ $this->args = array_splice( $parts, count( $paramKeys ) );
+ $this->params = array_combine( $paramKeys, $parts );
+ } else {
+ $this->args = $parts;
+ }
+
+ if ( $this->args ) {
+ $this->hash = array_pop( $this->args );
+ } else {
+ $this->hash = null;
+ }
+ }
+
+ public function needsUpdate() {
+ return parent::needsUpdate() || $this->params !== $this->getDefaultParams();
+ }
+
+ public function toString() {
+ return
+ ':' . $this->config['type'] . ':' .
+ implode( $this->getDelimiter(), array_merge( $this->params, $this->args ) ) .
+ $this->getDelimiter() . $this->hash;
+ }
+
+ /**
+ * Returns the delimiter for the parameters inside the hash
+ *
+ * @return string
+ */
+ abstract protected function getDelimiter();
+
+ /**
+ * Return an ordered array of default parameters for this password hash
+ *
+ * The keys should be the parameter names and the values should be the default
+ * values. Additionally, the order of the array should be the order in which they
+ * appear in the hash.
+ *
+ * When parsing a password hash, the constructor will split the hash based on
+ * the delimiter, and consume as many parts as it can, matching each to a parameter
+ * in this list. Once all the parameters have been filled, all remaining parts will
+ * be considered extra arguments, except, of course, for the very last part, which
+ * is the hash itself.
+ *
+ * @return array
+ */
+ abstract protected function getDefaultParams();
+}
--- /dev/null
+<?php
+/**
+ * Implements the Password class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Represents a password hash for use in authentication
+ *
+ * Note: All password types are transparently prefixed with :<TYPE>:, where <TYPE>
+ * is the registered type of the hash. This prefix is stripped in the constructor
+ * and is added back in the toString() function.
+ *
+ * When inheriting this class, there are a couple of expectations
+ * to be fulfilled:
+ * * If Password::toString() is called on an object, and the result is passed back in
+ * to PasswordFactory::newFromCiphertext(), the result will be identical to the original.
+ * * The string representations of two Password objects are equal only if
+ * the original plaintext passwords match. In other words, if the toString() result of
+ * two objects match, the passwords are the same, and the user will be logged in.
+ * Since the string representation of a hash includes its type name (@see Password::toString),
+ * this property is preserved across all classes that inherit Password.
+ * If a hashing scheme does not fulfill this expectation, it must make sure to override the
+ * Password::equals() function and use custom comparison logic. However, this is not
+ * recommended unless absolutely required by the hashing mechanism.
+ * With these two points in mind, when creating a new Password sub-class, there are some functions
+ * you have to override (because they are abstract) and others that you may want to override.
+ *
+ * The abstract functions that must be overridden are:
+ * * Password::crypt(), which takes a plaintext password and hashes it into a string hash suitable
+ * for being passed to the constructor of that class, and then stores that hash (and whatever
+ * other data) into the internal state of the object.
+ * The functions that can optionally be overridden are:
+ * * Password::parseHash(), which can be useful to override if you need to extract values from or
+ * otherwise parse a password hash when it's passed to the constructor.
+ * * Password::needsUpdate(), which can be useful if a specific password hash has different
+ * logic for when the hash needs to be updated.
+ * * Password::toString(), which can be useful if the hash was changed in the constructor and
+ * needs to be re-assembled before being returned as a string. This function is expected to add
+ * the type back on to the hash, so make sure to do that if you override the function.
+ * * Password::equals() - This function compares two Password objects to see if they are equal.
+ * The default is to just do a timing-safe string comparison on the $this->hash values.
+ *
+ * After creating a new password hash type, it can be registered using the static
+ * Password::register() method. The default type is set using the Password::setDefaultType() type.
+ * Types must be registered before they can be set as the default.
+ *
+ * @since 1.24
+ */
+abstract class Password {
+ /**
+ * @var PasswordFactory Factory that created the object
+ */
+ protected $factory;
+
+ /**
+ * String representation of the hash without the type
+ * @var string
+ */
+ protected $hash;
+
+ /**
+ * Array of configuration variables injected from the constructor
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * Construct the Password object using a string hash
+ *
+ * It is strongly recommended not to call this function directly unless you
+ * have a reason to. Use the PasswordFactory class instead.
+ *
+ * @throws MWException If $config does not contain required parameters
+ *
+ * @param PasswordFactory $factory Factory object that created the password
+ * @param array $config Array of engine configuration options for hashing
+ * @param string|null $hash The raw hash, including the type
+ */
+ final public function __construct( PasswordFactory $factory, array $config, $hash = null ) {
+ if ( !isset( $config['type'] ) ) {
+ throw new MWException( 'Password configuration must contain a type name.' );
+ }
+ $this->config = $config;
+ $this->factory = $factory;
+
+ if ( $hash !== null && strlen( $hash ) >= 3 ) {
+ // Strip the type from the hash for parsing
+ $hash = substr( $hash, strpos( $hash, ':', 1 ) + 1 );
+ }
+
+ $this->hash = $hash;
+ $this->parseHash( $hash );
+ }
+
+ /**
+ * Get the type name of the password
+ *
+ * @return string Password type
+ */
+ final public function getType() {
+ return $this->config['type'];
+ }
+
+ /**
+ * Perform any parsing necessary on the hash to see if the hash is valid
+ * and/or to perform logic for seeing if the hash needs updating.
+ *
+ * @param string $hash The hash, with the :<TYPE>: prefix stripped
+ * @throws PasswordError If there is an error in parsing the hash
+ */
+ protected function parseHash( $hash ) {
+ }
+
+ /**
+ * Determine if the hash needs to be updated
+ *
+ * @return bool True if needs update, false otherwise
+ */
+ public function needsUpdate() {
+ }
+
+ /**
+ * Compare one Password object to this object
+ *
+ * By default, do a timing-safe string comparison on the result of
+ * Password::toString() for each object. This can be overridden to do
+ * custom comparison, but it is not recommended unless necessary.
+ *
+ * @param Password|string $other The other password
+ * @return bool True if equal, false otherwise
+ */
+ public function equals( $other ) {
+ if ( !$other instanceof self ) {
+ // No need to use the factory because we're definitely making
+ // an object of the same type.
+ $obj = clone $this;
+ $obj->crypt( $other );
+ $other = $obj;
+ }
+
+ return hash_equals( $this->toString(), $other->toString() );
+ }
+
+ /**
+ * Convert this hash to a string that can be stored in the database
+ *
+ * The resulting string should be considered the seralized representation
+ * of this hash, i.e., if the return value were recycled back into
+ * PasswordFactory::newFromCiphertext, the returned object would be equivalent to
+ * this; also, if two objects return the same value from this function, they
+ * are considered equivalent.
+ *
+ * @return string
+ */
+ public function toString() {
+ return ':' . $this->config['type'] . ':' . $this->hash;
+ }
+
+ /**
+ * Hash a password and store the result in this object
+ *
+ * The result of the password hash should be put into the internal
+ * state of the hash object.
+ *
+ * @param string $password Password to hash
+ * @throws PasswordError If an internal error occurs in hashing
+ */
+ abstract public function crypt( $password );
+}
--- /dev/null
+<?php
+/**
+ * Implements the Password class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Factory class for creating and checking Password objects
+ *
+ * @since 1.24
+ */
+final class PasswordFactory {
+ /**
+ * The default PasswordHash type
+ *
+*@var string
+ * @see PasswordFactory::setDefaultType
+ */
+ private $default = '';
+
+ /**
+ * Mapping of password types to classes
+ * @var array
+ * @see PasswordFactory::register
+ * @see Setup.php
+ */
+ private $types = array(
+ '' => array( 'type' => '', 'class' => 'InvalidPassword' ),
+ );
+
+ /**
+ * Register a new type of password hash
+ *
+ * @param string $type Unique type name for the hash
+ * @param array $config Array of configuration options
+ */
+ public function register( $type, array $config ) {
+ $config['type'] = $type;
+ $this->types[$type] = $config;
+ }
+
+ /**
+ * Set the default password type
+ *
+ * @throws InvalidArgumentException If the type is not registered
+ * @param string $type Password hash type
+ */
+ public function setDefaultType( $type ) {
+ if ( !isset( $this->types[$type] ) ) {
+ throw new InvalidArgumentException( "Invalid password type $type." );
+ }
+ $this->default = $type;
+ }
+
+ /**
+ * Initialize the internal static variables using the global variables
+ *
+ * @param Config $config Configuration object to load data from
+ */
+ public function init( Config $config ) {
+ foreach ( $config->get( 'PasswordConfig' ) as $type => $options ) {
+ $this->register( $type, $options );
+ }
+
+ $this->setDefaultType( $config->get( 'PasswordDefault' ) );
+ }
+
+ /**
+ * Get the list of types of passwords
+ *
+ * @return array
+ */
+ public function getTypes() {
+ return $this->types;
+ }
+
+ /**
+ * Create a new Hash object from an existing string hash
+ *
+ * Parse the type of a hash and create a new hash object based on the parsed type.
+ * Pass the raw hash to the constructor of the new object. Use InvalidPassword type
+ * if a null hash is given.
+ *
+ * @param string|null $hash Existing hash or null for an invalid password
+ * @return Password object
+ * @throws PasswordError if hash is invalid or type is not recognized
+ */
+ public function newFromCiphertext( $hash ) {
+ if ( $hash === null || $hash === false || $hash === '' ) {
+ return new InvalidPassword( $this, array( 'type' => '' ), null );
+ } elseif ( $hash[0] !== ':' ) {
+ throw new PasswordError( 'Invalid hash given' );
+ }
+
+ $type = substr( $hash, 1, strpos( $hash, ':', 1 ) - 1 );
+ if ( !isset( $this->types[$type] ) ) {
+ throw new PasswordError( "Unrecognized password hash type $type." );
+ }
+
+ $config = $this->types[$type];
+
+ return new $config['class']( $this, $config, $hash );
+ }
+
+ /**
+ * Make a new default password of the given type.
+ *
+ * @param string $type Existing type
+ * @return Password object
+ * @throws PasswordError if hash is invalid or type is not recognized
+ */
+ public function newFromType( $type ) {
+ if ( !isset( $this->types[$type] ) ) {
+ throw new PasswordError( "Unrecognized password hash type $type." );
+ }
+
+ $config = $this->types[$type];
+
+ return new $config['class']( $this, $config );
+ }
+
+ /**
+ * Create a new Hash object from a plaintext password
+ *
+ * If no existing object is given, make a new default object. If one is given, clone that
+ * object. Then pass the plaintext to Password::crypt().
+ *
+ * @param string $password Plaintext password
+ * @param Password|null $existing Optional existing hash to get options from
+ * @return Password object
+ */
+ public function newFromPlaintext( $password, Password $existing = null ) {
+ if ( $existing === null ) {
+ $config = $this->types[$this->default];
+ $obj = new $config['class']( $this, $config );
+ } else {
+ $obj = clone $existing;
+ }
+
+ $obj->crypt( $password );
+
+ return $obj;
+ }
+
+ /**
+ * Determine whether a password object needs updating
+ *
+ * Check whether the given password is of the default type. If it is,
+ * pass off further needsUpdate checks to Password::needsUpdate.
+ *
+ * @param Password $password
+ *
+ * @return bool True if needs update, false otherwise
+ */
+ public function needsUpdate( Password $password ) {
+ if ( $password->getType() !== $this->default ) {
+ return true;
+ } else {
+ return $password->needsUpdate();
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * Implements the Pbkdf2Password class for the MediaWiki software.
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * A PBKDF2-hashed password
+ *
+ * This is a computationally complex password hash for use in modern applications.
+ * The number of rounds can be configured by $wgPasswordConfig['pbkdf2']['cost'].
+ *
+ * @since 1.24
+ */
+class Pbkdf2Password extends ParameterizedPassword {
+ protected function getDefaultParams() {
+ return array(
+ 'algo' => $this->config['algo'],
+ 'rounds' => $this->config['cost'],
+ 'length' => $this->config['length']
+ );
+ }
+
+ protected function getDelimiter() {
+ return ':';
+ }
+
+ public function crypt( $password ) {
+ if ( count( $this->args ) == 0 ) {
+ $this->args[] = base64_encode( MWCryptRand::generate( 16, true ) );
+ }
+
+ if ( function_exists( 'hash_pbkdf2' ) ) {
+ $hash = hash_pbkdf2(
+ $this->params['algo'],
+ $password,
+ base64_decode( $this->args[0] ),
+ $this->params['rounds'],
+ $this->params['length'],
+ true
+ );
+ } else {
+ $hashLen = strlen( hash( $this->params['algo'], '', true ) );
+ $blockCount = ceil( $this->params['length'] / $hashLen );
+
+ $hash = '';
+ $salt = base64_decode( $this->args[0] );
+ for ( $i = 1; $i <= $blockCount; ++$i ) {
+ $roundTotal = $lastRound = hash_hmac(
+ $this->params['algo'],
+ $salt . pack( 'N', $i ),
+ $password,
+ true
+ );
+
+ for ( $j = 1; $j < $this->params['rounds']; ++$j ) {
+ $lastRound = hash_hmac( $this->params['algo'], $lastRound, $password, true );
+ $roundTotal ^= $lastRound;
+ }
+
+ $hash .= $roundTotal;
+ }
+
+ $hash = substr( $hash, 0, $this->params['length'] );
+ }
+
+ $this->hash = base64_encode( $hash );
+ }
+}
--- /dev/null
+<?php
+/**
+ * Maintenance script to wrap all old-style passwords in a layered type
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to wrap all passwords of a certain type in a specified layered
+ * type that wraps around the old type.
+ *
+ * @since 1.24
+ * @ingroup Maintenance
+ */
+class WrapOldPasswords extends Maintenance {
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = "Wrap all passwords of a certain type in a new layered type";
+ $this->addOption( 'type',
+ 'Password type to wrap passwords in (must inherit LayeredParameterizedPassword)', true, true );
+ $this->addOption( 'verbose', 'Enables verbose output', false, false, 'v' );
+ $this->setBatchSize( 100 );
+ }
+
+ public function execute() {
+ global $wgAuth;
+
+ if ( !$wgAuth->allowSetLocalPassword() ) {
+ $this->error( '$wgAuth does not allow local passwords. Aborting.', true );
+ }
+
+ $passwordFactory = new PasswordFactory();
+ $passwordFactory->init( RequestContext::getMain()->getConfig() );
+
+ $typeInfo = $passwordFactory->getTypes();
+ $layeredType = $this->getOption( 'type' );
+
+ // Check that type exists and is a layered type
+ if ( !isset( $typeInfo[$layeredType] ) ) {
+ $this->error( 'Undefined password type', true );
+ }
+
+ $passObj = $passwordFactory->newFromType( $layeredType );
+ if ( !$passObj instanceof LayeredParameterizedPassword ) {
+ $this->error( 'Layered parameterized password type must be used.', true );
+ }
+
+ // Extract the first layer type
+ $typeConfig = $typeInfo[$layeredType];
+ $firstType = $typeConfig['types'][0];
+
+ // Get a list of password types that are applicable
+ $dbw = $this->getDB( DB_MASTER );
+ $typeCond = 'user_password' . $dbw->buildLike( ":$firstType:", $dbw->anyString() );
+
+ $minUserId = 0;
+ do {
+ $dbw->begin();
+
+ $res = $dbw->select( 'user',
+ array( 'user_id', 'user_name', 'user_password' ),
+ array(
+ 'user_id > ' . $dbw->addQuotes( $minUserId ),
+ $typeCond
+ ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'user_id',
+ 'LIMIT' => $this->mBatchSize,
+ 'LOCK IN SHARE MODE',
+ )
+ );
+
+ /** @var User[] $updateUsers */
+ $updateUsers = array();
+ foreach ( $res as $row ) {
+ if ( $this->hasOption( 'verbose' ) ) {
+ $this->output( "Updating password for user {$row->user_name} ({$row->user_id}).\n" );
+ }
+
+ $user = User::newFromId( $row->user_id );
+ /** @var ParameterizedPassword $password */
+ $password = $passwordFactory->newFromCiphertext( $row->user_password );
+ /** @var LayeredParameterizedPassword $layeredPassword */
+ $layeredPassword = $passwordFactory->newFromType( $layeredType );
+ $layeredPassword->partialCrypt( $password );
+
+ $updateUsers[] = $user;
+ $dbw->update( 'user',
+ array( 'user_password' => $layeredPassword->toString() ),
+ array( 'user_id' => $row->user_id ),
+ __METHOD__
+ );
+
+ $minUserId = $row->user_id;
+ }
+
+ $dbw->commit();
+
+ // Clear memcached so old passwords are wiped out
+ foreach ( $updateUsers as $user ) {
+ $user->clearSharedCache();
+ }
+ } while ( $res->numRows() );
+ }
+}
+
+$maintClass = "WrapOldPasswords";
+require_once RUN_MAINTENANCE_IF_MAIN;
'MediaWikiPHPUnitCommand' => "$testDir/phpunit/MediaWikiPHPUnitCommand.php",
'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php",
+ 'MediaWikiPasswordTestCase' => "$testDir/phpunit/MediaWikiPasswordTestCase.php",
'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php",
'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
--- /dev/null
+<?php
+/**
+ * Testing framework for the password hashes
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @since 1.23
+ */
+abstract class MediaWikiPasswordTestCase extends MediaWikiTestCase {
+ /**
+ * @var PasswordFactory
+ */
+ protected $passwordFactory;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->passwordFactory = new PasswordFactory();
+ foreach ( $this->getTypeConfigs() as $type => $config ) {
+ $this->passwordFactory->register( $type, $config );
+ }
+ }
+
+ /**
+ * Return an array of configs to be used for this class's password type.
+ *
+ * @return array[]
+ */
+ abstract protected function getTypeConfigs();
+
+ /**
+ * An array of tests in the form of (bool, string, string), where the first
+ * element is whether the second parameter (a password hash) and the third
+ * parameter (a password) should match.
+ *
+ * @return array
+ */
+ abstract public function providePasswordTests();
+
+ /**
+ * @dataProvider providePasswordTests
+ */
+ public function testHashing( $shouldMatch, $hash, $password ) {
+ $hash = $this->passwordFactory->newFromCiphertext( $hash );
+ $password = $this->passwordFactory->newFromPlaintext( $password, $hash );
+ $this->assertSame( $shouldMatch, $hash->equals( $password ) );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ */
+ public function testStringSerialization( $shouldMatch, $hash, $password ) {
+ $hashObj = $this->passwordFactory->newFromCiphertext( $hash );
+ $serialized = $hashObj->toString();
+ $unserialized = $this->passwordFactory->newFromCiphertext( $serialized );
+ $this->assertTrue( $hashObj->equals( $unserialized ) );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ * @covers InvalidPassword::equals
+ * @covers InvalidPassword::toString
+ */
+ public function testInvalidUnequalNormal( $shouldMatch, $hash, $password ) {
+ $invalid = $this->passwordFactory->newFromCiphertext( null );
+ $normal = $this->passwordFactory->newFromCiphertext( $hash );
+
+ $this->assertFalse( $invalid->equals( $normal ) );
+ $this->assertFalse( $normal->equals( $invalid ) );
+ }
+}
--- /dev/null
+<?php
+/**
+ * Testing framework for the Password infrastructure
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class PasswordTest extends MediaWikiTestCase {
+ /**
+ * @covers InvalidPassword::equals
+ */
+ public function testInvalidUnequalInvalid() {
+ $invalid1 = User::getPasswordFactory()->newFromCiphertext( null );
+ $invalid2 = User::getPasswordFactory()->newFromCiphertext( null );
+
+ $this->assertFalse( $invalid1->equals( $invalid2 ) );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group large
+ */
+class BcryptPasswordTestCase extends MediaWikiPasswordTestCase {
+ protected function getTypeConfigs() {
+ return array( 'bcrypt' => array(
+ 'class' => 'BcryptPassword',
+ 'cost' => 9,
+ ) );
+ }
+
+ public function providePasswordTests() {
+ /** @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong */
+ return array(
+ // Tests from glibc bcrypt implementation
+ array( true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "U*U" ),
+ array( true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$VGOzA784oUp/Z0DY336zx7pLYAy0lwK', "U*U*" ),
+ array( true, ':bcrypt:5$XXXXXXXXXXXXXXXXXXXXXO$AcXxm9kjPGEMsLznoKqmqw7tc8WCx4a', "U*U*U" ),
+ array( true, ':bcrypt:5$abcdefghijklmnopqrstuu$5s2v8.iXieOjg/.AySBTTZIIVFJeBui', "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789chars after 72 are ignored" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$CE5elHaaO4EbggVDjb8P19RukzXSM3e', "\xff\xff\xa3" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq', "\xa3" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq', "\xa3" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$o./n25XVfn6oAPaUvHe.Csk4zRfsYPi', "\xff\xa334\xff\xff\xff\xa3345" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$nRht2l/HRhr6zmCp9vYUvvsqynflf9e', "\xff\xa3345" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$nRht2l/HRhr6zmCp9vYUvvsqynflf9e', "\xff\xa3345" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$6IflQkJytoRVc1yuaNtHfiuq.FRlSIS', "\xa3ab" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$6IflQkJytoRVc1yuaNtHfiuq.FRlSIS', "\xa3ab" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6', "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaachars after 72 are ignored as usual" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy', "\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe', "\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff" ),
+ array( true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy', "" ),
+ // One or two false sanity tests
+ array( false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "UXU" ),
+ array( false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "" ),
+ );
+ /** @codingStandardsIgnoreEnd */
+ }
+}
--- /dev/null
+<?php
+
+class LayeredParameterizedPasswordTest extends MediaWikiPasswordTestCase {
+ protected function getTypeConfigs() {
+ return array(
+ 'testLargeLayeredTop' => array(
+ 'class' => 'LayeredParameterizedPassword',
+ 'types' => array(
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredFinal',
+ ),
+ ),
+ 'testLargeLayeredBottom' => array(
+ 'class' => 'Pbkdf2Password',
+ 'algo' => 'sha512',
+ 'cost' => 1024,
+ 'length' => 512,
+ ),
+ 'testLargeLayeredFinal' => array(
+ 'class' => 'BcryptPassword',
+ 'cost' => 5,
+ )
+ );
+ }
+
+ public function providePasswordTests() {
+ /** @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong */
+ return array(
+ array( true, ':testLargeLayeredTop:sha512:1024:512!sha512:1024:512!sha512:1024:512!sha512:1024:512!5!vnRy+2SrSA0fHt3dwhTP5g==!AVnwfZsAQjn+gULv7FSGjA==!xvHUX3WcpkeSn1lvjWcvBg==!It+OC/N9tu+d3ByHhuB0BQ==!Tb.gqUOiD.aWktVwHM.Q/O!7CcyMfXUPky5ptyATJsR2nq3vUqtnBC', 'testPassword123' ),
+ );
+ /** @codingStandardsIgnoreEnd */
+ }
+
+ /**
+ * @covers LayeredParameterizedPassword::partialCrypt
+ */
+ public function testLargeLayeredPartialUpdate() {
+ /** @var ParameterizedPassword $partialPassword */
+ $partialPassword = $this->passwordFactory->newFromType( 'testLargeLayeredBottom' );
+ $partialPassword->crypt( 'testPassword123' );
+
+ /** @var LayeredParameterizedPassword $totalPassword */
+ $totalPassword = $this->passwordFactory->newFromType( 'testLargeLayeredTop' );
+ $totalPassword->partialCrypt( $partialPassword );
+
+ $this->assertTrue( $totalPassword->equals( 'testPassword123' ) );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group large
+ */
+class Pbkdf2PasswordTest extends MediaWikiPasswordTestCase {
+ protected function getTypeConfigs() {
+ return array( 'pbkdf2' => array(
+ 'class' => 'Pbkdf2Password',
+ 'algo' => 'sha256',
+ 'cost' => '10000',
+ 'length' => '128',
+ ) );
+ }
+
+ public function providePasswordTests() {
+ return array(
+ array( true, ":pbkdf2:sha1:1:20:c2FsdA==:DGDID5YfDnHzqbUkr2ASBi/gN6Y=", 'password' ),
+ array( true, ":pbkdf2:sha1:2:20:c2FsdA==:6mwBTcctb4zNHtkqzh1B8NjeiVc=", 'password' ),
+ array( true, ":pbkdf2:sha1:4096:20:c2FsdA==:SwB5AbdlSJq+rUnZJvch0GWkKcE=", 'password' ),
+ array( true, ":pbkdf2:sha1:4096:16:c2EAbHQ=:Vvpqp1VICZ3MN9fwNCXgww==", "pass\x00word" ),
+ );
+ }
+}