summaryrefslogtreecommitdiff
path: root/Source/Core
diff options
context:
space:
mode:
authorLillyJadeKatrin <lilly.kitty.1988@gmail.com>2024-06-21 23:44:21 -0400
committerLillyJadeKatrin <lilly.kitty.1988@gmail.com>2024-07-07 13:42:09 -0400
commit0c14b0c8a70799072ddccb648006914af7c62855 (patch)
tree191a333148aeddfe0ba55c0bae5cde265bfe5416 /Source/Core
parent6ddfdc14836eaa24e08545486468dfdfd603f37a (diff)
Add Approved Patch Allowlist for Achievements
Prototype of a system to whitelist known game patches that are allowed to be used while RetroAchievements Hardcore mode is active. ApprovedInis.txt contains known hashes for the ini files as they appear in the repo, and can be compared to the local versions of these files to ensure they have not been edited locally by the player. ApprovedInis.txt is hashed and verified similarly first, with its hash residing as a const string within AchievementManager.h, ensuring ApprovedInis and the hashes within cannot be modified without editing Dolphin's source code and recompiling completely.
Diffstat (limited to 'Source/Core')
-rw-r--r--Source/Core/Core/AchievementManager.cpp72
-rw-r--r--Source/Core/Core/AchievementManager.h20
-rw-r--r--Source/Core/Core/PatchEngine.cpp10
3 files changed, 98 insertions, 4 deletions
diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp
index 7d7c0af374..1920ad0552 100644
--- a/Source/Core/Core/AchievementManager.cpp
+++ b/Source/Core/Core/AchievementManager.cpp
@@ -14,6 +14,7 @@
#include <rcheevos/include/rc_hash.h>
#include "Common/Assert.h"
+#include "Common/BitUtils.h"
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
@@ -26,6 +27,7 @@
#include "Core/Core.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/VideoInterface.h"
+#include "Core/PatchEngine.h"
#include "Core/PowerPC/MMU.h"
#include "Core/System.h"
#include "DiscIO/Blob.h"
@@ -70,6 +72,34 @@ void AchievementManager::Init()
}
}
+void AchievementManager::LoadApprovedList()
+{
+ picojson::value temp;
+ std::string error;
+ if (!JsonFromFile(fmt::format("{}{}{}", File::GetSysDirectory(), DIR_SEP, APPROVED_LIST_FILENAME),
+ &temp, &error))
+ {
+ WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load approved game settings list {}",
+ APPROVED_LIST_FILENAME);
+ WARN_LOG_FMT(ACHIEVEMENTS, "Error: {}", error);
+ return;
+ }
+ auto context = Common::SHA1::CreateContext();
+ context->Update(temp.serialize());
+ auto digest = context->Finish();
+ if (digest != APPROVED_LIST_HASH)
+ {
+ WARN_LOG_FMT(ACHIEVEMENTS, "Failed to verify approved game settings list {}",
+ APPROVED_LIST_FILENAME);
+ WARN_LOG_FMT(ACHIEVEMENTS, "Expected hash {}, found hash {}",
+ Common::SHA1::DigestToString(APPROVED_LIST_HASH),
+ Common::SHA1::DigestToString(digest));
+ return;
+ }
+ std::lock_guard lg{m_lock};
+ m_ini_root = std::move(temp);
+}
+
void AchievementManager::SetUpdateCallback(UpdateCallback callback)
{
m_update_callback = std::move(callback);
@@ -322,6 +352,48 @@ bool AchievementManager::IsHardcoreModeActive() const
return rc_client_is_processing_required(m_client);
}
+void AchievementManager::FilterApprovedPatches(std::vector<PatchEngine::Patch>& patches,
+ const std::string& game_ini_id) const
+{
+ if (!IsHardcoreModeActive())
+ return;
+
+ if (!m_ini_root.contains(game_ini_id))
+ patches.clear();
+ auto patch_itr = patches.begin();
+ while (patch_itr != patches.end())
+ {
+ INFO_LOG_FMT(ACHIEVEMENTS, "Verifying patch {}", patch_itr->name);
+
+ auto context = Common::SHA1::CreateContext();
+ context->Update(Common::BitCastToArray<u8>(static_cast<u64>(patch_itr->entries.size())));
+ for (const auto& entry : patch_itr->entries)
+ {
+ context->Update(Common::BitCastToArray<u8>(entry.type));
+ context->Update(Common::BitCastToArray<u8>(entry.address));
+ context->Update(Common::BitCastToArray<u8>(entry.value));
+ context->Update(Common::BitCastToArray<u8>(entry.comparand));
+ context->Update(Common::BitCastToArray<u8>(entry.conditional));
+ }
+ auto digest = context->Finish();
+
+ bool verified = m_ini_root.get(game_ini_id).contains(Common::SHA1::DigestToString(digest));
+ if (!verified)
+ {
+ patch_itr = patches.erase(patch_itr);
+ OSD::AddMessage(
+ fmt::format("Failed to verify patch {} from file {}.", patch_itr->name, game_ini_id),
+ OSD::Duration::VERY_LONG, OSD::Color::RED);
+ OSD::AddMessage("Disable hardcore mode to enable this patch.", OSD::Duration::VERY_LONG,
+ OSD::Color::RED);
+ }
+ else
+ {
+ patch_itr++;
+ }
+ }
+}
+
void AchievementManager::SetSpectatorMode()
{
rc_client_set_spectator_mode_enabled(m_client, Config::Get(Config::RA_SPECTATOR_ENABLED));
diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h
index 3996003040..efddd003a9 100644
--- a/Source/Core/Core/AchievementManager.h
+++ b/Source/Core/Core/AchievementManager.h
@@ -27,6 +27,7 @@
#include "Common/CommonTypes.h"
#include "Common/Event.h"
#include "Common/HttpRequest.h"
+#include "Common/JsonUtil.h"
#include "Common/WorkQueueThread.h"
#include "DiscIO/Volume.h"
#include "VideoCommon/Assets/CustomTextureData.h"
@@ -37,6 +38,11 @@ class CPUThreadGuard;
class System;
} // namespace Core
+namespace PatchEngine
+{
+struct Patch;
+} // namespace PatchEngine
+
class AchievementManager
{
public:
@@ -60,6 +66,10 @@ public:
static constexpr std::string_view GRAY = "transparent";
static constexpr std::string_view GOLD = "#FFD700";
static constexpr std::string_view BLUE = "#0B71C1";
+ static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json";
+ static const inline Common::SHA1::Digest APPROVED_LIST_HASH = {
+ 0x01, 0x1E, 0x2E, 0x74, 0xDD, 0x07, 0x79, 0xDA, 0x0E, 0x5D,
+ 0xF8, 0x51, 0x09, 0xC7, 0x9B, 0x46, 0x22, 0x95, 0x50, 0xE9};
struct LeaderboardEntry
{
@@ -109,6 +119,9 @@ public:
std::recursive_mutex& GetLock();
void SetHardcoreMode();
bool IsHardcoreModeActive() const;
+ void SetGameIniId(const std::string& game_ini_id) { m_game_ini_id = game_ini_id; }
+ void FilterApprovedPatches(std::vector<PatchEngine::Patch>& patches,
+ const std::string& game_ini_id) const;
void SetSpectatorMode();
std::string_view GetPlayerDisplayName() const;
u32 GetPlayerScore() const;
@@ -132,7 +145,7 @@ public:
void Shutdown();
private:
- AchievementManager() = default;
+ AchievementManager() { LoadApprovedList(); };
struct FilereaderState
{
@@ -140,6 +153,8 @@ private:
std::unique_ptr<DiscIO::Volume> volume;
};
+ void LoadApprovedList();
+
static void* FilereaderOpenByFilepath(const char* path_utf8);
static void* FilereaderOpenByVolume(const char* path_utf8);
static void FilereaderSeek(void* file_handle, int64_t offset, int origin);
@@ -211,6 +226,9 @@ private:
std::chrono::steady_clock::time_point m_last_rp_time = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point m_last_progress_message = std::chrono::steady_clock::now();
+ picojson::value m_ini_root;
+ std::string m_game_ini_id;
+
std::unordered_map<AchievementId, LeaderboardStatus> m_leaderboard_map;
bool m_challenges_updated = false;
std::unordered_set<AchievementId> m_active_challenges;
diff --git a/Source/Core/Core/PatchEngine.cpp b/Source/Core/Core/PatchEngine.cpp
index f020c754eb..0728637e47 100644
--- a/Source/Core/Core/PatchEngine.cpp
+++ b/Source/Core/Core/PatchEngine.cpp
@@ -182,6 +182,13 @@ void LoadPatches()
LoadPatchSection("OnFrame", &s_on_frame, globalIni, localIni);
+#ifdef USE_RETRO_ACHIEVEMENTS
+ {
+ std::lock_guard lg{AchievementManager::GetInstance().GetLock()};
+ AchievementManager::GetInstance().FilterApprovedPatches(s_on_frame, sconfig.GetGameID());
+ }
+#endif // USE_RETRO_ACHIEVEMENTS
+
// Check if I'm syncing Codes
if (Config::Get(Config::SESSION_CODE_SYNC_OVERRIDE))
{
@@ -197,9 +204,6 @@ void LoadPatches()
static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector<Patch>& patches)
{
- if (AchievementManager::GetInstance().IsHardcoreModeActive())
- return;
-
for (const Patch& patch : patches)
{
if (patch.enabled)