This at least doubles the speed, which would allow the number of iterations to be doubled and computation of the password hash to complete in the same amount of time as before, or maybe even a slight bit less. The doubling in speed is due to an optimization[1] that so far has not been accepted into PHP's hash extension.[2] In addition, OpenSSL has optimized assembly-language hash function implementations for several common CPU architectures. These provide a further, yet more slight, performance improvement. While OpenSSL's PKCS5_PBKDF2_HMAC() is not the fastest implementation around, using it does not add a new library dependency. And although better password hashing functions exist, PBKDF2 is still the default in MediaWiki. For these reasons, I think this change makes sense. [1]: https://github.com/openssl/openssl/commit/c10e3f0cffb3820d [2]: https://github.com/php/php-src/issues/9604 Change-Id: I7b06590d4c42581f8749336f9c17777f973a506c
148 lines
5.1 KiB
PHP
148 lines
5.1 KiB
PHP
<?php
|
|
/**
|
|
* Implements the AbstractPbkdf2Password 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
|
|
*/
|
|
|
|
declare( strict_types = 1 );
|
|
|
|
/**
|
|
* 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'].
|
|
*
|
|
* To support different native implementations of PBKDF2 and the underlying
|
|
* hash algorithms, the following subclasses are available:
|
|
*
|
|
* - Pbkdf2PasswordUsingOpenSSL is the preferred, more efficient option
|
|
* and is used by default when possible.
|
|
* - Pbkdf2PasswordUsingHashExtension provides compatibility with PBKDF2
|
|
* password hashes computed using legacy algorithms, as well as PHP
|
|
* installations lacking OpenSSL support.
|
|
*
|
|
* @since 1.40
|
|
*/
|
|
abstract class AbstractPbkdf2Password extends ParameterizedPassword {
|
|
/**
|
|
* Create a new AbstractPbkdf2Password object.
|
|
*
|
|
* In the default configuration, this is used as a factory function
|
|
* in order to select a PBKDF2 implementation automatically.
|
|
*
|
|
* @internal
|
|
* @see Password::__construct
|
|
* @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
|
|
* @return AbstractPbkdf2Password The created object
|
|
* @throws Exception If $config does not contain required parameters
|
|
*/
|
|
public static function newInstance(
|
|
PasswordFactory $factory,
|
|
array $config,
|
|
string $hash = null
|
|
): self {
|
|
if ( isset( $config['class'] ) && is_subclass_of( $config['class'], self::class ) ) {
|
|
// Use the configured subclass
|
|
return new $config['class']( $factory, $config, $hash );
|
|
} elseif ( self::canUseOpenSSL() ) {
|
|
return new Pbkdf2PasswordUsingOpenSSL( $factory, $config, $hash );
|
|
} else {
|
|
return new Pbkdf2PasswordUsingHashExtension( $factory, $config, $hash );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if OpenSSL can be used for computing PBKDF2 password hashes.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected static function canUseOpenSSL(): bool {
|
|
// OpenSSL 1.0.1f (released 2014-01-06) is the earliest version supported by
|
|
// PHP 7.1 through 8.0 that hashes the HMAC key blocks only once rather than
|
|
// on each iteration. Once support for these PHP versions is dropped, the
|
|
// version check can be removed. (PHP 8.1 requires OpenSSL >= 1.0.2)
|
|
return function_exists( 'openssl_pbkdf2' ) && OPENSSL_VERSION_NUMBER >= 0x1000106f;
|
|
}
|
|
|
|
protected function getDefaultParams(): array {
|
|
return [
|
|
'algo' => $this->config['algo'],
|
|
'rounds' => $this->config['cost'],
|
|
'length' => $this->config['length']
|
|
];
|
|
}
|
|
|
|
protected function getDelimiter(): string {
|
|
return ':';
|
|
}
|
|
|
|
public function crypt( string $password ): void {
|
|
if ( count( $this->args ) == 0 ) {
|
|
$this->args[] = base64_encode( random_bytes( 16 ) );
|
|
}
|
|
|
|
$algo = $this->params['algo'];
|
|
$salt = base64_decode( $this->args[0] );
|
|
$rounds = (int)$this->params['rounds'];
|
|
$length = (int)$this->params['length'];
|
|
|
|
$digestAlgo = $this->getDigestAlgo( $algo );
|
|
if ( $digestAlgo === null ) {
|
|
throw new PasswordError( "Unknown or unsupported algo: $algo" );
|
|
}
|
|
if ( $rounds <= 0 || $rounds >= 0x7fffffff ) {
|
|
throw new PasswordError( 'Invalid number of rounds.' );
|
|
}
|
|
if ( $length <= 0 || $length >= 0x7fffffff ) {
|
|
throw new PasswordError( 'Invalid length.' );
|
|
}
|
|
|
|
$hash = $this->pbkdf2( $digestAlgo, $password, $salt, $rounds, $length );
|
|
$this->hash = base64_encode( $hash );
|
|
}
|
|
|
|
/**
|
|
* Get the implementation specific name for a hash algorithm.
|
|
*
|
|
* @param string $algo Algorithm specified in the password hash string
|
|
* @return string|null $algo Implementation specific name, or null if unsupported
|
|
*/
|
|
abstract protected function getDigestAlgo( string $algo ): ?string;
|
|
|
|
/**
|
|
* Call the PBKDF2 implementation, which hashes the password.
|
|
*
|
|
* @param string $digestAlgo Implementation specific hash algorithm name
|
|
* @param string $password Password to hash
|
|
* @param string $salt Salt as a binary string
|
|
* @param int $rounds Number of iterations
|
|
* @param int $length Length of the hash value in bytes
|
|
* @return string Hash value as a binary string
|
|
* @throws PasswordError If an internal error occurs in hashing
|
|
*/
|
|
abstract protected function pbkdf2(
|
|
string $digestAlgo,
|
|
string $password,
|
|
string $salt,
|
|
int $rounds,
|
|
int $length
|
|
): string;
|
|
}
|