Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
20 / 21
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Password
95.24% covered (success)
95.24%
20 / 21
85.71% covered (warning)
85.71%
6 / 7
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSupported
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseHash
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsUpdate
n/a
0 / 0
n/a
0 / 0
0
 verify
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 toString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 assertIsSafeSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 crypt
n/a
0 / 0
n/a
0 / 0
0
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
25/**
26 * Represents a password hash for use in authentication
27 *
28 * Note: All password types are transparently prefixed with :<TYPE>:, where <TYPE>
29 * is the registered type of the hash. This prefix is stripped in the constructor
30 * and is added back in the toString() function.
31 *
32 * When inheriting this class, there are a couple of expectations
33 * to be fulfilled:
34 *  * If Password::toString() is called on an object, and the result is passed back in
35 *    to PasswordFactory::newFromCiphertext(), the result will be identical to the original.
36 * With these two points in mind, when creating a new Password sub-class, there are some functions
37 * you have to override (because they are abstract) and others that you may want to override.
38 *
39 * The abstract functions that must be overridden are:
40 *  * Password::crypt(), which takes a plaintext password and hashes it into a string hash suitable
41 *    for being passed to the constructor of that class, and then stores that hash (and whatever
42 *     other data) into the internal state of the object.
43 * The functions that can optionally be overridden are:
44 *  * Password::parseHash(), which can be useful to override if you need to extract values from or
45 *    otherwise parse a password hash when it's passed to the constructor.
46 *  * Password::needsUpdate(), which can be useful if a specific password hash has different
47 *    logic for when the hash needs to be updated.
48 *  * Password::toString(), which can be useful if the hash was changed in the constructor and
49 *    needs to be re-assembled before being returned as a string. This function is expected to add
50 *    the type back on to the hash, so make sure to do that if you override the function.
51 *  * Password::verify() - This function checks if $this->hash was generated with the given
52 *    password. The default is to just hash the password and do a timing-safe string comparison with
53 *    $this->hash.
54 *
55 * After creating a new password hash type, it can be registered using the static
56 * Password::register() method. The default type is set using the Password::setDefaultType() type.
57 * Types must be registered before they can be set as the default.
58 *
59 * @since 1.24
60 */
61abstract class Password {
62    /**
63     * @var PasswordFactory Factory that created the object
64     */
65    protected $factory;
66
67    /**
68     * String representation of the hash without the type
69     * @var string|null
70     */
71    protected $hash;
72
73    /**
74     * Array of configuration variables injected from the constructor
75     * @var array
76     */
77    protected $config;
78
79    /**
80     * Hash must fit in user_password, which is a tinyblob
81     */
82    private const MAX_HASH_SIZE = 255;
83
84    /**
85     * Construct the Password object using a string hash
86     *
87     * It is strongly recommended not to call this function directly unless you
88     * have a reason to. Use the PasswordFactory class instead.
89     *
90     * @param PasswordFactory $factory Factory object that created the password
91     * @param array $config Array of engine configuration options for hashing
92     * @param string|null $hash The raw hash, including the type
93     */
94    final public function __construct( PasswordFactory $factory, array $config, string $hash = null ) {
95        if ( !$this->isSupported() ) {
96            throw new RuntimeException( 'PHP support not found for ' . get_class( $this ) );
97        }
98        if ( !isset( $config['type'] ) ) {
99            throw new InvalidArgumentException( 'Password configuration must contain a type name.' );
100        }
101        $this->config = $config;
102        $this->factory = $factory;
103
104        if ( $hash !== null && strlen( $hash ) >= 3 ) {
105            // Strip the type from the hash for parsing
106            $hash = substr( $hash, strpos( $hash, ':', 1 ) + 1 );
107        }
108
109        $this->hash = $hash;
110        $this->parseHash( $hash );
111    }
112
113    /**
114     * Get the type name of the password
115     *
116     * @return string Password type
117     */
118    final public function getType(): string {
119        return $this->config['type'];
120    }
121
122    /**
123     * Whether current password type is supported on this system.
124     *
125     * @return bool
126     */
127    protected function isSupported(): bool {
128        return true;
129    }
130
131    /**
132     * Perform any parsing necessary on the hash to see if the hash is valid
133     * and/or to perform logic for seeing if the hash needs updating.
134     *
135     * @param string|null $hash The hash, with the :<TYPE>: prefix stripped
136     * @throws PasswordError If there is an error in parsing the hash
137     */
138    protected function parseHash( ?string $hash ): void {
139    }
140
141    /**
142     * Determine if the hash needs to be updated
143     *
144     * @return bool True if needs update, false otherwise
145     */
146    abstract public function needsUpdate(): bool;
147
148    /**
149     * Checks whether the given password matches the hash stored in this object.
150     *
151     * @param string $password Password to check
152     * @return bool
153     */
154    public function verify( string $password ): bool {
155        // No need to use the factory because we're definitely making
156        // an object of the same type.
157        $obj = clone $this;
158        $obj->crypt( $password );
159
160        return hash_equals( $this->toString(), $obj->toString() );
161    }
162
163    /**
164     * Convert this hash to a string that can be stored in the database
165     *
166     * The resulting string should be considered the serialized representation
167     * of this hash, i.e., if the return value were recycled back into
168     * PasswordFactory::newFromCiphertext, the returned object would be equivalent to
169     * this; also, if two objects return the same value from this function, they
170     * are considered equivalent.
171     *
172     * @return string
173     * @throws PasswordError if password cannot be serialized to fit a tinyblob.
174     */
175    public function toString(): string {
176        $result = ':' . $this->config['type'] . ':' . $this->hash;
177        $this->assertIsSafeSize( $result );
178        return $result;
179    }
180
181    /**
182     * Assert that hash will fit in a tinyblob field.
183     *
184     * This prevents MW from inserting it into the DB
185     * and having MySQL silently truncating it, locking
186     * the user out of their account.
187     *
188     * @param string $hash The hash in question.
189     * @throws PasswordError If hash does not fit in DB.
190     */
191    final protected function assertIsSafeSize( string $hash ): void {
192        if ( strlen( $hash ) > self::MAX_HASH_SIZE ) {
193            throw new PasswordError( "Password hash is too big" );
194        }
195    }
196
197    /**
198     * Hash a password and store the result in this object
199     *
200     * The result of the password hash should be put into the internal
201     * state of the hash object.
202     *
203     * @param string $password Password to hash
204     * @throws PasswordError If an internal error occurs in hashing
205     */
206    abstract public function crypt( string $password ): void;
207}