jenkins-bot has submitted this change and it was merged.
Change subject: Added password hashing API
......................................................................
Added password hashing API
Deprecated the old User::crypt, et. al password hashing
system and implemented an extensible password hashing
API.
The new Password class allows registering of child classes
and provides factory functions for creating new Password
objects. The built-in hash types are the old MediaWiki MD5
types, which are for backwards-compatibility only, and bcrypt.
Also included is support for wrapping existing hashes as well
as encrypting passwords with a configured encryption key.
Bug: 54948
Bug: 28419
Change-Id: I0a9c972931a0eff0cfb2619cef3ddffd03710285
---
M RELEASE-NOTES-1.24
M docs/hooks.txt
M includes/AutoLoader.php
M includes/DefaultSettings.php
M includes/User.php
A includes/password/BcryptPassword.php
A includes/password/EncryptedPassword.php
A includes/password/InvalidPassword.php
A includes/password/LayeredParameterizedPassword.php
A includes/password/MWOldPassword.php
A includes/password/MWSaltedPassword.php
A includes/password/ParameterizedPassword.php
A includes/password/Password.php
A includes/password/PasswordFactory.php
A includes/password/Pbkdf2Password.php
A maintenance/wrapOldPasswords.php
M tests/TestsAutoLoader.php
A tests/phpunit/MediaWikiPasswordTestCase.php
A tests/phpunit/includes/PasswordTest.php
A tests/phpunit/includes/password/BcryptPasswordTest.php
A tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
A tests/phpunit/includes/password/Pbkdf2PasswordTest.php
22 files changed, 1,597 insertions(+), 88 deletions(-)
Approvals:
Aaron Schulz: Looks good to me, approved
jenkins-bot: Verified
diff --git a/RELEASE-NOTES-1.24 b/RELEASE-NOTES-1.24
index 119f1a2..a74e76d 100644
--- a/RELEASE-NOTES-1.24
+++ b/RELEASE-NOTES-1.24
@@ -42,6 +42,8 @@
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
@@ -133,6 +135,9 @@
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
diff --git a/docs/hooks.txt b/docs/hooks.txt
index 3ee1221..dd9f905 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -2789,7 +2789,7 @@
$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
@@ -2801,7 +2801,7 @@
$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
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index ab5af1b..1d76e71 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -828,6 +828,18 @@
'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',
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 3ef59b3..9df920b 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -4012,6 +4012,7 @@
/**
* For compatibility with old installations set to false
+ * @deprecated since 1.24 will be removed in future
*/
$wgPasswordSalt = true;
@@ -4028,6 +4029,65 @@
*/
$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
diff --git a/includes/User.php b/includes/User.php
index fa20ebd..73d4959 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -30,7 +30,7 @@
* 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.
@@ -71,6 +71,11 @@
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
* corresponding database fields must call a cache-clearing function.
@@ -81,16 +86,12 @@
'mId',
'mName',
'mRealName',
- 'mPassword',
- 'mNewpassword',
- 'mNewpassTime',
'mEmail',
'mTouched',
'mToken',
'mEmailAuthenticated',
'mEmailToken',
'mEmailTokenExpires',
- 'mPasswordExpires',
'mRegistration',
'mEditCount',
// user_groups table
@@ -997,10 +998,13 @@
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;
@@ -1190,6 +1194,7 @@
*/
public function loadFromRow( $row, $data = null ) {
$all = true;
+ $passwordFactory = self::getPasswordFactory();
$this->mGroups = null; // deferred
@@ -1223,9 +1228,31 @@
}
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;
@@ -1235,7 +1262,6 @@
$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;
@@ -1283,6 +1309,26 @@
foreach ( $res as $row ) {
$this->mGroups[] = $row->ug_group;
}
+ }
+ }
+
+ /**
+ * 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__
+ ) );
}
}
@@ -2151,7 +2197,7 @@
*
* Called implicitly from invalidateCache() and saveSettings().
*/
- private function clearSharedCache() {
+ public function clearSharedCache() {
$this->load();
if ( $this->mId ) {
global $wgMemc;
@@ -2267,16 +2313,16 @@
* 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;
}
@@ -2323,7 +2369,7 @@
$this->mNewpassword = '';
$this->mNewpassTime = null;
} else {
- $this->mNewpassword = self::crypt( $str );
+ $this->mNewpassword =
self::getPasswordFactory()->newFromPlaintext( $str );
if ( $throttle ) {
$this->mNewpassTime = wfTimestampNow();
}
@@ -3411,6 +3457,7 @@
global $wgAuth;
$this->load();
+ $this->loadPasswords();
if ( wfReadOnly() ) {
return;
}
@@ -3420,15 +3467,15 @@
$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,
@@ -3490,6 +3537,7 @@
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;
@@ -3501,8 +3549,8 @@
$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 ),
@@ -3552,6 +3600,7 @@
*/
public function addToDatabase() {
$this->load();
+ $this->loadPasswords();
if ( !$this->mToken ) {
$this->setToken(); // init token
}
@@ -3565,8 +3614,8 @@
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 ),
@@ -3722,7 +3771,7 @@
*/
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
@@ -3737,19 +3786,27 @@
// 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;
}
/**
@@ -3764,7 +3821,7 @@
global $wgNewPasswordExpiry;
$this->load();
- if ( self::comparePasswords( $this->mNewpassword, $plaintext,
$this->getId() ) ) {
+ if ( $this->mNewpassword->equals( $plaintext ) ) {
if ( is_null( $this->mNewpassTime ) ) {
return true;
}
@@ -4551,45 +4608,18 @@
}
/**
- * 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 string $password Plain-text password
* @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();
}
/**
@@ -4601,26 +4631,24 @@
* @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 );
+ wfDeprecated( __METHOD__, '1.23' );
- $result = false;
- if ( !wfRunHooks( 'UserComparePasswords', array( &$hash,
&$password, &$userId, &$result ) ) ) {
- return $result;
+ // 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 );
}
/**
@@ -4825,6 +4853,20 @@
}
/**
+ * 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
* required, title, and/or pattern, depending on
$wgMinimalPasswordLength.
@@ -4892,16 +4934,12 @@
'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',
);
diff --git a/includes/password/BcryptPassword.php
b/includes/password/BcryptPassword.php
new file mode 100644
index 0000000..4e5e878
--- /dev/null
+++ b/includes/password/BcryptPassword.php
@@ -0,0 +1,88 @@
+<?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 );
+ }
+}
diff --git a/includes/password/EncryptedPassword.php
b/includes/password/EncryptedPassword.php
new file mode 100644
index 0000000..39da32d
--- /dev/null
+++ b/includes/password/EncryptedPassword.php
@@ -0,0 +1,98 @@
+<?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;
+ }
+}
diff --git a/includes/password/InvalidPassword.php
b/includes/password/InvalidPassword.php
new file mode 100644
index 0000000..e45b774
--- /dev/null
+++ b/includes/password/InvalidPassword.php
@@ -0,0 +1,47 @@
+<?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;
+ }
+}
diff --git a/includes/password/LayeredParameterizedPassword.php
b/includes/password/LayeredParameterizedPassword.php
new file mode 100644
index 0000000..5735e28
--- /dev/null
+++ b/includes/password/LayeredParameterizedPassword.php
@@ -0,0 +1,140 @@
+<?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;
+ }
+}
diff --git a/includes/password/MWOldPassword.php
b/includes/password/MWOldPassword.php
new file mode 100644
index 0000000..0ba407f
--- /dev/null
+++ b/includes/password/MWOldPassword.php
@@ -0,0 +1,48 @@
+<?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 );
+ }
+ }
+}
diff --git a/includes/password/MWSaltedPassword.php
b/includes/password/MWSaltedPassword.php
new file mode 100644
index 0000000..6c6895a
--- /dev/null
+++ b/includes/password/MWSaltedPassword.php
@@ -0,0 +1,46 @@
+<?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 ) );
+ }
+}
diff --git a/includes/password/ParameterizedPassword.php
b/includes/password/ParameterizedPassword.php
new file mode 100644
index 0000000..4d6e415
--- /dev/null
+++ b/includes/password/ParameterizedPassword.php
@@ -0,0 +1,115 @@
+<?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();
+}
diff --git a/includes/password/Password.php b/includes/password/Password.php
new file mode 100644
index 0000000..4e395b5
--- /dev/null
+++ b/includes/password/Password.php
@@ -0,0 +1,186 @@
+<?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 );
+}
diff --git a/includes/password/PasswordFactory.php
b/includes/password/PasswordFactory.php
new file mode 100644
index 0000000..7aa78a7
--- /dev/null
+++ b/includes/password/PasswordFactory.php
@@ -0,0 +1,178 @@
+<?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();
+ }
+ }
+}
diff --git a/includes/password/Pbkdf2Password.php
b/includes/password/Pbkdf2Password.php
new file mode 100644
index 0000000..417753f
--- /dev/null
+++ b/includes/password/Pbkdf2Password.php
@@ -0,0 +1,85 @@
+<?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 );
+ }
+}
diff --git a/maintenance/wrapOldPasswords.php b/maintenance/wrapOldPasswords.php
new file mode 100644
index 0000000..37272a0
--- /dev/null
+++ b/maintenance/wrapOldPasswords.php
@@ -0,0 +1,126 @@
+<?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;
diff --git a/tests/TestsAutoLoader.php b/tests/TestsAutoLoader.php
index ec5cda6..eee34de 100644
--- a/tests/TestsAutoLoader.php
+++ b/tests/TestsAutoLoader.php
@@ -41,6 +41,7 @@
'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",
diff --git a/tests/phpunit/MediaWikiPasswordTestCase.php
b/tests/phpunit/MediaWikiPasswordTestCase.php
new file mode 100644
index 0000000..a4cec09
--- /dev/null
+++ b/tests/phpunit/MediaWikiPasswordTestCase.php
@@ -0,0 +1,88 @@
+<?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 ) );
+ }
+}
diff --git a/tests/phpunit/includes/PasswordTest.php
b/tests/phpunit/includes/PasswordTest.php
new file mode 100644
index 0000000..ceb794b
--- /dev/null
+++ b/tests/phpunit/includes/PasswordTest.php
@@ -0,0 +1,33 @@
+<?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 ) );
+ }
+}
diff --git a/tests/phpunit/includes/password/BcryptPasswordTest.php
b/tests/phpunit/includes/password/BcryptPasswordTest.php
new file mode 100644
index 0000000..b4d5f99
--- /dev/null
+++ b/tests/phpunit/includes/password/BcryptPasswordTest.php
@@ -0,0 +1,40 @@
+<?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 */
+ }
+}
diff --git
a/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
new file mode 100644
index 0000000..c552253
--- /dev/null
+++ b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
@@ -0,0 +1,51 @@
+<?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' )
);
+ }
+}
diff --git a/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
b/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
new file mode 100644
index 0000000..c1b65d3
--- /dev/null
+++ b/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
@@ -0,0 +1,24 @@
+<?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" ),
+ );
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/77645
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I0a9c972931a0eff0cfb2619cef3ddffd03710285
Gerrit-PatchSet: 39
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Parent5446 <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Addshore <[email protected]>
Gerrit-Reviewer: Aude <[email protected]>
Gerrit-Reviewer: CSteipp <[email protected]>
Gerrit-Reviewer: Grunny <[email protected]>
Gerrit-Reviewer: MZMcBride <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Ori.livneh <[email protected]>
Gerrit-Reviewer: Parent5446 <[email protected]>
Gerrit-Reviewer: PleaseStand <[email protected]>
Gerrit-Reviewer: Skizzerz <[email protected]>
Gerrit-Reviewer: Tim Starling <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits