Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.24% |
20 / 21 |
|
85.71% |
6 / 7 |
CRAP | |
0.00% |
0 / 1 |
Password | |
95.24% |
20 / 21 |
|
85.71% |
6 / 7 |
12 | |
0.00% |
0 / 1 |
__construct | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
5.03 | |||
getType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSupported | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseHash | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
needsUpdate | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
verify | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
toString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
assertIsSafeSize | |
100.00% |
2 / 2 |
|
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 | |
23 | declare( 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 | */ |
61 | abstract 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 | } |