Index: source/lobby/IXmppClient.h =================================================================== --- source/lobby/IXmppClient.h +++ source/lobby/IXmppClient.h @@ -39,7 +39,7 @@ virtual void SendIqGetProfile(const std::string& player) = 0; virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; - virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password) = 0; + virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password, const std::string& clientSalt) = 0; virtual void SendIqUnregisterGame() = 0; virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0; virtual void SendIqLobbyAuth(const std::string& to, const std::string& token) = 0; Index: source/lobby/StanzaExtensions.h =================================================================== --- source/lobby/StanzaExtensions.h +++ source/lobby/StanzaExtensions.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -62,6 +62,7 @@ glooxwrapper::string m_Port; glooxwrapper::string m_UseSTUN; glooxwrapper::string m_Password; + glooxwrapper::string m_ClientSalt; glooxwrapper::string m_Error; }; Index: source/lobby/StanzaExtensions.cpp =================================================================== --- source/lobby/StanzaExtensions.cpp +++ source/lobby/StanzaExtensions.cpp @@ -306,6 +306,9 @@ const glooxwrapper::Tag* pw = tag->findTag_clone("connectiondata/password"); if (pw) m_Password = pw->cdata(); + const glooxwrapper::Tag* cs = tag->findTag_clone("connectiondata/clientsalt"); + if (cs) + m_ClientSalt = cs->cdata(); const glooxwrapper::Tag* e = tag->findTag_clone("connectiondata/error"); if (e) m_Error= e->cdata(); @@ -314,6 +317,7 @@ glooxwrapper::Tag::free(p); glooxwrapper::Tag::free(s); glooxwrapper::Tag::free(pw); + glooxwrapper::Tag::free(cs); glooxwrapper::Tag::free(e); } @@ -342,6 +346,8 @@ t->addChild(glooxwrapper::Tag::allocate("useSTUN", m_UseSTUN)); if (!m_Password.empty()) t->addChild(glooxwrapper::Tag::allocate("password", m_Password)); + if (!m_ClientSalt.empty()) + t->addChild(glooxwrapper::Tag::allocate("clientsalt", m_ClientSalt)); if (!m_Error.empty()) t->addChild(glooxwrapper::Tag::allocate("error", m_Error)); return t; Index: source/lobby/XmppClient.h =================================================================== --- source/lobby/XmppClient.h +++ source/lobby/XmppClient.h @@ -86,7 +86,7 @@ void SendIqGetProfile(const std::string& player); void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data); - void SendIqGetConnectionData(const std::string& jid, const std::string& password); + void SendIqGetConnectionData(const std::string& jid, const std::string& password, const std::string& clientSalt); void SendIqUnregisterGame(); void SendIqChangeStateGame(const std::string& nbp, const std::string& players); void SendIqLobbyAuth(const std::string& to, const std::string& token); Index: source/lobby/XmppClient.cpp =================================================================== --- source/lobby/XmppClient.cpp +++ source/lobby/XmppClient.cpp @@ -370,12 +370,13 @@ /** * Request the Connection data (ip, port...) from the server. */ -void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password) +void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password, const std::string& clientSalt) { glooxwrapper::JID targetJID(jid); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Password = password; + connectionData->m_ClientSalt = clientSalt; glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID()); iq.addExtension(connectionData); m_connectionDataJid = iq.from().full(); @@ -986,7 +987,7 @@ m_client->send(response); return true; } - if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.c_str()), iq.from().username())) + if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.c_str()), cd->m_ClientSalt.c_str())) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -103,6 +103,7 @@ /** * Set the game password. + * Must be called after SetUserName, as that is used to hash further. */ void SetGamePassword(const CStr& hashedPassword); @@ -125,6 +126,11 @@ */ bool SetupConnection(ENetHost* enetClient); + /** + * Request connection information over the lobby. + */ + void SetupConnectionViaLobby(); + /** * Connect to the remote networked server using lobby. * Push netstatus messages on failure. Index: source/network/NetClient.cpp =================================================================== --- source/network/NetClient.cpp +++ source/network/NetClient.cpp @@ -33,6 +33,7 @@ #include "ps/Compress.h" #include "ps/CStr.h" #include "ps/Game.h" +#include "ps/Hashing.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Threading.h" @@ -183,7 +184,9 @@ void CNetClient::SetGamePassword(const CStr& hashedPassword) { - m_Password = hashedPassword; + // Hash on top with the user's name, to make sure not all + // hashing data is in control of the host. + m_Password = HashPassword(hashedPassword, m_UserName.ToUTF8()); } void CNetClient::SetControllerSecret(const std::string& secret) @@ -201,6 +204,11 @@ return ok; } +void CNetClient::SetupConnectionViaLobby() +{ + g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8()); +} + void CNetClient::SetupServerData(CStr address, u16 port, bool stun) { ENSURE(!m_Session); Index: source/network/NetServer.h =================================================================== --- source/network/NetServer.h +++ source/network/NetServer.h @@ -226,6 +226,8 @@ CNetServerWorker(bool useLobbyAuth, int autostartPlayers); ~CNetServerWorker(); + bool CheckPassword(const CStr& password, const std::string& username) const; + void SetPassword(const CStr& hashedPassword); void SetControllerSecret(const std::string& secret); Index: source/network/NetServer.cpp =================================================================== --- source/network/NetServer.cpp +++ source/network/NetServer.cpp @@ -31,6 +31,7 @@ #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/GUID.h" +#include "ps/Hashing.h" #include "ps/Profile.h" #include "ps/Threading.h" #include "scriptinterface/ScriptContext.h" @@ -205,6 +206,12 @@ } +bool CNetServerWorker::CheckPassword(const CStr& password, const std::string& username) const +{ + return HashPassword(m_Password, username) == password; +} + + bool CNetServerWorker::SetupConnection(const u16 port) { ENSURE(m_State == SERVER_STATE_UNCONNECTED); @@ -998,7 +1005,7 @@ } // Check the password before anything else. - if (server.m_Password != message->m_Password) + if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8())) { // Noisy logerror because players are not supposed to be able to get the IP, // so this might be someone targeting the host for some reason @@ -1659,7 +1666,7 @@ bool CNetServer::CheckPasswordAndIncrement(const CStr& password, const std::string& username) { std::unordered_map::iterator it = m_FailedAttempts.find(username); - if (m_Password == password) + if (m_Worker->CheckPassword(password, username)) { if (it != m_FailedAttempts.end()) it->second = 0; Index: source/network/scripting/JSInterface_Network.cpp =================================================================== --- source/network/scripting/JSInterface_Network.cpp +++ source/network/scripting/JSInterface_Network.cpp @@ -30,6 +30,8 @@ #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/GUID.h" +#include "ps/Hashing.h" +#include "ps/Pyrogenesis.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" @@ -58,36 +60,6 @@ return !!g_NetClient; } -CStr HashPassword(const CStr& password) -{ - if (password.empty()) - return password; - - ENSURE(sodium_init() >= 0); - const int DIGESTSIZE = crypto_hash_sha256_BYTES; - constexpr int ITERATIONS = 1737; - - cassert(DIGESTSIZE == 32); - - static const unsigned char salt_base[DIGESTSIZE] = { - 244, 243, 249, 244, 32, 33, 19, 35, 16, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 88, 123, 234, 123 }; - - // initialize the salt buffer - unsigned char salt_buffer[DIGESTSIZE] = { 0 }; - crypto_hash_sha256_state state; - crypto_hash_sha256_init(&state); - crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); - - crypto_hash_sha256_final(&state, salt_buffer); - - // PBKDF2 to create the buffer - unsigned char encrypted[DIGESTSIZE]; - pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); - return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); -} - - void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, bool useSTUN, const CStr& password) { ENSURE(!g_NetClient); @@ -130,18 +102,38 @@ // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); - - // We will get hashed password from clients, so hash it once for server - CStr hashedPass = HashPassword(password); - g_NetServer->SetPassword(hashedPass); g_NetServer->SetControllerSecret(secret); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); + if (hasLobby) - g_NetClient->SetHostJID(g_XmppClient->GetJID()); - g_NetClient->SetGamePassword(hashedPass); + { + CStr hostJID = g_XmppClient->GetJID(); + + /** + * Password security - we want 0 A.D. to protect players from malicious hosts. We assume that clients + * might mistakenly send a personal password instead of the game password (e.g. enter their mail account's password on autopilot). + * Malicious dedicated servers might be set up to farm these failed logins and possibly obtain user credentials. + * Therefore, we hash the passwords on the client side before sending them to the server. + * This still makes the passwords potentially recoverable, but makes it much harder at scale. + * To prevent the creation of rainbow tables, hash with: + * - the host name + * - the client name (this makes rainbow tables completely unworkable unless a specific user is targeted, + * but that would require both computing the matching rainbow table _and_ for that specific user to mistype a personal password, + * at which point we assume the attacker would/could probably just rather use another means of obtaining the password). + * - the password itself + * - the engine version (so that the hashes change periodically) + * TODO: it should be possible to implement SRP or something along those lines to completely protect from this, + * but the cost/benefit ratio is probably not worth it. + */ + CStr hashedPass = HashPassword(password, hostJID + password + engine_version); + g_NetServer->SetPassword(hashedPass); + g_NetClient->SetHostJID(hostJID); + g_NetClient->SetGamePassword(hashedPass); + } + g_NetClient->SetupServerData("127.0.0.1", serverPort, false); g_NetClient->SetControllerSecret(secret); @@ -184,13 +176,13 @@ ENSURE(!g_NetServer); ENSURE(!g_Game); - CStr hashedPass = HashPassword(password); + CStr hashedPass = HashPassword(password, hostJID + password + engine_version); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); g_NetClient->SetHostJID(hostJID); g_NetClient->SetGamePassword(hashedPass); - g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str()); + g_NetClient->SetupConnectionViaLobby(); } void DisconnectNetworkGame() Index: source/ps/Hashing.h =================================================================== --- /dev/null +++ source/ps/Hashing.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2021 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_HASHING +#define INCLUDED_HASHING + +class CStr8; + +/** + * Hash a string in a cryptographically secure manner. + * @return a hex-encoded string. + */ +CStr8 HashPassword(const CStr8& password, const CStr8& salt); + +#endif Index: source/ps/Hashing.cpp =================================================================== --- /dev/null +++ source/ps/Hashing.cpp @@ -0,0 +1,52 @@ +/* Copyright (C) 2021 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ +#include "precompiled.h" + +#include "ps/CStr.h" +#include "ps/Util.h" + +#include "third_party/encryption/pkcs5_pbkdf2.h" + +CStr8 HashPassword(const CStr8& password, const CStr8& salt) +{ + if (password.empty()) + return password; + + ENSURE(sodium_init() >= 0); + const int DIGESTSIZE = crypto_hash_sha256_BYTES; + constexpr int ITERATIONS = 1737; + + cassert(DIGESTSIZE == 32); + + static const unsigned char salt_base[DIGESTSIZE] = { + 244, 243, 249, 244, 32, 33, 19, 35, 16, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 88, 123, 234, 123 }; + + // initialize the salt buffer + unsigned char salt_buffer[DIGESTSIZE] = { 0 }; + crypto_hash_sha256_state state; + crypto_hash_sha256_init(&state); + crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); + crypto_hash_sha256_update(&state, reinterpret_cast(salt.data()), salt.size()); + + crypto_hash_sha256_final(&state, salt_buffer); + + // PBKDF2 to create the buffer + unsigned char encrypted[DIGESTSIZE]; + pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); + return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); +}