diff --git a/boot.php b/boot.php index 4d2eac35a..eb3d4e973 100644 --- a/boot.php +++ b/boot.php @@ -41,7 +41,7 @@ define('FRIENDICA_PLATFORM', 'Friendica'); define('FRIENDICA_CODENAME', 'Asparagus'); define('FRIENDICA_VERSION', '3.6-dev'); define('DFRN_PROTOCOL_VERSION', '2.23'); -define('DB_UPDATE_VERSION', 1243); +define('DB_UPDATE_VERSION', 1244); define('NEW_UPDATE_ROUTINE_VERSION', 1170); /** diff --git a/database.sql b/database.sql index 054bcd9a1..506e252ec 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 3.6-dev (Asparagus) --- DB_UPDATE_VERSION 1243 +-- DB_UPDATE_VERSION 1244 -- ------------------------------------------ @@ -1006,6 +1006,7 @@ CREATE TABLE IF NOT EXISTS `user` ( `guid` varchar(64) NOT NULL DEFAULT '' COMMENT '', `username` varchar(255) NOT NULL DEFAULT '' COMMENT '', `password` varchar(255) NOT NULL DEFAULT '' COMMENT '', + `legacy_password` boolean NOT NULL DEFAULT 0 COMMENT 'Is the password hash double-hashed?', `nickname` varchar(255) NOT NULL DEFAULT '' COMMENT '', `email` varchar(255) NOT NULL DEFAULT '' COMMENT '', `openid` varchar(255) NOT NULL DEFAULT '' COMMENT '', diff --git a/mod/item.php b/mod/item.php index 64b773c21..b3509dc89 100644 --- a/mod/item.php +++ b/mod/item.php @@ -951,7 +951,7 @@ function handle_tag(App $a, &$body, &$inform, &$str_tags, $profile_uid, $tag, $n // Try to detect the contact in various ways if (strpos($name, 'http://')) { // At first we have to ensure that the contact exists - $id = Contact::getIdForURL($name); + Contact::getIdForURL($name); // Now we should have something $contact = Contact::getDetailsByURL($name); @@ -968,25 +968,26 @@ function handle_tag(App $a, &$body, &$inform, &$str_tags, $profile_uid, $tag, $n $contact = dba::selectFirst('contact', $fields, ['id' => $tagcid, 'uid' => $profile_uid]); } - // select someone by nick and the name passed in the current network + // select someone by nick or attag in the current network if (!DBM::is_result($contact) && ($network != "")) { - $condition = ['nick' => $name, 'network' => $network, 'uid' => $profile_uid]; + $condition = ["(`nick` = ? OR `attag` = ?) AND `network` = ? AND `uid` = ?", + $name, $name, $network, $profile_uid]; $contact = dba::selectFirst('contact', $fields, $condition); } - //select someone from this user's contacts by name in the current network + //select someone by name in the current network if (!DBM::is_result($contact) && ($network != "")) { $condition = ['name' => $name, 'network' => $network, 'uid' => $profile_uid]; $contact = dba::selectFirst('contact', $fields, $condition); } - // select someone by nick in any network + // select someone by nick or attag in any network if (!DBM::is_result($contact)) { - $condition = ['nick' => $name, 'uid' => $profile_uid]; + $condition = ["(`nick` = ? OR `attag` = ?) AND `uid` = ?", $name, $name, $profile_uid]; $contact = dba::selectFirst('contact', $fields, $condition); } - // select someone from this user's contacts by name + // select someone by name in any network if (!DBM::is_result($contact)) { $condition = ['name' => $name, 'uid' => $profile_uid]; $contact = dba::selectFirst('contact', $fields, $condition); diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index cb69b4952..b3eed0dd1 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -1708,6 +1708,7 @@ class DBStructure { "guid" => ["type" => "varchar(64)", "not null" => "1", "default" => "", "comment" => ""], "username" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "password" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], + "legacy_password" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Is the password hash double-hashed?"], "nickname" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "email" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "openid" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], diff --git a/src/Model/User.php b/src/Model/User.php index 382ec62cc..bbe424ecb 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -112,7 +112,7 @@ class User if (is_object($user_info)) { $user = (array) $user_info; } elseif (is_int($user_info)) { - $user = dba::selectFirst('user', ['uid', 'password'], + $user = dba::selectFirst('user', ['uid', 'password', 'legacy_password'], [ 'uid' => $user_info, 'blocked' => 0, @@ -122,7 +122,7 @@ class User ] ); } elseif (is_string($user_info)) { - $user = dba::fetch_first('SELECT `uid`, `password` + $user = dba::fetch_first('SELECT `uid`, `password`, `legacy_password` FROM `user` WHERE (`email` = ? OR `username` = ? OR `nickname` = ?) AND `blocked` = 0 @@ -138,17 +138,29 @@ class User $user = $user_info; } - if (!DBM::is_result($user) || !isset($user['uid']) || !isset($user['password'])) { - return false; + if (!DBM::is_result($user) + || !isset($user['uid']) + || !isset($user['password']) + || !isset($user['legacy_password']) + ) { + throw new Exception('Not enough information to authenticate'); } - $password_hashed = self::hashPassword($password); + if ($user['legacy_password']) { + if (password_verify(self::hashPasswordLegacy($password), $user['password'])) { + self::updatePassword($user['uid'], $password); - if ($password_hashed !== $user['password']) { - return false; + return $user['uid']; + } + } elseif (password_verify($password, $user['password'])) { + if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) { + self::updatePassword($user['uid'], $password); + } + + return $user['uid']; } - return $user['uid']; + return false; } /** @@ -161,15 +173,26 @@ class User return autoname(6) . mt_rand(100, 9999); } + /** + * Legacy hashing function, kept for password migration purposes + * + * @param string $password + * @return string + */ + private static function hashPasswordLegacy($password) + { + return hash('whirlpool', $password); + } + /** * Global user password hashing function * * @param string $password * @return string */ - private static function hashPassword($password) + public static function hashPassword($password) { - return hash('whirlpool', $password); + return password_hash($password, PASSWORD_DEFAULT); } /** @@ -197,7 +220,8 @@ class User $fields = [ 'password' => $pasword_hashed, 'pwdreset' => null, - 'pwdreset_time' => null + 'pwdreset_time' => null, + 'legacy_password' => false ]; return dba::update('user', $fields, ['uid' => $uid]); } diff --git a/src/Util/ExAuth.php b/src/Util/ExAuth.php index 847059d6d..8940c0205 100644 --- a/src/Util/ExAuth.php +++ b/src/Util/ExAuth.php @@ -226,7 +226,7 @@ class ExAuth if ($a->get_hostname() == $aCommand[2]) { $this->writeLog(LOG_INFO, 'internal auth for ' . $sUser . '@' . $aCommand[2]); - $aUser = dba::selectFirst('user', ['uid', 'password'], ['nickname' => $sUser]); + $aUser = dba::selectFirst('user', ['uid', 'password', 'legacy_password'], ['nickname' => $sUser]); if (DBM::is_result($aUser)) { $uid = $aUser['uid']; $success = User::authenticate($aUser, $aCommand[3]); diff --git a/update.php b/update.php index 3cfc39e19..00b8d890e 100644 --- a/update.php +++ b/update.php @@ -5,8 +5,7 @@ use Friendica\Core\Config; use Friendica\Core\PConfig; use Friendica\Core\Worker; use Friendica\Database\DBM; -use Friendica\Model\Photo; -use Friendica\Object\Image; +use Friendica\Model\User; /** * @@ -146,3 +145,19 @@ function update_1203() { $r = q("UPDATE `user` SET `account-type` = %d WHERE `page-flags` IN (%d, %d)", dbesc(ACCOUNT_TYPE_COMMUNITY), dbesc(PAGE_COMMUNITY), dbesc(PAGE_PRVGROUP)); } + +function update_1244() { + // Sets legacy_password for all legacy hashes + dba::update('user', ['legacy_password' => true], ['SUBSTR(password, 1, 4) != "$2y$"']); + + // All legacy hashes are re-hashed using the new secure hashing function + $stmt = dba::select('user', ['uid', 'password'], ['legacy_password' => true]); + while($user = dba::fetch($stmt)) { + dba::update('user', ['password' => User::hashPassword($user['password'])], ['uid' => $user['uid']]); + } + + // Logged in users are forcibly logged out + dba::delete('session', ['1 = 1']); + + return UPDATE_SUCCESS; +}