feat(Core/Authserver): TOTP rewrite (#5620)

This commit is contained in:
Kargatum
2021-05-13 07:57:10 +07:00
committed by GitHub
parent 681c3237df
commit 26f2abaaa9
61 changed files with 6049 additions and 211 deletions

View File

@@ -24,6 +24,8 @@
#include "RealmList.h"
#include "RealmAcceptor.h"
#include "DatabaseLoader.h"
#include "SecretMgr.h"
#include "SharedDefines.h"
#include <ace/Dev_Poll_Reactor.h>
#include <ace/TP_Reactor.h>
#include <ace/ACE.h>
@@ -57,6 +59,8 @@ void usage(const char* prog)
/// Launch the auth server
extern int main(int argc, char** argv)
{
acore::Impl::CurrentServerProcessHolder::_type = SERVER_PROCESS_AUTHSERVER;
// Command line parsing to get the configuration file name
std::string configFile = sConfigMgr->GetConfigPath() + std::string(_ACORE_REALM_CONFIG);
int count = 1;
@@ -124,6 +128,8 @@ extern int main(int argc, char** argv)
if (!StartDB())
return 1;
sSecretMgr->Initialize();
// Get the list of realms for the server
sRealmList->Initialize(sConfigMgr->GetOption<int32>("RealmsStateUpdateDelay", 20));
if (sRealmList->size() == 0)

View File

@@ -4,21 +4,23 @@
* Copyright (C) 2005-2009 MaNGOS <http://getmangos.com/>
*/
#include <algorithm>
#include <openssl/md5.h>
#include "AES.h"
#include "Common.h"
#include "CryptoGenerics.h"
#include "CryptoRandom.h"
#include "CryptoHash.h"
#include "Database/DatabaseEnv.h"
#include "DatabaseEnv.h"
#include "ByteBuffer.h"
#include "Configuration/Config.h"
#include "Config.h"
#include "Log.h"
#include "RealmList.h"
#include "AuthSocket.h"
#include "AuthCodes.h"
#include "SecretMgr.h"
#include "TOTP.h"
#include "openssl/crypto.h"
#include <algorithm>
#include <openssl/crypto.h>
#include <openssl/md5.h>
#define ChunkSize 2048
@@ -371,6 +373,7 @@ bool AuthSocket::_HandleLogonChallenge()
std::string const& ip_address = socket().getRemoteAddress();
PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_IP_BANNED);
stmt->setString(0, ip_address);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (result)
{
@@ -438,6 +441,26 @@ bool AuthSocket::_HandleLogonChallenge()
}
}
uint8 securityFlags = 0;
_totpSecret = fields[7].GetBinary();
// Check if a TOTP token is needed
if (!_totpSecret || !_totpSecret.value().empty())
{
securityFlags = 4;
if (auto const& secret = sSecretMgr->GetSecret(SECRET_TOTP_MASTER_KEY))
{
bool success = acore::Crypto::AEDecrypt<acore::Crypto::AES>(*_totpSecret, *secret);
if (!success)
{
pkt << uint8(WOW_FAIL_DB_BUSY);
LOG_ERROR("server.authserver", "[AuthChallenge] Account '%s' has invalid ciphertext for TOTP token key stored", _login.c_str());
locked = true;
}
}
}
if (!locked)
{
//set expired bans to inactive
@@ -446,18 +469,19 @@ bool AuthSocket::_HandleLogonChallenge()
// If the account is banned, reject the logon attempt
stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_BANNED);
stmt->setUInt32(0, fields[0].GetUInt32());
PreparedQueryResult banresult = LoginDatabase.Query(stmt);
if (banresult)
{
if ((*banresult)[0].GetUInt32() == (*banresult)[1].GetUInt32())
{
pkt << uint8(WOW_FAIL_BANNED);
LOG_DEBUG("network", "'%s:%d' [AuthChallenge] Banned account %s tried to login!", socket().getRemoteAddress().c_str(), socket().getRemotePort(), _login.c_str ());
LOG_DEBUG("network", "'%s:%d' [AuthChallenge] Banned account %s tried to login!", socket().getRemoteAddress().c_str(), socket().getRemotePort(), _login.c_str());
}
else
{
pkt << uint8(WOW_FAIL_SUSPENDED);
LOG_DEBUG("network", "'%s:%d' [AuthChallenge] Temporarily banned account %s tried to login!", socket().getRemoteAddress().c_str(), socket().getRemotePort(), _login.c_str ());
LOG_DEBUG("network", "'%s:%d' [AuthChallenge] Temporarily banned account %s tried to login!", socket().getRemoteAddress().c_str(), socket().getRemotePort(), _login.c_str());
}
}
else
@@ -481,12 +505,6 @@ bool AuthSocket::_HandleLogonChallenge()
pkt.append(_srp6->N);
pkt.append(_srp6->s);
pkt.append(unk3.ToByteArray<16>());
uint8 securityFlags = 0;
// Check if token is used
_tokenKey = fields[7].GetString();
if (!_tokenKey.empty())
securityFlags = 4;
pkt << uint8(securityFlags); // security flags (0x0...0x04)
@@ -515,9 +533,9 @@ bool AuthSocket::_HandleLogonChallenge()
for (int i = 0; i < 4; ++i)
_localizationName[i] = ch->country[4 - i - 1];
#if defined(ENABLE_EXTRAS) && defined(ENABLE_EXTRA_LOGS)
LOG_DEBUG("network", "'%s:%d' [AuthChallenge] account %s is using '%c%c%c%c' locale (%u)", socket().getRemoteAddress().c_str(), socket().getRemotePort(), _login.c_str (), ch->country[3], ch->country[2], ch->country[1], ch->country[0], GetLocaleByName(_localizationName) );
#endif
LOG_DEBUG("network", "'%s:%d' [AuthChallenge] account %s is using '%c%c%c%c' locale (%u)",
socket().getRemoteAddress().c_str(), socket().getRemotePort(), _login.c_str(), ch->country[3], ch->country[2], ch->country[1], ch->country[0], GetLocaleByName(_localizationName));
///- All good, await client's proof
_status = STATUS_LOGON_PROOF;
}
@@ -577,23 +595,24 @@ bool AuthSocket::_HandleLogonProof()
acore::Crypto::SHA1::Digest M2 = acore::Crypto::SRP6::GetSessionVerifier(lp.A, lp.clientM, _sessionKey);
// Check auth token
if ((lp.securityFlags & 0x04) || !_tokenKey.empty())
bool tokenSuccess = false;
bool sentToken = (lp.securityFlags & 0x04);
if (sentToken && _totpSecret)
{
uint8 size;
socket().recv((char*)&size, 1);
char* token = new char[size + 1];
token[size] = '\0';
socket().recv(token, size);
unsigned int validToken = TOTP::GenerateToken(_tokenKey.c_str());
unsigned int incomingToken = atoi(token);
delete[] token;
if (validToken != incomingToken)
{
char data[] = { AUTH_LOGON_PROOF, WOW_FAIL_UNKNOWN_ACCOUNT, 3, 0 };
socket().send(data, sizeof(data));
return false;
}
tokenSuccess = acore::Crypto::TOTP::ValidateToken(*_totpSecret, incomingToken);
memset(_totpSecret->data(), 0, _totpSecret->size());
}
else if (!sentToken && !_totpSecret)
tokenSuccess = true;
if (_expversion & POST_BC_EXP_FLAG) // 2.x and 3.x clients
{
@@ -616,6 +635,12 @@ bool AuthSocket::_HandleLogonProof()
socket().send((char*)&proof, sizeof(proof));
}
if (!tokenSuccess)
{
char data[4] = { AUTH_LOGON_PROOF, WOW_FAIL_UNKNOWN_ACCOUNT, 3, 0 };
socket().send(data, sizeof(data));
}
///- Set _status to authed!
_status = STATUS_AUTHED;
}

View File

@@ -9,6 +9,7 @@
#include "Common.h"
#include "CryptoHash.h"
#include "Optional.h"
#include "RealmSocket.h"
#include "SRP6.h"
@@ -65,7 +66,7 @@ private:
eStatus _status;
std::string _login;
std::string _tokenKey;
Optional<std::vector<uint8>> _totpSecret;
// Since GetLocaleByName() is _NOT_ bijective, we have to store the locale as a string. Otherwise we can't differ
// between enUS and enGB, which is important for the patch system

View File

@@ -1,86 +0,0 @@
/*
* Copyright (C) 2008-2013 TrinityCore <http://www.trinitycore.org/>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TOTP.h"
#include <cstring>
int base32_decode(const char* encoded, char* result, int bufSize)
{
// Base32 implementation
// Copyright 2010 Google Inc.
// Author: Markus Gutschke
// Licensed under the Apache License, Version 2.0
int buffer = 0;
int bitsLeft = 0;
int count = 0;
for (const char* ptr = encoded; count < bufSize && *ptr; ++ptr)
{
char ch = *ptr;
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-')
continue;
buffer <<= 5;
// Deal with commonly mistyped characters
if (ch == '0')
ch = 'O';
else if (ch == '1')
ch = 'L';
else if (ch == '8')
ch = 'B';
// Look up one base32 digit
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
ch = (ch & 0x1F) - 1;
else if (ch >= '2' && ch <= '7')
ch -= '2' - 26;
else
return -1;
buffer |= ch;
bitsLeft += 5;
if (bitsLeft >= 8)
{
result[count++] = buffer >> (bitsLeft - 8);
bitsLeft -= 8;
}
}
if (count < bufSize)
result[count] = '\000';
return count;
}
#define HMAC_RES_SIZE 20
namespace TOTP
{
unsigned int GenerateToken(const char* b32key)
{
size_t keySize = strlen(b32key);
int bufsize = (keySize + 7) / 8 * 5;
char* encoded = new char[bufsize];
memset(encoded, 0, bufsize);
unsigned int hmacResSize = HMAC_RES_SIZE;
unsigned char hmacRes[HMAC_RES_SIZE];
unsigned long timestamp = time(nullptr) / 30;
unsigned char challenge[8];
for (int i = 8; i--; timestamp >>= 8)
challenge[i] = timestamp;
base32_decode(b32key, encoded, bufsize);
HMAC(EVP_sha1(), encoded, bufsize, challenge, 8, hmacRes, &hmacResSize);
unsigned int offset = hmacRes[19] & 0xF;
unsigned int truncHash = (hmacRes[offset] << 24) | (hmacRes[offset + 1] << 16 ) | (hmacRes[offset + 2] << 8) | (hmacRes[offset + 3]);
truncHash &= 0x7FFFFFFF;
delete[] encoded;
return truncHash % 1000000;
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright (C) 2008-2013 TrinityCore <http://www.trinitycore.org/>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _TOTP_H
#define _TOTP_H
#include "openssl/hmac.h"
#include "openssl/evp.h"
namespace TOTP
{
unsigned int GenerateToken(const char* b32key);
}
#endif

View File

@@ -9,6 +9,8 @@
# EXAMPLE CONFIG
# AUTH SERVER SETTINGS
# MYSQL SETTINGS
# CRYPTOGRAPHY
# LOGGING SYSTEM SETTINGS
#
###################################################################################################
@@ -181,6 +183,24 @@ LoginDatabase.SynchThreads = 1
#
###################################################################################################
###################################################################################################
# CRYPTOGRAPHY
#
# TOTPMasterSecret
# Description: The master key used to encrypt TOTP secrets for database storage.
# If you want to change this, uncomment TOTPOldMasterSecret, then copy
# your old secret there and startup authserver once. Afterwards, you can re-
# comment that line and get rid of your old secret.
#
# Default: <blank> - (Store TOTP secrets unencrypted)
# Example: 000102030405060708090A0B0C0D0E0F
TOTPMasterSecret =
# TOTPOldMasterSecret =
#
###################################################################################################
###################################################################################################
#
# LOGGING SYSTEM SETTINGS

View File

@@ -26,7 +26,7 @@ void LoginDatabaseConnection::DoPrepareStatements()
PrepareStatement(LOGIN_SEL_SESSIONKEY, "SELECT a.session_key, a.id, aa.gmlevel FROM account a LEFT JOIN account_access aa ON (a.id = aa.id) WHERE username = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_UPD_LOGON, "UPDATE account SET salt = ?, verifier = ? 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.locked, a.lock_country, a.last_ip, aa.gmlevel, a.salt, a.verifier, a.token_key FROM account a LEFT JOIN account_access aa ON (a.id = aa.id) WHERE a.username = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_LOGONCHALLENGE, "SELECT a.id, a.locked, a.lock_country, a.last_ip, aa.gmlevel, a.salt, a.verifier, a.totp_secret FROM account a LEFT JOIN account_access aa ON (a.id = aa.id) WHERE a.username = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_LOGON_COUNTRY, "SELECT country FROM ip2nation WHERE ip < ? ORDER BY ip DESC LIMIT 0,1", CONNECTION_SYNCH);
PrepareStatement(LOGIN_UPD_FAILEDLOGINS, "UPDATE account SET failed_logins = failed_logins + 1 WHERE username = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_FAILEDLOGINS, "SELECT id, failed_logins FROM account WHERE username = ?", CONNECTION_SYNCH);
@@ -96,4 +96,12 @@ void LoginDatabaseConnection::DoPrepareStatements()
// DB logging
PrepareStatement(LOGIN_INS_LOG, "INSERT INTO logs (time, realm, type, level, string) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC);
// TOTP
PrepareStatement(LOGIN_SEL_SECRET_DIGEST, "SELECT digest FROM secret_digest WHERE id = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_INS_SECRET_DIGEST, "INSERT INTO secret_digest (id, digest) VALUES (?,?)", CONNECTION_ASYNC);
PrepareStatement(LOGIN_DEL_SECRET_DIGEST, "DELETE FROM secret_digest WHERE id = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_ACCOUNT_TOTP_SECRET, "SELECT totp_secret FROM account WHERE id = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_UPD_ACCOUNT_TOTP_SECRET, "UPDATE account SET totp_secret = ? WHERE id = ?", CONNECTION_ASYNC);
}

View File

@@ -113,6 +113,13 @@ enum LoginDatabaseStatements
LOGIN_INS_LOG,
LOGIN_SEL_SECRET_DIGEST,
LOGIN_INS_SECRET_DIGEST,
LOGIN_DEL_SECRET_DIGEST,
LOGIN_SEL_ACCOUNT_TOTP_SECRET,
LOGIN_UPD_ACCOUNT_TOTP_SECRET,
MAX_LOGINDATABASE_STATEMENTS
};

View File

@@ -76,7 +76,6 @@ void TotemAI::UpdateAI(uint32 /*diff*/)
me->VisitNearbyObject(max_range, checker);
}
if (!victim && me->GetCharmerOrOwnerOrSelf()->IsInCombat())
{
victim = me->GetCharmerOrOwnerOrSelf()->GetVictim();

View File

@@ -17,7 +17,6 @@
#include <map>
#include <vector>
enum RollType
{
ROLL_PASS = 0,

View File

@@ -94,7 +94,17 @@ enum AcoreStrings
LANG_RBAC_PERM_REVOKED_NOT_IN_LIST = 79,
LANG_PVPSTATS = 80,
LANG_PVPSTATS_DISABLED = 81,
// Free 82 - 95
// Free 82 - 86
LANG_UNKNOWN_ERROR = 87,
LANG_2FA_COMMANDS_NOT_SETUP = 88,
LANG_2FA_ALREADY_SETUP = 89,
LANG_2FA_INVALID_TOKEN = 90,
LANG_2FA_SECRET_SUGGESTION = 91,
LANG_2FA_SETUP_COMPLETE = 92,
LANG_2FA_NOT_SETUP = 93,
LANG_2FA_REMOVE_NEED_TOKEN = 94,
LANG_2FA_REMOVE_COMPLETE = 95,
LANG_GUILD_RENAME_ALREADY_EXISTS = 96,
LANG_GUILD_RENAME_DONE = 97,
@@ -191,7 +201,11 @@ enum AcoreStrings
LANG_GRID_POSITION = 178,
// 179-185 used in other client versions
LANG_TRANSPORT_POSITION = 186,
// Room for more level 1 187-199 not used
// 187
LANG_2FA_SECRET_TOO_LONG = 188,
LANG_2FA_SECRET_INVALID = 189,
LANG_2FA_SECRET_SET_COMPLETE = 190,
// free 191 - 199
// level 2 chat
LANG_NO_SELECTION = 200,

View File

@@ -3457,4 +3457,3 @@ void World::RemoveOldCorpses()
{
m_timers[WUPDATE_CORPSES].SetCurrent(m_timers[WUPDATE_CORPSES].GetInterval());
}

View File

@@ -12,10 +12,18 @@ Category: commandscripts
EndScriptData */
#include "AccountMgr.h"
#include "AES.h"
#include "Base32.h"
#include "Chat.h"
#include "CryptoGenerics.h"
#include "Language.h"
#include "Player.h"
#include "ScriptMgr.h"
#include "SecretMgr.h"
#include "StringConvert.h"
#include "TOTP.h"
#include <unordered_map>
#include <openssl/rand.h>
class account_commandscript : public CommandScript
{
@@ -26,33 +34,197 @@ public:
{
static std::vector<ChatCommand> accountSetCommandTable =
{
{ "addon", SEC_GAMEMASTER, true, &HandleAccountSetAddonCommand, "" },
{ "gmlevel", SEC_CONSOLE, true, &HandleAccountSetGmLevelCommand, "" },
{ "password", SEC_CONSOLE, true, &HandleAccountSetPasswordCommand, "" }
{ "addon", SEC_GAMEMASTER, true, &HandleAccountSetAddonCommand, "" },
{ "gmlevel", SEC_CONSOLE, true, &HandleAccountSetGmLevelCommand, "" },
{ "password", SEC_CONSOLE, true, &HandleAccountSetPasswordCommand, "" },
{ "2fa", SEC_PLAYER, true, &HandleAccountSet2FACommand, "" }
};
static std::vector<ChatCommand> accountLockCommandTable
{
{ "country", SEC_PLAYER, true, &HandleAccountLockCountryCommand, "" },
{ "ip", SEC_PLAYER, true, &HandleAccountLockIpCommand, "" }
{ "country", SEC_PLAYER, true, &HandleAccountLockCountryCommand, "" },
{ "ip", SEC_PLAYER, true, &HandleAccountLockIpCommand, "" }
};
static std::vector<ChatCommand> account2faCommandTable
{
{ "setup", SEC_PLAYER, false, &HandleAccount2FASetupCommand, "" },
{ "remove", SEC_PLAYER, false, &HandleAccount2FARemoveCommand, "" },
};
static std::vector<ChatCommand> accountCommandTable =
{
{ "addon", SEC_MODERATOR, false, &HandleAccountAddonCommand, "" },
{ "create", SEC_CONSOLE, true, &HandleAccountCreateCommand, "" },
{ "delete", SEC_CONSOLE, true, &HandleAccountDeleteCommand, "" },
{ "onlinelist", SEC_CONSOLE, true, &HandleAccountOnlineListCommand, "" },
{ "lock", SEC_PLAYER, false, nullptr, "", accountLockCommandTable },
{ "set", SEC_ADMINISTRATOR, true, nullptr, "", accountSetCommandTable },
{ "password", SEC_PLAYER, false, &HandleAccountPasswordCommand, "" },
{ "", SEC_PLAYER, false, &HandleAccountCommand, "" }
{ "2fa", SEC_PLAYER, true, nullptr, "", account2faCommandTable },
{ "addon", SEC_MODERATOR, false, &HandleAccountAddonCommand, "" },
{ "create", SEC_CONSOLE, true, &HandleAccountCreateCommand, "" },
{ "delete", SEC_CONSOLE, true, &HandleAccountDeleteCommand, "" },
{ "onlinelist", SEC_CONSOLE, true, &HandleAccountOnlineListCommand, "" },
{ "lock", SEC_PLAYER, false, nullptr, "", accountLockCommandTable },
{ "set", SEC_ADMINISTRATOR, true, nullptr, "", accountSetCommandTable },
{ "password", SEC_PLAYER, false, &HandleAccountPasswordCommand, "" },
{ "", SEC_PLAYER, false, &HandleAccountCommand, "" }
};
static std::vector<ChatCommand> commandTable =
{
{ "account", SEC_PLAYER, true, nullptr, "", accountCommandTable }
};
return commandTable;
}
static bool HandleAccount2FASetupCommand(ChatHandler* handler, char const* args)
{
if (!*args)
{
handler->SendSysMessage(LANG_CMD_SYNTAX);
handler->SetSentErrorMessage(true);
return false;
}
auto token = acore::StringTo<uint32>(args);
auto const& masterKey = sSecretMgr->GetSecret(SECRET_TOTP_MASTER_KEY);
if (!masterKey.IsAvailable())
{
handler->SendSysMessage(LANG_2FA_COMMANDS_NOT_SETUP);
handler->SetSentErrorMessage(true);
return false;
}
uint32 const accountId = handler->GetSession()->GetAccountId();
{ // check if 2FA already enabled
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_TOTP_SECRET);
stmt->setUInt32(0, accountId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (!result)
{
LOG_ERROR("misc", "Account %u not found in login database when processing .account 2fa setup command.", accountId);
handler->SendSysMessage(LANG_UNKNOWN_ERROR);
handler->SetSentErrorMessage(true);
return false;
}
if (!result->Fetch()->IsNull())
{
handler->SendSysMessage(LANG_2FA_ALREADY_SETUP);
handler->SetSentErrorMessage(true);
return false;
}
}
// store random suggested secrets
static std::unordered_map<uint32, acore::Crypto::TOTP::Secret> suggestions;
auto pair = suggestions.emplace(std::piecewise_construct, std::make_tuple(accountId), std::make_tuple(acore::Crypto::TOTP::RECOMMENDED_SECRET_LENGTH)); // std::vector 1-argument size_t constructor invokes resize
if (pair.second) // no suggestion yet, generate random secret
acore::Crypto::GetRandomBytes(pair.first->second);
if (!pair.second && token) // suggestion already existed and token specified - validate
{
if (acore::Crypto::TOTP::ValidateToken(pair.first->second, *token))
{
if (masterKey)
acore::Crypto::AEEncryptWithRandomIV<acore::Crypto::AES>(pair.first->second, *masterKey);
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_TOTP_SECRET);
stmt->setBinary(0, pair.first->second);
stmt->setUInt32(1, accountId);
LoginDatabase.Execute(stmt);
suggestions.erase(pair.first);
handler->SendSysMessage(LANG_2FA_SETUP_COMPLETE);
return true;
}
else
handler->SendSysMessage(LANG_2FA_INVALID_TOKEN);
}
// new suggestion, or no token specified, output TOTP parameters
handler->PSendSysMessage(LANG_2FA_SECRET_SUGGESTION, acore::Encoding::Base32::Encode(pair.first->second).c_str());
handler->SetSentErrorMessage(true);
return false;
}
static bool HandleAccount2FARemoveCommand(ChatHandler* handler, char const* args)
{
if (!*args)
{
handler->SendSysMessage(LANG_CMD_SYNTAX);
handler->SetSentErrorMessage(true);
return false;
}
auto token = acore::StringTo<uint32>(args);
auto const& masterKey = sSecretMgr->GetSecret(SECRET_TOTP_MASTER_KEY);
if (!masterKey.IsAvailable())
{
handler->SendSysMessage(LANG_2FA_COMMANDS_NOT_SETUP);
handler->SetSentErrorMessage(true);
return false;
}
uint32 const accountId = handler->GetSession()->GetAccountId();
acore::Crypto::TOTP::Secret secret;
{ // get current TOTP secret
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_TOTP_SECRET);
stmt->setUInt32(0, accountId);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (!result)
{
LOG_ERROR("misc", "Account %u not found in login database when processing .account 2fa setup command.", accountId);
handler->SendSysMessage(LANG_UNKNOWN_ERROR);
handler->SetSentErrorMessage(true);
return false;
}
Field* field = result->Fetch();
if (field->IsNull())
{ // 2FA not enabled
handler->SendSysMessage(LANG_2FA_NOT_SETUP);
handler->SetSentErrorMessage(true);
return false;
}
secret = field->GetBinary();
}
if (token)
{
if (masterKey)
{
bool success = acore::Crypto::AEDecrypt<acore::Crypto::AES>(secret, *masterKey);
if (!success)
{
LOG_ERROR("misc", "Account %u has invalid ciphertext in TOTP token.", accountId);
handler->SendSysMessage(LANG_UNKNOWN_ERROR);
handler->SetSentErrorMessage(true);
return false;
}
}
if (acore::Crypto::TOTP::ValidateToken(secret, *token))
{
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_TOTP_SECRET);
stmt->setNull(0);
stmt->setUInt32(1, accountId);
LoginDatabase.Execute(stmt);
handler->SendSysMessage(LANG_2FA_REMOVE_COMPLETE);
return true;
}
else
handler->SendSysMessage(LANG_2FA_INVALID_TOKEN);
}
handler->SendSysMessage(LANG_2FA_REMOVE_NEED_TOKEN);
handler->SetSentErrorMessage(true);
return false;
}
static bool HandleAccountAddonCommand(ChatHandler* handler, char const* args)
{
if (!*args)
@@ -385,6 +557,91 @@ public:
return true;
}
static bool HandleAccountSet2FACommand(ChatHandler* handler, char const* args)
{
if (!*args)
{
handler->SendSysMessage(LANG_CMD_SYNTAX);
handler->SetSentErrorMessage(true);
return false;
}
char* _account = strtok((char*)args, " ");
char* _secret = strtok(nullptr, " ");
if (!_account || !_secret)
{
handler->SendSysMessage(LANG_CMD_SYNTAX);
handler->SetSentErrorMessage(true);
return false;
}
std::string accountName = _account;
std::string secret = _secret;
if (!Utf8ToUpperOnlyLatin(accountName))
{
handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str());
handler->SetSentErrorMessage(true);
return false;
}
uint32 targetAccountId = AccountMgr::GetId(accountName);
if (!targetAccountId)
{
handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str());
handler->SetSentErrorMessage(true);
return false;
}
if (handler->HasLowerSecurityAccount(nullptr, targetAccountId, true))
return false;
if (secret == "off")
{
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_TOTP_SECRET);
stmt->setNull(0);
stmt->setUInt32(1, targetAccountId);
LoginDatabase.Execute(stmt);
handler->PSendSysMessage(LANG_2FA_REMOVE_COMPLETE);
return true;
}
auto const& masterKey = sSecretMgr->GetSecret(SECRET_TOTP_MASTER_KEY);
if (!masterKey.IsAvailable())
{
handler->SendSysMessage(LANG_2FA_COMMANDS_NOT_SETUP);
handler->SetSentErrorMessage(true);
return false;
}
Optional<std::vector<uint8>> decoded = acore::Encoding::Base32::Decode(secret);
if (!decoded)
{
handler->SendSysMessage(LANG_2FA_SECRET_INVALID);
handler->SetSentErrorMessage(true);
return false;
}
if (128 < (decoded->size() + acore::Crypto::AES::IV_SIZE_BYTES + acore::Crypto::AES::TAG_SIZE_BYTES))
{
handler->SendSysMessage(LANG_2FA_SECRET_TOO_LONG);
handler->SetSentErrorMessage(true);
return false;
}
if (masterKey)
acore::Crypto::AEEncryptWithRandomIV<acore::Crypto::AES>(*decoded, *masterKey);
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_TOTP_SECRET);
stmt->setBinary(0, *decoded);
stmt->setUInt32(1, targetAccountId);
LoginDatabase.Execute(stmt);
handler->PSendSysMessage(LANG_2FA_SECRET_SET_COMPLETE, accountName.c_str());
return true;
}
static bool HandleAccountCommand(ChatHandler* handler, char const* /*args*/)
{
AccountTypes gmLevel = handler->GetSession()->GetSecurity();

View File

@@ -629,7 +629,6 @@ public:
}
};
void AddSC_instance_scholomance()
{
new instance_scholomance();

View File

@@ -0,0 +1,226 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
* Copyright (C) 2021+ WarheadCore <https://github.com/WarheadCore>
*/
#include "SecretMgr.h"
#include "AES.h"
#include "Argon2.h"
#include "Config.h"
#include "CryptoGenerics.h"
#include "DatabaseEnv.h"
#include "Errors.h"
#include "Log.h"
#include "SharedDefines.h"
#include <functional>
#include <unordered_map>
#define SECRET_FLAG_FOR(key, val, server) server ## _ ## key = (val ## ull << (16*SERVER_PROCESS_ ## server))
#define SECRET_FLAG(key, val) SECRET_FLAG_ ## key = val, SECRET_FLAG_FOR(key, val, AUTHSERVER), SECRET_FLAG_FOR(key, val, WORLDSERVER)
enum SecretFlags : uint64
{
SECRET_FLAG(DEFER_LOAD, 0x1)
};
#undef SECRET_FLAG_FOR
#undef SECRET_FLAG
struct SecretInfo
{
char const* configKey;
char const* oldKey;
int bits;
ServerProcessTypes owner;
uint64 _flags;
uint16 flags() const { return static_cast<uint16>(_flags >> (16*THIS_SERVER_PROCESS)); }
};
static constexpr SecretInfo secret_info[NUM_SECRETS] =
{
{ "TOTPMasterSecret", "TOTPOldMasterSecret", 128, SERVER_PROCESS_AUTHSERVER, WORLDSERVER_DEFER_LOAD }
};
/*static*/ SecretMgr* SecretMgr::instance()
{
static SecretMgr instance;
return &instance;
}
static Optional<BigNumber> GetHexFromConfig(char const* configKey, int bits)
{
ASSERT(bits > 0);
std::string str = sConfigMgr->GetOption<std::string>(configKey, "");
if (str.empty())
return {};
BigNumber secret;
if (!secret.SetHexStr(str.c_str()))
{
LOG_FATAL("server.loading", "Invalid value for '%s' - specify a hexadecimal integer of up to %d bits with no prefix.", configKey, bits);
ABORT();
}
BigNumber threshold(2);
threshold <<= bits;
if (!((BigNumber(0) <= secret) && (secret < threshold)))
{
LOG_ERROR("server.loading", "Value for '%s' is out of bounds (should be an integer of up to %d bits with no prefix). Truncated to %d bits.", configKey, bits, bits);
secret %= threshold;
}
ASSERT(((BigNumber(0) <= secret) && (secret < threshold)));
return secret;
}
void SecretMgr::Initialize()
{
for (uint32 i = 0; i < NUM_SECRETS; ++i)
{
if (secret_info[i].flags() & SECRET_FLAG_DEFER_LOAD)
continue;
std::unique_lock<std::mutex> lock(_secrets[i].lock);
AttemptLoad(Secrets(i), LogLevel::LOG_LEVEL_FATAL, lock);
if (!_secrets[i].IsAvailable())
ABORT(); // load failed
}
}
SecretMgr::Secret const& SecretMgr::GetSecret(Secrets i)
{
std::unique_lock<std::mutex> lock(_secrets[i].lock);
if (_secrets[i].state == Secret::NOT_LOADED_YET)
AttemptLoad(i, LogLevel::LOG_LEVEL_ERROR, lock);
return _secrets[i];
}
void SecretMgr::AttemptLoad(Secrets i, LogLevel errorLevel, std::unique_lock<std::mutex> const&)
{
auto const& info = secret_info[i];
Optional<std::string> oldDigest;
{
auto* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_SECRET_DIGEST);
stmt->setUInt32(0, i);
PreparedQueryResult result = LoginDatabase.Query(stmt);
if (result)
oldDigest = result->Fetch()->GetString();
}
Optional<BigNumber> currentValue = GetHexFromConfig(info.configKey, info.bits);
// verify digest
if (
((!oldDigest) != (!currentValue)) || // there is an old digest, but no current secret (or vice versa)
(oldDigest && !acore::Crypto::Argon2::Verify(currentValue->AsHexStr(), *oldDigest)) // there is an old digest, and the current secret does not match it
)
{
if (info.owner != THIS_SERVER_PROCESS)
{
if (currentValue)
LOG_MESSAGE_BODY("server.loading", errorLevel, "Invalid value for '%s' specified - this is not actually the secret being used in your auth DB.", info.configKey);
else
LOG_MESSAGE_BODY("server.loading", errorLevel, "No value for '%s' specified - please specify the secret currently being used in your auth DB.", info.configKey);
_secrets[i].state = Secret::LOAD_FAILED;
return;
}
Optional<BigNumber> oldSecret;
if (oldDigest && info.oldKey) // there is an old digest, so there might be an old secret (if possible)
{
oldSecret = GetHexFromConfig(info.oldKey, info.bits);
if (oldSecret && !acore::Crypto::Argon2::Verify(oldSecret->AsHexStr(), *oldDigest))
{
LOG_MESSAGE_BODY("server.loading", errorLevel, "Invalid value for '%s' specified - this is not actually the secret previously used in your auth DB.", info.oldKey);
_secrets[i].state = Secret::LOAD_FAILED;
return;
}
}
// attempt to transition us to the new key, if possible
Optional<std::string> error = AttemptTransition(Secrets(i), currentValue, oldSecret, !!oldDigest);
if (error)
{
LOG_MESSAGE_BODY("server.loading", errorLevel, "Your value of '%s' changed, but we cannot transition your database to the new value:\n%s", info.configKey, error->c_str());
_secrets[i].state = Secret::LOAD_FAILED;
return;
}
LOG_INFO("server.loading", "Successfully transitioned database to new '%s' value.", info.configKey);
}
if (currentValue)
{
_secrets[i].state = Secret::PRESENT;
_secrets[i].value = *currentValue;
}
else
_secrets[i].state = Secret::NOT_PRESENT;
}
Optional<std::string> SecretMgr::AttemptTransition(Secrets i, Optional<BigNumber> const& newSecret, Optional<BigNumber> const& oldSecret, bool hadOldSecret) const
{
auto trans = LoginDatabase.BeginTransaction();
switch (i)
{
case SECRET_TOTP_MASTER_KEY:
{
QueryResult result = LoginDatabase.Query("SELECT id, totp_secret FROM account");
if (result) do
{
Field* fields = result->Fetch();
if (fields[1].IsNull())
continue;
uint32 id = fields[0].GetUInt32();
std::vector<uint8> totpSecret = fields[1].GetBinary();
if (hadOldSecret)
{
if (!oldSecret)
return acore::StringFormat("Cannot decrypt old TOTP tokens - add config key '%s' to authserver.conf!", secret_info[i].oldKey);
bool success = acore::Crypto::AEDecrypt<acore::Crypto::AES>(totpSecret, oldSecret->ToByteArray<acore::Crypto::AES::KEY_SIZE_BYTES>());
if (!success)
return acore::StringFormat("Cannot decrypt old TOTP tokens - value of '%s' is incorrect for some users!", secret_info[i].oldKey);
}
if (newSecret)
acore::Crypto::AEEncryptWithRandomIV<acore::Crypto::AES>(totpSecret, newSecret->ToByteArray<acore::Crypto::AES::KEY_SIZE_BYTES>());
auto* updateStmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_TOTP_SECRET);
updateStmt->setBinary(0, totpSecret);
updateStmt->setUInt32(1, id);
trans->Append(updateStmt);
} while (result->NextRow());
break;
}
default:
return std::string("Unknown secret index - huh?");
}
if (hadOldSecret)
{
auto* deleteStmt = LoginDatabase.GetPreparedStatement(LOGIN_DEL_SECRET_DIGEST);
deleteStmt->setUInt32(0, i);
trans->Append(deleteStmt);
}
if (newSecret)
{
BigNumber salt;
salt.SetRand(128);
Optional<std::string> hash = acore::Crypto::Argon2::Hash(newSecret->AsHexStr(), salt);
if (!hash)
return std::string("Failed to hash new secret");
auto* insertStmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_SECRET_DIGEST);
insertStmt->setUInt32(0, i);
insertStmt->setString(1, *hash);
trans->Append(insertStmt);
}
LoginDatabase.CommitTransaction(trans);
return {};
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
* Copyright (C) 2021+ WarheadCore <https://github.com/WarheadCore>
*/
#ifndef __WARHEAD_SECRETMGR_H__
#define __WARHEAD_SECRETMGR_H__
#include "BigNumber.h"
#include "Common.h"
#include "Optional.h"
#include "Log.h"
#include <array>
#include <mutex>
#include <string>
enum Secrets : uint32
{
SECRET_TOTP_MASTER_KEY = 0,
// only add new indices right above this line
NUM_SECRETS
};
class AC_SHARED_API SecretMgr
{
private:
SecretMgr() {}
~SecretMgr() {}
public:
SecretMgr(SecretMgr const&) = delete;
static SecretMgr* instance();
struct Secret
{
public:
explicit operator bool() const { return (state == PRESENT); }
BigNumber const& operator*() const { return value; }
BigNumber const* operator->() const { return &value; }
bool IsAvailable() const { return (state != NOT_LOADED_YET) && (state != LOAD_FAILED); }
private:
std::mutex lock;
enum { NOT_LOADED_YET, LOAD_FAILED, NOT_PRESENT, PRESENT } state = NOT_LOADED_YET;
BigNumber value;
friend class SecretMgr;
};
void Initialize();
Secret const& GetSecret(Secrets i);
private:
void AttemptLoad(Secrets i, LogLevel errorLevel, std::unique_lock<std::mutex> const&);
Optional<std::string> AttemptTransition(Secrets i, Optional<BigNumber> const& newSecret, Optional<BigNumber> const& oldSecret, bool hadOldSecret) const;
std::array<Secret, NUM_SECRETS> _secrets;
};
#define sSecretMgr SecretMgr::instance()
#endif

View File

@@ -0,0 +1,8 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
* Copyright (C) 2021+ WarheadCore <https://github.com/WarheadCore>
*/
#include "SharedDefines.h"
ServerProcessTypes acore::Impl::CurrentServerProcessHolder::_type = NUM_SERVER_PROCESS_TYPES;

View File

@@ -3567,4 +3567,23 @@ enum PartyResult
ERR_PARTY_LFG_TELEPORT_IN_COMBAT = 30
};
enum ServerProcessTypes
{
SERVER_PROCESS_AUTHSERVER = 0,
SERVER_PROCESS_WORLDSERVER = 1,
NUM_SERVER_PROCESS_TYPES
};
namespace acore::Impl
{
struct AC_SHARED_API CurrentServerProcessHolder
{
static ServerProcessTypes type() { return _type; }
static ServerProcessTypes _type;
};
}
#define THIS_SERVER_PROCESS (acore::Impl::CurrentServerProcessHolder::type())
#endif

View File

@@ -15,6 +15,7 @@
#include "Database/DatabaseEnv.h"
#include "Log.h"
#include "Master.h"
#include "SharedDefines.h"
#include <ace/Version.h>
#include <openssl/crypto.h>
#include <openssl/opensslv.h>
@@ -56,6 +57,8 @@ void usage(const char* prog)
/// Launch the Trinity server
extern int main(int argc, char** argv)
{
acore::Impl::CurrentServerProcessHolder::_type = SERVER_PROCESS_WORLDSERVER;
///- Command line parsing to get the configuration file name
std::string configFile = sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG);
int c = 1;

View File

@@ -30,6 +30,7 @@
#include "WorldSocket.h"
#include "WorldSocketMgr.h"
#include "DatabaseLoader.h"
#include "SecretMgr.h"
#include <ace/Sig_Handler.h>
#ifdef _WIN32
@@ -136,6 +137,7 @@ int Master::Run()
sConfigMgr->LoadModulesConfigs();
///- Initialize the World
sSecretMgr->Initialize();
sWorld->SetInitialWorldSettings();
sScriptMgr->OnStartup();

View File

@@ -11,6 +11,7 @@
# PERFORMANCE SETTINGS
# SERVER LOGGING
# SERVER SETTINGS
# CRYPTOGRAPHY
# WARDEN SETTINGS
# PLAYER INTERACTION
# CREATURE SETTINGS
@@ -1215,6 +1216,25 @@ IsPreloadedContinentTransport.Enabled = 0
#
###################################################################################################
###################################################################################################
# CRYPTOGRAPHY
#
# TOTPMasterSecret
# Description: The key used by authserver to decrypt TOTP secrets from database storage.
# You only need to set this here if you plan to use the in-game 2FA
# management commands (.account 2fa), otherwise this can be left blank.
#
# The server will auto-detect if this does not match your authserver setting,
# in which case any commands reliant on the secret will be disabled.
#
# Default: <blank>
#
TOTPMasterSecret =
#
###################################################################################################
###################################################################################################
# WARDEN SETTINGS
#