Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
106 / 106 |
|
100.00% |
10 / 10 |
CRAP | |
100.00% |
1 / 1 |
manager | |
100.00% |
106 / 106 |
|
100.00% |
10 / 10 |
46 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
initialize | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
register_default_type | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
fill_type_map | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
get_algorithm | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
detect_algorithm | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
hash | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
8 | |||
check | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
12 | |||
combined_hash_password | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
4 | |||
check_combined_hash | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\passwords; |
15 | |
16 | class manager |
17 | { |
18 | /** |
19 | * Default hashing method |
20 | */ |
21 | protected $type = false; |
22 | |
23 | /** |
24 | * Hashing algorithm type map |
25 | * Will be used to map hash prefix to type |
26 | */ |
27 | protected $type_map = []; |
28 | |
29 | /** |
30 | * Service collection of hashing algorithms |
31 | * Needs to be public for passwords helper |
32 | */ |
33 | public $algorithms = false; |
34 | |
35 | /** |
36 | * Password convert flag. Signals that password should be converted |
37 | */ |
38 | public $convert_flag = false; |
39 | |
40 | /** |
41 | * Passwords helper |
42 | * @var \phpbb\passwords\helper |
43 | */ |
44 | protected $helper; |
45 | |
46 | /** |
47 | * phpBB configuration |
48 | * @var \phpbb\config\config |
49 | */ |
50 | protected $config; |
51 | |
52 | /** |
53 | * @var bool Whether or not initialized() has been called |
54 | */ |
55 | private $initialized = false; |
56 | |
57 | /** |
58 | * @var array Hashing driver service collection |
59 | */ |
60 | private $hashing_algorithms; |
61 | |
62 | /** |
63 | * @var array List of default driver types |
64 | */ |
65 | private $defaults; |
66 | |
67 | /** |
68 | * Construct a passwords object |
69 | * |
70 | * @param \phpbb\config\config $config phpBB configuration |
71 | * @param array $hashing_algorithms Hashing driver service collection |
72 | * @param \phpbb\passwords\helper $helper Passwords helper object |
73 | * @param array $defaults List of default driver types |
74 | */ |
75 | public function __construct(\phpbb\config\config $config, $hashing_algorithms, helper $helper, $defaults) |
76 | { |
77 | $this->config = $config; |
78 | $this->helper = $helper; |
79 | $this->hashing_algorithms = $hashing_algorithms; |
80 | $this->defaults = $defaults; |
81 | } |
82 | |
83 | /** |
84 | * Initialize the internal state |
85 | */ |
86 | protected function initialize() |
87 | { |
88 | if (!$this->initialized) |
89 | { |
90 | $this->initialized = true; |
91 | $this->fill_type_map($this->hashing_algorithms); |
92 | $this->register_default_type($this->defaults); |
93 | } |
94 | } |
95 | |
96 | /** |
97 | * Register default type |
98 | * Will register the first supported type from the list of default types |
99 | * |
100 | * @param array $defaults List of default types in order from first to |
101 | * use to last to use |
102 | */ |
103 | protected function register_default_type($defaults) |
104 | { |
105 | foreach ($defaults as $type) |
106 | { |
107 | if ($this->algorithms[$type]->is_supported()) |
108 | { |
109 | $this->type = $this->algorithms[$type]->get_prefix(); |
110 | break; |
111 | } |
112 | } |
113 | } |
114 | |
115 | /** |
116 | * Fill algorithm type map |
117 | * |
118 | * @param \phpbb\di\service_collection|array $hashing_algorithms |
119 | */ |
120 | protected function fill_type_map($hashing_algorithms) |
121 | { |
122 | foreach ($hashing_algorithms as $algorithm) |
123 | { |
124 | if (!isset($this->type_map[$algorithm->get_prefix()])) |
125 | { |
126 | $this->type_map[$algorithm->get_prefix()] = $algorithm; |
127 | } |
128 | } |
129 | $this->algorithms = $hashing_algorithms; |
130 | } |
131 | |
132 | /** |
133 | * Get the algorithm specified by a specific prefix |
134 | * |
135 | * @param string $prefix Password hash prefix |
136 | * |
137 | * @return object|bool The hash type object or false if prefix is not |
138 | * supported |
139 | */ |
140 | protected function get_algorithm($prefix) |
141 | { |
142 | if (isset($this->type_map[$prefix])) |
143 | { |
144 | return $this->type_map[$prefix]; |
145 | } |
146 | else |
147 | { |
148 | return false; |
149 | } |
150 | } |
151 | |
152 | /** |
153 | * Detect the hash type of the supplied hash |
154 | * |
155 | * @param string $hash Password hash that should be checked |
156 | * |
157 | * @return array|bool|object The hash type object or false if the specified |
158 | * type is not supported |
159 | */ |
160 | public function detect_algorithm($hash) |
161 | { |
162 | /* |
163 | * preg_match() will also show hashing algos like $2a\H$, which |
164 | * is a combination of bcrypt and phpass. Legacy algorithms |
165 | * like md5 will not be matched by this and need to be treated |
166 | * differently. |
167 | */ |
168 | if (!preg_match('#^\$([a-zA-Z0-9\\\]*?)\$#', $hash, $match)) |
169 | { |
170 | return false; |
171 | } |
172 | |
173 | $this->initialize(); |
174 | |
175 | // Be on the lookout for multiple hashing algorithms |
176 | // 2 is correct: H\2a > 2, H\P > 2 |
177 | if (strlen($match[1]) > 2 && strpos($match[1], '\\') !== false) |
178 | { |
179 | $hash_types = explode('\\', $match[1]); |
180 | $return_ary = array(); |
181 | foreach ($hash_types as $type) |
182 | { |
183 | // we do not support the same hashing |
184 | // algorithm more than once |
185 | if (isset($return_ary[$type])) |
186 | { |
187 | return false; |
188 | } |
189 | |
190 | $return_ary[$type] = $this->get_algorithm('$' . $type . '$'); |
191 | |
192 | if (empty($return_ary[$type])) |
193 | { |
194 | return false; |
195 | } |
196 | } |
197 | return $return_ary; |
198 | } |
199 | |
200 | // get_algorithm() will automatically return false if prefix |
201 | // is not supported |
202 | return $this->get_algorithm($match[0]); |
203 | } |
204 | |
205 | /** |
206 | * Hash supplied password |
207 | * |
208 | * @param string $password Password that should be hashed |
209 | * @param string|array $type Hash type. Will default to standard hash type if |
210 | * none is supplied, array for combined hashing |
211 | * @return string|bool Password hash of supplied password or false if |
212 | * if something went wrong during hashing |
213 | */ |
214 | public function hash($password, $type = '') |
215 | { |
216 | if (strlen($password) > 4096) |
217 | { |
218 | // If the password is too huge, we will simply reject it |
219 | // and not let the server try to hash it. |
220 | return false; |
221 | } |
222 | |
223 | $this->initialize(); |
224 | |
225 | // Try to retrieve algorithm by service name if type doesn't |
226 | // start with dollar sign |
227 | if (!is_array($type) && strpos($type, '$') !== 0 && isset($this->algorithms[$type])) |
228 | { |
229 | $type = $this->algorithms[$type]->get_prefix(); |
230 | } |
231 | |
232 | $type = ($type === '') ? $this->type : $type; |
233 | |
234 | if (is_array($type)) |
235 | { |
236 | return $this->combined_hash_password($password, $type); |
237 | } |
238 | |
239 | if (isset($this->type_map[$type])) |
240 | { |
241 | $hashing_algorithm = $this->type_map[$type]; |
242 | } |
243 | else |
244 | { |
245 | return false; |
246 | } |
247 | |
248 | return $hashing_algorithm->hash($password); |
249 | } |
250 | |
251 | /** |
252 | * Check supplied password against hash and set convert_flag if password |
253 | * needs to be converted to different format (preferably newer one) |
254 | * |
255 | * @param string $password Password that should be checked |
256 | * @param string $hash Stored hash |
257 | * @param array $user_row User's row in users table |
258 | * @return string|bool True if password is correct, false if not |
259 | */ |
260 | public function check($password, $hash, $user_row = array()) |
261 | { |
262 | if (strlen($password) > 4096) |
263 | { |
264 | // If the password is too huge, we will simply reject it |
265 | // and not let the server try to hash it. |
266 | return false; |
267 | } |
268 | |
269 | // Empty hashes can't be checked |
270 | if (empty($hash)) |
271 | { |
272 | return false; |
273 | } |
274 | |
275 | $this->initialize(); |
276 | |
277 | // First find out what kind of hash we're dealing with |
278 | $stored_hash_type = $this->detect_algorithm($hash); |
279 | if (!$stored_hash_type) |
280 | { |
281 | // Still check MD5 hashes as that is what the installer |
282 | // will default to for the admin user |
283 | return $this->get_algorithm('$H$')->check($password, $hash); |
284 | } |
285 | |
286 | // Multiple hash passes needed |
287 | if (is_array($stored_hash_type)) |
288 | { |
289 | $correct = $this->check_combined_hash($password, $stored_hash_type, $hash); |
290 | $this->convert_flag = ($correct === true) ? true : false; |
291 | return $correct; |
292 | } |
293 | |
294 | if ($stored_hash_type->get_prefix() !== $this->type) |
295 | { |
296 | $this->convert_flag = true; |
297 | } |
298 | else |
299 | { |
300 | if ($stored_hash_type instanceof driver\rehashable_driver_interface) |
301 | { |
302 | $this->convert_flag = $stored_hash_type->needs_rehash($hash); |
303 | } |
304 | else |
305 | { |
306 | $this->convert_flag = false; |
307 | } |
308 | } |
309 | |
310 | // Check all legacy hash types if prefix is $CP$ |
311 | if ($stored_hash_type->get_prefix() === '$CP$') |
312 | { |
313 | // Remove $CP$ prefix for proper checking |
314 | $hash = substr($hash, 4); |
315 | |
316 | foreach ($this->type_map as $algorithm) |
317 | { |
318 | if ($algorithm->is_legacy() && $algorithm->check($password, $hash, $user_row) === true) |
319 | { |
320 | return true; |
321 | } |
322 | } |
323 | } |
324 | |
325 | return $stored_hash_type->check($password, $hash); |
326 | } |
327 | |
328 | /** |
329 | * Create combined hash from already hashed password |
330 | * |
331 | * @param string $password_hash Complete current password hash |
332 | * @param array $type Type of the hashing algorithm the password hash |
333 | * should be combined with |
334 | * @return string|bool Combined password hash if combined hashing was |
335 | * successful, else false |
336 | */ |
337 | public function combined_hash_password($password_hash, array $type) |
338 | { |
339 | $this->initialize(); |
340 | |
341 | $data = array( |
342 | 'prefix' => '$', |
343 | 'settings' => '$', |
344 | ); |
345 | $hash_settings = $this->helper->get_combined_hash_settings($password_hash); |
346 | $hash = $hash_settings[0]; |
347 | |
348 | // Put settings of current hash into data array |
349 | $stored_hash_type = $this->detect_algorithm($password_hash); |
350 | $this->helper->combine_hash_output($data, 'prefix', $stored_hash_type->get_prefix()); |
351 | $this->helper->combine_hash_output($data, 'settings', $stored_hash_type->get_settings_only($password_hash)); |
352 | |
353 | // Hash current hash with the defined types |
354 | foreach ($type as $cur_type) |
355 | { |
356 | if (isset($this->algorithms[$cur_type])) |
357 | { |
358 | $new_hash_type = $this->algorithms[$cur_type]; |
359 | } |
360 | else |
361 | { |
362 | $new_hash_type = $this->get_algorithm($cur_type); |
363 | } |
364 | |
365 | if (!$new_hash_type) |
366 | { |
367 | return false; |
368 | } |
369 | |
370 | $new_hash = $new_hash_type->hash(str_replace($stored_hash_type->get_settings_only($password_hash), '', $hash)); |
371 | $this->helper->combine_hash_output($data, 'prefix', $new_hash_type->get_prefix()); |
372 | $this->helper->combine_hash_output($data, 'settings', substr(str_replace('$', '\\', $new_hash_type->get_settings_only($new_hash, true)), 0)); |
373 | $hash = str_replace($new_hash_type->get_settings_only($new_hash), '', $this->helper->obtain_hash_only($new_hash)); |
374 | } |
375 | return $this->helper->combine_hash_output($data, 'hash', $hash); |
376 | } |
377 | |
378 | /** |
379 | * Check combined password hash against the supplied password |
380 | * |
381 | * @param string $password Password entered by user |
382 | * @param array $stored_hash_type An array containing the hash types |
383 | * as described by stored password hash |
384 | * @param string $hash Stored password hash |
385 | * @param bool $skip_phpbb2_check True if phpBB2 password check should be skipped |
386 | * |
387 | * @return bool True if password is correct, false if not |
388 | */ |
389 | public function check_combined_hash($password, $stored_hash_type, $hash, bool $skip_phpbb2_check = false) |
390 | { |
391 | // Special case for passwords converted from phpBB2: |
392 | // These could be phpass(md5(password)) and hence already be double |
393 | // hashed. For these, try to also check combined hash output of |
394 | // md5 version of supplied password. |
395 | $is_valid_phpbb2_pass = false; |
396 | if (!$skip_phpbb2_check) |
397 | { |
398 | $is_valid_phpbb2_pass = $this->check_combined_hash(md5($password), $stored_hash_type, $hash, true); |
399 | } |
400 | |
401 | $i = 0; |
402 | $data = array( |
403 | 'prefix' => '$', |
404 | 'settings' => '$', |
405 | ); |
406 | $hash_settings = $this->helper->get_combined_hash_settings($hash); |
407 | foreach ($stored_hash_type as $key => $hash_type) |
408 | { |
409 | $rebuilt_hash = $this->helper->rebuild_hash($hash_type->get_prefix(), $hash_settings[$i]); |
410 | $this->helper->combine_hash_output($data, 'prefix', $key); |
411 | $this->helper->combine_hash_output($data, 'settings', $hash_settings[$i]); |
412 | $cur_hash = $hash_type->hash($password, $rebuilt_hash); |
413 | $password = str_replace($rebuilt_hash, '', $cur_hash); |
414 | $i++; |
415 | } |
416 | |
417 | return hash_equals($hash, $this->helper->combine_hash_output($data, 'hash', $password)) || $is_valid_phpbb2_pass; |
418 | } |
419 | } |