Skip to content

Commit 7019387

Browse files
committed
Lessened security requirements for ungraded accounts, added /changeusername
1 parent 64daae5 commit 7019387

5 files changed

Lines changed: 349 additions & 76 deletions

File tree

AuthServer/src/AuthService.cpp

Lines changed: 3 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -195,87 +195,14 @@ namespace Auth
195195

196196

197197
Auth::Enums::Login AuthService::authorizeUngraded(const Auth::Structures::BasicAccountInfo& ainfo,const std::optional<std::string>& token,const std::string& plainPw)
198-
{
199-
const bool enhancedSecurity = Common::Utils::SetupParser::getInstance().getAuthSetup().enhancedSecurity;
200-
const bool passwordOk = validatePassword(plainPw, ainfo.hashedPassword);
201-
202-
bool tokenOk = true;
203-
if (ainfo.secret.empty())
198+
{
199+
if (!validatePassword(plainPw, ainfo.hashedPassword))
204200
{
205-
if (enhancedSecurity)
206-
{
207-
m_persistentDatabase.logGameEvent("AuthUngradedLogin",
208-
"Failed login: missing mandatory secret for ungraded account " + std::to_string(ainfo.ainfoClient.accountId),"MEDIUM");
209-
return Auth::Enums::Login::INCORRECT;
210-
}
211-
}
212-
if (!ainfo.secret.empty())
213-
{
214-
tokenOk = verifyToken(ainfo.secret, token);
215-
}
216-
217-
auto& counters = m_badLoginAttempts[ainfo.ainfoClient.accountId];
218-
if (!passwordOk)
219-
{
220-
++counters.totalWrongPasswords;
221-
m_persistentDatabase.logGameEvent("AuthUngradedLogin",
222-
"Failed login: incorrect password for ungraded account " + std::to_string(ainfo.ainfoClient.accountId),"LOW");
223-
}
224-
else if (!tokenOk)
225-
{
226-
++counters.totalWrong2fas;
227-
m_persistentDatabase.logGameEvent("AuthUngradedLogin",
228-
"Failed login: invalid 2FA token for ungraded account " + std::to_string(ainfo.ainfoClient.accountId),"LOW");
201+
return Auth::Enums::Login::INCORRECT;
229202
}
230203

231-
constexpr std::uint32_t MAX_2FA_ATTEMPTS = 5;
232204
std::uint64_t suspendedUntilEpoch = Common::Utils::datetimeToEpoch(ainfo.suspendedUntil);
233205
std::uint64_t currentEpoch = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count());
234-
235-
if (counters.totalWrong2fas >= MAX_2FA_ATTEMPTS && enhancedSecurity)
236-
{
237-
if (suspendedUntilEpoch > currentEpoch)
238-
{
239-
return Auth::Enums::Login::INCORRECT;
240-
}
241-
if (auto decrypted = Common::Utils::decryptEmail(ainfo.encryptedEmail, Common::Utils::SetupParser::getInstance().getGeneralSetup().emailSecret))
242-
{
243-
m_emailDispatcher.sendEmailAsync({ decrypted.value() }, "[Security] Your ToyBattles account was banned",
244-
"This is an automatic notification. For security reasons, your TB account was suspended due to 5 wrong login attempts in the game (5 wrong 2FA tokens used).",
245-
[accountId = ainfo.ainfoClient.accountId, this]() {
246-
m_persistentDatabase.logGameEvent("EmailNotification",
247-
"Failed to send email to target after the account was locked due to too many 2FA wrong attempts for accountID: " + std::to_string(accountId),
248-
"HIGH");
249-
}
250-
);
251-
}
252-
else
253-
{
254-
m_persistentDatabase.logGameEvent("EmailDecryption",
255-
"Failed to decrypt email to contact target after their account was locked due to too many 2FA wrong attempts for accountID: "
256-
+ std::to_string(ainfo.ainfoClient.accountId), "HIGH");
257-
}
258-
if (!tryLockAccount(ainfo.ainfoClient.accountId, false))
259-
{
260-
m_persistentDatabase.logGameEvent("AuthUngradedLogin",
261-
"Account lock attempt failed for ungraded account " + std::to_string(ainfo.ainfoClient.accountId), "HIGH");
262-
return Auth::Enums::Login::INCORRECT;
263-
}
264-
265-
m_persistentDatabase.logGameEvent("AuthUngradedLogin",
266-
"Account locked due to repeated failed 2FA attempts: " + std::to_string(ainfo.ainfoClient.accountId), "HIGH");
267-
268-
m_badLoginAttempts.erase(ainfo.ainfoClient.accountId);
269-
return Auth::Enums::Login::INCORRECT;
270-
}
271-
272-
if (!passwordOk || !tokenOk)
273-
{
274-
return Auth::Enums::Login::INCORRECT;
275-
}
276-
277-
m_badLoginAttempts.erase(ainfo.ainfoClient.accountId);
278-
279206
if (suspendedUntilEpoch > currentEpoch)
280207
{
281208
m_persistentDatabase.logGameEvent("AuthUngradedLogin","Login attempt on suspended ungraded account " + std::to_string(ainfo.ainfoClient.accountId),"LOW");
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
#ifndef CHANGE_USERNAME_COMMAND_HEADER
2+
#define CHANGE_USERNAME_COMMAND_HEADER
3+
4+
#include "../ICommand.h"
5+
#include "../ChatCommands.h"
6+
#include "Utils/Utils.h"
7+
#include "../../MainServer.h"
8+
#include <source_location>
9+
#include <regex>
10+
#include <libbcrypt/include/bcrypt/BCrypt.hpp>
11+
#include <libcppotp/auth.h>
12+
13+
namespace Main
14+
{
15+
namespace Command
16+
{
17+
class ChangeUsername final : public ICommand
18+
{
19+
private:
20+
std::string m_password{};
21+
std::string m_2faToken{};
22+
std::string m_newUsername{};
23+
24+
bool validatePassword(const std::string& plain, std::string hash)
25+
{
26+
if (BCrypt::validatePassword(plain, hash))
27+
return true;
28+
29+
if (hash.substr(0, 4) == "$2b$")
30+
{
31+
std::string hash_2a = hash;
32+
hash_2a[2] = 'a';
33+
return BCrypt::validatePassword(plain, hash_2a);
34+
}
35+
36+
return false;
37+
}
38+
39+
bool parseCommand(const std::string& providedCommand) override
40+
{
41+
std::smatch match;
42+
if (std::regex_match(providedCommand, match, this->m_pattern))
43+
{
44+
m_password = match[2].str();
45+
m_2faToken = match[3].str();
46+
m_newUsername = match[4].str();
47+
return true;
48+
}
49+
return false;
50+
}
51+
52+
bool verifyToken(const std::string& encryptedSecret, const std::optional<std::string>& token)
53+
{
54+
if (encryptedSecret.empty() || !token.has_value()) return false;
55+
56+
auto optSecret = Common::Utils::decrypt2FASecret(encryptedSecret,
57+
Common::Utils::SetupParser::getInstance().getGeneralSetup().twoFaSecret);
58+
if (!optSecret) return false;
59+
60+
const int t_interval = 30;
61+
std::time_t now = std::time(nullptr);
62+
63+
for (int i = -1; i <= 1; ++i)
64+
{
65+
auto expectedToken = auth::generateToken(optSecret.value(), now + i * t_interval, t_interval);
66+
std::ostringstream oss;
67+
oss << std::setw(6) << std::setfill('0') << expectedToken;
68+
69+
if (oss.str() == token.value()) return true;
70+
}
71+
72+
return false;
73+
}
74+
75+
bool validateUsername(const std::string& username)
76+
{
77+
if (username.length() < 4 || username.length() > 20)
78+
return false;
79+
80+
if (!std::isalpha(username[0]))
81+
return false;
82+
83+
for (char c : username)
84+
{
85+
if (!std::isalnum(c) && c != '_')
86+
return false;
87+
}
88+
89+
if (username.find("__") != std::string::npos)
90+
return false;
91+
92+
bool hasLetter = false;
93+
for (char c : username)
94+
{
95+
if (std::isalpha(c))
96+
{
97+
hasLetter = true;
98+
break;
99+
}
100+
}
101+
102+
return hasLetter;
103+
}
104+
105+
public:
106+
explicit ChangeUsername(const Common::Enums::PlayerGrade requiredGrade)
107+
: ICommand{ requiredGrade, "/changeusername <Password> <2faToken> <NewUsername>",
108+
R"(^(\S+)\s*(\S+)\s*(\S+)\s*(\S+)\s*$)" }
109+
{
110+
}
111+
112+
void execute(const std::string& command, std::shared_ptr<Main::Network::Session> session,
113+
MN::SessionsManager& sessionsManager, MC::RoomsManager&, MP::MainScheduler& scheduler,
114+
std::uint32_t, Main::MainServer& server) override
115+
{
116+
const auto& ainfo = session->getAccountInfo();
117+
if (ainfo.playerGrade >= Common::Enums::PlayerGrade::GRADE_MOD)
118+
{
119+
session->sendMessage("error: staff accounts cannot change username. Contact admins.");
120+
return;
121+
}
122+
123+
if (!parseCommand(command))
124+
{
125+
session->sendMessage("error: invalid command format. Usage: /changeusername <Password> <2faToken> <NewUsername>");
126+
return;
127+
}
128+
129+
if (!validateUsername(m_newUsername))
130+
{
131+
session->sendMessage("error: invalid username. Must be 4-20 characters, start with a letter, and contain only letters, numbers, and underscores (no consecutive underscores)");
132+
return;
133+
}
134+
135+
auto encryptedEmail = scheduler.immediatePersist(std::source_location::current(),
136+
&Main::Persistence::PersistentDatabase::getColumnByAid, "Email", ainfo.accountID);
137+
138+
if (!encryptedEmail || encryptedEmail->empty())
139+
{
140+
session->sendMessage("error: account does not have a registered email. Username change not allowed.");
141+
return;
142+
}
143+
144+
auto decryptedEmail = Common::Utils::decryptEmail(encryptedEmail.value(),
145+
Common::Utils::SetupParser::getInstance().getGeneralSetup().emailSecret);
146+
147+
if (!decryptedEmail)
148+
{
149+
session->sendMessage("error: failed to fetch email from database.");
150+
return;
151+
}
152+
153+
bool usernameExists = scheduler.immediatePersist(std::source_location::current(),
154+
&Main::Persistence::PersistentDatabase::checkUsernameExists, m_newUsername);
155+
156+
if (usernameExists)
157+
{
158+
session->sendMessage("error: username already taken");
159+
return;
160+
}
161+
162+
auto hashedPassword = scheduler.immediatePersist(std::source_location::current(),
163+
&Main::Persistence::PersistentDatabase::getColumnByAid, "Password", ainfo.accountID);
164+
165+
if (!hashedPassword.has_value())
166+
{
167+
session->sendMessage("error: failed to retrieve account data");
168+
return;
169+
}
170+
171+
const std::uint32_t maxAttempts = 5;
172+
173+
if (!validatePassword(m_password, hashedPassword.value()))
174+
{
175+
session->m_totalWrongUsernameChange++;
176+
177+
if (session->m_totalWrongUsernameChange >= maxAttempts)
178+
{
179+
sessionsManager.removeSession(ainfo.uniqueId.session);
180+
if (!session->banAccount(9999, "[Automatic] Too many wrong passwords while changing username",
181+
Common::Enums::GRADE_SYSTEM))
182+
{
183+
session->sendMessage("error: unknown error");
184+
}
185+
else
186+
{
187+
auto subject = "Your TB Account Was Banned";
188+
auto body = "Hello " + std::string(ainfo.nickname) +
189+
", your TB account was automatically banned as you attempted to change your username "
190+
"but provided the wrong password " + std::to_string(maxAttempts) +
191+
" consecutive times! Contact support for help.";
192+
193+
server.emailDispatcher.sendEmailAsync(
194+
{ decryptedEmail.value() }, subject, body,
195+
[accountId = ainfo.accountID, &scheduler, this]() {
196+
scheduler.immediatePersist(std::source_location::current(),
197+
&Main::Persistence::PersistentDatabase::logGameEvent,
198+
"EmailUsernameChange",
199+
"Failed to send account ban email for accountID: " + std::to_string(accountId),
200+
"HIGH");
201+
});
202+
}
203+
return;
204+
}
205+
206+
session->sendMessage("error: password is incorrect");
207+
return;
208+
}
209+
210+
auto encryptedSecret = scheduler.immediatePersist(std::source_location::current(),
211+
&Main::Persistence::PersistentDatabase::getColumnByAid, "Secret", ainfo.accountID);
212+
213+
if (!encryptedSecret.has_value())
214+
{
215+
session->sendMessage("error: failed to retrieve 2FA data");
216+
return;
217+
}
218+
219+
if (!verifyToken(encryptedSecret.value(), m_2faToken))
220+
{
221+
session->m_totalWrong2FaUsernameChange++;
222+
223+
if (session->m_totalWrong2FaUsernameChange >= maxAttempts)
224+
{
225+
sessionsManager.removeSession(ainfo.uniqueId.session);
226+
if (!session->banAccount(9999, "[Automatic] Too many wrong 2FAs while changing username",
227+
Common::Enums::GRADE_SYSTEM))
228+
{
229+
session->sendMessage("error: unknown error");
230+
}
231+
else
232+
{
233+
auto subject = "Your TB Account Was Banned";
234+
auto body = "Hello " + std::string(ainfo.nickname) +
235+
", your TB account was automatically banned as you attempted to change your username "
236+
"but provided the wrong 2FA token " + std::to_string(maxAttempts) +
237+
" consecutive times! Contact support for help.";
238+
239+
server.emailDispatcher.sendEmailAsync(
240+
{ decryptedEmail.value() }, subject, body,
241+
[accountId = ainfo.accountID, &scheduler, this]() {
242+
scheduler.immediatePersist(std::source_location::current(),
243+
&Main::Persistence::PersistentDatabase::logGameEvent,
244+
"EmailUsernameChange",
245+
"Failed to send account ban email for accountID: " + std::to_string(accountId),
246+
"HIGH");
247+
});
248+
}
249+
return;
250+
}
251+
252+
session->sendMessage("error: invalid 2FA token");
253+
return;
254+
}
255+
256+
bool success = scheduler.immediatePersist(std::source_location::current(),
257+
&Main::Persistence::PersistentDatabase::updateUsernameByAid,
258+
ainfo.accountID, m_newUsername, ainfo.nickname);
259+
260+
if (!success)
261+
{
262+
session->sendMessage("error: failed to update username");
263+
return;
264+
}
265+
266+
session->m_totalWrongUsernameChange = 0;
267+
session->m_totalWrong2FaUsernameChange = 0;
268+
269+
270+
auto subject = "Your ToyBattles Username Has Been Changed";
271+
auto body = "Hello " + m_newUsername + ",\n\n"
272+
"Your username has been successfully changed " +
273+
"' to '" + m_newUsername + "'.\n\n"
274+
"If you did not make this change, please contact support immediately.\n\n"
275+
"Regards,\nToyBattles Team";
276+
277+
server.emailDispatcher.sendEmailAsync(
278+
{ decryptedEmail.value() }, subject, body,
279+
[accountId = ainfo.accountID, &scheduler, this]() {
280+
scheduler.immediatePersist(std::source_location::current(),
281+
&Main::Persistence::PersistentDatabase::logGameEvent,
282+
"EmailUsernameChange",
283+
"Failed to send username change confirmation email for accountID: " +
284+
std::to_string(accountId), "MEDIUM");
285+
});
286+
287+
session->sendMessage("success: your username has been changed to '" + m_newUsername + "'. You will need to relog with your new username.");
288+
}
289+
};
290+
291+
REGISTER_CMD(ChangeUsername, Common::Enums::PlayerGrade::GRADE_NORMAL)
292+
};
293+
}
294+
295+
#endif

MainServer/include/Network/MainSession.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ namespace Main
5151
bool m_isInvisible{};
5252

5353
std::uint32_t m_totalWrongPasswordReset{};
54+
std::uint32_t m_totalWrong2FaUsernameChange{};
55+
std::uint32_t m_totalWrongUsernameChange{};
5456
std::uint32_t m_totalWrong2FaReset{};
5557

5658
std::string m_hwid{ "" };

0 commit comments

Comments
 (0)