From c898071b723d6e8b539d034d11d6f16b011cbee9 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Mon, 19 Jan 2026 18:49:57 +0200 Subject: [PATCH] Introducing key_manager for storing encryption keys . (#3935) * Introducing key_manager for storing encryption keys . Currently only trophy key is neccesary * keep gcc happy? * addded logging to keymanager * revert file * added npbind file format and rewrote part of trp file format --- CMakeLists.txt | 6 +- src/common/key_manager.cpp | 161 ++++++++++++++++++ src/common/key_manager.h | 79 +++++++++ src/common/logging/filter.cpp | 2 + src/common/logging/types.h | 2 + src/core/file_format/npbind.cpp | 114 +++++++++++++ src/core/file_format/npbind.h | 87 ++++++++++ src/core/file_format/trp.cpp | 280 +++++++++++++++++++++++--------- src/core/file_format/trp.h | 11 +- src/main.cpp | 11 ++ 10 files changed, 675 insertions(+), 78 deletions(-) create mode 100644 src/common/key_manager.cpp create mode 100644 src/common/key_manager.h create mode 100644 src/core/file_format/npbind.cpp create mode 100644 src/core/file_format/npbind.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ab144aa37..929e0ebc7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +# SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later # Version 3.24 needed for FetchContent OVERRIDE_FIND_PACKAGE @@ -744,6 +744,8 @@ set(COMMON src/common/logging/backend.cpp src/common/memory_patcher.cpp ${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp src/common/scm_rev.h + src/common/key_manager.cpp + src/common/key_manager.h ) if (ENABLE_DISCORD_RPC) @@ -787,6 +789,8 @@ set(CORE src/core/aerolib/stubs.cpp src/core/file_format/playgo_chunk.h src/core/file_format/trp.cpp src/core/file_format/trp.h + src/core/file_format/npbind.cpp + src/core/file_format/npbind.h src/core/file_sys/fs.cpp src/core/file_sys/fs.h src/core/ipc/ipc.cpp diff --git a/src/common/key_manager.cpp b/src/common/key_manager.cpp new file mode 100644 index 000000000..cd0f668bf --- /dev/null +++ b/src/common/key_manager.cpp @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "common/logging/log.h" +#include "key_manager.h" +#include "path_util.h" + +std::shared_ptr KeyManager::s_instance = nullptr; +std::mutex KeyManager::s_mutex; + +// ------------------- Constructor & Singleton ------------------- +KeyManager::KeyManager() { + SetDefaultKeys(); +} +KeyManager::~KeyManager() { + SaveToFile(); +} + +std::shared_ptr KeyManager::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void KeyManager::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = instance; +} + +// ------------------- Load / Save ------------------- +bool KeyManager::LoadFromFile() { + try { + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto keysPath = userDir / "keys.json"; + + if (!std::filesystem::exists(keysPath)) { + SetDefaultKeys(); + SaveToFile(); + LOG_DEBUG(KeyManager, "Created default key file: {}", keysPath.string()); + return true; + } + + std::ifstream file(keysPath); + if (!file.is_open()) { + LOG_ERROR(KeyManager, "Could not open key file: {}", keysPath.string()); + return false; + } + + json j; + file >> j; + + SetDefaultKeys(); // start from defaults + + if (j.contains("TrophyKeySet")) + j.at("TrophyKeySet").get_to(m_keys.TrophyKeySet); + + LOG_DEBUG(KeyManager, "Successfully loaded keys from: {}", keysPath.string()); + return true; + + } catch (const std::exception& e) { + LOG_ERROR(KeyManager, "Error loading keys, using defaults: {}", e.what()); + SetDefaultKeys(); + return false; + } +} + +bool KeyManager::SaveToFile() { + try { + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto keysPath = userDir / "keys.json"; + + json j; + KeysToJson(j); + + std::ofstream file(keysPath); + if (!file.is_open()) { + LOG_ERROR(KeyManager, "Could not open key file for writing: {}", keysPath.string()); + return false; + } + + file << std::setw(4) << j; + file.flush(); + + if (file.fail()) { + LOG_ERROR(KeyManager, "Failed to write keys to: {}", keysPath.string()); + return false; + } + + LOG_DEBUG(KeyManager, "Successfully saved keys to: {}", keysPath.string()); + return true; + + } catch (const std::exception& e) { + LOG_ERROR(KeyManager, "Error saving keys: {}", e.what()); + return false; + } +} + +// ------------------- JSON conversion ------------------- +void KeyManager::KeysToJson(json& j) const { + j = m_keys; +} +void KeyManager::JsonToKeys(const json& j) { + json current = m_keys; // serialize current defaults + current.update(j); // merge only fields present in file + m_keys = current.get(); // deserialize back +} + +// ------------------- Defaults / Checks ------------------- +void KeyManager::SetDefaultKeys() { + m_keys = AllKeys{}; +} + +bool KeyManager::HasKeys() const { + return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty(); +} + +// ------------------- Hex conversion ------------------- +std::vector KeyManager::HexStringToBytes(const std::string& hexStr) { + std::vector bytes; + if (hexStr.empty()) + return bytes; + + if (hexStr.size() % 2 != 0) + throw std::runtime_error("Invalid hex string length"); + + bytes.reserve(hexStr.size() / 2); + + auto hexCharToInt = [](char c) -> u8 { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + throw std::runtime_error("Invalid hex character"); + }; + + for (size_t i = 0; i < hexStr.size(); i += 2) { + u8 high = hexCharToInt(hexStr[i]); + u8 low = hexCharToInt(hexStr[i + 1]); + bytes.push_back((high << 4) | low); + } + + return bytes; +} + +std::string KeyManager::BytesToHexString(const std::vector& bytes) { + static const char hexDigits[] = "0123456789ABCDEF"; + std::string hexStr; + hexStr.reserve(bytes.size() * 2); + for (u8 b : bytes) { + hexStr.push_back(hexDigits[(b >> 4) & 0xF]); + hexStr.push_back(hexDigits[b & 0xF]); + } + return hexStr; +} \ No newline at end of file diff --git a/src/common/key_manager.h b/src/common/key_manager.h new file mode 100644 index 000000000..8925ccbd0 --- /dev/null +++ b/src/common/key_manager.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/types.h" +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +class KeyManager { +public: + // ------------------- Nested keysets ------------------- + struct TrophyKeySet { + std::vector ReleaseTrophyKey; + }; + + struct AllKeys { + KeyManager::TrophyKeySet TrophyKeySet; + }; + + // ------------------- Construction ------------------- + KeyManager(); + ~KeyManager(); + + // ------------------- Singleton ------------------- + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + + // ------------------- File operations ------------------- + bool LoadFromFile(); + bool SaveToFile(); + + // ------------------- Key operations ------------------- + void SetDefaultKeys(); + bool HasKeys() const; + + // ------------------- Getters / Setters ------------------- + const AllKeys& GetAllKeys() const { + return m_keys; + } + void SetAllKeys(const AllKeys& keys) { + m_keys = keys; + } + + static std::vector HexStringToBytes(const std::string& hexStr); + static std::string BytesToHexString(const std::vector& bytes); + +private: + void KeysToJson(json& j) const; + void JsonToKeys(const json& j); + + AllKeys m_keys{}; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; +}; + +// ------------------- NLOHMANN macros ------------------- +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::TrophyKeySet, ReleaseTrophyKey) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::AllKeys, TrophyKeySet) + +namespace nlohmann { +template <> +struct adl_serializer> { + static void to_json(json& j, const std::vector& vec) { + j = KeyManager::BytesToHexString(vec); + } + static void from_json(const json& j, std::vector& vec) { + vec = KeyManager::HexStringToBytes(j.get()); + } +}; +} // namespace nlohmann diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index d954f8601..9c8b80255 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -159,6 +160,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(ImGui) \ CLS(Input) \ CLS(Tty) \ + CLS(KeyManager) \ CLS(Loader) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/common/logging/types.h b/src/common/logging/types.h index ee18cd161..dc7c561a4 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2023 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -127,6 +128,7 @@ enum class Class : u8 { Loader, ///< ROM loader Input, ///< Input emulation Tty, ///< Debug output from emu + KeyManager, ///< Key management system Count ///< Total number of logging classes }; diff --git a/src/core/file_format/npbind.cpp b/src/core/file_format/npbind.cpp new file mode 100644 index 000000000..b2900efa0 --- /dev/null +++ b/src/core/file_format/npbind.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include "npbind.h" + +bool NPBindFile::Load(const std::string& path) { + Clear(); // Clear any existing data + + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) + return false; + + std::streamsize sz = f.tellg(); + if (sz <= 0) + return false; + + f.seekg(0, std::ios::beg); + std::vector buf(static_cast(sz)); + if (!f.read(reinterpret_cast(buf.data()), sz)) + return false; + + const u64 size = buf.size(); + if (size < sizeof(NpBindHeader)) + return false; + + // Read header + memcpy(&m_header, buf.data(), sizeof(NpBindHeader)); + if (m_header.magic != NPBIND_MAGIC) + return false; + + // offset start of bodies + size_t offset = sizeof(NpBindHeader); + + m_bodies.reserve(static_cast(m_header.num_entries)); + + // For each body: read 4 TLV entries then skip padding (0x98 = 152 bytes) + const u64 body_padding = 0x98; // 152 + + for (u64 bi = 0; bi < m_header.num_entries; ++bi) { + // Ensure we have room for 4 entries' headers at least + if (offset + 4 * 4 > size) + return false; // 4 entries x (type+size) + + NPBindBody body; + + // helper lambda to read one entry + auto read_entry = [&](NPBindEntryRaw& e) -> bool { + if (offset + 4 > size) + return false; + + memcpy(&e.type, &buf[offset], 2); + memcpy(&e.size, &buf[offset + 2], 2); + offset += 4; + + if (offset + e.size > size) + return false; + + e.data.assign(buf.begin() + offset, buf.begin() + offset + e.size); + offset += e.size; + return true; + }; + + // read 4 entries in order + if (!read_entry(body.npcommid)) + return false; + if (!read_entry(body.trophy)) + return false; + if (!read_entry(body.unk1)) + return false; + if (!read_entry(body.unk2)) + return false; + + // skip fixed padding after body if present (but don't overrun) + if (offset + body_padding <= size) { + offset += body_padding; + } else { + // If padding not fully present, allow file to end (some variants may omit) + offset = size; + } + + m_bodies.push_back(std::move(body)); + } + + // Read digest if available + if (size >= 20) { + // Digest is typically the last 20 bytes, independent of offset + memcpy(m_digest, &buf[size - 20], 20); + } else { + memset(m_digest, 0, 20); + } + + return true; +} + +std::vector NPBindFile::GetNpCommIds() const { + std::vector npcommids; + npcommids.reserve(m_bodies.size()); + + for (const auto& body : m_bodies) { + // Convert binary data to string directly + if (!body.npcommid.data.empty()) { + std::string raw_string(reinterpret_cast(body.npcommid.data.data()), + body.npcommid.data.size()); + npcommids.push_back(raw_string); + } + } + + return npcommids; +} \ No newline at end of file diff --git a/src/core/file_format/npbind.h b/src/core/file_format/npbind.h new file mode 100644 index 000000000..44d6528bd --- /dev/null +++ b/src/core/file_format/npbind.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include +#include "common/endian.h" +#include "common/types.h" + +#define NPBIND_MAGIC 0xD294A018u + +#pragma pack(push, 1) +struct NpBindHeader { + u32_be magic; + u32_be version; + u64_be file_size; + u64_be entry_size; + u64_be num_entries; + char padding[0x60]; // 96 bytes +}; +#pragma pack(pop) + +struct NPBindEntryRaw { + u16_be type; + u16_be size; // includes internal padding + std::vector data; +}; + +struct NPBindBody { + NPBindEntryRaw npcommid; // expected type 0x0010, size 12 + NPBindEntryRaw trophy; // expected type 0x0011, size 12 + NPBindEntryRaw unk1; // expected type 0x0012, size 176 + NPBindEntryRaw unk2; // expected type 0x0013, size 16 + // The 0x98 padding after these entries is skipped while parsing +}; + +class NPBindFile { +private: + NpBindHeader m_header; + std::vector m_bodies; + u8 m_digest[20]; // zeroed if absent + +public: + NPBindFile() { + memset(m_digest, 0, sizeof(m_digest)); + } + + // Load from file + bool Load(const std::string& path); + + // Accessors + const NpBindHeader& Header() const { + return m_header; + } + const std::vector& Bodies() const { + return m_bodies; + } + const u8* Digest() const { + return m_digest; + } + + // Get npcommid data + std::vector GetNpCommIds() const; + + // Get specific body + const NPBindBody& GetBody(size_t index) const { + return m_bodies.at(index); + } + + // Get number of bodies + u64 BodyCount() const { + return m_bodies.size(); + } + + // Check if file was loaded successfully + bool IsValid() const { + return m_header.magic == NPBIND_MAGIC; + } + + // Clear all data + void Clear() { + m_header = NpBindHeader{}; + m_bodies.clear(); + memset(m_digest, 0, sizeof(m_digest)); + } +}; \ No newline at end of file diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp index ab873d451..f0a258c12 100644 --- a/src/core/file_format/trp.cpp +++ b/src/core/file_format/trp.cpp @@ -1,44 +1,31 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/aes.h" -#include "common/config.h" +#include "common/key_manager.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "core/file_format/npbind.h" #include "core/file_format/trp.h" -static void DecryptEFSM(std::span trophyKey, std::span NPcommID, - std::span efsmIv, std::span ciphertext, +static void DecryptEFSM(std::span trophyKey, std::span NPcommID, + std::span efsmIv, std::span ciphertext, std::span decrypted) { // Step 1: Encrypt NPcommID std::array trophyIv{}; std::array trpKey; + // Convert spans to pointers for the aes functions aes::encrypt_cbc(NPcommID.data(), NPcommID.size(), trophyKey.data(), trophyKey.size(), trophyIv.data(), trpKey.data(), trpKey.size(), false); // Step 2: Decrypt EFSM aes::decrypt_cbc(ciphertext.data(), ciphertext.size(), trpKey.data(), trpKey.size(), - efsmIv.data(), decrypted.data(), decrypted.size(), nullptr); + const_cast(efsmIv.data()), decrypted.data(), decrypted.size(), nullptr); } TRP::TRP() = default; TRP::~TRP() = default; -void TRP::GetNPcommID(const std::filesystem::path& trophyPath, int index) { - std::filesystem::path trpPath = trophyPath / "sce_sys/npbind.dat"; - Common::FS::IOFile npbindFile(trpPath, Common::FS::FileAccessMode::Read); - if (!npbindFile.IsOpen()) { - LOG_CRITICAL(Common_Filesystem, "Failed to open npbind.dat file"); - return; - } - if (!npbindFile.Seek(0x84 + (index * 0x180))) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to NPbind offset"); - return; - } - npbindFile.ReadRaw(np_comm_id.data(), 12); - std::fill(np_comm_id.begin() + 12, np_comm_id.end(), 0); // fill with 0, we need 16 bytes. -} - static void removePadding(std::vector& vec) { for (auto it = vec.rbegin(); it != vec.rend(); ++it) { if (*it == '>') { @@ -63,91 +50,232 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit return false; } - const auto user_key_str = Config::getTrophyKey(); - if (user_key_str.size() != 32) { + const auto& user_key_vec = + KeyManager::GetInstance()->GetAllKeys().TrophyKeySet.ReleaseTrophyKey; + + if (user_key_vec.size() != 16) { LOG_INFO(Common_Filesystem, "Trophy decryption key is not specified"); return false; } std::array user_key{}; - hexToBytes(user_key_str.c_str(), user_key.data()); + std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin()); - for (int index = 0; const auto& it : std::filesystem::directory_iterator(gameSysDir)) { - if (it.is_regular_file()) { - GetNPcommID(trophyPath, index); + // Load npbind.dat using the new class + std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat"; + NPBindFile npbind; + if (!npbind.Load(npbindPath.string())) { + LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); + } + + auto npCommIds = npbind.GetNpCommIds(); + if (npCommIds.empty()) { + LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat"); + } + + bool success = true; + int trpFileIndex = 0; + + try { + // Process each TRP file in the trophy directory + for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) { + if (!it.is_regular_file() || it.path().extension() != ".trp") { + continue; // Skip non-TRP files + } + + // Get NPCommID for this TRP file (if available) + std::string npCommId; + if (trpFileIndex < static_cast(npCommIds.size())) { + npCommId = npCommIds[trpFileIndex]; + LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId, + it.path().filename().string()); + } else { + LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}", + trpFileIndex); + } Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read); if (!file.IsOpen()) { - LOG_CRITICAL(Common_Filesystem, "Unable to open trophy file for read"); - return false; + LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string()); + success = false; + continue; } TrpHeader header; - file.Read(header); - if (header.magic != 0xDCA24D00) { - LOG_CRITICAL(Common_Filesystem, "Wrong trophy magic number"); - return false; + if (!file.Read(header)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", + it.path().string()); + success = false; + continue; + } + + if (header.magic != TRP_MAGIC) { + LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string()); + success = false; + continue; } s64 seekPos = sizeof(TrpHeader); std::filesystem::path trpFilesPath( Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId / "TrophyFiles" / it.path().stem()); - std::filesystem::create_directories(trpFilesPath / "Icons"); - std::filesystem::create_directory(trpFilesPath / "Xml"); + // Create output directories + if (!std::filesystem::create_directories(trpFilesPath / "Icons") || + !std::filesystem::create_directories(trpFilesPath / "Xml")) { + LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId); + success = false; + continue; + } + + // Process each entry in the TRP file for (int i = 0; i < header.entry_num; i++) { if (!file.Seek(seekPos)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); - return false; + LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset"); + success = false; + break; } - seekPos += (s64)header.entry_size; + seekPos += static_cast(header.entry_size); + TrpEntry entry; - file.Read(entry); - std::string_view name(entry.entry_name); - if (entry.flag == 0) { // PNG - if (!file.Seek(entry.entry_pos)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); - return false; - } - std::vector icon(entry.entry_len); - file.Read(icon); - Common::FS::IOFile::WriteBytes(trpFilesPath / "Icons" / name, icon); + if (!file.Read(entry)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP entry"); + success = false; + break; } - if (entry.flag == 3 && np_comm_id[0] == 'N' && - np_comm_id[1] == 'P') { // ESFM, encrypted. - if (!file.Seek(entry.entry_pos)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); - return false; + + std::string_view name(entry.entry_name); + + if (entry.flag == ENTRY_FLAG_PNG) { + if (!ProcessPngEntry(file, entry, trpFilesPath, name)) { + success = false; + // Continue with next entry } - file.Read(esfmIv); // get iv key. - // Skip the first 16 bytes which are the iv key on every entry as we want a - // clean xml file. - std::vector ESFM(entry.entry_len - iv_len); - std::vector XML(entry.entry_len - iv_len); - if (!file.Seek(entry.entry_pos + iv_len)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset"); - return false; - } - file.Read(ESFM); - DecryptEFSM(user_key, np_comm_id, esfmIv, ESFM, XML); // decrypt - removePadding(XML); - std::string xml_name = entry.entry_name; - size_t pos = xml_name.find("ESFM"); - if (pos != std::string::npos) - xml_name.replace(pos, xml_name.length(), "XML"); - std::filesystem::path path = trpFilesPath / "Xml" / xml_name; - size_t written = Common::FS::IOFile::WriteBytes(path, XML); - if (written != XML.size()) { - LOG_CRITICAL( - Common_Filesystem, - "Trophy XML {} write failed, wanted to write {} bytes, wrote {}", - fmt::UTF(path.u8string()), XML.size(), written); + } else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) { + // Check if we have a valid NPCommID for decryption + if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') { + if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key, + npCommId)) { + success = false; + // Continue with next entry + } + } else { + LOG_WARNING(Common_Filesystem, + "Skipping encrypted XML entry - invalid NPCommID"); + // Skip this entry but continue } + } else { + LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}", + static_cast(entry.flag), name); } } + + trpFileIndex++; } - index++; + } catch (const std::filesystem::filesystem_error& e) { + LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what()); + return false; + } catch (const std::exception& e) { + LOG_CRITICAL(Common_Filesystem, "Error during trophy extraction: {}", e.what()); + return false; } + + if (success) { + LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex, + titleId); + } + + return success; +} + +bool TRP::ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name) { + if (!file.Seek(entry.entry_pos)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to PNG entry offset"); + return false; + } + + std::vector icon(entry.entry_len); + if (!file.Read(icon)) { + LOG_ERROR(Common_Filesystem, "Failed to read PNG data"); + return false; + } + + auto outputFile = outputPath / "Icons" / name; + size_t written = Common::FS::IOFile::WriteBytes(outputFile, icon); + if (written != icon.size()) { + LOG_ERROR(Common_Filesystem, "PNG write failed: wanted {} bytes, wrote {}", icon.size(), + written); + return false; + } + return true; } + +bool TRP::ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name, + const std::array& user_key, + const std::string& npCommId) { + constexpr size_t IV_LEN = 16; + + if (!file.Seek(entry.entry_pos)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted XML entry offset"); + return false; + } + + std::array esfmIv; + if (!file.Read(esfmIv)) { + LOG_ERROR(Common_Filesystem, "Failed to read IV for encrypted XML"); + return false; + } + + if (entry.entry_len <= IV_LEN) { + LOG_ERROR(Common_Filesystem, "Encrypted XML entry too small"); + return false; + } + + // Skip to the encrypted data (after IV) + if (!file.Seek(entry.entry_pos + IV_LEN)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted data"); + return false; + } + + std::vector ESFM(entry.entry_len - IV_LEN); + std::vector XML(entry.entry_len - IV_LEN); + + if (!file.Read(ESFM)) { + LOG_ERROR(Common_Filesystem, "Failed to read encrypted XML data"); + return false; + } + + // Decrypt the data - FIX: Don't check return value since DecryptEFSM returns void + std::span key_span(user_key); + + // Convert npCommId string to span (pad or truncate to 16 bytes) + std::array npcommid_array{}; + size_t copy_len = std::min(npCommId.size(), npcommid_array.size()); + std::memcpy(npcommid_array.data(), npCommId.data(), copy_len); + std::span npcommid_span(npcommid_array); + + DecryptEFSM(key_span, npcommid_span, esfmIv, ESFM, XML); + + // Remove padding + removePadding(XML); + + // Create output filename (replace ESFM with XML) + std::string xml_name(entry.entry_name); + size_t pos = xml_name.find("ESFM"); + if (pos != std::string::npos) { + xml_name.replace(pos, 4, "XML"); + } + + auto outputFile = outputPath / "Xml" / xml_name; + size_t written = Common::FS::IOFile::WriteBytes(outputFile, XML); + if (written != XML.size()) { + LOG_ERROR(Common_Filesystem, "XML write failed: wanted {} bytes, wrote {}", XML.size(), + written); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/core/file_format/trp.h b/src/core/file_format/trp.h index 01207475b..2b52a4d57 100644 --- a/src/core/file_format/trp.h +++ b/src/core/file_format/trp.h @@ -8,6 +8,10 @@ #include "common/io_file.h" #include "common/types.h" +static constexpr u32 TRP_MAGIC = 0xDCA24D00; +static constexpr u8 ENTRY_FLAG_PNG = 0; +static constexpr u8 ENTRY_FLAG_ENCRYPTED_XML = 3; + struct TrpHeader { u32_be magic; // (0xDCA24D00) u32_be version; @@ -33,9 +37,14 @@ public: TRP(); ~TRP(); bool Extract(const std::filesystem::path& trophyPath, const std::string titleId); - void GetNPcommID(const std::filesystem::path& trophyPath, int index); private: + bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name); + bool ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name, + const std::array& user_key, const std::string& npCommId); + std::vector NPcommID = std::vector(12); std::array np_comm_id{}; std::array esfmIv{}; diff --git a/src/main.cpp b/src/main.cpp index aa3f4de45..b09ea7f4d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,7 @@ #ifdef _WIN32 #include #endif +#include int main(int argc, char* argv[]) { #ifdef _WIN32 @@ -34,7 +35,17 @@ int main(int argc, char* argv[]) { // Load configurations const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml"); + // temp copy the trophy key from old config to key manager if exists + auto key_manager = KeyManager::GetInstance(); + if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty()) { + if (!Config::getTrophyKey().empty()) { + key_manager->SetAllKeys( + {.TrophyKeySet = {.ReleaseTrophyKey = + KeyManager::HexStringToBytes(Config::getTrophyKey())}}); + key_manager->SaveToFile(); + } + } bool has_game_argument = false; std::string game_path; std::vector game_args{};