diff --git a/CMakeLists.txt b/CMakeLists.txt index 34750afb7d..4f3758a35c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ option(HTTP "Enable HTTP support" ON) if (HTTP) list(APPEND VCPKG_MANIFEST_FEATURES "http") set(BOOST_REQUIRED_VERSION 1.75.0) + add_compile_definitions(HTTP) endif() option(BUILD_TESTING "Build unit tests" OFF) diff --git a/config.lua.dist b/config.lua.dist index 96c1221bd3..2960f02a38 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -27,6 +27,7 @@ gameProtocolPort = 7172 statusProtocolPort = 7171 httpPort = 8080 httpWorkers = 1 +sessionSecret = "c2VjcmV0" -- CHANGE THIS VALUE! maxPlayers = 0 onePlayerOnlinePerAccount = true allowClones = false diff --git a/src/base64.cpp b/src/base64.cpp index 6b96f3512b..52f30efb1c 100644 --- a/src/base64.cpp +++ b/src/base64.cpp @@ -2,34 +2,87 @@ #include "base64.h" -#include +static constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; std::string tfs::base64::encode(std::string_view input) { - std::unique_ptr b64(BIO_new(BIO_f_base64()), BIO_free_all); - BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL); + std::string output; + output.reserve((input.size() * 4 + 2) / 3); // Estimated size - BIO* sink = BIO_new(BIO_s_mem()); - BIO_push(b64.get(), sink); - BIO_write(b64.get(), input.data(), input.size()); - BIO_flush(b64.get()); + size_t i = 0; + while (i + 3 <= input.size()) { + uint32_t chunk = (static_cast(input[i]) << 16) | (static_cast(input[i + 1]) << 8) | + static_cast(input[i + 2]); - const char* encoded; - auto len = BIO_get_mem_data(sink, &encoded); - return {encoded, static_cast(len)}; + output.push_back(alphabet[(chunk >> 18) & 0x3F]); + output.push_back(alphabet[(chunk >> 12) & 0x3F]); + output.push_back(alphabet[(chunk >> 6) & 0x3F]); + output.push_back(alphabet[chunk & 0x3F]); + i += 3; + } + + if (i < input.size()) { + uint32_t chunk = static_cast(input[i]) << 16; + if (i + 1 < input.size()) { + chunk |= static_cast(input[i + 1]) << 8; + } + + output.push_back(alphabet[(chunk >> 18) & 0x3F]); + output.push_back(alphabet[(chunk >> 12) & 0x3F]); + if (i + 1 < input.size()) { + output.push_back(alphabet[(chunk >> 6) & 0x3F]); + } + } + + return output; } std::string tfs::base64::decode(std::string_view input) { - std::unique_ptr b64(BIO_new(BIO_f_base64()), BIO_free_all); - BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL); + static constexpr auto reverse_table = [] { + std::array table{}; + table.fill(-1); + for (size_t i = 0; i < alphabet.size(); ++i) { + table[static_cast(alphabet[i])] = static_cast(i); + } + return table; + }(); + + size_t len = input.size(); + assert(len % 4 != 1); + + std::string output; + output.reserve((len * 3) / 4); // Estimated + + size_t i = 0; + while (i + 4 <= len) { + uint32_t chunk = (reverse_table[static_cast(input[i])] << 18) | + (reverse_table[static_cast(input[i + 1])] << 12) | + (reverse_table[static_cast(input[i + 2])] << 6) | + reverse_table[static_cast(input[i + 3])]; + + output.push_back((chunk >> 16) & 0xFF); + output.push_back((chunk >> 8) & 0xFF); + output.push_back(chunk & 0xFF); + i += 4; + } + + if (i < len) { + uint32_t chunk = 0; + int padding = 0; + + chunk |= reverse_table[static_cast(input[i])] << 18; + chunk |= reverse_table[static_cast(input[i + 1])] << 12; - BIO* source = BIO_new_mem_buf(input.data(), input.size()); // read-only source - BIO_push(b64.get(), source); + if (i + 2 < len) { + chunk |= reverse_table[static_cast(input[i + 2])] << 6; + } else { + ++padding; + } - size_t decodedLength = 3 * input.size() / 4; - auto decoded = std::string(decodedLength, '\0'); + output.push_back((chunk >> 16) & 0xFF); + if (padding < 1) output.push_back((chunk >> 8) & 0xFF); + } - const int len = BIO_read(b64.get(), decoded.data(), decodedLength); - return decoded.substr(0, len); + return output; } diff --git a/src/configmanager.cpp b/src/configmanager.cpp index 976558d6cf..c0c26a92d5 100644 --- a/src/configmanager.cpp +++ b/src/configmanager.cpp @@ -6,6 +6,7 @@ #include "configmanager.h" +#include "base64.h" #include "game.h" #include "monster.h" #include "pugicast.h" @@ -253,6 +254,7 @@ bool ConfigManager::load() string[URL] = getGlobalString(L, "url", ""); string[LOCATION] = getGlobalString(L, "location", ""); string[WORLD_TYPE] = getGlobalString(L, "worldType", "pvp"); + string[SESSION_SECRET] = tfs::base64::decode(getGlobalString(L, "sessionSecret", "")); integer[MAX_PLAYERS] = getGlobalNumber(L, "maxPlayers"); integer[PZ_LOCKED] = getGlobalNumber(L, "pzLocked", 60000); diff --git a/src/configmanager.h b/src/configmanager.h index 5c0ab0b0fa..e3c6802bc3 100644 --- a/src/configmanager.h +++ b/src/configmanager.h @@ -62,6 +62,7 @@ enum string_config_t LOCATION, IP, WORLD_TYPE, + SESSION_SECRET, MYSQL_HOST, MYSQL_USER, MYSQL_PASS, diff --git a/src/http/cacheinfo.cpp b/src/http/cacheinfo.cpp index 3e7b8f3214..90ad411e72 100644 --- a/src/http/cacheinfo.cpp +++ b/src/http/cacheinfo.cpp @@ -9,7 +9,7 @@ namespace beast = boost::beast; namespace json = boost::json; using boost::beast::http::status; -std::pair tfs::http::handle_cacheinfo(const json::object&, std::string_view) +std::pair tfs::http::handle_cacheinfo(const json::object&) { thread_local auto& db = Database::getInstance(); auto result = db.storeQuery("SELECT COUNT(*) AS `count` FROM `players_online`"); diff --git a/src/http/cacheinfo.h b/src/http/cacheinfo.h index 5ef267ea04..c4a4dd316d 100644 --- a/src/http/cacheinfo.h +++ b/src/http/cacheinfo.h @@ -5,7 +5,6 @@ namespace tfs::http { -std::pair handle_cacheinfo(const boost::json::object& body, - std::string_view ip); +std::pair handle_cacheinfo(const boost::json::object& body); } diff --git a/src/http/login.cpp b/src/http/login.cpp index bd22458930..b28148a3e1 100644 --- a/src/http/login.cpp +++ b/src/http/login.cpp @@ -6,8 +6,6 @@ #include "../game.h" #include "error.h" -#include - extern Game g_game; extern Vocations g_vocations; @@ -17,6 +15,18 @@ using boost::beast::http::status; namespace { +[[noreturn]] inline void unreachable() +{ + // Uses compiler specific extensions if possible. + // Even if no extension is used, undefined behavior is still raised by + // an empty function body and the noreturn attribute. +#if defined(_MSC_VER) && !defined(__clang__) // MSVC + __assume(false); +#else // GCC, Clang + __builtin_unreachable(); +#endif +} + int getPvpType() { switch (g_game.getWorldType()) { @@ -28,12 +38,12 @@ int getPvpType() return 2; } - tfs::unreachable(); + unreachable(); } } // namespace -std::pair tfs::http::handle_login(const json::object& body, std::string_view ip) +std::pair tfs::http::handle_login(const json::object& body) { using namespace std::chrono; @@ -50,7 +60,7 @@ std::pair tfs::http::handle_login(const json::object& body, } thread_local auto& db = Database::getInstance(); - auto result = db.storeQuery(fmt::format( + auto result = db.storeQuery(std::format( "SELECT `id`, UNHEX(`password`) AS `password`, `secret`, `premium_ends_at` FROM `accounts` WHERE `email` = {:s}", db.escapeString(emailField->get_string()))); if (!result) { @@ -84,14 +94,11 @@ std::pair tfs::http::handle_login(const json::object& body, auto accountId = result->getNumber("id"); auto premiumEndsAt = result->getNumber("premium_ends_at"); - std::string sessionKey = randomBytes(16); - if (!db.executeQuery( - fmt::format("INSERT INTO `sessions` (`token`, `account_id`, `ip`) VALUES ({:s}, {:d}, INET6_ATON({:s}))", - db.escapeString(sessionKey), accountId, db.escapeString(ip)))) { - return make_error_response(); - } + const auto& payload = std::format("{:s}{:s}", tfs::to_bytes(accountId), tfs::to_bytes(now)); + const auto& signature = hmac("SHA256", getString(ConfigManager::SESSION_SECRET), payload); + const auto& sessionKey = std::format("{:s}{:s}", payload, signature); - result = db.storeQuery(fmt::format( + result = db.storeQuery(std::format( "SELECT `id`, `name`, `level`, `vocation`, `lastlogin`, `sex`, `looktype`, `lookhead`, `lookbody`, `looklegs`, `lookfeet`, `lookaddons` FROM `players` WHERE `account_id` = {:d}", accountId)); diff --git a/src/http/login.h b/src/http/login.h index effcfa4003..7e200c73f2 100644 --- a/src/http/login.h +++ b/src/http/login.h @@ -5,7 +5,6 @@ namespace tfs::http { -std::pair handle_login(const boost::json::object& body, - std::string_view ip); +std::pair handle_login(const boost::json::object& body); } diff --git a/src/http/router.cpp b/src/http/router.cpp index e5c0bdaebb..800ff5c997 100644 --- a/src/http/router.cpp +++ b/src/http/router.cpp @@ -13,15 +13,15 @@ namespace json = boost::json; namespace { -auto router(std::string_view type, const json::object& body, std::string_view ip) +auto router(std::string_view type, const json::object& body) { using namespace tfs::http; if (type == "cacheinfo") { - return handle_cacheinfo(body, ip); + return handle_cacheinfo(body); } if (type == "login") { - return handle_login(body, ip); + return handle_login(body); } return make_error_response(); @@ -47,7 +47,7 @@ beast::http::message_generator tfs::http::handle_request(const beast::http::requ return make_error_response({.code = 2, .message = "Invalid request type."}); } - return router(typeField->get_string(), requestBodyObj, ip); + return router(typeField->get_string(), requestBodyObj); }(); beast::http::response res{status, req.version()}; diff --git a/src/http/tests/test_cacheinfo.cpp b/src/http/tests/test_cacheinfo.cpp index 68e26b81da..5c2ee58b9d 100644 --- a/src/http/tests/test_cacheinfo.cpp +++ b/src/http/tests/test_cacheinfo.cpp @@ -57,7 +57,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_success_with_token, CacheInfoFixture) BOOST_TEST(db.executeQuery(fmt::format( "INSERT INTO `players_online` (`player_id`) SELECT `id` FROM `players` WHERE `account_id` = {:d}", id))); - auto&& [status, body] = tfs::http::handle_cacheinfo({{"type", "cacheinfo"}}, ip); + auto&& [status, body] = tfs::http::handle_cacheinfo({{"type", "cacheinfo"}}); BOOST_TEST(status == status::ok); BOOST_TEST(body.at("playersonline").as_uint64() == 3); diff --git a/src/http/tests/test_login.cpp b/src/http/tests/test_login.cpp index b35c00e8d1..ef6e1568a2 100644 --- a/src/http/tests/test_login.cpp +++ b/src/http/tests/test_login.cpp @@ -151,7 +151,7 @@ using status = boost::beast::http::status; BOOST_FIXTURE_TEST_CASE(test_login_missing_email, LoginFixture) { - auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"password", "bar"}}, ip); + auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"password", "bar"}}); BOOST_TEST(status == status::ok); BOOST_TEST(body.at("errorCode").as_int64() == 3); @@ -160,7 +160,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_missing_email, LoginFixture) BOOST_FIXTURE_TEST_CASE(test_login_account_does_not_exist, LoginFixture) { auto&& [status, body] = - tfs::http::handle_login({{"type", "login"}, {"email", "k@example.com"}, {"password", "bar"}}, ip); + tfs::http::handle_login({{"type", "login"}, {"email", "k@example.com"}, {"password", "bar"}}); BOOST_TEST(status == status::ok); BOOST_TEST(body.at("errorCode").as_int64() == 3); @@ -168,7 +168,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_account_does_not_exist, LoginFixture) BOOST_FIXTURE_TEST_CASE(test_login_missing_password, LoginFixture) { - auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}}, ip); + auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}}); BOOST_TEST(status == status::ok); BOOST_TEST(body.at("errorCode").as_int64() == 3); @@ -180,7 +180,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_invalid_password, LoginFixture) "INSERT INTO `accounts` (`name`, `email`, `password`) VALUES ('abc', 'foo@example.com', SHA1('bar'))")); auto&& [status, body] = - tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}, {"password", "baz"}}, ip); + tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}, {"password", "baz"}}); BOOST_TEST(status == status::ok); BOOST_TEST(body.at("errorCode").as_int64() == 3); @@ -193,13 +193,11 @@ BOOST_FIXTURE_TEST_CASE(test_login_missing_token, LoginFixture) auto now = duration_cast(system_clock::now().time_since_epoch()); - auto&& [status, body] = tfs::http::handle_login( - { - {"type", "login"}, - {"email", "fooba@example.com"}, - {"password", "bar"}, - }, - ip); + auto&& [status, body] = tfs::http::handle_login({ + {"type", "login"}, + {"email", "fooba@example.com"}, + {"password", "bar"}, + }); BOOST_TEST(status == status::ok); BOOST_TEST(body.at("errorCode").as_int64() == 6); @@ -211,7 +209,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_success_no_players, LoginFixture) "INSERT INTO `accounts` (`name`, `email`, `password`) VALUES ('defg', 'foobar@example.com', SHA1('bar'))")); auto&& [status, body] = - tfs::http::handle_login({{"type", "login"}, {"email", "foobar@example.com"}, {"password", "bar"}}, ip); + tfs::http::handle_login({{"type", "login"}, {"email", "foobar@example.com"}, {"password", "bar"}}); BOOST_TEST(status == status::ok); auto& characters = body.at("playdata").at("characters").as_array(); @@ -229,12 +227,12 @@ BOOST_FIXTURE_TEST_CASE(test_login_success, LoginFixture) DBInsert insert( "INSERT INTO `players` (`account_id`, `name`, `level`, `vocation`, `lastlogin`, `sex`, `looktype`, `lookhead`, `lookbody`, `looklegs`, `lookfeet`, `lookaddons`) VALUES"); - insert.addRow(fmt::format("{:d}, \"{:s}\", {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}", id, - "Test", 2597, 6, 1715719401, 1, 1094, 78, 132, 114, 0, 1)); + insert.addRow(fmt::format("{:d}, \"{:s}\", {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}", id, "Test", + 2597, 6, 1715719401, 1, 1094, 78, 132, 114, 0, 1)); BOOST_TEST(insert.execute()); auto&& [status, body] = - tfs::http::handle_login({{"type", "login"}, {"email", "ghij@example.com"}, {"password", "bar"}}, ip); + tfs::http::handle_login({{"type", "login"}, {"email", "ghij@example.com"}, {"password", "bar"}}); BOOST_TEST(status == status::ok); @@ -285,14 +283,12 @@ BOOST_FIXTURE_TEST_CASE(test_login_success_with_token, LoginFixture) insert.addRow(fmt::format("{:d}, \"{:s}\", {:d}, {:d}, {:d}", id, "Testtoken", 2597, 6, 1715719401)); BOOST_TEST(insert.execute()); - auto&& [status, body] = tfs::http::handle_login( - { - {"type", "login"}, - {"email", "nbdj@example.com"}, - {"password", "bar"}, - {"token", generateToken("", now.count() / AUTHENTICATOR_PERIOD)}, - }, - ip); + auto&& [status, body] = tfs::http::handle_login({ + {"type", "login"}, + {"email", "nbdj@example.com"}, + {"password", "bar"}, + {"token", generateToken("", now.count() / AUTHENTICATOR_PERIOD)}, + }); BOOST_TEST(status == status::ok); } diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index fec7d4143c..49c3702c9a 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -401,6 +401,16 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg) return; } + std::string_view view{sessionToken}; + auto tokenAccountId = view.substr(0, sizeof(uint64_t)); + auto tokenTimestamp = view.substr(sizeof(uint64_t), sizeof(int64_t)); + auto tokenSignature = view.substr(sizeof(uint64_t) + sizeof(int64_t)); + if (tokenSignature != hmac("SHA256", getString(ConfigManager::SESSION_SECRET), + std::format("{:s}{:s}", tokenAccountId, tokenTimestamp))) { + disconnectClient("Malformed session key."); + return; + } + if (operatingSystem == CLIENTOS_QT_LINUX) { msg.getString(); // OS name (?) msg.getString(); // OS version (?) @@ -433,24 +443,19 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg) Database& db = Database::getInstance(); auto result = db.storeQuery(fmt::format( - "SELECT `a`.`id` AS `account_id`, INET6_NTOA(`s`.`ip`) AS `session_ip`, `p`.`id` AS `character_id` FROM `accounts` `a` JOIN `sessions` `s` ON `a`.`id` = `s`.`account_id` JOIN `players` `p` ON `a`.`id` = `p`.`account_id` WHERE `s`.`token` = {:s} AND `s`.`expired_at` IS NULL AND `p`.`name` = {:s} AND `p`.`deletion` = 0", - db.escapeString(sessionToken), db.escapeString(characterName))); + "SELECT `a`.`id` AS `account_id`, `p`.`id` AS `character_id` FROM `accounts` `a` JOIN `players` `p` ON `a`.`id` = `p`.`account_id` WHERE `p`.`name` = {:s} AND `p`.`deletion` = 0", + db.escapeString(characterName))); if (!result) { disconnectClient("Account name or password is not correct."); return; } uint32_t accountId = result->getNumber("account_id"); - if (accountId == 0) { + if (accountId == 0 || accountId != tfs::from_bytes(tokenAccountId)) { disconnectClient("Account name or password is not correct."); return; } - Connection::Address sessionIP = boost::asio::ip::make_address(result->getString("session_ip")); - if (!sessionIP.is_loopback() && ip != sessionIP) { - disconnectClient("Your game session is already locked to a different IP. Please log in again."); - } - g_dispatcher.addTask([=, thisPtr = getThis(), characterId = result->getNumber("character_id")]() { thisPtr->login(characterId, accountId, operatingSystem); }); diff --git a/src/protocollogin.cpp b/src/protocollogin.cpp index 66e149afe2..46df277c7f 100644 --- a/src/protocollogin.cpp +++ b/src/protocollogin.cpp @@ -14,6 +14,8 @@ #include "rsa.h" #include "tasks.h" +#include + extern Game g_game; namespace { @@ -82,7 +84,7 @@ void ProtocolLogin::getCharacterList(const std::string& accountName, const std:: return; } - auto id = result->getNumber("id"); + auto id = result->getNumber("id"); auto key = decodeSecret(result->getString("secret")); auto premiumEndsAt = result->getNumber("premium_ends_at"); @@ -95,11 +97,11 @@ void ProtocolLogin::getCharacterList(const std::string& accountName, const std:: } while (result->next()); } - uint32_t ticks = duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count() / - AUTHENTICATOR_PERIOD; + auto now = duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); auto output = tfs::net::make_output_message(); if (!key.empty()) { + auto ticks = static_cast(now) / AUTHENTICATOR_PERIOD; if (token.empty() || !(token == generateToken(key, ticks) || token == generateToken(key, ticks - 1) || token == generateToken(key, ticks + 1))) { output->addByte(0x0D); @@ -113,20 +115,12 @@ void ProtocolLogin::getCharacterList(const std::string& accountName, const std:: } // Generate and add session key - static std::independent_bits_engine rbe; - std::string sessionKey(16, '\x00'); - std::generate(sessionKey.begin(), sessionKey.end(), std::ref(rbe)); + const auto& payload = std::format("{:s}{:s}", tfs::to_bytes(id), tfs::to_bytes(now)); + const auto& signature = hmac("SHA256", getString(ConfigManager::SESSION_SECRET), payload); + const auto& sessionKey = std::format("{:s}{:s}", payload, signature); output->addByte(0x28); - output->addString(tfs::base64::encode({sessionKey.data(), sessionKey.size()})); - - if (!db.executeQuery( - fmt::format("INSERT INTO `sessions` (`token`, `account_id`, `ip`) VALUES ({:s}, {:d}, INET6_ATON({:s}))", - db.escapeBlob(sessionKey.data(), sessionKey.size()), id, - db.escapeString(connection->getIP().to_string())))) { - disconnectClient("Failed to create session.\nPlease try again later.", version); - return; - } + output->addString(tfs::base64::encode(sessionKey)); // Add char list output->addByte(0x64); diff --git a/src/tests/test_base64.cpp b/src/tests/test_base64.cpp index 7ac1b0a646..467560b77a 100644 --- a/src/tests/test_base64.cpp +++ b/src/tests/test_base64.cpp @@ -8,18 +8,18 @@ struct Basee64Fixture { - std::string_view plain; - std::string_view encoded; + std::string plain; + std::string encoded; }; // test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 auto testVectors = std::vector{ {.plain = "", .encoded = ""}, - {.plain = "f", .encoded = "Zg=="}, - {.plain = "fo", .encoded = "Zm8="}, + {.plain = "f", .encoded = "Zg"}, + {.plain = "fo", .encoded = "Zm8"}, {.plain = "foo", .encoded = "Zm9v"}, - {.plain = "foob", .encoded = "Zm9vYg=="}, - {.plain = "fooba", .encoded = "Zm9vYmE="}, + {.plain = "foob", .encoded = "Zm9vYg"}, + {.plain = "fooba", .encoded = "Zm9vYmE"}, {.plain = "foobar", .encoded = "Zm9vYmFy"}, }; diff --git a/src/tools.cpp b/src/tools.cpp index d950718c4f..2cab9bb5ea 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -12,6 +12,20 @@ #include #include +namespace { + +struct Deleter +{ + void operator()(BIO* bio) const { BIO_free(bio); } + void operator()(EVP_MD* md) const { EVP_MD_free(md); } + void operator()(EVP_MD_CTX* ctx) const { EVP_MD_CTX_free(ctx); } +}; + +template +using C_ptr = std::unique_ptr; + +} // namespace + void printXMLError(const std::string& where, std::string_view fileName, const pugi::xml_parse_result& result) { std::cout << '[' << where << "] Failed to load " << fileName << ": " << result.description() << std::endl; @@ -63,12 +77,12 @@ void printXMLError(const std::string& where, std::string_view fileName, const pu std::string transformToSHA1(std::string_view input) { - std::unique_ptr ctx{EVP_MD_CTX_new(), EVP_MD_CTX_free}; + C_ptr ctx{EVP_MD_CTX_new()}; if (!ctx) { throw std::runtime_error("Failed to create EVP context"); } - std::unique_ptr md{EVP_MD_fetch(nullptr, "SHA1", nullptr), EVP_MD_free}; + C_ptr md{EVP_MD_fetch(nullptr, "SHA1", nullptr)}; if (!md) { throw std::runtime_error("Failed to fetch SHA1"); } @@ -92,25 +106,23 @@ std::string transformToSHA1(std::string_view input) std::string hmac(std::string_view algorithm, std::string_view key, std::string_view message) { - std::unique_ptr ctx{EVP_MD_CTX_new(), EVP_MD_CTX_free}; + C_ptr ctx{EVP_MD_CTX_new()}; if (!ctx) { throw std::runtime_error("Failed to create EVP context"); } - std::unique_ptr md{EVP_MD_fetch(nullptr, algorithm.data(), nullptr), EVP_MD_free}; + C_ptr md{EVP_MD_fetch(nullptr, algorithm.data(), nullptr)}; if (!md) { throw std::runtime_error(fmt::format("Failed to fetch {:s}", algorithm)); } - std::array result; - unsigned int len; - - if (!HMAC(md.get(), key.data(), key.size(), reinterpret_cast(message.data()), message.size(), - result.data(), &len)) { + std::string result(EVP_MD_get_size(md.get()), '\0'); + if (!HMAC(md.get(), key.data(), key.size(), reinterpret_cast(message.data()), message.size(), + reinterpret_cast(result.data()), nullptr)) { throw std::runtime_error("HMAC failed"); } - return {reinterpret_cast(result.data()), len}; + return result; } std::string generateToken(std::string_view key, uint64_t counter, size_t length /*= AUTHENTICATOR_DIGITS*/) diff --git a/src/tools.h b/src/tools.h index 97b2381718..a7588ea223 100644 --- a/src/tools.h +++ b/src/tools.h @@ -88,25 +88,39 @@ inline constexpr auto to_underlying(auto e) noexcept { return static_cast +constexpr T byteswap(T value) noexcept +{ + static_assert(std::has_unique_object_representations_v, "T may not have padding bits"); + auto repr = std::bit_cast>(value); + std::ranges::reverse(repr); + return std::bit_cast(repr); +} -[[noreturn]] inline void unreachable() +template +T from_bytes(std::string_view repr) { - // Uses compiler specific extensions if possible. - // Even if no extension is used, undefined behavior is still raised by - // an empty function body and the noreturn attribute. -#if defined(_MSC_VER) && !defined(__clang__) // MSVC - __assume(false); -#else // GCC, Clang - __builtin_unreachable(); -#endif + if (repr.size() != sizeof(T)) { + throw std::invalid_argument("Invalid byte representation size"); + } + + T value; + std::memcpy(&value, repr.data(), sizeof(T)); + if constexpr (std::endian::native == std::endian::big) { + value = byteswap(value); + } + return value; } -#endif +template +constexpr std::string to_bytes(T value) noexcept +{ + if constexpr (std::endian::native == std::endian::big) { + value = byteswap(value); + } + auto repr = std::bit_cast>(value); + return {repr.begin(), repr.end()}; +} } // namespace tfs