* @license GNU General Public License, version 2 (GPL-2.0) * * For full copyright and license information, please see * the docs/CREDITS.txt file. * */ namespace phpbb\passwords; class manager { /** * Default hashing method */ protected $type = false; /** * Hashing algorithm type map * Will be used to map hash prefix to type */ protected $type_map = false; /** * Service collection of hashing algorithms * Needs to be public for passwords helper */ public $algorithms = false; /** * Password convert flag. Signals that password should be converted */ public $convert_flag = false; /** * Passwords helper * @var \phpbb\passwords\helper */ protected $helper; /** * phpBB configuration * @var \phpbb\config\config */ protected $config; /** * @var bool Whether or not initialized() has been called */ private $initialized = false; /** * @var array Hashing driver service collection */ private $hashing_algorithms; /** * @var array List of default driver types */ private $defaults; /** * Construct a passwords object * * @param \phpbb\config\config $config phpBB configuration * @param array $hashing_algorithms Hashing driver service collection * @param \phpbb\passwords\helper $helper Passwords helper object * @param array $defaults List of default driver types */ public function __construct(\phpbb\config\config $config, $hashing_algorithms, helper $helper, $defaults) { $this->config = $config; $this->helper = $helper; $this->hashing_algorithms = $hashing_algorithms; $this->defaults = $defaults; } /** * Initialize the internal state */ protected function initialize() { if (!$this->initialized) { $this->initialized = true; $this->fill_type_map($this->hashing_algorithms); $this->register_default_type($this->defaults); } } /** * Register default type * Will register the first supported type from the list of default types * * @param array $defaults List of default types in order from first to * use to last to use */ protected function register_default_type($defaults) { foreach ($defaults as $type) { if ($this->algorithms[$type]->is_supported()) { $this->type = $this->algorithms[$type]->get_prefix(); break; } } } /** * Fill algorithm type map * * @param \phpbb\di\service_collection $hashing_algorithms */ protected function fill_type_map($hashing_algorithms) { foreach ($hashing_algorithms as $algorithm) { if (!isset($this->type_map[$algorithm->get_prefix()])) { $this->type_map[$algorithm->get_prefix()] = $algorithm; } } $this->algorithms = $hashing_algorithms; } /** * Get the algorithm specified by a specific prefix * * @param string $prefix Password hash prefix * * @return object|bool The hash type object or false if prefix is not * supported */ protected function get_algorithm($prefix) { if (isset($this->type_map[$prefix])) { return $this->type_map[$prefix]; } else { return false; } } /** * Detect the hash type of the supplied hash * * @param string $hash Password hash that should be checked * * @return object|bool The hash type object or false if the specified * type is not supported */ public function detect_algorithm($hash) { /* * preg_match() will also show hashing algos like $2a\H$, which * is a combination of bcrypt and phpass. Legacy algorithms * like md5 will not be matched by this and need to be treated * differently. */ if (!preg_match('#^\$([a-zA-Z0-9\\\]*?)\$#', $hash, $match)) { return false; } $this->initialize(); // Be on the lookout for multiple hashing algorithms // 2 is correct: H\2a > 2, H\P > 2 if (strlen($match[1]) > 2 && strpos($match[1], '\\') !== false) { $hash_types = explode('\\', $match[1]); $return_ary = array(); foreach ($hash_types as $type) { // we do not support the same hashing // algorithm more than once if (isset($return_ary[$type])) { return false; } $return_ary[$type] = $this->get_algorithm('$' . $type . '$'); if (empty($return_ary[$type])) { return false; } } return $return_ary; } // get_algorithm() will automatically return false if prefix // is not supported return $this->get_algorithm($match[0]); } /** * Hash supplied password * * @param string $password Password that should be hashed * @param string $type Hash type. Will default to standard hash type if * none is supplied * @return string|bool Password hash of supplied password or false if * if something went wrong during hashing */ public function hash($password, $type = '') { if (strlen($password) > 4096) { // If the password is too huge, we will simply reject it // and not let the server try to hash it. return false; } $this->initialize(); // Try to retrieve algorithm by service name if type doesn't // start with dollar sign if (!is_array($type) && strpos($type, '$') !== 0 && isset($this->algorithms[$type])) { $type = $this->algorithms[$type]->get_prefix(); } $type = ($type === '') ? $this->type : $type; if (is_array($type)) { return $this->combined_hash_password($password, $type); } if (isset($this->type_map[$type])) { $hashing_algorithm = $this->type_map[$type]; } else { return false; } return $hashing_algorithm->hash($password); } /** * Check supplied password against hash and set convert_flag if password * needs to be converted to different format (preferably newer one) * * @param string $password Password that should be checked * @param string $hash Stored hash * @param array $user_row User's row in users table * @return string|bool True if password is correct, false if not */ public function check($password, $hash, $user_row = array()) { if (strlen($password) > 4096) { // If the password is too huge, we will simply reject it // and not let the server try to hash it. return false; } // Empty hashes can't be checked if (empty($hash)) { return false; } $this->initialize(); // First find out what kind of hash we're dealing with $stored_hash_type = $this->detect_algorithm($hash); if ($stored_hash_type == false) { // Still check MD5 hashes as that is what the installer // will default to for the admin user return $this->get_algorithm('$H$')->check($password, $hash); } // Multiple hash passes needed if (is_array($stored_hash_type)) { $correct = $this->check_combined_hash($password, $stored_hash_type, $hash); $this->convert_flag = ($correct === true) ? true : false; return $correct; } if ($stored_hash_type->get_prefix() !== $this->type) { $this->convert_flag = true; } else { if ($stored_hash_type instanceof driver\rehashable_driver_interface) { $this->convert_flag = $stored_hash_type->needs_rehash($hash); } else { $this->convert_flag = false; } } // Check all legacy hash types if prefix is $CP$ if ($stored_hash_type->get_prefix() === '$CP$') { // Remove $CP$ prefix for proper checking $hash = substr($hash, 4); foreach ($this->type_map as $algorithm) { if ($algorithm->is_legacy() && $algorithm->check($password, $hash, $user_row) === true) { return true; } } } return $stored_hash_type->check($password, $hash); } /** * Create combined hash from already hashed password * * @param string $password_hash Complete current password hash * @param string $type Type of the hashing algorithm the password hash * should be combined with * @return string|bool Combined password hash if combined hashing was * successful, else false */ public function combined_hash_password($password_hash, $type) { $this->initialize(); $data = array( 'prefix' => '$', 'settings' => '$', ); $hash_settings = $this->helper->get_combined_hash_settings($password_hash); $hash = $hash_settings[0]; // Put settings of current hash into data array $stored_hash_type = $this->detect_algorithm($password_hash); $this->helper->combine_hash_output($data, 'prefix', $stored_hash_type->get_prefix()); $this->helper->combine_hash_output($data, 'settings', $stored_hash_type->get_settings_only($password_hash)); // Hash current hash with the defined types foreach ($type as $cur_type) { if (isset($this->algorithms[$cur_type])) { $new_hash_type = $this->algorithms[$cur_type]; } else { $new_hash_type = $this->get_algorithm($cur_type); } if (!$new_hash_type) { return false; } $new_hash = $new_hash_type->hash(str_replace($stored_hash_type->get_settings_only($password_hash), '', $hash)); $this->helper->combine_hash_output($data, 'prefix', $new_hash_type->get_prefix()); $this->helper->combine_hash_output($data, 'settings', substr(str_replace('$', '\\', $new_hash_type->get_settings_only($new_hash, true)), 0)); $hash = str_replace($new_hash_type->get_settings_only($new_hash), '', $this->helper->obtain_hash_only($new_hash)); } return $this->helper->combine_hash_output($data, 'hash', $hash); } /** * Check combined password hash against the supplied password * * @param string $password Password entered by user * @param array $stored_hash_type An array containing the hash types * as described by stored password hash * @param string $hash Stored password hash * @param bool $skip_phpbb2_check True if phpBB2 password check should be skipped * * @return bool True if password is correct, false if not */ public function check_combined_hash($password, $stored_hash_type, $hash, bool $skip_phpbb2_check = false) { // Special case for passwords converted from phpBB2: // These could be phpass(md5(password)) and hence already be double // hashed. For these, try to also check combined hash output of // md5 version of supplied password. $is_valid_phpbb2_pass = false; if (!$skip_phpbb2_check) { $is_valid_phpbb2_pass = $this->check_combined_hash(md5($password), $stored_hash_type, $hash, true); } $i = 0; $data = array( 'prefix' => '$', 'settings' => '$', ); $hash_settings = $this->helper->get_combined_hash_settings($hash); foreach ($stored_hash_type as $key => $hash_type) { $rebuilt_hash = $this->helper->rebuild_hash($hash_type->get_prefix(), $hash_settings[$i]); $this->helper->combine_hash_output($data, 'prefix', $key); $this->helper->combine_hash_output($data, 'settings', $hash_settings[$i]); $cur_hash = $hash_type->hash($password, $rebuilt_hash); $password = str_replace($rebuilt_hash, '', $cur_hash); $i++; } return hash_equals($hash, $this->helper->combine_hash_output($data, 'hash', $password)) || $is_valid_phpbb2_pass; } }