Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
106 / 106
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
manager
100.00% covered (success)
100.00%
106 / 106
100.00% covered (success)
100.00%
10 / 10
46
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 initialize
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 register_default_type
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 fill_type_map
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 get_algorithm
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 detect_algorithm
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 hash
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 check
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
12
 combined_hash_password
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 check_combined_hash
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
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
14namespace phpbb\passwords;
15
16class 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}