A tool for deriving PKG packet encryption keys for ps4 written in c++
| 1 | // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project |
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | |
| 4 | #include "common/config.h" |
| 5 | #include "common/logging/log.h" |
| 6 | #include "common/path_util.h" |
| 7 | #include "trp.h" |
| 8 | |
| 9 | TRP::TRP() = default; |
| 10 | TRP::~TRP() = default; |
| 11 | |
| 12 | void TRP::GetNPcommID(const std::filesystem::path& trophyPath, int index) { |
| 13 | std::filesystem::path trpPath = trophyPath / "sce_sys/npbind.dat"; |
| 14 | Common::FS::IOFile npbindFile(trpPath, Common::FS::FileAccessMode::Read); |
| 15 | if (!npbindFile.IsOpen()) { |
| 16 | LOG_CRITICAL(Common_Filesystem, "Failed to open npbind.dat file"); |
| 17 | return; |
| 18 | } |
| 19 | if (!npbindFile.Seek(0x84 + (index * 0x180))) { |
| 20 | LOG_CRITICAL(Common_Filesystem, "Failed to seek to NPbind offset"); |
| 21 | return; |
| 22 | } |
| 23 | npbindFile.ReadRaw<u8>(np_comm_id.data(), 12); |
| 24 | std::fill(np_comm_id.begin() + 12, np_comm_id.end(), 0); // fill with 0, we need 16 bytes. |
| 25 | } |
| 26 | |
| 27 | static void removePadding(std::vector<u8>& vec) { |
| 28 | for (auto it = vec.rbegin(); it != vec.rend(); ++it) { |
| 29 | if (*it == '>') { |
| 30 | size_t pos = std::distance(vec.begin(), it.base()); |
| 31 | vec.resize(pos); |
| 32 | break; |
| 33 | } |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | static void hexToBytes(const char* hex, unsigned char* dst) { |
| 38 | for (size_t i = 0; hex[i] != 0; i++) { |
| 39 | const unsigned char value = (hex[i] < 0x3A) ? (hex[i] - 0x30) : (hex[i] - 0x37); |
| 40 | dst[i / 2] |= ((i % 2) == 0) ? (value << 4) : (value); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) { |
| 45 | std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/"; |
| 46 | if (!std::filesystem::exists(gameSysDir)) { |
| 47 | LOG_CRITICAL(Common_Filesystem, "Game sce_sys directory doesn't exist"); |
| 48 | return false; |
| 49 | } |
| 50 | |
| 51 | const auto user_key_str = Config::getTrophyKey(); |
| 52 | if (user_key_str.size() != 32) { |
| 53 | LOG_CRITICAL(Common_Filesystem, "Trophy decryption key is not specified"); |
| 54 | return false; |
| 55 | } |
| 56 | |
| 57 | std::array<CryptoPP::byte, 16> user_key{}; |
| 58 | hexToBytes(user_key_str.c_str(), user_key.data()); |
| 59 | |
| 60 | for (int index = 0; const auto& it : std::filesystem::directory_iterator(gameSysDir)) { |
| 61 | if (it.is_regular_file()) { |
| 62 | GetNPcommID(trophyPath, index); |
| 63 | |
| 64 | Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read); |
| 65 | if (!file.IsOpen()) { |
| 66 | LOG_CRITICAL(Common_Filesystem, "Unable to open trophy file for read"); |
| 67 | return false; |
| 68 | } |
| 69 | |
| 70 | TrpHeader header; |
| 71 | file.Read(header); |
| 72 | if (header.magic != 0xDCA24D00) { |
| 73 | LOG_CRITICAL(Common_Filesystem, "Wrong trophy magic number"); |
| 74 | return false; |
| 75 | } |
| 76 | |
| 77 | s64 seekPos = sizeof(TrpHeader); |
| 78 | std::filesystem::path trpFilesPath( |
| 79 | Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId / |
| 80 | "TrophyFiles" / it.path().stem()); |
| 81 | std::filesystem::create_directories(trpFilesPath / "Icons"); |
| 82 | std::filesystem::create_directories(trpFilesPath / "Xml"); |
| 83 | |
| 84 | for (int i = 0; i < header.entry_num; i++) { |
| 85 | if (!file.Seek(seekPos)) { |
| 86 | LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); |
| 87 | return false; |
| 88 | } |
| 89 | seekPos += (s64)header.entry_size; |
| 90 | TrpEntry entry; |
| 91 | file.Read(entry); |
| 92 | std::string_view name(entry.entry_name); |
| 93 | if (entry.flag == 0 && name.find("TROP") != std::string::npos) { // PNG |
| 94 | if (!file.Seek(entry.entry_pos)) { |
| 95 | LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); |
| 96 | return false; |
| 97 | } |
| 98 | std::vector<u8> icon(entry.entry_len); |
| 99 | file.Read(icon); |
| 100 | Common::FS::IOFile::WriteBytes(trpFilesPath / "Icons" / name, icon); |
| 101 | } |
| 102 | if (entry.flag == 3 && np_comm_id[0] == 'N' && |
| 103 | np_comm_id[1] == 'P') { // ESFM, encrypted. |
| 104 | if (!file.Seek(entry.entry_pos)) { |
| 105 | LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); |
| 106 | return false; |
| 107 | } |
| 108 | file.Read(esfmIv); // get iv key. |
| 109 | // Skip the first 16 bytes which are the iv key on every entry as we want a |
| 110 | // clean xml file. |
| 111 | std::vector<u8> ESFM(entry.entry_len - iv_len); |
| 112 | std::vector<u8> XML(entry.entry_len - iv_len); |
| 113 | if (!file.Seek(entry.entry_pos + iv_len)) { |
| 114 | LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset"); |
| 115 | return false; |
| 116 | } |
| 117 | file.Read(ESFM); |
| 118 | crypto.decryptEFSM(user_key, np_comm_id, esfmIv, ESFM, XML); // decrypt |
| 119 | removePadding(XML); |
| 120 | std::string xml_name = entry.entry_name; |
| 121 | size_t pos = xml_name.find("ESFM"); |
| 122 | if (pos != std::string::npos) |
| 123 | xml_name.replace(pos, xml_name.length(), "XML"); |
| 124 | std::filesystem::path path = trpFilesPath / "Xml" / xml_name; |
| 125 | size_t written = Common::FS::IOFile::WriteBytes(path, XML); |
| 126 | if (written != XML.size()) { |
| 127 | LOG_CRITICAL( |
| 128 | Common_Filesystem, |
| 129 | "Trophy XML {} write failed, wanted to write {} bytes, wrote {}", |
| 130 | fmt::UTF(path.u8string()), XML.size(), written); |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | index++; |
| 136 | } |
| 137 | return true; |
| 138 | } |
| 139 |