diff options
| author | Treeston <treeston.mmoc@gmail.com> | 2019-08-10 21:34:51 +0200 |
|---|---|---|
| committer | Shauren <shauren.trinity@gmail.com> | 2021-12-18 20:24:50 +0100 |
| commit | 0b61c3b7b1399f5dd0cab90da36002b7d8e0af6b (patch) | |
| tree | d869f832263c29814004569c1353251fc8e8538c /src/server | |
| parent | 92e92e818b704803377f44dbc6a8158b3d38225b (diff) | |
[3.3.5] Core/Authserver: TOTP rewrite: (PR #23633)
- Proper management commands (.account 2fa)
- Secrets can now be encrypted (set TOTPTokenSecret in .conf)
- Secret now stored in binary
- Argon2 and AES primitives
- Base32/64 support
(cherry picked from commit 4211645834c467a03c60248e80818d3607be9ea7)
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/bnetserver/Main.cpp | 3 | ||||
| -rw-r--r-- | src/server/bnetserver/bnetserver.conf.dist | 19 | ||||
| -rw-r--r-- | src/server/database/Database/Implementation/LoginDatabase.cpp | 7 | ||||
| -rw-r--r-- | src/server/database/Database/Implementation/LoginDatabase.h | 7 | ||||
| -rw-r--r-- | src/server/scripts/Commands/cs_account.cpp | 209 | ||||
| -rw-r--r-- | src/server/shared/Secrets/SecretMgr.cpp | 239 | ||||
| -rw-r--r-- | src/server/shared/Secrets/SecretMgr.h | 85 | ||||
| -rw-r--r-- | src/server/worldserver/Main.cpp | 3 | ||||
| -rw-r--r-- | src/server/worldserver/worldserver.conf.dist | 20 |
9 files changed, 592 insertions, 0 deletions
diff --git a/src/server/bnetserver/Main.cpp b/src/server/bnetserver/Main.cpp index c4d87548fc4..42a85e4f8ff 100644 --- a/src/server/bnetserver/Main.cpp +++ b/src/server/bnetserver/Main.cpp @@ -36,6 +36,7 @@ #include "MySQLThreading.h" #include "ProcessPriority.h" #include "RealmList.h" +#include "SecretMgr.h" #include "SessionManager.h" #include "SslContext.h" #include "Util.h" @@ -156,6 +157,8 @@ int main(int argc, char** argv) if (!StartDB()) return 1; + sSecretMgr->Initialize(SECRET_OWNER_BNETSERVER); + sSessionMgr.FixLegacyAuthHashes(); // Load IP Location Database diff --git a/src/server/bnetserver/bnetserver.conf.dist b/src/server/bnetserver/bnetserver.conf.dist index 367c0f3c326..37e92682429 100644 --- a/src/server/bnetserver/bnetserver.conf.dist +++ b/src/server/bnetserver/bnetserver.conf.dist @@ -9,6 +9,7 @@ # EXAMPLE CONFIG # AUTH SERVER SETTINGS # MYSQL SETTINGS +# CRYPTOGRAPHY # UPDATE SETTINGS # LOGGING SYSTEM SETTINGS # @@ -244,6 +245,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 = + +# +################################################################################################### + +################################################################################################### # UPDATE SETTINGS # # Updates.EnableDatabases diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index 57c777f5c15..f5a6c4aab81 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -113,6 +113,13 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_SEL_ACCOUNT_MUTE_INFO, "SELECT mutedate, mutetime, mutereason, mutedby FROM account_muted WHERE guid = ? ORDER BY mutedate ASC", CONNECTION_SYNCH); PrepareStatement(LOGIN_DEL_ACCOUNT_MUTED, "DELETE FROM account_muted WHERE guid = ?", CONNECTION_ASYNC); + 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); + #define BnetAccountInfo "ba.id, UPPER(ba.email), ba.locked, ba.lock_country, ba.last_ip, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, bab.unbandate = bab.bandate" #define BnetGameAccountInfo "a.id, a.username, ab.unbandate, ab.unbandate = ab.bandate, aa.SecurityLevel" diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index e18a3de612e..1f0d9ff6e52 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -109,6 +109,13 @@ enum LoginDatabaseStatements : uint32 LOGIN_SEL_ACCOUNT_MUTE_INFO, LOGIN_DEL_ACCOUNT_MUTED, + LOGIN_SEL_SECRET_DIGEST, + LOGIN_INS_SECRET_DIGEST, + LOGIN_DEL_SECRET_DIGEST, + + LOGIN_SEL_ACCOUNT_TOTP_SECRET, + LOGIN_UPD_ACCOUNT_TOTP_SECRET, + LOGIN_SEL_BNET_AUTHENTICATION, LOGIN_UPD_BNET_AUTHENTICATION, LOGIN_SEL_BNET_EXISTING_AUTHENTICATION, diff --git a/src/server/scripts/Commands/cs_account.cpp b/src/server/scripts/Commands/cs_account.cpp index 9c4503560c2..c4a68da5401 100644 --- a/src/server/scripts/Commands/cs_account.cpp +++ b/src/server/scripts/Commands/cs_account.cpp @@ -23,7 +23,10 @@ Category: commandscripts EndScriptData */ #include "AccountMgr.h" +#include "AES.h" +#include "Base32.h" #include "Chat.h" +#include "CryptoGenerics.h" #include "DatabaseEnv.h" #include "IpAddress.h" #include "IPLocation.h" @@ -31,8 +34,12 @@ EndScriptData */ #include "Log.h" #include "Player.h" #include "ScriptMgr.h" +#include "SecretMgr.h" +#include "TOTP.h" #include "World.h" #include "WorldSession.h" +#include <unordered_map> +#include <openssl/rand.h> using namespace Trinity::ChatCommands; @@ -55,6 +62,12 @@ public: { "gmlevel", rbac::RBAC_PERM_COMMAND_ACCOUNT_SET_SECLEVEL, true, &HandleAccountSetSecLevelCommand, "" }, // temp for a transition period { "seclevel", rbac::RBAC_PERM_COMMAND_ACCOUNT_SET_SECLEVEL, true, &HandleAccountSetSecLevelCommand, "" }, { "password", rbac::RBAC_PERM_COMMAND_ACCOUNT_SET_PASSWORD, true, &HandleAccountSetPasswordCommand, "" }, + { "2fa", rbac::RBAC_PERM_COMMAND_ACCOUNT_SET_2FA, true, &HandleAccountSet2FACommand, "" }, + }; + static std::vector<ChatCommand> account2FACommandTable = + { + { "setup", rbac::RBAC_PERM_COMMAND_ACCOUNT_2FA_SETUP, false, &HandleAccount2FASetupCommand, "" }, + { "remove", rbac::RBAC_PERM_COMMAND_ACCOUNT_2FA_REMOVE, false, &HandleAccount2FARemoveCommand, "" }, }; static std::vector<ChatCommand> accountLockCommandTable = { @@ -63,6 +76,7 @@ public: }; static std::vector<ChatCommand> accountCommandTable = { + { "2fa", rbac::RBAC_PERM_COMMAND_ACCOUNT_2FA, false, nullptr, "", account2FACommandTable }, { "addon", rbac::RBAC_PERM_COMMAND_ACCOUNT_ADDON, false, &HandleAccountAddonCommand, "" }, { "create", rbac::RBAC_PERM_COMMAND_ACCOUNT_CREATE, true, &HandleAccountCreateCommand, "" }, { "delete", rbac::RBAC_PERM_COMMAND_ACCOUNT_DELETE, true, &HandleAccountDeleteCommand, "" }, @@ -80,6 +94,138 @@ public: return commandTable; } + static bool HandleAccount2FASetupCommand(ChatHandler* handler, Optional<uint32> token) + { + 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 + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_TOTP_SECRET); + stmt->setUInt32(0, accountId); + PreparedQueryResult result = LoginDatabase.Query(stmt); + + if (!result) + { + TC_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, Trinity::Crypto::TOTP::Secret> suggestions; + auto pair = suggestions.emplace(std::piecewise_construct, std::make_tuple(accountId), std::make_tuple(Trinity::Crypto::TOTP::RECOMMENDED_SECRET_LENGTH)); // std::vector 1-argument size_t constructor invokes resize + if (pair.second) // no suggestion yet, generate random secret + RAND_bytes(pair.first->second.data(), pair.first->second.size()); + + if (!pair.second && token) // suggestion already existed and token specified - validate + { + if (Trinity::Crypto::TOTP::ValidateToken(pair.first->second, *token)) + { + if (masterKey) + Trinity::Crypto::AEEncryptWithRandomIV<Trinity::Crypto::AES>(pair.first->second, *masterKey); + + LoginDatabasePreparedStatement* 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, Trinity::Encoding::Base32::Encode(pair.first->second)); + handler->SetSentErrorMessage(true); + return false; + } + + static bool HandleAccount2FARemoveCommand(ChatHandler* handler, Optional<uint32> token) + { + 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(); + Trinity::Crypto::TOTP::Secret secret; + { // get current TOTP secret + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_TOTP_SECRET); + stmt->setUInt32(0, accountId); + PreparedQueryResult result = LoginDatabase.Query(stmt); + + if (!result) + { + TC_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 = Trinity::Crypto::AEDecrypt<Trinity::Crypto::AES>(secret, *masterKey); + if (!success) + { + TC_LOG_ERROR("misc", "Account %u has invalid ciphertext in TOTP token.", accountId); + handler->SendSysMessage(LANG_UNKNOWN_ERROR); + handler->SetSentErrorMessage(true); + return false; + } + } + + if (Trinity::Crypto::TOTP::ValidateToken(secret, *token)) + { + LoginDatabasePreparedStatement* 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, uint8 expansion) { if (expansion > sWorld->getIntConfig(CONFIG_EXPANSION)) @@ -657,6 +803,69 @@ public: return true; } + static bool HandleAccountSet2FACommand(ChatHandler* handler, std::string accountName, std::string 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") + { + LoginDatabasePreparedStatement* 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 = Trinity::Encoding::Base32::Decode(secret); + if (!decoded) + { + handler->SendSysMessage(LANG_2FA_SECRET_INVALID); + handler->SetSentErrorMessage(true); + return false; + } + if (128 < (decoded->size() + Trinity::Crypto::AES::IV_SIZE_BYTES + Trinity::Crypto::AES::TAG_SIZE_BYTES)) + { + handler->SendSysMessage(LANG_2FA_SECRET_TOO_LONG); + handler->SetSentErrorMessage(true); + return false; + } + + if (masterKey) + Trinity::Crypto::AEEncryptWithRandomIV<Trinity::Crypto::AES>(*decoded, *masterKey); + + LoginDatabasePreparedStatement* 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; + } + /// Set normal email for account static bool HandleAccountSetEmailCommand(ChatHandler* handler, std::string accountName, std::string const& email, std::string const& confirmEmail) { diff --git a/src/server/shared/Secrets/SecretMgr.cpp b/src/server/shared/Secrets/SecretMgr.cpp new file mode 100644 index 00000000000..de3794a148e --- /dev/null +++ b/src/server/shared/Secrets/SecretMgr.cpp @@ -0,0 +1,239 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 "SecretMgr.h" +#include "AES.h" +#include "Argon2.h" +#include "Config.h" +#include "CryptoGenerics.h" +#include "DatabaseEnv.h" +#include "Errors.h" +#include "Log.h" +#include <functional> +#include <unordered_map> + +#define SECRET_FLAG_FOR(key, val, server) server ## _ ## key = (val ## ull << (16*SECRET_OWNER_ ## server)) +#define SECRET_FLAG(key, val) SECRET_FLAG_ ## key = val, SECRET_FLAG_FOR(key, val, BNETSERVER), 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; + SecretOwner owner; + uint64 _flags; + uint16 flags() const { return static_cast<uint16>(_flags >> (16*SecretMgr::OWNER)); } +}; + +static constexpr SecretInfo secret_info[NUM_SECRETS] = +{ + { "TOTPMasterSecret", "TOTPOldMasterSecret", 128, SECRET_OWNER_BNETSERVER, WORLDSERVER_DEFER_LOAD } +}; + +SecretOwner SecretMgr::OWNER; + +/*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->GetStringDefault(configKey, ""); + if (str.empty()) + return {}; + + BigNumber secret; + if (!secret.SetHexStr(str.c_str())) + { + TC_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))) + { + TC_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(SecretOwner owner) +{ + OWNER = owner; + + 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), 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, 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; + { + LoginDatabasePreparedStatement* 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 && !Trinity::Crypto::Argon2::Verify(currentValue->AsHexStr(), *oldDigest)) // there is an old digest, and the current secret does not match it + ) + { + if (info.owner != OWNER) + { + if (currentValue) + TC_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 + TC_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 && !Trinity::Crypto::Argon2::Verify(oldSecret->AsHexStr(), *oldDigest)) + { + TC_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) + { + TC_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; + } + + TC_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 +{ + LoginDatabaseTransaction 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 Trinity::StringFormat("Cannot decrypt old TOTP tokens - add config key '%s' to authserver.conf!", secret_info[i].oldKey); + + bool success = Trinity::Crypto::AEDecrypt<Trinity::Crypto::AES>(totpSecret, oldSecret->ToByteArray<Trinity::Crypto::AES::KEY_SIZE_BYTES>()); + if (!success) + return Trinity::StringFormat("Cannot decrypt old TOTP tokens - value of '%s' is incorrect for some users!", secret_info[i].oldKey); + } + + if (newSecret) + Trinity::Crypto::AEEncryptWithRandomIV<Trinity::Crypto::AES>(totpSecret, newSecret->ToByteArray<Trinity::Crypto::AES::KEY_SIZE_BYTES>()); + + LoginDatabasePreparedStatement* 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) + { + LoginDatabasePreparedStatement* 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 = Trinity::Crypto::Argon2::Hash(newSecret->AsHexStr(), salt); + if (!hash) + return std::string("Failed to hash new secret"); + + LoginDatabasePreparedStatement* insertStmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_SECRET_DIGEST); + insertStmt->setUInt32(0, i); + insertStmt->setString(1, *hash); + trans->Append(insertStmt); + } + + LoginDatabase.CommitTransaction(trans); + return {}; +} diff --git a/src/server/shared/Secrets/SecretMgr.h b/src/server/shared/Secrets/SecretMgr.h new file mode 100644 index 00000000000..fdf6f32cca9 --- /dev/null +++ b/src/server/shared/Secrets/SecretMgr.h @@ -0,0 +1,85 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * 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 __TRINITY_SECRETMGR_H__ +#define __TRINITY_SECRETMGR_H__ + +#include "BigNumber.h" +#include "Common.h" +#include "LogCommon.h" +#include "Optional.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 +}; + +enum SecretOwner +{ + SECRET_OWNER_BNETSERVER, + SECRET_OWNER_WORLDSERVER, + + NUM_SECRET_OWNERS +}; + +class TC_SHARED_API SecretMgr +{ + private: + SecretMgr() {} + ~SecretMgr() {} + + public: + static SecretOwner OWNER; + + 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(SecretOwner owner); + 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 diff --git a/src/server/worldserver/Main.cpp b/src/server/worldserver/Main.cpp index 2f6cf319c8a..c24d60eef54 100644 --- a/src/server/worldserver/Main.cpp +++ b/src/server/worldserver/Main.cpp @@ -45,6 +45,8 @@ #include "ScriptLoader.h" #include "ScriptMgr.h" #include "ScriptReloadMgr.h" +#include "SecretMgr.h" +#include "SharedDefines.h" #include "TCSoap.h" #include "World.h" #include "WorldSocket.h" @@ -256,6 +258,7 @@ extern int main(int argc, char** argv) }); // Initialize the World + sSecretMgr->Initialize(SECRET_OWNER_WORLDSERVER); sWorld->SetInitialWorldSettings(); std::shared_ptr<void> mapManagementHandle(nullptr, [](void*) diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 138816786ab..3a64aedf766 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -11,6 +11,7 @@ # PERFORMANCE SETTINGS # SERVER LOGGING # SERVER SETTINGS +# CRYPTOGRAPHY # UPDATE SETTINGS # HOTSWAP SETTINGS # WARDEN SETTINGS @@ -1348,6 +1349,25 @@ FeatureSystem.CharacterUndelete.Cooldown = 2592000 ################################################################################################### ################################################################################################### +# 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 = + +# +################################################################################################### + +################################################################################################### # UPDATE SETTINGS # # Updates.EnableDatabases |
