Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.74% covered (success)
95.74%
45 / 47
92.31% covered (success)
92.31%
12 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
PasswordFactory
95.74% covered (success)
95.74%
45 / 47
92.31% covered (success)
92.31%
12 / 13
25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 register
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDefaultType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromCiphertext
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 newFromType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromTypeAndHash
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 newFromPlaintext
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 needsUpdate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 generateRandomPasswordString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newInvalidPassword
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
1<?php
2/**
3 * Implements the Password class for the MediaWiki software.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 */
22
23declare( strict_types = 1 );
24
25use MediaWiki\Config\Config;
26use MediaWiki\MainConfigNames;
27use Wikimedia\ObjectFactory\ObjectFactory;
28
29/**
30 * Factory class for creating and checking Password objects
31 *
32 * @since 1.24
33 */
34final class PasswordFactory {
35    /**
36     * The default PasswordHash type
37     *
38     * @var string
39     * @see PasswordFactory::setDefaultType
40     */
41    private $default = '';
42
43    /**
44     * Mapping of password types to classes
45     *
46     * @var array[]
47     * @see PasswordFactory::register
48     * @see Setup.php
49     */
50    private $types = [
51        '' => [ 'type' => '', 'class' => InvalidPassword::class ],
52    ];
53
54    /**
55     * Most of the time you'll want to use MediaWikiServices::getInstance()->getPasswordFactory
56     * instead.
57     * @param array $config Mapping of password type => config
58     * @param string $default Default password type
59     * @see PasswordFactory::register
60     * @see PasswordFactory::setDefaultType
61     */
62    public function __construct( array $config = [], string $default = '' ) {
63        foreach ( $config as $type => $options ) {
64            $this->register( $type, $options );
65        }
66
67        if ( $default !== '' ) {
68            $this->setDefaultType( $default );
69        }
70    }
71
72    /**
73     * Register a new type of password hash
74     *
75     * @param string $type Unique type name for the hash. Will be prefixed to the password hashes
76     *   to identify what hashing method was used.
77     * @param array $config Array of configuration options. 'class' is required (the Password
78     *   subclass name), everything else is passed to the constructor of that class.
79     */
80    public function register( string $type, array $config ): void {
81        $config['type'] = $type;
82        $this->types[$type] = $config;
83    }
84
85    /**
86     * Set the default password type
87     *
88     * This type will be used for creating new passwords when the type is not specified.
89     * Passwords of a different type will be considered outdated and in need of update.
90     *
91     * @param string $type Password hash type
92     * @throws InvalidArgumentException If the type is not registered
93     */
94    public function setDefaultType( string $type ): void {
95        if ( !isset( $this->types[$type] ) ) {
96            throw new InvalidArgumentException( "Invalid password type $type." );
97        }
98        $this->default = $type;
99    }
100
101    /**
102     * Get the default password type
103     *
104     * @return string
105     */
106    public function getDefaultType(): string {
107        return $this->default;
108    }
109
110    /**
111     * @deprecated since 1.32 Initialize settings using the constructor
112     *   Emitting deprecation warnings since 1.41.
113     *
114     * Initialize the internal static variables using the global variables
115     *
116     * @param Config $config Configuration object to load data from
117     */
118    public function init( Config $config ): void {
119        wfDeprecated( __METHOD__, '1.32' );
120        foreach ( $config->get( MainConfigNames::PasswordConfig ) as $type => $options ) {
121            $this->register( $type, $options );
122        }
123
124        $this->setDefaultType( $config->get( MainConfigNames::PasswordDefault ) );
125    }
126
127    /**
128     * Get the list of types of passwords
129     *
130     * @return array[]
131     */
132    public function getTypes(): array {
133        return $this->types;
134    }
135
136    /**
137     * Create a new Password object from an existing string hash
138     *
139     * Parse the type of a hash and create a new hash object based on the parsed type.
140     * Pass the raw hash to the constructor of the new object. Use InvalidPassword type
141     * if a null hash is given.
142     *
143     * @param string|null $hash Existing hash or null for an invalid password
144     * @return Password
145     * @throws PasswordError If hash is invalid or type is not recognized
146     */
147    public function newFromCiphertext( ?string $hash ): Password {
148        if ( $hash === null || $hash === '' ) {
149            return new InvalidPassword( $this, [ 'type' => '' ], null );
150        } elseif ( $hash[0] !== ':' ) {
151            throw new PasswordError( 'Invalid hash given' );
152        }
153
154        $type = substr( $hash, 1, strpos( $hash, ':', 1 ) - 1 );
155        return $this->newFromTypeAndHash( $type, $hash );
156    }
157
158    /**
159     * Create a new Password object of the given type.
160     *
161     * @param string $type Existing type
162     * @return Password
163     * @throws PasswordError If type is not recognized
164     */
165    public function newFromType( string $type ): Password {
166        return $this->newFromTypeAndHash( $type, null );
167    }
168
169    /**
170     * Create a new Password object of the given type, optionally with an existing string hash.
171     *
172     * @param string $type Existing type
173     * @param string|null $hash Existing hash
174     * @return Password
175     * @throws PasswordError If hash is invalid or type is not recognized
176     */
177    private function newFromTypeAndHash( string $type, ?string $hash ): Password {
178        if ( !isset( $this->types[$type] ) ) {
179            throw new PasswordError( "Unrecognized password hash type $type." );
180        }
181
182        $config = $this->types[$type];
183
184        // @phan-suppress-next-line PhanTypeInvalidCallableArrayKey
185        return ObjectFactory::getObjectFromSpec( $config, [
186            'extraArgs' => [ $this, $config, $hash ],
187            'assertClass' => Password::class,
188        ] );
189    }
190
191    /**
192     * Create a new Password object from a plaintext password
193     *
194     * If no existing object is given, make a new default object. If one is given, clone that
195     * object. Then pass the plaintext to Password::crypt().
196     *
197     * @param string|null $password Plaintext password, or null for an invalid password
198     * @param Password|null $existing Optional existing hash to get options from
199     * @return Password
200     */
201    public function newFromPlaintext( ?string $password, Password $existing = null ): Password {
202        if ( $password === null ) {
203            return new InvalidPassword( $this, [ 'type' => '' ], null );
204        }
205
206        if ( $existing === null ) {
207            $obj = $this->newFromType( $this->default );
208        } else {
209            $obj = clone $existing;
210        }
211
212        $obj->crypt( $password );
213
214        return $obj;
215    }
216
217    /**
218     * Determine whether a password object needs updating
219     *
220     * Check whether the given password is of the default type. If it is,
221     * pass off further needsUpdate checks to Password::needsUpdate.
222     *
223     * @param Password $password
224     *
225     * @return bool True if needs update, false otherwise
226     */
227    public function needsUpdate( Password $password ): bool {
228        if ( $password->getType() !== $this->default ) {
229            return true;
230        } else {
231            return $password->needsUpdate();
232        }
233    }
234
235    /**
236     * Generate a random string suitable for a password
237     *
238     * @param int $minLength Minimum length of password to generate
239     * @return string
240     */
241    public static function generateRandomPasswordString( int $minLength = 10 ): string {
242        // Decide the final password length based on our min password length,
243        // stopping at a minimum of 10 chars.
244        $length = max( 10, $minLength );
245        // Multiply by 1.25 to get the number of hex characters we need
246        // Generate random hex chars
247        $hex = MWCryptRand::generateHex( ceil( $length * 1.25 ) );
248        // Convert from base 16 to base 32 to get a proper password like string
249        return substr( Wikimedia\base_convert( $hex, 16, 32, $length ), -$length );
250    }
251
252    /**
253     * Create an InvalidPassword
254     *
255     * @return InvalidPassword
256     */
257    public static function newInvalidPassword(): InvalidPassword {
258        static $password = null;
259
260        if ( $password === null ) {
261            $factory = new self();
262            $password = new InvalidPassword( $factory, [ 'type' => '' ], null );
263        }
264
265        return $password;
266    }
267}