mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-01-31 00:55:19 +01:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
161
src/common/key_manager.cpp
Normal file
161
src/common/key_manager.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include "common/logging/log.h"
|
||||
#include "key_manager.h"
|
||||
#include "path_util.h"
|
||||
|
||||
std::shared_ptr<KeyManager> KeyManager::s_instance = nullptr;
|
||||
std::mutex KeyManager::s_mutex;
|
||||
|
||||
// ------------------- Constructor & Singleton -------------------
|
||||
KeyManager::KeyManager() {
|
||||
SetDefaultKeys();
|
||||
}
|
||||
KeyManager::~KeyManager() {
|
||||
SaveToFile();
|
||||
}
|
||||
|
||||
std::shared_ptr<KeyManager> KeyManager::GetInstance() {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
if (!s_instance)
|
||||
s_instance = std::make_shared<KeyManager>();
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void KeyManager::SetInstance(std::shared_ptr<KeyManager> instance) {
|
||||
std::lock_guard<std::mutex> 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<AllKeys>(); // deserialize back
|
||||
}
|
||||
|
||||
// ------------------- Defaults / Checks -------------------
|
||||
void KeyManager::SetDefaultKeys() {
|
||||
m_keys = AllKeys{};
|
||||
}
|
||||
|
||||
bool KeyManager::HasKeys() const {
|
||||
return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty();
|
||||
}
|
||||
|
||||
// ------------------- Hex conversion -------------------
|
||||
std::vector<u8> KeyManager::HexStringToBytes(const std::string& hexStr) {
|
||||
std::vector<u8> 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<u8>& 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;
|
||||
}
|
||||
79
src/common/key_manager.h
Normal file
79
src/common/key_manager.h
Normal file
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/types.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
class KeyManager {
|
||||
public:
|
||||
// ------------------- Nested keysets -------------------
|
||||
struct TrophyKeySet {
|
||||
std::vector<u8> ReleaseTrophyKey;
|
||||
};
|
||||
|
||||
struct AllKeys {
|
||||
KeyManager::TrophyKeySet TrophyKeySet;
|
||||
};
|
||||
|
||||
// ------------------- Construction -------------------
|
||||
KeyManager();
|
||||
~KeyManager();
|
||||
|
||||
// ------------------- Singleton -------------------
|
||||
static std::shared_ptr<KeyManager> GetInstance();
|
||||
static void SetInstance(std::shared_ptr<KeyManager> 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<u8> HexStringToBytes(const std::string& hexStr);
|
||||
static std::string BytesToHexString(const std::vector<u8>& bytes);
|
||||
|
||||
private:
|
||||
void KeysToJson(json& j) const;
|
||||
void JsonToKeys(const json& j);
|
||||
|
||||
AllKeys m_keys{};
|
||||
|
||||
static std::shared_ptr<KeyManager> 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<std::vector<u8>> {
|
||||
static void to_json(json& j, const std::vector<u8>& vec) {
|
||||
j = KeyManager::BytesToHexString(vec);
|
||||
}
|
||||
static void from_json(const json& j, std::vector<u8>& vec) {
|
||||
vec = KeyManager::HexStringToBytes(j.get<std::string>());
|
||||
}
|
||||
};
|
||||
} // namespace nlohmann
|
||||
@@ -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 <algorithm>
|
||||
@@ -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...
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
114
src/core/file_format/npbind.cpp
Normal file
114
src/core/file_format/npbind.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#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<u8> buf(static_cast<size_t>(sz));
|
||||
if (!f.read(reinterpret_cast<char*>(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<size_t>(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<std::string> NPBindFile::GetNpCommIds() const {
|
||||
std::vector<std::string> 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<const char*>(body.npcommid.data.data()),
|
||||
body.npcommid.data.size());
|
||||
npcommids.push_back(raw_string);
|
||||
}
|
||||
}
|
||||
|
||||
return npcommids;
|
||||
}
|
||||
87
src/core/file_format/npbind.h
Normal file
87
src/core/file_format/npbind.h
Normal file
@@ -0,0 +1,87 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#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<u8> 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<NPBindBody> 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<NPBindBody>& Bodies() const {
|
||||
return m_bodies;
|
||||
}
|
||||
const u8* Digest() const {
|
||||
return m_digest;
|
||||
}
|
||||
|
||||
// Get npcommid data
|
||||
std::vector<std::string> 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));
|
||||
}
|
||||
};
|
||||
@@ -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<u8, 16> trophyKey, std::span<u8, 16> NPcommID,
|
||||
std::span<u8, 16> efsmIv, std::span<u8> ciphertext,
|
||||
static void DecryptEFSM(std::span<const u8, 16> trophyKey, std::span<const u8, 16> NPcommID,
|
||||
std::span<const u8, 16> efsmIv, std::span<const u8> ciphertext,
|
||||
std::span<u8> decrypted) {
|
||||
// Step 1: Encrypt NPcommID
|
||||
std::array<u8, 16> trophyIv{};
|
||||
std::array<u8, 16> 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<u8*>(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<u8>(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<u8>& 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<u8, 16> 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<int>(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<s64>(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<u8> 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<u8> ESFM(entry.entry_len - iv_len);
|
||||
std::vector<u8> 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<unsigned int>(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<u8> 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<u8, 16>& 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<u8, IV_LEN> 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<u8> ESFM(entry.entry_len - IV_LEN);
|
||||
std::vector<u8> 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<const u8, 16> key_span(user_key);
|
||||
|
||||
// Convert npCommId string to span (pad or truncate to 16 bytes)
|
||||
std::array<u8, 16> npcommid_array{};
|
||||
size_t copy_len = std::min(npCommId.size(), npcommid_array.size());
|
||||
std::memcpy(npcommid_array.data(), npCommId.data(), copy_len);
|
||||
std::span<const u8, 16> 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;
|
||||
}
|
||||
@@ -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<u8, 16>& user_key, const std::string& npCommId);
|
||||
|
||||
std::vector<u8> NPcommID = std::vector<u8>(12);
|
||||
std::array<u8, 16> np_comm_id{};
|
||||
std::array<u8, 16> esfmIv{};
|
||||
|
||||
11
src/main.cpp
11
src/main.cpp
@@ -22,6 +22,7 @@
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <common/key_manager.h>
|
||||
|
||||
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<std::string> game_args{};
|
||||
|
||||
Reference in New Issue
Block a user