diff --git a/sql/base/auth_database.sql b/sql/base/auth_database.sql index 8627f10817b76..bf2370b1aa65a 100644 --- a/sql/base/auth_database.sql +++ b/sql/base/auth_database.sql @@ -25,10 +25,12 @@ DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Identifier', `username` varchar(32) NOT NULL DEFAULT '', + `salt` BINARY(32), + `verifier` BINARY(32), + `session_key` BINARY(40) DEFAULT NULL, `sha_pass_hash` varchar(40) NOT NULL DEFAULT '', - `sessionkey` varchar(80) NOT NULL DEFAULT '', - `v` varchar(64) NOT NULL DEFAULT '', - `s` varchar(64) NOT NULL DEFAULT '', + `v` varchar(64) NOT NULL DEFAULT 'dummy value, use `verifier` instead', + `s` varchar(64) NOT NULL DEFAULT 'dummy value, use `salt` instead', `totp_secret` varbinary(128) DEFAULT NULL, `email` varchar(255) NOT NULL DEFAULT '', `reg_mail` varchar(255) NOT NULL DEFAULT '', @@ -1966,7 +1968,8 @@ INSERT INTO `updates` VALUES ('2020_05_15_00_auth.sql','765389B45F97A02160A58B373D63166F7F7D4427','ARCHIVED','2020-05-15 08:55:56',0), ('2020_06_15_00_auth.sql','3158036285CC9A4AB7D39063F9687649A21D0A94','ARCHIVED','2020-06-15 07:48:08',0), ('2020_06_20_00_auth.sql','85345FAF20B91DA7B157AE1E17DF5B6446C2E109','ARCHIVED','2020-06-11 10:48:00',0), -('2020_07_15_00_auth.sql','56748440894EA78C3BE72C4A3F2E97E256E6EE40','ARCHIVED','2020-07-15 10:35:41',0); +('2020_07_15_00_auth.sql','56748440894EA78C3BE72C4A3F2E97E256E6EE40','ARCHIVED','2020-07-15 10:35:41',0), +('2020_08_02_00_auth.sql','B0290F6558C59262D9DDD8071060A8803DD56930','ARCHIVED','2020-08-02 00:00:00',0); /*!40000 ALTER TABLE `updates` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/updates/auth/3.3.5/2020_08_02_00_auth.sql b/sql/updates/auth/3.3.5/2020_08_02_00_auth.sql new file mode 100644 index 0000000000000..035ca4c43678f --- /dev/null +++ b/sql/updates/auth/3.3.5/2020_08_02_00_auth.sql @@ -0,0 +1,13 @@ +-- AUTH CLEANUP PHASE 3 +-- update `account` structure +-- sha_pass_hash/s/v kept around for now, for backwards compatibility +ALTER TABLE `account` + DROP COLUMN `sessionkey`, + ADD COLUMN `salt` BINARY(32) AFTER `username`, + ADD COLUMN `verifier` BINARY(32) AFTER `salt`, + ADD COLUMN `session_key` BINARY(40) AFTER `verifier`, + MODIFY COLUMN `s` VARCHAR(64) NOT NULL DEFAULT 'dummy value, use `salt` instead', + MODIFY COLUMN `v` VARCHAR(64) NOT NULL DEFAULT 'dummy value, use `verifier` instead'; + +UPDATE `account` SET `salt`=REVERSE(UNHEX(`s`)), `s`=DEFAULT WHERE LENGTH(`s`)=64; +UPDATE `account` SET `verifier`=REVERSE(UNHEX(`v`)), `v`=DEFAULT WHERE LENGTH(`v`)=64; diff --git a/src/common/Cryptography/Authentication/SRP6.cpp b/src/common/Cryptography/Authentication/SRP6.cpp index de05ffd169339..11125a3211b6d 100644 --- a/src/common/Cryptography/Authentication/SRP6.cpp +++ b/src/common/Cryptography/Authentication/SRP6.cpp @@ -54,7 +54,7 @@ using SRP6 = Trinity::Crypto::SRP6; // merge this into CalculateVerifier once the sha_pass hack finally gets nuked from orbit /*static*/ SRP6::Verifier SRP6::CalculateVerifierFromHash(SHA1::Digest const& hash, SRP6::Salt const& salt) { - return _g.ModExp(SHA1::GetDigestOf(salt, hash), _N).ToByteArray<32>(false); + return _g.ModExp(SHA1::GetDigestOf(salt, hash), _N).ToByteArray<32>(); } /*static*/ SessionKey SRP6::SHA1Interleave(SRP6::EphemeralKey const& S) @@ -88,7 +88,7 @@ using SRP6 = Trinity::Crypto::SRP6; } SRP6::SRP6(std::string const& username, Salt const& salt, Verifier const& verifier) - : _I(SHA1::GetDigestOf(username)), _b(Crypto::GetRandomBytes<19>()), _v(verifier, false), s(salt), B(_B(_b, _v)) {} + : _I(SHA1::GetDigestOf(username)), _b(Crypto::GetRandomBytes<32>()), _v(verifier), s(salt), B(_B(_b, _v)) {} std::optional SRP6::VerifyChallengeResponse(EphemeralKey const& A, SHA1::Digest const& clientM) { diff --git a/src/server/authserver/Main.cpp b/src/server/authserver/Main.cpp index 7a6a503767629..f5566355a3d52 100644 --- a/src/server/authserver/Main.cpp +++ b/src/server/authserver/Main.cpp @@ -143,6 +143,8 @@ int main(int argc, char** argv) sSecretMgr->Initialize(); + AuthSession::ServerStartup(); + // Load IP Location Database sIPLocation->Load(); diff --git a/src/server/authserver/Server/AuthSession.cpp b/src/server/authserver/Server/AuthSession.cpp index 1eb64a7830e4b..0c80379807408 100644 --- a/src/server/authserver/Server/AuthSession.cpp +++ b/src/server/authserver/Server/AuthSession.cpp @@ -28,6 +28,7 @@ #include "Log.h" #include "RealmList.h" #include "SecretMgr.h" +#include "Timer.h" #include "TOTP.h" #include "Util.h" #include @@ -120,6 +121,48 @@ std::array VersionChallenge = { { 0xBA, 0xA3, 0x1E, 0x99, 0xA0, 0x0B, #define AUTH_LOGON_CHALLENGE_INITIAL_SIZE 4 #define REALM_LIST_PACKET_SIZE 5 +/*static*/ void AuthSession::ServerStartup() +{ + TC_LOG_INFO("server.authserver", "Updating password hashes..."); + uint32 const start = getMSTime(); + // the auth update query nulls salt/verifier if they cannot be converted + // if they are non-null but s/v have been cleared, that means a legacy tool touched our auth DB (otherwise, the core might've done it itself, it used to use those hacks too) + QueryResult result = LoginDatabase.Query("SELECT id, sha_pass_hash, IF((salt IS null) AND (verifier IS null), 0, 1) AS shouldWarn FROM account WHERE s != DEFAULT(s) OR v != DEFAULT(v) OR salt IS NULL OR verifier IS NULL"); + if (!result) + { + TC_LOG_INFO("server.authserver", ">> No password hashes to update - this took us %u ms to realize", GetMSTimeDiffToNow(start)); + return; + } + + bool hadWarning = false; + uint32 c = 0; + LoginDatabaseTransaction tx = LoginDatabase.BeginTransaction(); + do + { + uint32 const id = (*result)[0].GetUInt32(); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationDataFromHash_DEPRECATED_DONOTUSE( + HexStrToByteArray((*result)[1].GetString()) + ); + + if ((*result)[2].GetInt64() && !hadWarning) + { + hadWarning = true; + TC_LOG_WARN("server.authserver", "(!) You appear to be using an outdated external account management tool.\n(!!) This is INSECURE, has been deprecated, and will cease to function entirely in the near future.\n(!) Update your external tool.\n(!!) If no update is available, refer your tool's developer to https://github.com/TrinityCore/TrinityCore/issues/25157."); + } + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON); + stmt->setBinary(0, salt); + stmt->setBinary(1, verifier); + stmt->setUInt32(2, id); + tx->Append(stmt); + + ++c; + } while (result->NextRow()); + LoginDatabase.CommitTransaction(tx); + + TC_LOG_INFO("server.authserver", ">> %u password hashes updated in %u ms", c, GetMSTimeDiffToNow(start)); +} + std::unordered_map AuthSession::InitHandlers() { std::unordered_map handlers; @@ -398,25 +441,24 @@ void AuthSession::LogonChallengeCallback(PreparedQueryResult result) if (!fields[10].IsNull()) { - // if this is reached, s/v are empty and we need to recalculate them + // if this is reached, s/v were reset and we need to recalculate from sha_pass_hash Trinity::Crypto::SHA1::Digest sha_pass_hash; HexStrToByteArray(fields[10].GetString(), sha_pass_hash); - auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationDataFromHash_DEPRECATED_DONOTUSE(sha_pass_hash); - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_SV); - stmt->setString(0, ByteArrayToHexStr(salt, true)); /* this is actually flipped in the DB right now, old core did hexstr (big endian) -> bignum -> byte array (little-endian) */ - stmt->setString(1, ByteArrayToHexStr(verifier)); + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON); + stmt->setBinary(0, salt); + stmt->setBinary(1, verifier); stmt->setUInt32(2, _accountInfo.Id); LoginDatabase.Execute(stmt); + TC_LOG_WARN("server.authserver", "(!) You appear to be using an outdated external account management tool.\n(!!) This is INSECURE, has been deprecated, and will cease to function entirely in the near future.\n(!) Update your external tool.\n(!!) If no update is available, refer your tool's developer to https://github.com/TrinityCore/TrinityCore/issues/25157."); + _srp6.emplace(_accountInfo.Login, salt, verifier); } else { - Trinity::Crypto::SRP6::Salt salt; - Trinity::Crypto::SRP6::Verifier verifier; - HexStrToByteArray(fields[11].GetString(), salt, true); /* this is actually flipped in the DB right now, old core did hexstr (big endian) -> bignum -> byte array (little-endian) */ - HexStrToByteArray(fields[12].GetString(), verifier); + Trinity::Crypto::SRP6::Salt salt = fields[11].GetBinary(); + Trinity::Crypto::SRP6::Verifier verifier = fields[12].GetBinary(); _srp6.emplace(_accountInfo.Login, salt, verifier); } @@ -525,7 +567,7 @@ bool AuthSession::HandleLogonProof() // No SQL injection (escaped user name) and IP address as received by socket LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGONPROOF); - stmt->setString(0, ByteArrayToHexStr(_sessionKey)); + stmt->setBinary(0, _sessionKey); stmt->setString(1, GetRemoteIpAddress().to_string()); stmt->setUInt32(2, GetLocaleByName(_localizationName)); stmt->setString(3, _os); @@ -675,7 +717,7 @@ void AuthSession::ReconnectChallengeCallback(PreparedQueryResult result) Field* fields = result->Fetch(); _accountInfo.LoadResult(fields); - HexStrToByteArray(fields[9].GetString(), _sessionKey); + _sessionKey = fields[9].GetBinary(); Trinity::Crypto::GetRandomBytes(_reconnectProof); _status = STATUS_RECONNECT_PROOF; diff --git a/src/server/authserver/Server/AuthSession.h b/src/server/authserver/Server/AuthSession.h index 9982399bcbc8d..47973d0ff1d70 100644 --- a/src/server/authserver/Server/AuthSession.h +++ b/src/server/authserver/Server/AuthSession.h @@ -65,6 +65,7 @@ class AuthSession : public Socket typedef Socket AuthSocket; public: + static void ServerStartup(); static std::unordered_map InitHandlers(); AuthSession(tcp::socket&& socket); diff --git a/src/server/database/Database/Field.cpp b/src/server/database/Database/Field.cpp index f891bfd22b934..b9d25b3da1b44 100644 --- a/src/server/database/Database/Field.cpp +++ b/src/server/database/Database/Field.cpp @@ -16,6 +16,7 @@ */ #include "Field.h" +#include "Errors.h" #include "Log.h" #include "MySQLHacks.h" @@ -247,6 +248,12 @@ std::vector Field::GetBinary() const return result; } +void Field::GetBinarySizeChecked(uint8* buf, size_t length) const +{ + ASSERT(data.value && (data.length == length), "Expected %zu-byte binary blob, got %sdata (%u bytes) instead", length, data.value ? "" : "no ", data.length); + memcpy(buf, data.value, length); +} + void Field::SetByteValue(char const* newValue, uint32 length) { // This value stores raw bytes that have to be explicitly cast later diff --git a/src/server/database/Database/Field.h b/src/server/database/Database/Field.h index ea7c82d1876a9..dcb033440317a 100644 --- a/src/server/database/Database/Field.h +++ b/src/server/database/Database/Field.h @@ -20,6 +20,7 @@ #include "Define.h" #include "DatabaseEnvFwd.h" +#include #include enum class DatabaseFieldTypes : uint8 @@ -104,6 +105,14 @@ class TC_DATABASE_API Field char const* GetCString() const; std::string GetString() const; std::vector GetBinary() const; + template + std::array GetBinary() const + { + std::array buf; + GetBinarySizeChecked(buf.data(), S); + return buf; + } + bool IsNull() const { @@ -129,6 +138,8 @@ class TC_DATABASE_API Field QueryResultFieldMetadata const* meta; void LogWrongType(char const* getter) const; void SetMetadata(QueryResultFieldMetadata const* fieldMeta); + + void GetBinarySizeChecked(uint8* buf, size_t size) const; }; #endif diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index 39337f039635d..74a291bf3fe56 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -35,20 +35,21 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_SEL_ACCOUNT_BANNED_BY_USERNAME, "SELECT account.id, username FROM account, account_banned WHERE account.id = account_banned.id AND active = 1 AND username = ? GROUP BY account.id", CONNECTION_SYNCH); PrepareStatement(LOGIN_INS_ACCOUNT_AUTO_BANNED, "INSERT INTO account_banned (id, bandate, unbandate, bannedby, banreason, active) VALUES (?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, 'Trinity Auth', 'Failed login autoban', 1)", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_ACCOUNT_BANNED, "DELETE FROM account_banned WHERE id = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_UPD_SV, "UPDATE account SET s = ?, v = ? WHERE id = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_UPD_LOGONPROOF, "UPDATE account SET sessionkey = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?", CONNECTION_SYNCH); + PrepareStatement(LOGIN_UPD_LOGON, "UPDATE account SET salt = ?, verifier = ?, s = DEFAULT, v = DEFAULT WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_LOGON_LEGACY, "UPDATE account SET sha_pass_hash = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_LOGONPROOF, "UPDATE account SET session_key = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_LOGONCHALLENGE, "SELECT a.id, a.username, a.locked, a.lock_country, a.last_ip, a.failed_logins, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, " - "ab.unbandate = ab.bandate, aa.SecurityLevel, a.totp_secret, IF(length(a.s)<2 OR length(a.v)<2, a.sha_pass_hash, NULL), a.s, a.v " + "ab.unbandate = ab.bandate, aa.SecurityLevel, a.totp_secret, IF(a.s != DEFAULT(a.s) OR a.v != DEFAULT(a.v) OR a.salt IS NULL OR a.verifier IS NULL, a.sha_pass_hash, NULL), a.salt, a.verifier " "FROM account a LEFT JOIN account_access aa ON a.id = aa.AccountID LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 WHERE a.username = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_RECONNECTCHALLENGE, "SELECT a.id, UPPER(a.username), a.locked, a.lock_country, a.last_ip, a.failed_logins, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, " - "ab.unbandate = ab.bandate, aa.SecurityLevel, a.sessionKey " - "FROM account a LEFT JOIN account_access aa ON a.id = aa.AccountID LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 WHERE a.username = ?", CONNECTION_ASYNC); + "ab.unbandate = ab.bandate, aa.SecurityLevel, a.session_key " + "FROM account a LEFT JOIN account_access aa ON a.id = aa.AccountID LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 WHERE a.username = ? AND a.session_key IS NOT NULL", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_FAILEDLOGINS, "UPDATE account SET failed_logins = failed_logins + 1 WHERE username = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_ACCOUNT_ID_BY_NAME, "SELECT id FROM account WHERE username = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_ACCOUNT_LIST_BY_NAME, "SELECT id, username FROM account WHERE username = ?", CONNECTION_SYNCH); - PrepareStatement(LOGIN_SEL_ACCOUNT_INFO_BY_NAME, "SELECT a.id, a.sessionkey, a.last_ip, a.locked, a.lock_country, a.expansion, a.mutetime, a.locale, a.recruiter, a.os, aa.SecurityLevel, " + PrepareStatement(LOGIN_SEL_ACCOUNT_INFO_BY_NAME, "SELECT a.id, a.session_key, a.last_ip, a.locked, a.lock_country, a.expansion, a.mutetime, a.locale, a.recruiter, a.os, aa.SecurityLevel, " "ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, r.id FROM account a LEFT JOIN account_access aa ON a.id = aa.AccountID AND aa.RealmID IN (-1, ?) " - "LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 LEFT JOIN account r ON a.id = r.recruiter WHERE a.username = ? ORDER BY aa.RealmID DESC LIMIT 1", CONNECTION_ASYNC); + "LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 LEFT JOIN account r ON a.id = r.recruiter WHERE a.username = ? AND a.session_key IS NOT NULL ORDER BY aa.RealmID DESC LIMIT 1", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_ACCOUNT_LIST_BY_EMAIL, "SELECT id, username FROM account WHERE email = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_REALM_CHARACTER_COUNTS, "SELECT realmid, numchars FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_ACCOUNT_BY_IP, "SELECT id, username FROM account WHERE last_ip = ?", CONNECTION_SYNCH); @@ -61,14 +62,13 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_DEL_REALM_CHARACTERS, "DELETE FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_REALM_CHARACTERS, "INSERT INTO realmcharacters (numchars, acctid, realmid) VALUES (?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_SUM_REALM_CHARACTERS, "SELECT SUM(numchars) FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, sha_pass_hash, reg_mail, email, joindate) VALUES(?, ?, ?, ?, NOW())", CONNECTION_SYNCH); + PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, salt, verifier, reg_mail, email, joindate) VALUES(?, ?, ?, ?, ?, NOW())", CONNECTION_SYNCH); PrepareStatement(LOGIN_INS_REALM_CHARACTERS_INIT, "INSERT INTO realmcharacters (realmid, acctid, numchars) SELECT realmlist.id, account.id, 0 FROM realmlist, account LEFT JOIN realmcharacters ON acctid = account.id WHERE acctid IS NULL", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_EXPANSION, "UPDATE account SET expansion = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_ACCOUNT_LOCK, "UPDATE account SET locked = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_ACCOUNT_LOCK_COUNTRY, "UPDATE account SET lock_country = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_LOG, "INSERT INTO logs (time, realm, type, level, string) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC); - PrepareStatement(LOGIN_UPD_USERNAME, "UPDATE account SET v = 0, s = 0, username = ?, sha_pass_hash = ? WHERE id = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_UPD_PASSWORD, "UPDATE account SET v = 0, s = 0, sha_pass_hash = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_USERNAME, "UPDATE account SET username = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_EMAIL, "UPDATE account SET email = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_REG_EMAIL, "UPDATE account SET reg_mail = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_MUTE_TIME, "UPDATE account SET mutetime = ? , mutereason = ? , muteby = ? WHERE id = ?", CONNECTION_ASYNC); @@ -84,8 +84,8 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_GET_ACCOUNT_ID_BY_USERNAME, "SELECT id FROM account WHERE username = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_GET_GMLEVEL_BY_REALMID, "SELECT SecurityLevel FROM account_access WHERE AccountID = ? AND (RealmID = ? OR RealmID = -1) ORDER BY RealmID DESC", CONNECTION_BOTH); PrepareStatement(LOGIN_GET_USERNAME_BY_ID, "SELECT username FROM account WHERE id = ?", CONNECTION_SYNCH); - PrepareStatement(LOGIN_SEL_CHECK_PASSWORD, "SELECT 1 FROM account WHERE id = ? AND sha_pass_hash = ?", CONNECTION_SYNCH); - PrepareStatement(LOGIN_SEL_CHECK_PASSWORD_BY_NAME, "SELECT 1 FROM account WHERE username = ? AND sha_pass_hash = ?", CONNECTION_SYNCH); + PrepareStatement(LOGIN_SEL_CHECK_PASSWORD, "SELECT salt, verifier FROM account WHERE id = ? AND salt IS NOT NULL AND verifier IS NOT NULL", CONNECTION_SYNCH); + PrepareStatement(LOGIN_SEL_CHECK_PASSWORD_BY_NAME, "SELECT salt, verifier FROM account WHERE username = ? AND salt IS NOT NULL AND verifier IS NOT NULL", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_PINFO, "SELECT a.username, aa.SecurityLevel, a.email, a.reg_mail, a.last_ip, DATE_FORMAT(a.last_login, '%Y-%m-%d %T'), a.mutetime, a.mutereason, a.muteby, a.failed_logins, a.locked, a.OS FROM account a LEFT JOIN account_access aa ON (a.id = aa.AccountID AND (aa.RealmID = ? OR aa.RealmID = -1)) WHERE a.id = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_PINFO_BANS, "SELECT unbandate, bandate = unbandate, bannedby, banreason FROM account_banned WHERE id = ? AND active ORDER BY bandate ASC LIMIT 1", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_GM_ACCOUNTS, "SELECT a.username, aa.SecurityLevel FROM account a, account_access aa WHERE a.id = aa.AccountID AND aa.SecurityLevel >= ? AND (aa.RealmID = -1 OR aa.RealmID = ?)", CONNECTION_SYNCH); diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index eee8302457c9f..28eae7180ae78 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -38,7 +38,8 @@ enum LoginDatabaseStatements : uint32 LOGIN_SEL_ACCOUNT_BANNED_BY_USERNAME, LOGIN_INS_ACCOUNT_AUTO_BANNED, LOGIN_DEL_ACCOUNT_BANNED, - LOGIN_UPD_SV, + LOGIN_UPD_LOGON, + LOGIN_UPD_LOGON_LEGACY, LOGIN_UPD_LOGONPROOF, LOGIN_SEL_LOGONCHALLENGE, LOGIN_SEL_RECONNECTCHALLENGE, @@ -67,7 +68,6 @@ enum LoginDatabaseStatements : uint32 LOGIN_UPD_ACCOUNT_LOCK_COUNTRY, LOGIN_INS_LOG, LOGIN_UPD_USERNAME, - LOGIN_UPD_PASSWORD, LOGIN_UPD_EMAIL, LOGIN_UPD_REG_EMAIL, LOGIN_UPD_MUTE_TIME, diff --git a/src/server/database/Database/PreparedStatement.h b/src/server/database/Database/PreparedStatement.h index e7f39156252df..b920289623e12 100644 --- a/src/server/database/Database/PreparedStatement.h +++ b/src/server/database/Database/PreparedStatement.h @@ -77,6 +77,12 @@ class TC_DATABASE_API PreparedStatementBase void setDouble(const uint8 index, const double value); void setString(const uint8 index, const std::string& value); void setBinary(const uint8 index, const std::vector& value); + template + void setBinary(const uint8 index, std::array const& value) + { + std::vector vec(value.begin(), value.end()); + setBinary(index, vec); + } uint32 GetIndex() const { return m_index; } std::vector const& GetParameters() const { return statement_data; } diff --git a/src/server/game/Accounts/AccountMgr.cpp b/src/server/game/Accounts/AccountMgr.cpp index 36c2e7955cbce..19876a4e19205 100644 --- a/src/server/game/Accounts/AccountMgr.cpp +++ b/src/server/game/Accounts/AccountMgr.cpp @@ -24,6 +24,7 @@ #include "Player.h" #include "Realm.h" #include "ScriptMgr.h" +#include "SRP6.h" #include "Util.h" #include "World.h" #include "WorldSession.h" @@ -59,9 +60,11 @@ AccountOpResult AccountMgr::CreateAccount(std::string username, std::string pass LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_ACCOUNT); stmt->setString(0, username); - stmt->setString(1, CalculateShaPassHash(username, password)); - stmt->setString(2, email); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData(username, password); + stmt->setBinary(1, salt); + stmt->setBinary(2, verifier); stmt->setString(3, email); + stmt->setString(4, email); LoginDatabase.DirectExecute(stmt); // Enforce saving, otherwise AddGroup can fail @@ -146,6 +149,13 @@ AccountOpResult AccountMgr::DeleteAccount(uint32 accountId) return AccountOpResult::AOR_OK; } +// Do not use this. Use the appropriate methods on Trinity::Crypto::SRP6 to do whatever you are trying to do. +// See issue #25157. +static std::string CalculateShaPassHash_DEPRECATED_DONOTUSE(std::string const& name, std::string const& password) +{ + return ByteArrayToHexStr(Trinity::Crypto::SHA1::GetDigestOf(name, ":", password)); +} + AccountOpResult AccountMgr::ChangeUsername(uint32 accountId, std::string newUsername, std::string newPassword) { // Check if accounts exists @@ -166,13 +176,24 @@ AccountOpResult AccountMgr::ChangeUsername(uint32 accountId, std::string newUser Utf8ToUpperOnlyLatin(newPassword); stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_USERNAME); - stmt->setString(0, newUsername); - stmt->setString(1, CalculateShaPassHash(newUsername, newPassword)); - stmt->setUInt32(2, accountId); + stmt->setUInt32(1, accountId); + LoginDatabase.Execute(stmt); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData(newUsername, newPassword); + stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON); + stmt->setBinary(0, salt); + stmt->setBinary(1, verifier); + stmt->setUInt32(2, accountId); LoginDatabase.Execute(stmt); + { + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON_LEGACY); + stmt->setString(0, CalculateShaPassHash_DEPRECATED_DONOTUSE(newUsername, newPassword)); + stmt->setUInt32(1, accountId); + LoginDatabase.Execute(stmt); + } + return AccountOpResult::AOR_OK; } @@ -194,21 +215,20 @@ AccountOpResult AccountMgr::ChangePassword(uint32 accountId, std::string newPass Utf8ToUpperOnlyLatin(username); Utf8ToUpperOnlyLatin(newPassword); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData(username, newPassword); - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_PASSWORD); - - stmt->setString(0, CalculateShaPassHash(username, newPassword)); - stmt->setUInt32(1, accountId); - + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON); + stmt->setBinary(0, salt); + stmt->setBinary(1, verifier); + stmt->setUInt32(2, accountId);; LoginDatabase.Execute(stmt); - stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_SV); - - stmt->setString(0, ""); - stmt->setString(1, ""); - stmt->setString(2, username); - - LoginDatabase.Execute(stmt); + { + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON_LEGACY); + stmt->setString(0, CalculateShaPassHash_DEPRECATED_DONOTUSE(username, newPassword)); + stmt->setUInt32(1, accountId); + LoginDatabase.Execute(stmt); + } sScriptMgr->OnPasswordChange(accountId); return AccountOpResult::AOR_OK; @@ -346,10 +366,16 @@ bool AccountMgr::CheckPassword(uint32 accountId, std::string password) LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_CHECK_PASSWORD); stmt->setUInt32(0, accountId); - stmt->setString(1, CalculateShaPassHash(username, password)); - PreparedQueryResult result = LoginDatabase.Query(stmt); - return (result) ? true : false; + if (PreparedQueryResult result = LoginDatabase.Query(stmt)) + { + Trinity::Crypto::SRP6::Salt salt = (*result)[0].GetBinary(); + Trinity::Crypto::SRP6::Verifier verifier = (*result)[1].GetBinary(); + if (Trinity::Crypto::SRP6::CheckLogin(username, password, salt, verifier)) + return true; + } + + return false; } bool AccountMgr::CheckEmail(uint32 accountId, std::string newEmail) @@ -379,11 +405,6 @@ uint32 AccountMgr::GetCharactersCount(uint32 accountId) return (result) ? (*result)[0].GetUInt64() : 0; } -std::string AccountMgr::CalculateShaPassHash(std::string const& name, std::string const& password) -{ - return ByteArrayToHexStr(Trinity::Crypto::SHA1::GetDigestOf(name, ":", password)); -} - bool AccountMgr::IsBannedAccount(std::string const& name) { LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_BANNED_BY_USERNAME); diff --git a/src/server/game/Accounts/AccountMgr.h b/src/server/game/Accounts/AccountMgr.h index 07a3d6944926a..446ec93f7a3a0 100644 --- a/src/server/game/Accounts/AccountMgr.h +++ b/src/server/game/Accounts/AccountMgr.h @@ -73,7 +73,6 @@ class TC_GAME_API AccountMgr static bool GetEmail(uint32 accountId, std::string& email); static uint32 GetCharactersCount(uint32 accountId); - static std::string CalculateShaPassHash(std::string const& name, std::string const& password); static bool IsBannedAccount(std::string const& name); static bool IsPlayerAccount(uint32 gmlevel); static bool IsAdminAccount(uint32 gmlevel); diff --git a/src/server/game/Server/WorldSocket.cpp b/src/server/game/Server/WorldSocket.cpp index 35fc14aa0349b..90b42221598fa 100644 --- a/src/server/game/Server/WorldSocket.cpp +++ b/src/server/game/Server/WorldSocket.cpp @@ -270,7 +270,7 @@ struct AccountInfo // LEFT JOIN account r ON a.id = r.recruiter // WHERE a.username = ? ORDER BY aa.RealmID DESC LIMIT 1 Id = fields[0].GetUInt32(); - HexStrToByteArray(fields[1].GetCString(), SessionKey); + SessionKey = fields[1].GetBinary(); LastIP = fields[2].GetString(); IsLockedToIP = fields[3].GetBool(); LockCountry = fields[4].GetString(); diff --git a/src/server/worldserver/RemoteAccess/RASession.cpp b/src/server/worldserver/RemoteAccess/RASession.cpp index 65fb88a26800b..6c8cb8b34681a 100644 --- a/src/server/worldserver/RemoteAccess/RASession.cpp +++ b/src/server/worldserver/RemoteAccess/RASession.cpp @@ -20,9 +20,10 @@ #include "Config.h" #include "DatabaseEnv.h" #include "Log.h" +#include "ServerMotd.h" +#include "SRP6.h" #include "Util.h" #include "World.h" -#include "ServerMotd.h" #include #include #include @@ -159,22 +160,21 @@ bool RASession::CheckPassword(const std::string& user, const std::string& pass) Utf8ToUpperOnlyLatin(safe_pass); std::transform(safe_pass.begin(), safe_pass.end(), safe_pass.begin(), ::toupper); - std::string hash = AccountMgr::CalculateShaPassHash(safe_user, safe_pass); - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_CHECK_PASSWORD_BY_NAME); stmt->setString(0, safe_user); - stmt->setString(1, hash); - - PreparedQueryResult result = LoginDatabase.Query(stmt); - if (!result) + if (PreparedQueryResult result = LoginDatabase.Query(stmt)) { - TC_LOG_INFO("commands.ra", "Wrong password for user: %s", user.c_str()); - return false; + Trinity::Crypto::SRP6::Salt salt = (*result)[0].GetBinary(); + Trinity::Crypto::SRP6::Verifier verifier = (*result)[1].GetBinary(); + + if (Trinity::Crypto::SRP6::CheckLogin(safe_user, safe_pass, salt, verifier)) + return true; } - return true; + TC_LOG_INFO("commands.ra", "Wrong password for user: %s", user.c_str()); + return false; } bool RASession::ProcessCommand(std::string& command)