diff options
| author | Treeston <treeston.mmoc@gmail.com> | 2019-08-10 21:34:51 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-08-10 21:34:51 +0200 |
| commit | 4211645834c467a03c60248e80818d3607be9ea7 (patch) | |
| tree | 673a1695581503b6ea3e49da5c3e0d06bf5d892e /src/server | |
| parent | 3d356b97d4cc4c7ec4c641487241eae6dcc0558e (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
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/authserver/Authentication/TOTP.cpp | 95 | ||||
| -rw-r--r-- | src/server/authserver/Main.cpp | 5 | ||||
| -rw-r--r-- | src/server/authserver/Server/AuthSession.cpp | 63 | ||||
| -rw-r--r-- | src/server/authserver/Server/AuthSession.h | 8 | ||||
| -rw-r--r-- | src/server/authserver/authserver.conf.dist | 19 | ||||
| -rw-r--r-- | src/server/database/Database/Implementation/LoginDatabase.cpp | 9 | ||||
| -rw-r--r-- | src/server/database/Database/Implementation/LoginDatabase.h | 8 | ||||
| -rw-r--r-- | src/server/scripts/Commands/cs_account.cpp | 209 | ||||
| -rw-r--r-- | src/server/shared/Secrets/SecretMgr.cpp | 237 | ||||
| -rw-r--r-- | src/server/shared/Secrets/SecretMgr.h | 75 | ||||
| -rw-r--r-- | src/server/shared/SharedDefines.cpp (renamed from src/server/authserver/Authentication/TOTP.h) | 13 | ||||
| -rw-r--r-- | src/server/shared/SharedDefines.h | 21 | ||||
| -rw-r--r-- | src/server/worldserver/Main.cpp | 4 | ||||
| -rw-r--r-- | src/server/worldserver/worldserver.conf.dist | 20 |
14 files changed, 654 insertions, 132 deletions
diff --git a/src/server/authserver/Authentication/TOTP.cpp b/src/server/authserver/Authentication/TOTP.cpp deleted file mode 100644 index 3f7062d6b7f..00000000000 --- a/src/server/authserver/Authentication/TOTP.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2008-2019 TrinityCore <https://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(char const* 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(char const* 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; - } -} diff --git a/src/server/authserver/Main.cpp b/src/server/authserver/Main.cpp index 1b58aa35155..fe9ff9f9b3f 100644 --- a/src/server/authserver/Main.cpp +++ b/src/server/authserver/Main.cpp @@ -37,6 +37,8 @@ #include "MySQLThreading.h" #include "ProcessPriority.h" #include "RealmList.h" +#include "SecretMgr.h" +#include "SharedDefines.h" #include "Util.h" #include <boost/asio/signal_set.hpp> #include <boost/program_options.hpp> @@ -79,6 +81,7 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, s int main(int argc, char** argv) { + Trinity::Impl::CurrentServerProcessHolder::_type = SERVER_PROCESS_AUTHSERVER; signal(SIGABRT, &Trinity::AbortHandler); auto configFile = fs::absolute(_TRINITY_REALM_CONFIG); @@ -139,6 +142,8 @@ int main(int argc, char** argv) if (!StartDB()) return 1; + sSecretMgr->Initialize(); + // Load IP Location Database sIPLocation->Load(); diff --git a/src/server/authserver/Server/AuthSession.cpp b/src/server/authserver/Server/AuthSession.cpp index 78aa977ab92..5c0628334fd 100644 --- a/src/server/authserver/Server/AuthSession.cpp +++ b/src/server/authserver/Server/AuthSession.cpp @@ -17,13 +17,16 @@ */ #include "AuthSession.h" +#include "AES.h" #include "AuthCodes.h" #include "Config.h" +#include "CryptoGenerics.h" +#include "DatabaseEnv.h" #include "Errors.h" #include "IPLocation.h" #include "Log.h" -#include "DatabaseEnv.h" #include "RealmList.h" +#include "SecretMgr.h" #include "SHA1.h" #include "TOTP.h" #include "Util.h" @@ -139,7 +142,7 @@ void AccountInfo::LoadResult(Field* fields) // 0 1 2 3 4 5 6 //SELECT a.id, a.username, a.locked, a.lock_country, a.last_ip, a.failed_logins, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, // 7 8 9 10 11 12 - // ab.unbandate = ab.bandate, aa.gmlevel, a.token_key, a.sha_pass_hash, a.v, a.s + // ab.unbandate = ab.bandate, aa.gmlevel, a.totp_secret, a.sha_pass_hash, a.v, a.s //FROM account a LEFT JOIN account_access aa ON a.id = aa.id LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 WHERE a.username = ? Id = fields[0].GetUInt32(); @@ -380,6 +383,25 @@ void AuthSession::LogonChallengeCallback(PreparedQueryResult result) } } + uint8 securityFlags = 0; + // Check if a TOTP token is needed + if (!fields[9].IsNull()) + { + securityFlags = 4; + _totpSecret = fields[9].GetBinary(); + if (auto const& secret = sSecretMgr->GetSecret(SECRET_TOTP_MASTER_KEY)) + { + bool success = Trinity::Crypto::AEDecrypt<Trinity::Crypto::AES>(*_totpSecret, *secret); + if (!success) + { + pkt << uint8(WOW_FAIL_DB_BUSY); + TC_LOG_ERROR("server.authserver", "[AuthChallenge] Account '%s' has invalid ciphertext for TOTP token key stored", _accountInfo.Login.c_str()); + SendPacket(pkt); + return; + } + } + } + // Get the password from the account table, upper it, and make the SRP6 calculation std::string rI = fields[10].GetString(); @@ -421,13 +443,6 @@ void AuthSession::LogonChallengeCallback(PreparedQueryResult result) pkt.append(N.AsByteArray(32).get(), 32); pkt.append(s.AsByteArray(int32(BufferSizes::SRP_6_S)).get(), size_t(BufferSizes::SRP_6_S)); // 32 bytes pkt.append(VersionChallenge.data(), VersionChallenge.size()); - uint8 securityFlags = 0; - - // Check if token is used - _tokenKey = fields[9].GetString(); - if (!_tokenKey.empty()) - securityFlags = 4; - pkt << uint8(securityFlags); // security flags (0x0...0x04) if (securityFlags & 0x01) // PIN input @@ -548,23 +563,29 @@ bool AuthSession::HandleLogonProof() if (!memcmp(M.AsByteArray(sha.GetLength()).get(), logonProof->M1, 20)) { // Check auth token - if ((logonProof->securityFlags & 0x04) || !_tokenKey.empty()) + bool tokenSuccess = false; + bool sentToken = (logonProof->securityFlags & 0x04); + if (sentToken && _totpSecret) { uint8 size = *(GetReadBuffer().GetReadPointer() + sizeof(sAuthLogonProof_C)); std::string token(reinterpret_cast<char*>(GetReadBuffer().GetReadPointer() + sizeof(sAuthLogonProof_C) + sizeof(size)), size); GetReadBuffer().ReadCompleted(sizeof(size) + size); - uint32 validToken = TOTP::GenerateToken(_tokenKey.c_str()); - _tokenKey.clear(); + uint32 incomingToken = atoi(token.c_str()); - if (validToken != incomingToken) - { - ByteBuffer packet; - packet << uint8(AUTH_LOGON_PROOF); - packet << uint8(WOW_FAIL_UNKNOWN_ACCOUNT); - packet << uint16(0); // LoginFlags, 1 has account message - SendPacket(packet); - return true; - } + tokenSuccess = Trinity::Crypto::TOTP::ValidateToken(*_totpSecret, incomingToken); + memset(_totpSecret->data(), 0, _totpSecret->size()); + } + else if (!sentToken && !_totpSecret) + tokenSuccess = true; + + if (!tokenSuccess) + { + ByteBuffer packet; + packet << uint8(AUTH_LOGON_PROOF); + packet << uint8(WOW_FAIL_UNKNOWN_ACCOUNT); + packet << uint16(0); // LoginFlags, 1 has account message + SendPacket(packet); + return true; } if (!VerifyVersion(logonProof->A, sizeof(logonProof->A), logonProof->crc_hash, false)) diff --git a/src/server/authserver/Server/AuthSession.h b/src/server/authserver/Server/AuthSession.h index 9c603616225..cac1450b401 100644 --- a/src/server/authserver/Server/AuthSession.h +++ b/src/server/authserver/Server/AuthSession.h @@ -19,10 +19,11 @@ #ifndef __AUTHSESSION_H__ #define __AUTHSESSION_H__ -#include "Common.h" +#include "BigNumber.h" #include "ByteBuffer.h" +#include "Common.h" +#include "Optional.h" #include "Socket.h" -#include "BigNumber.h" #include "QueryResult.h" #include "QueryCallbackProcessor.h" #include <memory> @@ -56,7 +57,6 @@ struct AccountInfo bool IsBanned = false; bool IsPermanenetlyBanned = false; AccountTypes SecurityLevel = SEC_PLAYER; - std::string TokenKey; }; class AuthSession : public Socket<AuthSession> @@ -99,7 +99,7 @@ private: AuthStatus _status; AccountInfo _accountInfo; - std::string _tokenKey; + Optional<std::vector<uint8>> _totpSecret; std::string _localizationName; std::string _os; std::string _ipCountry; diff --git a/src/server/authserver/authserver.conf.dist b/src/server/authserver/authserver.conf.dist index f3aa6f6e67e..480a49eca8e 100644 --- a/src/server/authserver/authserver.conf.dist +++ b/src/server/authserver/authserver.conf.dist @@ -9,6 +9,7 @@ # EXAMPLE CONFIG # AUTH SERVER SETTINGS # MYSQL SETTINGS +# CRYPTOGRAPHY # UPDATE SETTINGS # LOGGING SYSTEM SETTINGS # @@ -213,6 +214,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 cc3f7d4f17f..c098a3ceb80 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -38,7 +38,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_UPD_VS, "UPDATE account SET v = ?, s = ? WHERE username = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_LOGONPROOF, "UPDATE account SET sessionkey = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_LOGONCHALLENGE, "SELECT a.id, a.username, a.locked, a.lock_country, a.last_ip, a.failed_logins, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, " - "ab.unbandate = ab.bandate, aa.gmlevel, a.token_key, a.sha_pass_hash, a.v, a.s " + "ab.unbandate = ab.bandate, aa.gmlevel, a.totp_secret, a.sha_pass_hash, a.v, a.s " "FROM account a LEFT JOIN account_access aa ON a.id = aa.id LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 WHERE a.username = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_RECONNECTCHALLENGE, "SELECT a.id, UPPER(a.username), a.locked, a.lock_country, a.last_ip, a.failed_logins, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, " "ab.unbandate = ab.bandate, aa.gmlevel, a.sessionKey " @@ -116,6 +116,13 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_INS_ACCOUNT_MUTE, "INSERT INTO account_muted VALUES (?, UNIX_TIMESTAMP(), ?, ?, ?)", CONNECTION_ASYNC); 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); } LoginDatabaseConnection::LoginDatabaseConnection(MySQLConnectionInfo& connInfo) : MySQLConnection(connInfo) diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index 63b510ef29c..bcc985f5eb0 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -112,6 +112,14 @@ enum LoginDatabaseStatements : uint32 LOGIN_INS_ACCOUNT_MUTE, 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, + MAX_LOGINDATABASE_STATEMENTS }; diff --git a/src/server/scripts/Commands/cs_account.cpp b/src/server/scripts/Commands/cs_account.cpp index a70a0f376ce..0917cd2a021 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; @@ -54,6 +61,12 @@ public: { "sec", rbac::RBAC_PERM_COMMAND_ACCOUNT_SET_SEC, true, nullptr, "", accountSetSecTable }, { "gmlevel", rbac::RBAC_PERM_COMMAND_ACCOUNT_SET_GMLEVEL, true, &HandleAccountSetGmLevelCommand, "" }, { "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 = { @@ -62,6 +75,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, "" }, @@ -79,6 +93,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 + PreparedStatement* 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); + + PreparedStatement* 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 + PreparedStatement* 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)) + { + PreparedStatement* 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)) @@ -659,6 +805,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") + { + PreparedStatement* 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); + + PreparedStatement* 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..2f476005f8a --- /dev/null +++ b/src/server/shared/Secrets/SecretMgr.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2008-2019 TrinityCore <https://www.trinitycore.org/> + * Copyright (C) 2005-2009 MaNGOS <http://getmangos.com/> + * + * 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 "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->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() +{ + 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; + { + PreparedStatement* 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 != THIS_SERVER_PROCESS) + { + 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 +{ + SQLTransaction 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->AsByteArray<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->AsByteArray<Trinity::Crypto::AES::KEY_SIZE_BYTES>()); + + PreparedStatement* 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) + { + PreparedStatement* 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"); + + PreparedStatement* 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..faaefab98a9 --- /dev/null +++ b/src/server/shared/Secrets/SecretMgr.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008-2019 TrinityCore <https://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 __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 +}; + +class TC_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 diff --git a/src/server/authserver/Authentication/TOTP.h b/src/server/shared/SharedDefines.cpp index 0c66820a879..f5f9ad98b7d 100644 --- a/src/server/authserver/Authentication/TOTP.h +++ b/src/server/shared/SharedDefines.cpp @@ -15,15 +15,6 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef _TOTP_H -#define _TOTP_H +#include "SharedDefines.h" -#include "openssl/hmac.h" -#include "openssl/evp.h" - -namespace TOTP -{ - unsigned int GenerateToken(char const* b32key); -} - -#endif +ServerProcessTypes Trinity::Impl::CurrentServerProcessHolder::_type = NUM_SERVER_PROCESS_TYPES; diff --git a/src/server/shared/SharedDefines.h b/src/server/shared/SharedDefines.h index b829a984925..3c6505c7ac2 100644 --- a/src/server/shared/SharedDefines.h +++ b/src/server/shared/SharedDefines.h @@ -3787,6 +3787,27 @@ enum LineOfSightChecks LINEOFSIGHT_ALL_CHECKS = (LINEOFSIGHT_CHECK_VMAP | LINEOFSIGHT_CHECK_GOBJECT) }; +enum ServerProcessTypes +{ + SERVER_PROCESS_AUTHSERVER = 0, + SERVER_PROCESS_WORLDSERVER = 1, + + NUM_SERVER_PROCESS_TYPES +}; + +namespace Trinity +{ +namespace Impl +{ + struct TC_SHARED_API CurrentServerProcessHolder + { + static ServerProcessTypes type() { return _type; } + static ServerProcessTypes _type; + }; +} +} +#define THIS_SERVER_PROCESS (Trinity::Impl::CurrentServerProcessHolder::type()) + #define MAX_CREATURE_SPELL_DATA_SLOT 4 #endif diff --git a/src/server/worldserver/Main.cpp b/src/server/worldserver/Main.cpp index e163dd2566d..2accd7297c2 100644 --- a/src/server/worldserver/Main.cpp +++ b/src/server/worldserver/Main.cpp @@ -47,6 +47,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" @@ -116,6 +118,7 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, s /// Launch the Trinity server extern int main(int argc, char** argv) { + Trinity::Impl::CurrentServerProcessHolder::_type = SERVER_PROCESS_WORLDSERVER; signal(SIGABRT, &Trinity::AbortHandler); auto configFile = fs::absolute(_TRINITY_CORE_CONFIG); @@ -247,6 +250,7 @@ extern int main(int argc, char** argv) }); // Initialize the World + sSecretMgr->Initialize(); 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 c34a49a9e8d..c308bc9d03d 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 @@ -1296,6 +1297,25 @@ CacheDataQueries = 1 ################################################################################################### ################################################################################################### +# 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 |
