mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-02-16 16:56:07 +00:00
feat(Core/Authserver): TOTP rewrite (#5620)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ void TotemAI::UpdateAI(uint32 /*diff*/)
|
||||
me->VisitNearbyObject(max_range, checker);
|
||||
}
|
||||
|
||||
|
||||
if (!victim && me->GetCharmerOrOwnerOrSelf()->IsInCombat())
|
||||
{
|
||||
victim = me->GetCharmerOrOwnerOrSelf()->GetVictim();
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
|
||||
enum RollType
|
||||
{
|
||||
ROLL_PASS = 0,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3457,4 +3457,3 @@ void World::RemoveOldCorpses()
|
||||
{
|
||||
m_timers[WUPDATE_CORPSES].SetCurrent(m_timers[WUPDATE_CORPSES].GetInterval());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -629,7 +629,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void AddSC_instance_scholomance()
|
||||
{
|
||||
new instance_scholomance();
|
||||
|
||||
226
src/server/shared/Secrets/SecretMgr.cpp
Normal file
226
src/server/shared/Secrets/SecretMgr.cpp
Normal 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 {};
|
||||
}
|
||||
63
src/server/shared/Secrets/SecretMgr.h
Normal file
63
src/server/shared/Secrets/SecretMgr.h
Normal 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
|
||||
8
src/server/shared/SharedDefines.cpp
Normal file
8
src/server/shared/SharedDefines.cpp
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user