diff --git a/scripts/convert_private_key.bat b/scripts/convert_private_key.bat new file mode 100644 index 00000000..226fb35a --- /dev/null +++ b/scripts/convert_private_key.bat @@ -0,0 +1,3 @@ +@echo off +echo Exporting DER rsa-private.key to PEM... +openssl rsa -in rsa-private.key -inform DER -outform PEM -out exported-rsa-private.key diff --git a/scripts/convert_private_key.sh b/scripts/convert_private_key.sh new file mode 100644 index 00000000..e2f6c9e5 --- /dev/null +++ b/scripts/convert_private_key.sh @@ -0,0 +1,4 @@ + #!/bin/bash + +echo "Exporting DER rsa-private.key to PEM..." +openssl rsa -in rsa-private.key -inform DER -outform PEM -out exported-rsa-private.key diff --git a/src/Components/Modules/RCon.cpp b/src/Components/Modules/RCon.cpp index 52a5cc25..f0e952ba 100644 --- a/src/Components/Modules/RCon.cpp +++ b/src/Components/Modules/RCon.cpp @@ -9,16 +9,18 @@ namespace Components { std::unordered_map RCon::RateLimit; - std::vector RCon::RconAddresses; + std::vector RCon::RConAddresses; - RCon::Container RCon::RconContainer; - Utils::Cryptography::ECC::Key RCon::RconKey; + RCon::Container RCon::RConContainer; + Utils::Cryptography::ECC::Key RCon::RConKey; std::string RCon::Password; - Dvar::Var RCon::RconPassword; - Dvar::Var RCon::RconLogRequests; - Dvar::Var RCon::RconTimeout; + Dvar::Var RCon::RConPassword; + Dvar::Var RCon::RConLogRequests; + Dvar::Var RCon::RConTimeout; + + std::string RCon::RConOutputBuffer; void RCon::AddCommands() { @@ -61,11 +63,44 @@ namespace Components Logger::Print("You are connected to an invalid server\n"); }); + Command::Add("rconSafe", [](const Command::Params* params) + { + if (params->size() < 2) + { + Logger::Print("Usage: {} \n", params->get(0)); + return; + } + + const auto command = params->join(1); + + auto* addr = reinterpret_cast(0xA5EA44); + Network::Address target(addr); + if (!target.isValid() || target.getIP().full == 0) + { + target = Party::Target(); + } + + if (!target.isValid()) + { + Logger::Print("You are connected to an invalid server\n"); + return; + } + + const auto& key = CryptoKeyRSA::GetPrivateKey(); + const auto signature = Utils::Cryptography::RSA::SignMessage(key, command); + + Proto::RCon::Command directive; + directive.set_command(command); + directive.set_signature(signature); + + Network::SendCommand(target, "rconSafe", directive.SerializeAsString()); + }); + Command::Add("remoteCommand", [](const Command::Params* params) { if (params->size() < 2) return; - RconContainer.command = params->get(1); + RConContainer.command = params->get(1); auto* addr = reinterpret_cast(0xA5EA44); Network::Address target(addr); @@ -91,9 +126,9 @@ namespace Components Network::Address address(params->get(1)); const auto hash = std::hash()(*reinterpret_cast(&address.getIP().bytes[0])); - if (address.isValid() && std::ranges::find(RconAddresses, hash) == RconAddresses.end()) + if (address.isValid() && std::ranges::find(RConAddresses, hash) == RConAddresses.end()) { - RconAddresses.push_back(hash); + RConAddresses.push_back(hash); } }); } @@ -113,7 +148,7 @@ namespace Components const auto ip = address.getIP(); const auto lastTime = RateLimit[ip.full]; - if (lastTime && (time - lastTime) < RconTimeout.get()) + if (lastTime && (time - lastTime) < RConTimeout.get()) { return false; // Flooding } @@ -127,7 +162,7 @@ namespace Components for (auto i = RateLimit.begin(); i != RateLimit.end();) { // No longer at risk of flooding, remove - if ((time - i->second) > RconTimeout.get()) + if ((time - i->second) > RConTimeout.get()) { i = RateLimit.erase(i); } @@ -138,7 +173,7 @@ namespace Components } } - void RCon::RconExecuter(const Network::Address& address, std::string data) + void RCon::RConExecutor(const Network::Address& address, std::string data) { Utils::String::Trim(data); @@ -159,7 +194,7 @@ namespace Components password.erase(password.begin()); } - const auto svPassword = RconPassword.get(); + const auto svPassword = RConPassword.get(); if (svPassword.empty()) { Logger::Print(Game::CON_CHANNEL_NETWORK, "RCon request from {} dropped. No password set!\n", address.getString()); @@ -172,8 +207,7 @@ namespace Components return; } - static std::string outputBuffer; - outputBuffer.clear(); + RConOutputBuffer.clear(); #ifndef _DEBUG if (RconLogRequests.get()) @@ -184,15 +218,39 @@ namespace Components Logger::PipeOutput([](const std::string& output) { - outputBuffer.append(output); + RConOutputBuffer.append(output); }); Command::Execute(command, true); Logger::PipeOutput(nullptr); - Network::SendCommand(address, "print", outputBuffer); - outputBuffer.clear(); + Network::SendCommand(address, "print", RConOutputBuffer); + RConOutputBuffer.clear(); + } + + void RCon::RConSafeExecutor(const Network::Address& address, std::string command) + { + RConOutputBuffer.clear(); + +#ifndef _DEBUG + if (RConLogRequests.get()) +#endif + { + Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing Safe RCon request from {}: {}\n", address.getString(), command); + } + + Logger::PipeOutput([](const std::string& output) + { + RConOutputBuffer.append(output); + }); + + Command::Execute(command, true); + + Logger::PipeOutput(nullptr); + + Network::SendCommand(address, "print", RConOutputBuffer); + RConOutputBuffer.clear(); } RCon::RCon() @@ -203,16 +261,16 @@ namespace Components { Network::OnClientPacket("rconAuthorization", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { - if (RconContainer.command.empty()) + if (RConContainer.command.empty()) { return; } - const auto& key = CryptoKey::Get(); + const auto& key = CryptoKeyECC::Get(); const auto signedMsg = Utils::Cryptography::ECC::SignMessage(key, data); Proto::RCon::Command rconExec; - rconExec.set_command(RconContainer.command); + rconExec.set_command(RConContainer.command); rconExec.set_signature(signedMsg); Network::SendCommand(address, "rconExecute", rconExec.SerializeAsString()); @@ -238,21 +296,21 @@ namespace Components 0x08 }; - RconKey.set(std::string(reinterpret_cast(publicKey), sizeof(publicKey))); + RConKey.set(std::string(reinterpret_cast(publicKey), sizeof(publicKey))); - RconContainer.timestamp = 0; + RConContainer.timestamp = 0; Events::OnDvarInit([] { - RconPassword = Dvar::Register("rcon_password", "", Game::DVAR_NONE, "The password for rcon"); - RconLogRequests = Dvar::Register("rcon_log_requests", false, Game::DVAR_NONE, "Print remote commands in log"); - RconTimeout = Dvar::Register("rcon_timeout", 500, 100, 10000, Game::DVAR_NONE, ""); + RConPassword = Dvar::Register("rcon_password", "", Game::DVAR_NONE, "The password for rcon"); + RConLogRequests = Dvar::Register("rcon_log_requests", false, Game::DVAR_NONE, "Print remote commands in log"); + RConTimeout = Dvar::Register("rcon_timeout", 500, 100, 10000, Game::DVAR_NONE, ""); }); Network::OnClientPacket("rcon", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { const auto hash = std::hash()(*reinterpret_cast(&address.getIP().bytes[0])); - if (!RconAddresses.empty() && std::ranges::find(RconAddresses, hash) == RconAddresses.end()) + if (!RConAddresses.empty() && std::ranges::find(RConAddresses, hash) == RConAddresses.end()) { return; } @@ -268,50 +326,97 @@ namespace Components auto rconData = data; Scheduler::Once([address, s = std::move(rconData)] { - RconExecuter(address, s); + RConExecutor(address, s); + }, Scheduler::Pipeline::MAIN); + }); + + Network::OnClientPacket("rconSafe", [](const Network::Address& address, [[maybe_unused]] const std::string& data) -> void + { + const auto hash = std::hash()(*reinterpret_cast(&address.getIP().bytes[0])); + if (!RConAddresses.empty() && std::ranges::find(RConAddresses, hash) == RConAddresses.end()) + { + return; + } + + const auto time = Game::Sys_Milliseconds(); + if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time)) + { + return; + } + + RateLimitCleanup(time); + + if (!CryptoKeyRSA::HasPublicKey()) + { + return; + } + + auto& key = CryptoKeyRSA::GetPublicKey(); + if (!key.isValid()) + { + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "RSA public key is invalid\n"); + } + + Proto::RCon::Command directive; + if (!directive.ParseFromString(data)) + { + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "Unable to parse secure command from {}\n", address.getString()); + return; + } + + if (!Utils::Cryptography::RSA::VerifyMessage(key, directive.command(), directive.signature())) + { + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "RSA signature verification failed for message from {}\n", address.getString()); + return; + } + + std::string rconData = directive.command(); + Scheduler::Once([address, s = std::move(rconData)] + { + RConSafeExecutor(address, s); }, Scheduler::Pipeline::MAIN); }); Network::OnClientPacket("rconRequest", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { - RconContainer.address = address; - RconContainer.challenge = Utils::Cryptography::Rand::GenerateChallenge(); - RconContainer.timestamp = Game::Sys_Milliseconds(); + RConContainer.address = address; + RConContainer.challenge = Utils::Cryptography::Rand::GenerateChallenge(); + RConContainer.timestamp = Game::Sys_Milliseconds(); - Network::SendCommand(address, "rconAuthorization", RconContainer.challenge); + Network::SendCommand(address, "rconAuthorization", RConContainer.challenge); }); Network::OnClientPacket("rconExecute", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { - if (address != RconContainer.address) return; // Invalid IP - if (!RconContainer.timestamp || (Game::Sys_Milliseconds() - RconContainer.timestamp) > (1000 * 10)) return; // Timeout + if (address != RConContainer.address) return; // Invalid IP + if (!RConContainer.timestamp || (Game::Sys_Milliseconds() - RConContainer.timestamp) > (1000 * 10)) return; // Timeout - RconContainer.timestamp = 0; + RConContainer.timestamp = 0; Proto::RCon::Command rconExec; rconExec.ParseFromString(data); - if (!Utils::Cryptography::ECC::VerifyMessage(RconKey, RconContainer.challenge, rconExec.signature())) + if (!Utils::Cryptography::ECC::VerifyMessage(RConKey, RConContainer.challenge, rconExec.signature())) { return; } - RconContainer.output.clear(); + RConContainer.output.clear(); Logger::PipeOutput([](const std::string& output) { - RconContainer.output.append(output); + RConContainer.output.append(output); }); Command::Execute(rconExec.command(), true); Logger::PipeOutput(nullptr); - Network::SendCommand(address, "print", RconContainer.output); - RconContainer.output.clear(); + Network::SendCommand(address, "print", RConContainer.output); + RConContainer.output.clear(); }); } - bool RCon::CryptoKey::LoadKey(Utils::Cryptography::ECC::Key& key) + bool RCon::CryptoKeyECC::LoadKey(Utils::Cryptography::ECC::Key& key) { std::string data; if (!Utils::IO::ReadFile("./private.key", &data)) @@ -323,7 +428,7 @@ namespace Components return key.isValid(); } - Utils::Cryptography::ECC::Key RCon::CryptoKey::GenerateKey() + Utils::Cryptography::ECC::Key RCon::CryptoKeyECC::GenerateKey() { auto key = Utils::Cryptography::ECC::GenerateKey(512); if (!key.isValid()) @@ -339,7 +444,7 @@ namespace Components return key; } - Utils::Cryptography::ECC::Key RCon::CryptoKey::LoadOrGenerateKey() + Utils::Cryptography::ECC::Key RCon::CryptoKeyECC::LoadOrGenerateKey() { Utils::Cryptography::ECC::Key key; if (LoadKey(key)) @@ -350,16 +455,103 @@ namespace Components return GenerateKey(); } - Utils::Cryptography::ECC::Key RCon::CryptoKey::GetKeyInternal() + Utils::Cryptography::ECC::Key RCon::CryptoKeyECC::GetKeyInternal() { auto key = LoadOrGenerateKey(); Utils::IO::WriteFile("./public.key", key.getPublicKey()); return key; } - const Utils::Cryptography::ECC::Key& RCon::CryptoKey::Get() + Utils::Cryptography::ECC::Key& RCon::CryptoKeyECC::Get() { static auto key = GetKeyInternal(); return key; } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::LoadPublicKey() + { + Utils::Cryptography::RSA::Key key; + std::string data; + + if (!Utils::IO::ReadFile("./rsa-public.key", &data)) + { + return key; + } + + key.set(data); + return key; + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::GetPublicKeyInternal() + { + auto key = LoadPublicKey(); + return key; + } + + Utils::Cryptography::RSA::Key& RCon::CryptoKeyRSA::GetPublicKey() + { + static auto key = GetPublicKeyInternal(); + return key; + } + + bool RCon::CryptoKeyRSA::LoadPrivateKey(Utils::Cryptography::RSA::Key& key) + { + std::string data; + if (!Utils::IO::ReadFile("./rsa-private.key", &data)) + { + return false; + } + + key.set(data); + return key.isValid(); + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::GenerateKeyPair() + { + auto key = Utils::Cryptography::RSA::GenerateKey(4096); + if (!key.isValid()) + { + throw std::runtime_error("Failed to generate RSA key!"); + } + + if (!Utils::IO::WriteFile("./rsa-private.key", key.serialize(PK_PRIVATE))) + { + throw std::runtime_error("Failed to write RSA private key!"); + } + + if (!Utils::IO::WriteFile("./rsa-public.key", key.serialize(PK_PUBLIC))) + { + throw std::runtime_error("Failed to write RSA public key!"); + } + + return key; + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::LoadOrGeneratePrivateKey() + { + Utils::Cryptography::RSA::Key key; + if (LoadPrivateKey(key)) + { + return key; + } + + return GenerateKeyPair(); + } + + Utils::Cryptography::RSA::Key RCon::CryptoKeyRSA::GetPrivateKeyInternal() + { + auto key = LoadOrGeneratePrivateKey(); + return key; + } + + Utils::Cryptography::RSA::Key& RCon::CryptoKeyRSA::GetPrivateKey() + { + static auto key = GetPrivateKeyInternal(); + return key; + } + + bool RCon::CryptoKeyRSA::HasPublicKey() + { + return Utils::IO::FileExists("./rsa-public.key"); + } } diff --git a/src/Components/Modules/RCon.hpp b/src/Components/Modules/RCon.hpp index eef8d404..ef691ce1 100644 --- a/src/Components/Modules/RCon.hpp +++ b/src/Components/Modules/RCon.hpp @@ -11,17 +11,17 @@ namespace Components class Container { public: - int timestamp; - std::string output; - std::string command; - std::string challenge; - Network::Address address; + int timestamp{}; + std::string output{}; + std::string command{}; + std::string challenge{}; + Network::Address address{}; }; - class CryptoKey + class CryptoKeyECC { public: - static const Utils::Cryptography::ECC::Key& Get(); + static Utils::Cryptography::ECC::Key& Get(); private: static bool LoadKey(Utils::Cryptography::ECC::Key& key); static Utils::Cryptography::ECC::Key GenerateKey(); @@ -29,18 +29,39 @@ namespace Components static Utils::Cryptography::ECC::Key GetKeyInternal(); }; + class CryptoKeyRSA + { + public: + static bool HasPublicKey(); + + static Utils::Cryptography::RSA::Key& GetPublicKey(); + static Utils::Cryptography::RSA::Key& GetPrivateKey(); + + private: + static Utils::Cryptography::RSA::Key GenerateKeyPair(); + + static Utils::Cryptography::RSA::Key LoadPublicKey(); + static Utils::Cryptography::RSA::Key GetPublicKeyInternal(); + + static bool LoadPrivateKey(Utils::Cryptography::RSA::Key& key); + static Utils::Cryptography::RSA::Key LoadOrGeneratePrivateKey(); + static Utils::Cryptography::RSA::Key GetPrivateKeyInternal(); + }; + static std::unordered_map RateLimit; - static std::vector RconAddresses; + static std::vector RConAddresses; - static Container RconContainer; - static Utils::Cryptography::ECC::Key RconKey; + static Container RConContainer; + static Utils::Cryptography::ECC::Key RConKey; static std::string Password; - static Dvar::Var RconPassword; - static Dvar::Var RconLogRequests; - static Dvar::Var RconTimeout; + static std::string RConOutputBuffer; + + static Dvar::Var RConPassword; + static Dvar::Var RConLogRequests; + static Dvar::Var RConTimeout; static void AddCommands(); @@ -48,6 +69,7 @@ namespace Components static bool RateLimitCheck(const Network::Address& address, int time); static void RateLimitCleanup(int time); - static void RconExecuter(const Network::Address& address, std::string data); + static void RConExecutor(const Network::Address& address, std::string data); + static void RConSafeExecutor(const Network::Address& address, std::string command); }; }