diff --git a/CMakeLists.txt b/CMakeLists.txt index be52b07..40970da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -368,6 +368,7 @@ target_link_libraries(outrun2006tweaks PUBLIC SDL3-static Winmm.lib Setupapi.lib + Crypt32.lib ) target_link_options(outrun2006tweaks PUBLIC diff --git a/OutRun2006Tweaks.ini b/OutRun2006Tweaks.ini index 87e8644..2c6e576 100644 --- a/OutRun2006Tweaks.ini +++ b/OutRun2006Tweaks.ini @@ -268,8 +268,17 @@ RandomHighwayAnimSets = false # (Tweaks will try to port forward for you using UPnP, but you may need to forward ports 41455 / 41456 / 41457 in order to host games) DemonwareServerOverride = clarissa.port0.org +# Protects online login data by removing it from savegame files & encrypting against your Windows user account +# Making sure that your login details won't be exposed if you share savegame files +# (older non-protected data can be protected by changing one of the settings in-game, an OnlineLoginData.bin should then be created) +# +# NOTE: if you move your OR2006 install between different machines, the login data will likely fail to decrypt +# in that case the tweak can be disabled here +ProtectLoginData = true + [Overlay] -# Enables OR2006Tweaks overlay +# Enables the OutRun2006Tweaks overlay, accessible via F11 key +# (more settings for Overlay are available in the overlay itself) Enabled = true [Bugfixes] diff --git a/cmake.toml b/cmake.toml index b361e04..a58db08 100644 --- a/cmake.toml +++ b/cmake.toml @@ -101,10 +101,11 @@ link-libraries = [ "version.lib", "xinput9_1_0.lib", "Hid.lib", - "libminiupnpc-static", - "SDL3-static", - "Winmm.lib", - "Setupapi.lib" + "libminiupnpc-static", + "SDL3-static", + "Winmm.lib", + "Setupapi.lib", + "Crypt32.lib" ] [target.outrun2006tweaks.properties] diff --git a/src/dllmain.cpp b/src/dllmain.cpp index 0b37c55..68d9e05 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -131,6 +131,7 @@ namespace Settings spdlog::info(" - AllowCharacterSelection: {}", AllowCharacterSelection); spdlog::info(" - RandomHighwayAnimSets: {}", RandomHighwayAnimSets); spdlog::info(" - DemonwareServerOverride: {}", DemonwareServerOverride); + spdlog::info(" - ProtectLoginData: {}", ProtectLoginData); spdlog::info(" - OverlayEnabled: {}", OverlayEnabled); @@ -246,6 +247,7 @@ namespace Settings AllowCharacterSelection = ini.Get("Misc", "AllowCharacterSelection", AllowCharacterSelection); RandomHighwayAnimSets = ini.Get("Misc", "RandomHighwayAnimSets", RandomHighwayAnimSets); DemonwareServerOverride = ini.Get("Misc", "DemonwareServerOverride", DemonwareServerOverride); + ProtectLoginData = ini.Get("Misc", "ProtectLoginData", ProtectLoginData); OverlayEnabled = ini.Get("Overlay", "Enabled", OverlayEnabled); diff --git a/src/hooks_misc.cpp b/src/hooks_misc.cpp index 8f52a81..4b3314d 100644 --- a/src/hooks_misc.cpp +++ b/src/hooks_misc.cpp @@ -8,6 +8,172 @@ #include #include #include +#include +#include + +class ProtectLoginData : public Hook +{ + // C2C saves plaintext online login details into SaveGame/Common.dat by default + // It's not obvious this file contains login info though, so some might share it out freely + // Now that online is restored those details could be extracted and reused + // + // Instead of keeping it inside SaveGame/Common.dat, we'll store it in a seperate file next to game EXE + // and zero-out the data inside Common.dat before it gets saved to disk + // + // We'll also encrypt the data using CryptProtectData, which encrypts it against the users Windows account + // So even if the file does get shared, it'd be difficult for others to be able to decrypt it + const static inline std::string LoginDataFilename = "OnlineLoginData.dat"; + + static void EncryptDataToFile(const uint8_t* inputData, int dataLength, const std::filesystem::path& outputFilePath) + { + DATA_BLOB inputBlob = { static_cast(dataLength), const_cast(inputData) }; + DATA_BLOB outputBlob; + + if (!CryptProtectData(&inputBlob, L"OnlineLoginData", nullptr, nullptr, nullptr, 0, &outputBlob)) + throw std::runtime_error("CryptProtectData = false"); + else + { + std::ofstream outputFile(outputFilePath, std::ios::binary); + if (outputFile.is_open()) + { + outputFile.write((char*)outputBlob.pbData, outputBlob.cbData); + outputFile.close(); + } + LocalFree(outputBlob.pbData); + } + } + + static bool DecryptDataFromFile(uint8_t* outputData, int dataLength, const std::filesystem::path& inputFilePath) + { + if (!std::filesystem::exists(inputFilePath)) + return false; + + std::ifstream inputFile(inputFilePath, std::ios::binary | std::ios::ate); + if (inputFile.is_open()) + { + std::streamsize size = inputFile.tellg(); + inputFile.seekg(0, std::ios::beg); + std::vector encryptedData(size); + if (inputFile.read(encryptedData.data(), size)) + { + DATA_BLOB inputBlob = { static_cast(size), reinterpret_cast(encryptedData.data()) }; + DATA_BLOB outputBlob; + + if (!CryptUnprotectData(&inputBlob, nullptr, nullptr, nullptr, nullptr, 0, &outputBlob)) + throw std::runtime_error("CryptUnprotectData = false"); + else + { + std::memcpy(outputData, outputBlob.pbData, min(int(outputBlob.cbData), dataLength)); + LocalFree(outputBlob.pbData); + return true; + } + } + inputFile.close(); + } + + return false; + } + + inline static SafetyHookInline Sumo_CommonDat_Read_hook = {}; + static int Sumo_CommonDat_Read_dest() + { + auto ret = Sumo_CommonDat_Read_hook.call(); + + uint8_t* loginData = Module::exe_ptr(0x3C205C); + int loginDataLength = (0x10 * 8) + (8 * 8); + + try + { + DecryptDataFromFile(loginData, loginDataLength, Module::ExePath.parent_path() / LoginDataFilename); + } + catch (const std::exception& e) + { + memset(loginData, 0, loginDataLength); + spdlog::error("ProtectLoginData: Failed to decrypt login data from {}: {}", LoginDataFilename, e.what()); + } + + return ret; + } + inline static SafetyHookInline Sumo_CommonDat_Write_hook = {}; + static int Sumo_CommonDat_Write_dest() + { + constexpr int loginDataLength = (0x10 * 8) + (8 * 8); + uint8_t* loginData = Module::exe_ptr(0x3C205C); + + uint8_t tempLoginData[loginDataLength]; + memcpy(tempLoginData, loginData, loginDataLength); + + try + { + EncryptDataToFile(loginData, loginDataLength, Module::ExePath.parent_path() / LoginDataFilename); + } + catch (const std::exception& e) + { + spdlog::error("ProtectLoginData: failed to encrypt login data to {} ({}), login details will be scrubbed from common.dat", e.what(), LoginDataFilename); + } + + SecureZeroMemory(loginData, loginDataLength); + + auto ret = Sumo_CommonDat_Write_hook.call(); + + memcpy(loginData, tempLoginData, loginDataLength); + + return ret; + } + inline static SafetyHookInline Sumo_CommonDat_WriteRaw_hook = {}; + static int Sumo_CommonDat_WriteRaw_dest() + { + constexpr int loginDataLength = (0x10 * 8) + (8 * 8); + uint8_t* loginData = Module::exe_ptr(0x3C205C); + + uint8_t tempLoginData[loginDataLength]; + memcpy(tempLoginData, loginData, loginDataLength); + + try + { + EncryptDataToFile(loginData, loginDataLength, Module::ExePath.parent_path() / LoginDataFilename); + } + catch (const std::exception& e) + { + spdlog::error("ProtectLoginData: failed to encrypt login data to {} ({}), login details will be scrubbed from common.dat", e.what(), LoginDataFilename); + } + + SecureZeroMemory(loginData, loginDataLength); + + auto ret = Sumo_CommonDat_WriteRaw_hook.call(); + + memcpy(loginData, tempLoginData, loginDataLength); + + return ret; + } + +public: + std::string_view description() override + { + return "ProtectLoginData"; + } + + bool validate() override + { + return Settings::ProtectLoginData; + } + + bool apply() override + { + constexpr int Sumo_CommonDat_Read_Addr = 0x16380; + constexpr int Sumo_CommonDat_Write_Addr = 0x16420; + constexpr int Sumo_CommonDat_WriteRaw_Addr = 0x164D0; + + Sumo_CommonDat_Read_hook = safetyhook::create_inline(Module::exe_ptr(Sumo_CommonDat_Read_Addr), Sumo_CommonDat_Read_dest); + Sumo_CommonDat_Write_hook = safetyhook::create_inline(Module::exe_ptr(Sumo_CommonDat_Write_Addr), Sumo_CommonDat_Write_dest); + Sumo_CommonDat_WriteRaw_hook = safetyhook::create_inline(Module::exe_ptr(Sumo_CommonDat_WriteRaw_Addr), Sumo_CommonDat_WriteRaw_dest); + + return true; + } + + static ProtectLoginData instance; +}; +ProtectLoginData ProtectLoginData::instance; class PlaySegaJingle : public Hook { diff --git a/src/plugin.hpp b/src/plugin.hpp index 989350e..51af675 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -151,6 +151,7 @@ namespace Settings inline bool AllowCharacterSelection = false; inline bool RandomHighwayAnimSets = false; inline std::string DemonwareServerOverride = "clarissa.port0.org"; + inline bool ProtectLoginData = true; inline bool OverlayEnabled = true;