mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-29 09:09:52 -06:00
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.
This commit is contained in:
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user