3 * Implements the Password class for the MediaWiki software.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 declare( strict_types
= 1 );
25 namespace MediaWiki\Password
;
27 use InvalidArgumentException
;
28 use MediaWiki\Config\Config
;
29 use MediaWiki\MainConfigNames
;
31 use Wikimedia\ObjectFactory\ObjectFactory
;
34 * Factory class for creating and checking Password objects
38 final class PasswordFactory
{
40 * The default PasswordHash type
43 * @see PasswordFactory::setDefaultType
45 private $default = '';
48 * Mapping of password types to classes
51 * @see PasswordFactory::register
55 '' => [ 'type' => '', 'class' => InvalidPassword
::class ],
58 private const MIN_RANDOM_PASSWORD_LENGTH
= 10;
61 * Most of the time you'll want to use MediaWikiServices::getInstance()->getPasswordFactory
63 * @param array $config Mapping of password type => config
64 * @param string $default Default password type
65 * @see PasswordFactory::register
66 * @see PasswordFactory::setDefaultType
68 public function __construct( array $config = [], string $default = '' ) {
69 foreach ( $config as $type => $options ) {
70 $this->register( $type, $options );
73 if ( $default !== '' ) {
74 $this->setDefaultType( $default );
79 * Register a new type of password hash
81 * @param string $type Unique type name for the hash. Will be prefixed to the password hashes
82 * to identify what hashing method was used.
83 * @param array $config Array of configuration options. 'class' is required (the Password
84 * subclass name), everything else is passed to the constructor of that class.
86 public function register( string $type, array $config ): void
{
87 $config['type'] = $type;
88 $this->types
[$type] = $config;
92 * Set the default password type
94 * This type will be used for creating new passwords when the type is not specified.
95 * Passwords of a different type will be considered outdated and in need of update.
97 * @param string $type Password hash type
98 * @throws InvalidArgumentException If the type is not registered
100 public function setDefaultType( string $type ): void
{
101 if ( !isset( $this->types
[$type] ) ) {
102 throw new InvalidArgumentException( "Invalid password type $type." );
104 $this->default = $type;
108 * Get the default password type
112 public function getDefaultType(): string {
113 return $this->default;
117 * @deprecated since 1.32 Initialize settings using the constructor
118 * Emitting deprecation warnings since 1.41.
120 * Initialize the internal static variables using the global variables
122 * @param Config $config Configuration object to load data from
124 public function init( Config
$config ): void
{
125 wfDeprecated( __METHOD__
, '1.32' );
126 foreach ( $config->get( MainConfigNames
::PasswordConfig
) as $type => $options ) {
127 $this->register( $type, $options );
130 $this->setDefaultType( $config->get( MainConfigNames
::PasswordDefault
) );
134 * Get the list of types of passwords
138 public function getTypes(): array {
143 * Create a new Password object from an existing string hash
145 * Parse the type of a hash and create a new hash object based on the parsed type.
146 * Pass the raw hash to the constructor of the new object. Use InvalidPassword type
147 * if a null hash is given.
149 * @param string|null $hash Existing hash or null for an invalid password
151 * @throws PasswordError If hash is invalid or type is not recognized
153 public function newFromCiphertext( ?
string $hash ): Password
{
154 if ( $hash === null ||
$hash === '' ) {
155 return new InvalidPassword( $this, [ 'type' => '' ], null );
156 } elseif ( $hash[0] !== ':' ) {
157 throw new PasswordError( 'Invalid hash given' );
160 $type = substr( $hash, 1, strpos( $hash, ':', 1 ) - 1 );
161 return $this->newFromTypeAndHash( $type, $hash );
165 * Create a new Password object of the given type.
167 * @param string $type Existing type
169 * @throws PasswordError If type is not recognized
171 public function newFromType( string $type ): Password
{
172 return $this->newFromTypeAndHash( $type, null );
176 * Create a new Password object of the given type, optionally with an existing string hash.
178 * @param string $type Existing type
179 * @param string|null $hash Existing hash
181 * @throws PasswordError If hash is invalid or type is not recognized
183 private function newFromTypeAndHash( string $type, ?
string $hash ): Password
{
184 if ( !isset( $this->types
[$type] ) ) {
185 throw new PasswordError( "Unrecognized password hash type $type." );
188 $config = $this->types
[$type];
190 // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
191 return ObjectFactory
::getObjectFromSpec( $config, [
192 'extraArgs' => [ $this, $config, $hash ],
193 'assertClass' => Password
::class,
198 * Create a new Password object from a plaintext password
200 * If no existing object is given, make a new default object. If one is given, clone that
201 * object. Then pass the plaintext to Password::crypt().
203 * @param string|null $password Plaintext password, or null for an invalid password
204 * @param Password|null $existing Optional existing hash to get options from
207 public function newFromPlaintext( ?
string $password, ?Password
$existing = null ): Password
{
208 if ( $password === null ) {
209 return new InvalidPassword( $this, [ 'type' => '' ], null );
212 if ( $existing === null ) {
213 $obj = $this->newFromType( $this->default );
215 $obj = clone $existing;
218 $obj->crypt( $password );
224 * Determine whether a password object needs updating
226 * Check whether the given password is of the default type. If it is,
227 * pass off further needsUpdate checks to Password::needsUpdate.
229 * @param Password $password
231 * @return bool True if needs update, false otherwise
233 public function needsUpdate( Password
$password ): bool {
234 if ( $password->getType() !== $this->default ) {
237 return $password->needsUpdate();
242 * Generate a random string suitable for a password
244 * @param int $minLength Minimum length of password to generate
247 public static function generateRandomPasswordString( int $minLength = 10 ): string {
248 // Decide the final password length based on our min password length,
249 // requiring at least a minimum of self::MIN_RANDOM_PASSWORD_LENGTH chars.
250 $length = max( self
::MIN_RANDOM_PASSWORD_LENGTH
, $minLength );
251 // Multiply by 1.25 to get the number of hex characters we need
252 $hex = MWCryptRand
::generateHex( ceil( $length * 1.25 ) );
253 // Convert from base 16 to base 32 to get a proper password like string
254 return substr( \Wikimedia\base_convert
( $hex, 16, 32, $length ), -$length );
258 * Create an InvalidPassword
260 * @return InvalidPassword
262 public static function newInvalidPassword(): InvalidPassword
{
263 static $password = null;
265 if ( $password === null ) {
266 $factory = new self();
267 $password = new InvalidPassword( $factory, [ 'type' => '' ], null );
274 /** @deprecated since 1.43 use MediaWiki\\Password\\PasswordFactory */
275 class_alias( PasswordFactory
::class, 'PasswordFactory' );