aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--phpBB/config/passwords.yml64
-rw-r--r--phpBB/config/services.yml1
-rw-r--r--phpBB/install/schemas/firebird_schema.sql2
-rw-r--r--phpBB/install/schemas/mssql_schema.sql2
-rw-r--r--phpBB/install/schemas/mysql_40_schema.sql2
-rw-r--r--phpBB/install/schemas/mysql_41_schema.sql2
-rw-r--r--phpBB/install/schemas/oracle_schema.sql4
-rw-r--r--phpBB/install/schemas/postgres_schema.sql2
-rw-r--r--phpBB/install/schemas/sqlite_schema.sql2
-rw-r--r--phpBB/phpbb/db/migration/data/310/passwords.php38
-rw-r--r--phpBB/phpbb/passwords/driver/base.php68
-rw-r--r--phpBB/phpbb/passwords/driver/bcrypt.php100
-rw-r--r--phpBB/phpbb/passwords/driver/bcrypt_2y.php40
-rw-r--r--phpBB/phpbb/passwords/driver/helper.php132
-rw-r--r--phpBB/phpbb/passwords/driver/interface.php68
-rw-r--r--phpBB/phpbb/passwords/driver/phpass.php32
-rw-r--r--phpBB/phpbb/passwords/driver/salted_md5.php144
-rw-r--r--phpBB/phpbb/passwords/helper.php198
-rw-r--r--phpBB/phpbb/passwords/manager.php255
-rw-r--r--tests/passwords/manager_test.php249
20 files changed, 1397 insertions, 8 deletions
diff --git a/phpBB/config/passwords.yml b/phpBB/config/passwords.yml
new file mode 100644
index 0000000000..41373fc83f
--- /dev/null
+++ b/phpBB/config/passwords.yml
@@ -0,0 +1,64 @@
+parameters:
+ passwords.algorithm: passwords.driver.bcrypt_2y
+
+services:
+ passwords.driver.bcrypt:
+ class: phpbb_passwords_driver_bcrypt
+ arguments:
+ - @config
+ - @passwords.driver_helper
+ calls:
+ - [set_name, [passwords.driver.bcrypt]]
+ tags:
+ - { name: passwords.driver }
+
+ passwords.driver.bcrypt_2y:
+ class: phpbb_passwords_driver_bcrypt_2y
+ arguments:
+ - @config
+ - @passwords.driver_helper
+ calls:
+ - [set_name, [passwords.driver.bcrypt_2y]]
+ tags:
+ - { name: passwords.driver }
+
+ passwords.driver.salted_md5:
+ class: phpbb_passwords_driver_salted_md5
+ arguments:
+ - @config
+ - @passwords.driver_helper
+ calls:
+ - [set_name, [passwords.driver.salted_md5]]
+ tags:
+ - { name: passwords.driver }
+
+ passwords.driver.phpass:
+ class: phpbb_passwords_driver_phpass
+ arguments:
+ - @config
+ - @passwords.driver_helper
+ calls:
+ - [set_name, [passwords.driver.phpass]]
+ tags:
+ - { name: passwords.driver }
+
+ passwords.driver_collection:
+ class: phpbb_di_service_collection
+ arguments:
+ - @service_container
+ tags:
+ - { name: service_collection, tag: passwords.driver }
+
+ passwords.driver_helper:
+ class: phpbb_passwords_driver_helper
+
+ passwords.manager:
+ class: phpbb_passwords_manager
+ arguments:
+ - @config
+ - @passwords.driver_collection
+ - @passwords.helper
+ - %passwords.algorithm%
+
+ passwords.helper:
+ class: phpbb_passwords_helper
diff --git a/phpBB/config/services.yml b/phpBB/config/services.yml
index e33db0c2bc..7a0a215262 100644
--- a/phpBB/config/services.yml
+++ b/phpBB/config/services.yml
@@ -6,6 +6,7 @@ imports:
- { resource: avatars.yml }
- { resource: feed.yml }
- { resource: auth_providers.yml }
+ - { resource: passwords.yml }
services:
acl.permissions:
diff --git a/phpBB/install/schemas/firebird_schema.sql b/phpBB/install/schemas/firebird_schema.sql
index 1e47008d73..a9b7f58099 100644
--- a/phpBB/install/schemas/firebird_schema.sql
+++ b/phpBB/install/schemas/firebird_schema.sql
@@ -1365,7 +1365,7 @@ CREATE TABLE phpbb_users (
user_regdate INTEGER DEFAULT 0 NOT NULL,
username VARCHAR(255) CHARACTER SET UTF8 DEFAULT '' NOT NULL COLLATE UNICODE,
username_clean VARCHAR(255) CHARACTER SET UTF8 DEFAULT '' NOT NULL COLLATE UNICODE,
- user_password VARCHAR(40) CHARACTER SET UTF8 DEFAULT '' NOT NULL COLLATE UNICODE,
+ user_password VARCHAR(255) CHARACTER SET UTF8 DEFAULT '' NOT NULL COLLATE UNICODE,
user_passchg INTEGER DEFAULT 0 NOT NULL,
user_pass_convert INTEGER DEFAULT 0 NOT NULL,
user_email VARCHAR(100) CHARACTER SET UTF8 DEFAULT '' NOT NULL COLLATE UNICODE,
diff --git a/phpBB/install/schemas/mssql_schema.sql b/phpBB/install/schemas/mssql_schema.sql
index 922313236e..bea116b7d6 100644
--- a/phpBB/install/schemas/mssql_schema.sql
+++ b/phpBB/install/schemas/mssql_schema.sql
@@ -1681,7 +1681,7 @@ CREATE TABLE [phpbb_users] (
[user_regdate] [int] DEFAULT (0) NOT NULL ,
[username] [varchar] (255) DEFAULT ('') NOT NULL ,
[username_clean] [varchar] (255) DEFAULT ('') NOT NULL ,
- [user_password] [varchar] (40) DEFAULT ('') NOT NULL ,
+ [user_password] [varchar] (255) DEFAULT ('') NOT NULL ,
[user_passchg] [int] DEFAULT (0) NOT NULL ,
[user_pass_convert] [int] DEFAULT (0) NOT NULL ,
[user_email] [varchar] (100) DEFAULT ('') NOT NULL ,
diff --git a/phpBB/install/schemas/mysql_40_schema.sql b/phpBB/install/schemas/mysql_40_schema.sql
index e07a768387..da464f3ae7 100644
--- a/phpBB/install/schemas/mysql_40_schema.sql
+++ b/phpBB/install/schemas/mysql_40_schema.sql
@@ -974,7 +974,7 @@ CREATE TABLE phpbb_users (
user_regdate int(11) UNSIGNED DEFAULT '0' NOT NULL,
username blob NOT NULL,
username_clean blob NOT NULL,
- user_password varbinary(120) DEFAULT '' NOT NULL,
+ user_password blob DEFAULT '' NOT NULL,
user_passchg int(11) UNSIGNED DEFAULT '0' NOT NULL,
user_pass_convert tinyint(1) UNSIGNED DEFAULT '0' NOT NULL,
user_email blob NOT NULL,
diff --git a/phpBB/install/schemas/mysql_41_schema.sql b/phpBB/install/schemas/mysql_41_schema.sql
index d3ed1ee15e..fa8929d860 100644
--- a/phpBB/install/schemas/mysql_41_schema.sql
+++ b/phpBB/install/schemas/mysql_41_schema.sql
@@ -974,7 +974,7 @@ CREATE TABLE phpbb_users (
user_regdate int(11) UNSIGNED DEFAULT '0' NOT NULL,
username varchar(255) DEFAULT '' NOT NULL,
username_clean varchar(255) DEFAULT '' NOT NULL,
- user_password varchar(40) DEFAULT '' NOT NULL,
+ user_password varchar(255) DEFAULT '' NOT NULL,
user_passchg int(11) UNSIGNED DEFAULT '0' NOT NULL,
user_pass_convert tinyint(1) UNSIGNED DEFAULT '0' NOT NULL,
user_email varchar(100) DEFAULT '' NOT NULL,
diff --git a/phpBB/install/schemas/oracle_schema.sql b/phpBB/install/schemas/oracle_schema.sql
index f32980e378..a2e9780b9f 100644
--- a/phpBB/install/schemas/oracle_schema.sql
+++ b/phpBB/install/schemas/oracle_schema.sql
@@ -986,7 +986,7 @@ CREATE TABLE phpbb_poll_votes (
topic_id number(8) DEFAULT '0' NOT NULL,
poll_option_id number(4) DEFAULT '0' NOT NULL,
vote_user_id number(8) DEFAULT '0' NOT NULL,
- vote_user_ip varchar2(40) DEFAULT ''
+ vote_user_ip varchar2(40) DEFAULT ''
)
/
@@ -1798,7 +1798,7 @@ CREATE TABLE phpbb_users (
user_regdate number(11) DEFAULT '0' NOT NULL,
username varchar2(255) DEFAULT '' ,
username_clean varchar2(255) DEFAULT '' ,
- user_password varchar2(120) DEFAULT '' ,
+ user_password varchar2(255) DEFAULT '' ,
user_passchg number(11) DEFAULT '0' NOT NULL,
user_pass_convert number(1) DEFAULT '0' NOT NULL,
user_email varchar2(300) DEFAULT '' ,
diff --git a/phpBB/install/schemas/postgres_schema.sql b/phpBB/install/schemas/postgres_schema.sql
index 14435898eb..ecafe8f43f 100644
--- a/phpBB/install/schemas/postgres_schema.sql
+++ b/phpBB/install/schemas/postgres_schema.sql
@@ -1246,7 +1246,7 @@ CREATE TABLE phpbb_users (
user_regdate INT4 DEFAULT '0' NOT NULL CHECK (user_regdate >= 0),
username varchar_ci DEFAULT '' NOT NULL,
username_clean varchar_ci DEFAULT '' NOT NULL,
- user_password varchar(40) DEFAULT '' NOT NULL,
+ user_password varchar_ci DEFAULT '' NOT NULL,
user_passchg INT4 DEFAULT '0' NOT NULL CHECK (user_passchg >= 0),
user_pass_convert INT2 DEFAULT '0' NOT NULL CHECK (user_pass_convert >= 0),
user_email varchar(100) DEFAULT '' NOT NULL,
diff --git a/phpBB/install/schemas/sqlite_schema.sql b/phpBB/install/schemas/sqlite_schema.sql
index de88900f06..d076b9823b 100644
--- a/phpBB/install/schemas/sqlite_schema.sql
+++ b/phpBB/install/schemas/sqlite_schema.sql
@@ -945,7 +945,7 @@ CREATE TABLE phpbb_users (
user_regdate INTEGER UNSIGNED NOT NULL DEFAULT '0',
username varchar(255) NOT NULL DEFAULT '',
username_clean varchar(255) NOT NULL DEFAULT '',
- user_password varchar(40) NOT NULL DEFAULT '',
+ user_password varchar(255) NOT NULL DEFAULT '',
user_passchg INTEGER UNSIGNED NOT NULL DEFAULT '0',
user_pass_convert INTEGER UNSIGNED NOT NULL DEFAULT '0',
user_email varchar(100) NOT NULL DEFAULT '',
diff --git a/phpBB/phpbb/db/migration/data/310/passwords.php b/phpBB/phpbb/db/migration/data/310/passwords.php
new file mode 100644
index 0000000000..0611abb79d
--- /dev/null
+++ b/phpBB/phpbb/db/migration/data/310/passwords.php
@@ -0,0 +1,38 @@
+<?php
+/**
+*
+* @package migration
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-license.php GNU Public License v2
+*
+*/
+
+class phpbb_db_migration_data_310_passwords extends phpbb_db_migration
+{
+ static public function depends_on()
+ {
+ return array('phpbb_db_migration_data_30x_3_0_11');
+ }
+
+ public function update_schema()
+ {
+ return array(
+ 'change_columns' => array(
+ $this->table_prefix . 'users' => array(
+ 'user_password' => array('VCHAR:255', ''),
+ ),
+ ),
+ );
+ }
+
+ public function revert_schema()
+ {
+ return array(
+ 'change_columns' => array(
+ $this->table_prefix . 'users' => array(
+ 'user_password' => array('VCHAR:40', ''),
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpBB/phpbb/passwords/driver/base.php b/phpBB/phpbb/passwords/driver/base.php
new file mode 100644
index 0000000000..2984dafef7
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/base.php
@@ -0,0 +1,68 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+abstract class phpbb_passwords_driver_base implements phpbb_passwords_driver_interface
+{
+ /** @var phpbb_config */
+ protected $config;
+
+ /** @var phpbb_passwords_driver_helper */
+ protected $helper;
+
+ /** @var driver name */
+ protected $name;
+
+ /**
+ * Constructor of passwords driver object
+ *
+ * @return string Hash prefix
+ */
+ public function __construct(phpbb_config $config, phpbb_passwords_driver_helper $helper)
+ {
+ $this->config = $config;
+ $this->helper = $helper;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function is_supported()
+ {
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get_name()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set driver name
+ *
+ * @param string $name Driver name
+ */
+ public function set_name($name)
+ {
+ $this->name = $name;
+ }
+}
diff --git a/phpBB/phpbb/passwords/driver/bcrypt.php b/phpBB/phpbb/passwords/driver/bcrypt.php
new file mode 100644
index 0000000000..db41fe6b38
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/bcrypt.php
@@ -0,0 +1,100 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_driver_bcrypt extends phpbb_passwords_driver_base
+{
+ const PREFIX = '$2a$';
+
+ /**
+ * @inheritdoc
+ */
+ public function get_prefix()
+ {
+ return self::PREFIX;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hash($password, $salt = '')
+ {
+ // The 2x and 2y prefixes of bcrypt might not be supported
+ // Revert to 2a if this is the case
+ $prefix = (!$this->is_supported()) ? '$2a$' : $this->get_prefix();
+
+ if ($salt == '')
+ {
+ $salt = $prefix . '10$' . $this->get_random_salt();
+ }
+
+ $hash = crypt($password, $salt);
+ if (strlen($hash) < 60)
+ {
+ return false;
+ }
+ return $hash;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function check($password, $hash)
+ {
+ $salt = substr($hash, 0, 29);
+ if (strlen($salt) != 29)
+ {
+ return false;
+ }
+
+ if ($hash == $this->hash($password, $salt))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get a random salt value with a length of 22 characters
+ *
+ * @return string Salt for password hashing
+ */
+ protected function get_random_salt()
+ {
+ return $this->helper->hash_encode64($this->helper->get_random_salt(22), 22);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get_settings_only($hash, $full = false)
+ {
+ if ($full)
+ {
+ $pos = stripos($hash, '$', 1) + 1;
+ $length = 22 + (strripos($hash, '$') + 1 - $pos);
+ }
+ else
+ {
+ $pos = strripos($hash, '$') + 1;
+ $length = 22;
+ }
+ return substr($hash, $pos, $length);
+ }
+}
diff --git a/phpBB/phpbb/passwords/driver/bcrypt_2y.php b/phpBB/phpbb/passwords/driver/bcrypt_2y.php
new file mode 100644
index 0000000000..5b0dbdd311
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/bcrypt_2y.php
@@ -0,0 +1,40 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_driver_bcrypt_2y extends phpbb_passwords_driver_bcrypt
+{
+ const PREFIX = '$2y$';
+
+ /**
+ * @inheritdoc
+ */
+ public function get_prefix()
+ {
+ return self::PREFIX;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function is_supported()
+ {
+ return (version_compare(PHP_VERSION, '5.3.7', '<')) ? false : true;
+ }
+}
diff --git a/phpBB/phpbb/passwords/driver/helper.php b/phpBB/phpbb/passwords/driver/helper.php
new file mode 100644
index 0000000000..a50ac8819e
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/helper.php
@@ -0,0 +1,132 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_driver_helper
+{
+ /**
+ * base64 alphabet
+ * @var string
+ */
+ public $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+ /**
+ * Base64 encode hash
+ *
+ * @param string $input Input string
+ * @param int $count Input string length
+ *
+ * @return string base64 encoded string
+ */
+ public function hash_encode64($input, $count)
+ {
+ $output = '';
+ $i = 0;
+
+ do
+ {
+ $value = ord($input[$i++]);
+ $output .= $this->itoa64[$value & 0x3f];
+
+ if ($i < $count)
+ {
+ $value |= ord($input[$i]) << 8;
+ }
+
+ $output .= $this->itoa64[($value >> 6) & 0x3f];
+
+ if ($i++ >= $count)
+ {
+ break;
+ }
+
+ if ($i < $count)
+ {
+ $value |= ord($input[$i]) << 16;
+ }
+
+ $output .= $this->itoa64[($value >> 12) & 0x3f];
+
+ if ($i++ >= $count)
+ {
+ break;
+ }
+
+ $output .= $this->itoa64[($value >> 18) & 0x3f];
+ }
+ while ($i < $count);
+
+ return $output;
+ }
+
+ /**
+ * Return unique id
+ * @param string $extra additional entropy
+ *
+ * @return string Unique id
+ */
+ public function unique_id($extra = 'c')
+ {
+ static $dss_seeded = false;
+ global $config;
+
+ $val = $config['rand_seed'] . microtime();
+ $val = md5($val);
+ $config['rand_seed'] = md5($config['rand_seed'] . $val . $extra);
+
+ if ($dss_seeded !== true && ($config['rand_seed_last_update'] < time() - rand(1,10)))
+ {
+ set_config('rand_seed_last_update', time(), true);
+ set_config('rand_seed', $config['rand_seed'], true);
+ $dss_seeded = true;
+ }
+
+ return substr($val, 4, 16);
+ }
+
+ /**
+ * Get random salt with specified length
+ *
+ * @param int $length Salt length
+ */
+ public function get_random_salt($length)
+ {
+ $random = '';
+
+ if (($fh = @fopen('/dev/urandom', 'rb')))
+ {
+ $random = fread($fh, $length);
+ fclose($fh);
+ }
+
+ if (strlen($random) < $length)
+ {
+ $random = '';
+ $random_state = $this->unique_id();
+
+ for ($i = 0; $i < $length; $i += 16)
+ {
+ $random_state = md5($this->unique_id() . $random_state);
+ $random .= pack('H*', md5($random_state));
+ }
+ $random = substr($random, 0, $length);
+ }
+ return $random;
+ }
+}
diff --git a/phpBB/phpbb/passwords/driver/interface.php b/phpBB/phpbb/passwords/driver/interface.php
new file mode 100644
index 0000000000..a2088db81c
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/interface.php
@@ -0,0 +1,68 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+interface phpbb_passwords_driver_interface
+{
+ /**
+ * Check if hash type is supported
+ *
+ * @return bool True if supported, false if not
+ */
+ public function is_supported();
+ /**
+ * Returns the hash prefix
+ *
+ * @return string Hash prefix
+ */
+ public function get_prefix();
+
+ /**
+ * Hash the password
+ *
+ * @return string Password hash
+ */
+ public function hash($password);
+
+ /**
+ * Check the password against the supplied hash
+ *
+ * @param string $password The password to check
+ * @param string $hash The password hash to check against
+ * @return bool True if password is correct, else false
+ */
+ public function check($password, $hash);
+
+ /**
+ * Get only the settings of the specified hash
+ *
+ * @param string $hash Password hash
+ * @param bool $full Return full settings or only settings
+ * related to the salt
+ * @return string String containing the hash settings
+ */
+ public function get_settings_only($hash, $full = false);
+
+ /**
+ * Get the driver name
+ *
+ * @return string Driver name
+ */
+ public function get_name();
+}
diff --git a/phpBB/phpbb/passwords/driver/phpass.php b/phpBB/phpbb/passwords/driver/phpass.php
new file mode 100644
index 0000000000..d52ce96d11
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/phpass.php
@@ -0,0 +1,32 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_driver_phpass extends phpbb_passwords_driver_salted_md5
+{
+ const PREFIX = '$P$';
+
+ /**
+ * @inheritdoc
+ */
+ public function get_prefix()
+ {
+ return self::PREFIX;
+ }
+}
diff --git a/phpBB/phpbb/passwords/driver/salted_md5.php b/phpBB/phpbb/passwords/driver/salted_md5.php
new file mode 100644
index 0000000000..6fa12948be
--- /dev/null
+++ b/phpBB/phpbb/passwords/driver/salted_md5.php
@@ -0,0 +1,144 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_driver_salted_md5 extends phpbb_passwords_driver_base
+{
+ const PREFIX = '$H$';
+
+ /**
+ * @inheritdoc
+ */
+ public function get_prefix()
+ {
+ return self::PREFIX;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hash($password, $setting = '')
+ {
+ if ($setting != '')
+ {
+ if (($settings = $this->get_hash_settings($setting)) === false)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (($settings = $this->get_hash_settings($this->generate_salt())) === false)
+ {
+ return false;
+ }
+ }
+
+ $hash = md5($settings['salt'] . $password, true);
+ do
+ {
+ $hash = md5($hash . $password, true);
+ }
+ while (--$settings['count']);
+
+ $output = $settings['full'];
+ $output .= $this->helper->hash_encode64($hash, 16);
+
+ if (strlen($output) == 34)
+ {
+ return $output;
+ }
+
+ // Should we really just return the md5 of the password? O.o
+ return md5($password);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function check($password, $hash)
+ {
+ if (strlen($hash) !== 34)
+ {
+ return (md5($password) === $hash) ? true : false;
+ }
+ // No need to check prefix, already did that in manage
+
+ if ($hash === $this->hash($password, $hash))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Generate salt for hashing method
+ *
+ * @return string Salt for hashing method
+ */
+ protected function generate_salt()
+ {
+ $salt = '';
+ $random = '';
+ $count = 6;
+
+ $random = $this->helper->get_random_salt($count);
+
+ $salt = $this->get_prefix();
+ $salt .= $this->helper->itoa64[min($count + 5, 30)];
+ $salt .= $this->helper->hash_encode64($random, $count);
+
+ return $salt;
+ }
+
+ /**
+ * Get hash settings
+ *
+ * @return array Array containing the count_log2, salt, and full hash
+ * settings string
+ */
+ public function get_hash_settings($hash)
+ {
+ if (empty($hash))
+ {
+ return false;
+ }
+ $count_log2 = strpos($this->helper->itoa64, $hash[3]);
+ $salt = substr($hash, 4, 8);
+
+ if ($count_log2 < 7 || $count_log2 > 30 || strlen($salt) != 8)
+ {
+ return false;
+ }
+
+ return array(
+ 'count' => 1 << $count_log2,
+ 'salt' => $salt,
+ 'full' => substr($hash, 0, 12),
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get_settings_only($hash, $full = false)
+ {
+ return substr($hash, 3, 9);
+ }
+}
diff --git a/phpBB/phpbb/passwords/helper.php b/phpBB/phpbb/passwords/helper.php
new file mode 100644
index 0000000000..0ce1d3be45
--- /dev/null
+++ b/phpBB/phpbb/passwords/helper.php
@@ -0,0 +1,198 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_helper
+{
+ /**
+ * @var phpbb_passwords_manager
+ */
+ protected $manager;
+
+ /**
+ * Set the passwords manager instance
+ *
+ * @param phpbb_passwords_manager $manager Crypto manager object
+ */
+ public function set_manager(phpbb_passwords_manager $manager)
+ {
+ if ($this->manager === null)
+ {
+ $this->manager = $manager;
+ }
+ }
+
+ /**
+ * Get hash settings from combined hash
+ *
+ * @param string $hash Password hash of combined hash
+ *
+ * @return array An array containing the hash settings for the hash
+ * types in successive order as described by the comined
+ * password hash
+ */
+ protected function get_combined_hash_settings($hash)
+ {
+ preg_match('#^\$([a-zA-Z0-9\\\]*?)\$#', $hash, $match);
+ $hash_settings = substr($hash, strpos($hash, $match[1]) + strlen($match[1]) + 1);
+ $matches = explode('\\', $match[1]);
+ foreach ($matches as $cur_type)
+ {
+ $dollar_position = strpos($hash_settings, '$');
+ $output[] = substr($hash_settings, 0, ($dollar_position != false) ? $dollar_position : strlen($hash_settings));
+ $hash_settings = substr($hash_settings, $dollar_position + 1);
+ }
+
+ return $output;
+ }
+
+ /**
+ * 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)
+ {
+ $data = array(
+ 'prefix' => '$',
+ 'settings' => '$',
+ );
+ $hash_settings = $this->get_combined_hash_settings($password_hash);
+ $hash = $hash_settings[0];
+
+ // Put settings of current hash into data array
+ $stored_hash_type = $this->manager->detect_algorithm($password_hash);
+ $this->combine_hash_output($data, 'prefix', $stored_hash_type->get_prefix());
+ $this->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->manager->algorithms[$cur_type]))
+ {
+ $new_hash_type = $this->manager->algorithms[$cur_type];
+ }
+ else
+ {
+ return false;
+ }
+
+ $new_hash = $new_hash_type->hash(str_replace($stored_hash_type->get_settings_only($password_hash), '', $hash));
+ $this->combine_hash_output($data, 'prefix', $new_hash_type->get_prefix());
+ $this->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->obtain_hash_only($new_hash));
+ }
+ return $this->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
+ *
+ * @return bool True if password is correct, false if not
+ */
+ public function check_combined_hash($password, $stored_hash_type, $hash)
+ {
+ $cur_hash = '';
+ $i = 0;
+ $data = array(
+ 'prefix' => '$',
+ 'settings' => '$',
+ );
+ $hash_settings = $this->get_combined_hash_settings($hash);
+ foreach ($stored_hash_type as $key => $hash_type)
+ {
+ $rebuilt_hash = $this->rebuild_hash($hash_type->get_prefix(), $hash_settings[$i]);
+ $this->combine_hash_output($data, 'prefix', $key);
+ $this->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 === $this->combine_hash_output($data, 'hash', $password));
+ }
+
+ /**
+ * Combine hash prefixes, settings, and actual hash
+ *
+ * @param array $data Array containing the keys 'prefix' and 'settings'.
+ * It will hold the prefixes and settings
+ * @param string $type Data type of the supplied value
+ * @param string $value Value that should be put into the data array
+ *
+ * @return string|none Return complete combined hash if type is neither
+ * 'prefix' nor 'settings', nothing if it is
+ */
+ protected function combine_hash_output(&$data, $type, $value)
+ {
+ if ($type == 'prefix')
+ {
+ $data[$type] .= ($data[$type] !== '$') ? '\\' : '';
+ $data[$type] .= str_replace('$', '', $value);
+ }
+ elseif ($type == 'settings')
+ {
+ $data[$type] .= ($data[$type] !== '$') ? '$' : '';
+ $data[$type] .= $value;
+ }
+ else
+ {
+ // Return full hash
+ return $data['prefix'] . $data['settings'] . '$' . $value;
+ }
+ }
+
+ /**
+ * Rebuild hash for hashing functions
+ *
+ * @param string $prefix Hash prefix
+ * @param string $settings Hash settings
+ *
+ * @return string Rebuilt hash for hashing functions
+ */
+ protected function rebuild_hash($prefix, $settings)
+ {
+ $rebuilt_hash = $prefix;
+ if (strpos($settings, '\\') !== false)
+ {
+ $settings = str_replace('\\', '$', $settings);
+ }
+ $rebuilt_hash .= $settings;
+ return $rebuilt_hash;
+ }
+
+ /**
+ * Obtain only the actual hash after the prefixes
+ *
+ * @param string $hash The full password hash
+ * @return string Actual hash (incl. settings)
+ */
+ protected function obtain_hash_only($hash)
+ {
+ return substr($hash, strripos($hash, '$') + 1);
+ }
+}
diff --git a/phpBB/phpbb/passwords/manager.php b/phpBB/phpbb/passwords/manager.php
new file mode 100644
index 0000000000..6cc3510f8e
--- /dev/null
+++ b/phpBB/phpbb/passwords/manager.php
@@ -0,0 +1,255 @@
+<?php
+/**
+*
+* @package phpBB3
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+/**
+* @ignore
+*/
+if (!defined('IN_PHPBB'))
+{
+ exit;
+}
+
+/**
+* @package passwords
+*/
+class phpbb_passwords_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;
+
+ /**
+ * Crypto helper
+ * @var phpbb_passwords_helper
+ */
+ protected $helper;
+
+ /**
+ * phpBB configuration
+ * @var phpbb_config
+ */
+ protected $config;
+
+ /**
+ * Construct a passwords object
+ *
+ * @param phpbb_config $config phpBB configuration
+ * @param phpbb_di_service_collection $hashing_algorithms Hashing driver
+ * service collection
+ * @param phpbb_passwords_helper $helper Passwords helper object
+ * @param string $default Default driver name
+ */
+ public function __construct($config, $hashing_algorithms, $helper, $default)
+ {
+ $this->config = $config;
+ $this->type = $default;
+
+ $this->fill_type_map($hashing_algorithms);
+ $this->load_passwords_helper($helper);
+ }
+
+ /**
+ * 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->algorithms[$algorithm->get_name()]))
+ {
+ $this->algorithms[$algorithm->get_name()] = $algorithm;
+ }
+
+ if (!isset($this->type_map[$algorithm->get_prefix()]))
+ {
+ $this->type_map[$algorithm->get_prefix()] = $algorithm->get_name();
+ }
+ }
+ }
+
+ /**
+ * Load passwords helper class
+ *
+ * @param phpbb_passwords_helper $helper Passwords helper object
+ */
+ protected function load_passwords_helper($helper)
+ {
+ if ($this->helper === null)
+ {
+ $this->helper = $helper;
+ $this->helper->set_manager($this);
+ }
+ }
+
+ /**
+ * Get the algorithm specified by a specific prefix
+ *
+ * @param string $prefix Password hash prefix
+ *
+ * @return object The hash type object
+ */
+ protected function get_algorithm($prefix)
+ {
+ if (isset($this->type_map[$prefix]) && isset($this->algorithms[$this->type_map[$prefix]]))
+ {
+ return $this->algorithms[$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 $this->get_algorithm('$H$');
+ }
+
+ // Be on the lookout for multiple hashing algorithms
+ // 2 is correct: H\2a > 2, H\P > 2
+ if (strlen($match[1]) > 2)
+ {
+ $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($password, $type = '')
+ {
+ $type = ($type === '') ? $this->type : $type;
+
+ if (is_array($type))
+ {
+ return $this->helper->combined_hash_password($password, $type);
+ }
+
+ if (isset($this->algorithms[$type]))
+ {
+ $hashing_algorithm = $this->algorithms[$type];
+ }
+ else
+ {
+ return false;
+ }
+
+ // Do not support 8-bit characters with $2a$ bcrypt
+ // Also see http://www.php.net/security/crypt_blowfish.php
+ if ($type === 'passwords.driver.bcrypt' || ($type === 'passwords.driver.bcrypt_2y' && !$hashing_algorithm->is_supported()))
+ {
+ if (ord($password[strlen($password)-1]) & 128)
+ {
+ 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 (preferrably newer one)
+ *
+ * @param string $password Password that should be checked
+ * @param string $hash Stored hash
+ * @return string|bool True if password is correct, false if not
+ */
+ public function check_hash($password, $hash)
+ {
+ // First find out what kind of hash we're dealing with
+ $stored_hash_type = $this->detect_algorithm($hash);
+ if ($stored_hash_type == false)
+ {
+ return false;
+ }
+
+ // Multiple hash passes needed
+ if (is_array($stored_hash_type))
+ {
+ return $this->helper->check_combined_hash($password, $stored_hash_type, $hash);
+ }
+
+ if ($stored_hash_type->get_name() !== $this->type)
+ {
+ $this->convert_flag = true;
+ }
+ else
+ {
+ $this->convert_flag = false;
+ }
+
+ return $stored_hash_type->check($password, $hash);
+ }
+}
diff --git a/tests/passwords/manager_test.php b/tests/passwords/manager_test.php
new file mode 100644
index 0000000000..d06e09a014
--- /dev/null
+++ b/tests/passwords/manager_test.php
@@ -0,0 +1,249 @@
+<?php
+/**
+*
+* @package testing
+* @copyright (c) 2013 phpBB Group
+* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
+*
+*/
+
+require_once dirname(__FILE__) . '/../mock/container_builder.php';
+
+class phpbb_passwords_manager_test extends PHPUnit_Framework_TestCase
+{
+ protected $passwords_drivers;
+
+ protected $pw_characters = '0123456789abcdefghijklmnopqrstuvwyzABCDEFGHIJKLMNOPQRSTUVXYZ.,_!?/\\';
+
+ protected $default_pw = 'foobar';
+
+ public function setUp()
+ {
+ global $phpbb_root_path, $phpEx;
+
+ // Mock phpbb_container
+ $this->phpbb_container = new phpbb_mock_container_builder;
+
+ // Prepare dependencies for manager and driver
+ $config = new phpbb_config(array());
+ $driver_helper = new phpbb_passwords_driver_helper;
+
+ $this->passwords_drivers = array(
+ 'passwords.driver.bcrypt' => new phpbb_passwords_driver_bcrypt($config, $driver_helper),
+ 'passwords.driver.bcrypt_2y' => new phpbb_passwords_driver_bcrypt_2y($config, $driver_helper),
+ 'passwords.driver.salted_md5' => new phpbb_passwords_driver_salted_md5($config, $driver_helper),
+ 'passwords.driver.phpass' => new phpbb_passwords_driver_phpass($config, $driver_helper),
+ );
+
+ foreach ($this->passwords_drivers as $key => $driver)
+ {
+ $driver->set_name($key);
+ $this->phpbb_container->set($key, $driver);
+ }
+
+ $this->helper = new phpbb_passwords_helper;
+ // Set up passwords manager
+ $this->manager = new phpbb_passwords_manager($config, $this->passwords_drivers, $this->helper, 'passwords.driver.bcrypt_2y');
+ }
+
+ public function hash_password_data()
+ {
+ if (version_compare(PHP_VERSION, '5.3.7', '<'))
+ {
+ return array(
+ array('', '2a', 60),
+ array('passwords.driver.bcrypt_2y', '2a', 60),
+ array('passwords.driver.bcrypt', '2a', 60),
+ array('passwords.driver.salted_md5', 'H', 34),
+ array('passwords.driver.foobar', '', false),
+ );
+ }
+ else
+ {
+ return array(
+ array('', '2y', 60),
+ array('passwords.driver.bcrypt_2y', '2y', 60),
+ array('passwords.driver.bcrypt', '2a', 60),
+ array('passwords.driver.salted_md5', 'H', 34),
+ array('passwords.driver.foobar', '', false),
+ );
+ }
+ }
+
+ /**
+ * @dataProvider hash_password_data
+ */
+ public function test_hash_password($type, $prefix, $length)
+ {
+ $password = $this->default_pw;
+
+ if (!$length)
+ {
+ $this->assertEquals(false, $hash = $this->manager->hash_password($password, $type));
+ return;
+ }
+ $time = microtime(true);
+
+ // Limit each test to 1 second
+ while ((microtime(true) - $time) < 1)
+ {
+ $hash = $this->manager->hash_password($password, $type);
+ preg_match('#^\$([a-zA-Z0-9\\\]*?)\$#', $hash, $match);
+ $this->assertEquals($prefix, $match[1]);
+ $this->assertEquals($length, strlen($hash));
+ $password .= $this->pw_characters[mt_rand(0, 66)];
+ }
+ }
+
+ public function check_password_data()
+ {
+ if (version_compare(PHP_VERSION, '5.3.7', '<'))
+ {
+ return array(
+ array('passwords.driver.bcrypt'),
+ array('passwords.driver.salted_md5'),
+ array('passwords.driver.phpass'),
+ );
+ }
+ else
+ {
+ return array(
+ array('passwords.driver.bcrypt_2y'),
+ array('passwords.driver.bcrypt'),
+ array('passwords.driver.salted_md5'),
+ array('passwords.driver.phpass'),
+ );
+ }
+ }
+
+ /**
+ * @dataProvider check_password_data
+ */
+ public function test_check_password($hash_type)
+ {
+ $password = $this->default_pw;
+ $time = microtime(true);
+ // Limit each test to 1 second
+ while ((microtime(true) - $time) < 1)
+ {
+ $hash = $this->manager->hash_password($password, $hash_type);
+ $this->assertEquals(true, $this->manager->check_hash($password, $hash));
+ $password .= $this->pw_characters[mt_rand(0, 66)];
+ $this->assertEquals(false, $this->manager->check_hash($password, $hash));
+ }
+
+ // Check if convert_flag is correctly set
+ $this->assertEquals(($hash_type !== 'passwords.driver.bcrypt_2y'), $this->manager->convert_flag);
+ }
+
+
+ public function check_hash_exceptions_data()
+ {
+ return array(
+ array('foobar', '3858f62230ac3c915f300c664312c63f', true),
+ array('foobar', '$S$b57a939fa4f2c04413a4eea9734a0903647b7adb93181295', false),
+ array('foobar', '$2a\S$kkkkaakdkdiej39023903204j2k3490234jk234j02349', false),
+ array('foobar', '$H$kklk938d023k//k3023', false),
+ array('foobar', '$H$3PtYMgXb39lrIWkgoxYLWtRkZtY3AY/', false),
+ array('foobar', '$2a$kwiweorurlaeirw', false),
+ );
+ }
+
+ /**
+ * @dataProvider check_hash_exceptions_data
+ */
+ public function test_check_hash_exceptions($password, $hash, $expected)
+ {
+ $this->assertEquals($expected, $this->manager->check_hash($password, $hash));
+ }
+
+ public function test_hash_password_length()
+ {
+ foreach ($this->passwords_drivers as $driver)
+ {
+ $this->assertEquals(false, $driver->hash('foobar', 'foobar'));
+ }
+ }
+
+ public function test_hash_password_8bit_bcrypt()
+ {
+ $this->assertEquals(false, $this->manager->hash_password('foobar𝄞', 'passwords.driver.bcrypt'));
+ }
+
+ public function test_combined_hash_data()
+ {
+ if (version_compare(PHP_VERSION, '5.3.7', '<'))
+ {
+ return array(
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.bcrypt'),
+ ),
+ array(
+ 'passwords.driver.phpass',
+ array('passwords.driver.salted_md5'),
+ ),
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.phpass', 'passwords.driver.bcrypt'),
+ ),
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.salted_md5'),
+ false,
+ ),
+ );
+ }
+ else
+ {
+ return array(
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.bcrypt_2y'),
+ ),
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.bcrypt'),
+ ),
+ array(
+ 'passwords.driver.phpass',
+ array('passwords.driver.salted_md5'),
+ ),
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.bcrypt_2y', 'passwords.driver.bcrypt'),
+ ),
+ array(
+ 'passwords.driver.salted_md5',
+ array('passwords.driver.salted_md5'),
+ false,
+ ),
+ );
+ }
+ }
+
+ /**
+ * @dataProvider test_combined_hash_data
+ */
+ public function test_combined_hash_password($first_type, $second_type, $expected = true)
+ {
+ $password = $this->default_pw;
+ $time = microtime(true);
+ // Limit each test to 1 second
+ while ((microtime(true) - $time) < 1)
+ {
+ $hash = $this->manager->hash_password($password, $first_type);
+ $combined_hash = $this->manager->hash_password($hash, $second_type);
+ $this->assertEquals($expected, $this->manager->check_hash($password, $combined_hash));
+ $password .= $this->pw_characters[mt_rand(0, 66)];
+ $this->assertEquals(false, $this->manager->check_hash($password, $combined_hash));
+
+ // If we are expecting the check to fail then there is
+ // no need to run this more than once
+ if (!$expected)
+ {
+ break;
+ }
+ }
+ }
+}