Index: source/network/NetServer.h =================================================================== --- source/network/NetServer.h +++ source/network/NetServer.h @@ -224,6 +224,8 @@ CNetServerWorker(bool useLobbyAuth, int autostartPlayers); ~CNetServerWorker(); + bool CheckPassword(const CStr& password, const std::string& username) const; + void SetPassword(const CStr& hashedPassword); /** 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" @@ -187,6 +188,12 @@ m_Password = hashedPassword; } +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); @@ -986,7 +993,7 @@ } // Check the password before anything else. - if (server.m_Password != message->m_Password) + if (!server.CheckPassword(message->m_Password, username.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 @@ -1633,7 +1640,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.h =================================================================== --- source/network/scripting/JSInterface_Network.h +++ source/network/scripting/JSInterface_Network.h @@ -47,7 +47,6 @@ void SendNetworkReady(ScriptInterface::CmptPrivate* pCmptPrivate, int message); void SetTurnLength(ScriptInterface::CmptPrivate* pCmptPrivate, int length); - CStr HashPassword(const CStr& password); void RegisterScriptFunctions(const ScriptInterface& scriptInterface); } Index: source/network/scripting/JSInterface_Network.cpp =================================================================== --- source/network/scripting/JSInterface_Network.cpp +++ source/network/scripting/JSInterface_Network.cpp @@ -29,6 +29,9 @@ #include "network/StunClient.h" #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/ScriptInterface.h" @@ -49,35 +52,6 @@ return !!g_NetClient; } -CStr JSI_Network::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 JSI_Network::StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password) { ENSURE(!g_NetClient); @@ -120,14 +94,31 @@ return; } - // We will get hashed password from clients, so hash it once for server - CStr hashedPass = HashPassword(password); + /** + * 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, playerName.ToUTF8().LowerCase() + password + engine_version); g_NetServer->SetPassword(hashedPass); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game, true); g_NetClient->SetUserName(playerName); g_NetClient->SetHostingPlayerName(hostLobbyName); + // Hash the password further with the client name. + hashedPass = HashPassword(hashedPass, playerName.ToUTF8().LowerCase()); g_NetClient->SetGamePassword(hashedPass); g_NetClient->SetupServerData("127.0.0.1", serverPort, false); @@ -168,7 +159,9 @@ ENSURE(!g_NetServer); ENSURE(!g_Game); - CStr hashedPass = HashPassword(password); + CStr hashedPass = HashPassword(password, playerName.ToUTF8().LowerCase() + password + engine_version); + // Hash the password further with the client name. + hashedPass = HashPassword(hashedPass, playerName.ToUTF8().LowerCase()); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game, false); g_NetClient->SetUserName(playerName); 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(); +}