diff --git a/AGENTS.md b/AGENTS.md index c0b11c2708..3e8d5571ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,7 @@ for it: | [docs/PsarFileFormat.md](docs/PsarFileFormat.md) | The PSAR archive inside a firmware updater - the format `Core/Util/PSARUnpack.cpp` walks | | [docs/pkg_notes.md](docs/pkg_notes.md) | The NPDRM `.pkg` format PSP game updates ship in, and how PPSSPP installs them | | [docs/cpu-equivalence.md](docs/cpu-equivalence.md) | Making the CPU cores agree bit for bit on every host: the `CPUEquivalence` fuzzer, what it found, and the differences still open (several waiting on a hardware test) | +| [docs/dnas-patches.md](docs/dnas-patches.md) | Game patches installed on a real PSP (Killzone: Liberation's DLC): installing one over PSPLink, and the Fuse-ID-keyed PGD and type 3 PRX that `sceKernelLoadModuleDNAS` decrypts | | [docs/kernel-hle-review.md](docs/kernel-hle-review.md) | Findings from a review pass over `Core/HLE/sceKernel*.cpp`, and what was verified clean | | [docs/metal-backend.md](docs/metal-backend.md) | What a native Metal backend would take, and why programmable blending is the reason to want one | diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 3745081c7e..25dee0f601 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -232,6 +232,8 @@ static const CommandLineParam g_autoParams[] = { {POFF(reRawBase), CmdParamType::String, "re-raw-base", '\0', "Treat --re-module as a raw code image loaded at this address, e.g. 0x08300000", CmdLineMode::Headless}, {POFF(reDecrypt), CmdParamType::String, "re-decrypt", '\0', "Decrypt one encrypted PSP file (PRX or ME image) and exit", CmdLineMode::Headless}, {POFF(reDecryptOut), CmdParamType::String, "re-decrypt-out", '\0', "Output file for --re-decrypt (default: decrypted.bin)", CmdLineMode::Headless}, + {POFF(reDecryptKey), CmdParamType::String, "re-decrypt-key", '\0', "Secure install ID (32 hex digits) for --re-decrypt of a game patch installed on a PSP; also needs --fuse-id", CmdLineMode::Headless}, + {POFF(fuseId), CmdParamType::String, "fuse-id", '\0', "Fuse ID (12 hex digits) of the PSP a game patch was installed on"}, {POFF(dumpFile), CmdParamType::String, "dump-file", '\0', "Copy one file out of the disc image, e.g. disc0:/PSP_GAME/USRDIR/MOVIE.PMF, or list a directory, and exit", CmdLineMode::Headless}, {POFF(dumpFileOut), CmdParamType::String, "dump-file-out", '\0', "Output file for --dump-file (default: the file's own name)", CmdLineMode::Headless}, {POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless}, @@ -532,6 +534,10 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[], C } void CommandLineOptions::ApplyToConfig() const { + if (fuseId.has_value()) { + g_Config.sFuseID = fuseId.value(); + g_Config.DoNotSaveSetting(&g_Config.sFuseID); + } if (fullscreen.has_value()) { g_Config.bFullScreen = fullscreen.value(); g_Config.DoNotSaveSetting(&g_Config.bFullScreen); diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 5c2fcf4258..c2a3814b22 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -130,6 +130,11 @@ struct CommandLineOptions { std::optional reDecrypt; // Where the plaintext goes. Defaults to "decrypted.bin". std::optional reDecryptOut; + // The 16-byte secure install ID (32 hex digits) a game patch installed on a PSP was decrypted + // with - what the game passes to sceKernelLoadModuleDNAS. + std::optional reDecryptKey; + // The Fuse ID of the PSP a game patch was installed on, overriding the FuseID setting. + std::optional fuseId; // Headless: copy one file out of the disc image given as the positional argument, to // --dump-file-out, and exit. std::optional dumpFile; diff --git a/Core/Config.cpp b/Core/Config.cpp index 8d461a4212..88f40cdfc7 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1112,6 +1112,7 @@ static const ConfigSetting systemParamSettings[] = { ConfigSetting("WlanPowerSave", SETTING(g_Config, bWlanPowerSave), (bool) PSP_SYSTEMPARAM_WLAN_POWERSAVE_OFF, CfgFlag::PER_GAME), ConfigSetting("EncryptSave", SETTING(g_Config, bEncryptSave), true, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("MemStickSize", SETTING(g_Config, iMemStickSizeGB), 16, CfgFlag::DEFAULT), + ConfigSetting("FuseID", SETTING(g_Config, sFuseID), "", CfgFlag::DEFAULT), }; static const ConfigSetting debuggerSettings[] = { diff --git a/Core/Config.h b/Core/Config.h index f32420b1f0..9458f6274a 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -577,6 +577,8 @@ public: // SystemParam std::string sNickName; // AdHoc and system nickname std::string sMACAddress; + // The Fuse ID of the PSP a game patch was installed on (12 hex digits), to decrypt it with. + std::string sFuseID; int iLanguage; int iTimeFormat; diff --git a/Core/ELF/PrxDecrypter.cpp b/Core/ELF/PrxDecrypter.cpp index 79b6dfdb66..266a2363b1 100644 --- a/Core/ELF/PrxDecrypter.cpp +++ b/Core/ELF/PrxDecrypter.cpp @@ -3,6 +3,7 @@ #include #include "ext/libkirk/kirk_engine.h" +#include "ext/libkirk/amctrl.h" #include "ext/libkirk/SHA1.h" #include "Common/Common.h" #include "Common/Log.h" @@ -175,6 +176,9 @@ static const u8 pauth_98b83b5d_xor[] = {0xA9, 0x1E, 0xDD, 0x7B, 0x09, 0xBB, 0x22 static const u8 pauth_f7aa47f6_1[] = {0xC5, 0xFB, 0x69, 0x03, 0x20, 0x7A, 0xCF, 0xBA, 0x2C, 0x90, 0xF8, 0xB8, 0x4D, 0xD2, 0xF1, 0xDE}; static const u8 pauth_f7aa47f6_2[] = {0x3A, 0x6B, 0x48, 0x96, 0x86, 0xA5, 0xC8, 0x80, 0x69, 0x6C, 0xE6, 0x4B, 0xF6, 0x04, 0x17, 0x44}; static const u8 pauth_f7aa47f6_xor[] = {0xA9, 0x1E, 0xDD, 0x7B, 0x09, 0xBB, 0x22, 0xB5, 0x9D, 0xA3, 0x30, 0x69, 0x13, 0x6E, 0x0E, 0xD8}; +// Retail game patch installed by the DRM BB install utility (decrypt mode 18). JPCSP KeyVault. +static const u8 key_89742B04[] = {0xD7, 0xEB, 0xC9, 0x24, 0x7E, 0x23, 0x3D, 0x89, 0x46, 0xE7, 0x2E, 0x47, 0xAD, 0xDB, 0x0D, 0x09}; +static const u8 xor_89742B04[] = {0xFF, 0x5E, 0xF1, 0xE9, 0xB1, 0xC9, 0x3E, 0xC5, 0xDB, 0xE0, 0x67, 0x82, 0x95, 0x3A, 0x8E, 0xA5}; // PRXDecrypter 144-byte tag keys. static const u32 g_key0[] = { @@ -531,6 +535,7 @@ static const TAG_INFO2 g_tagInfo2[] = { 0x2FD313F0, pauth_98b83b5d_1, 0x47, 5, pauth_98b83b5d_xor }, { 0x2FD312F0, pauth_f7aa47f6_1, 0x47, 5, pauth_f7aa47f6_xor }, { 0x2FD311F0, pauth_f7aa47f6_2, 0x47, 5, pauth_f7aa47f6_xor }, + { 0x89742B04, key_89742B04, 0x65, 3, xor_89742B04 }, }; static const TAG_INFO2 *GetTagInfo2(u32 tagFind) @@ -694,7 +699,10 @@ struct PRXType5 memcpy(prxHeader, prx, sizeof(prxHeader)); } - void decrypt(int key, const u8 *xor1, const u8 *xor2) + // cmd3Header is for type 3, a game patch installed on a PSP: the first step goes through KIRK + // command 3 there, keyed to the console, with the header the install put at 0xEC (JPCSP's + // PRX.DecryptPRX). Fails only if that command does. + bool decrypt(int key, const u8 *xor1, const u8 *xor2, KirkState *kirk = nullptr, const u8 *cmd3Header = nullptr) { // first step is to decrypt kirk header + SHA1 u8 data[0x50]; @@ -714,7 +722,23 @@ struct PRXType5 } } - kirk7(data, data, 0x50, key); + if (cmd3Header) + { + u8 block[sizeof(KIRK_CMD1_HEADER) + sizeof(data)]{}; + KIRK_CMD1_HEADER *header = reinterpret_cast(block); + memcpy(block, cmd3Header, 0x40); + header->mode = KIRK_MODE_CMD3; + header->data_size = sizeof(data); + memcpy(block + sizeof(KIRK_CMD1_HEADER), data, sizeof(data)); + if (kirk_CMD3(kirk, data, block, sizeof(block)) != KIRK_OPERATION_SUCCESS) + { + return false; + } + } + else + { + kirk7(data, data, 0x50, key); + } // copy the result back memcpy(kirkHeader, data, sizeof(kirkHeader)); @@ -731,6 +755,7 @@ struct PRXType5 } kirk7(id, id, 0x60, key); + return true; } u8 tag[4]; @@ -1025,33 +1050,11 @@ static int pspDecryptType2(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz return decryptSize; } -static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) - { - INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); +// The part of type 5 decryption after its kirk header and SHA1 have been decrypted, which type 3 +// shares. +static int pspFinishType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size, PRXType5 &type5, const std::array &xorbuf, int code) +{ const auto decryptSize = *(s32_le*)&inbuf[0xB0]; - const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); - - if (!pti) - { - return -1; - } - - // check if range is non-zero - if (std::any_of(inbuf+0xD4+1, inbuf+0xD4+0x58, [](u8 x) { return x != 0; })) - { - return -2; - } - - // expand the seed into a xor buffer - auto xorbuf = expandSeed(pti->key, pti->code, seed); - - // The XOR the decrypt_mode implies wins over the tag table's, which is only the fallback - - // same precedence as JPCSP, and it leaves every tag that has a seed of its own alone. - const u8 *xor1 = inbuf[0x7C] == PRX_DECRYPT_MODE_SPRX ? xor_91E0A9AD : pti->seed; - - // construct the header format for a type 2 prx - PRXType5 type5(inbuf); - type5.decrypt(pti->code, xor1, seed); SHA_CTX ctx; SHAInit(&ctx); @@ -1082,7 +1085,7 @@ static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz memset(header, 0, sizeof(KIRK_CMD1_HEADER)); memcpy(reinterpret_cast(&header->data_size), type5.kirkMetadata, sizeof(type5.kirkMetadata)); memcpy(reinterpret_cast(header)+sizeof(KIRK_CMD1_HEADER), type5.prxHeader, sizeof(type5.prxHeader)); - decryptKirkHeader(reinterpret_cast(header), type5.kirkHeader, xorbuf.cbegin()+0x10, pti->code); + decryptKirkHeader(reinterpret_cast(header), type5.kirkHeader, xorbuf.cbegin()+0x10, code); header->mode = 1; if (!KirkOutputFits(header, size)) @@ -1096,8 +1099,60 @@ static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz } return decryptSize; +} + +static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) + { + INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); + const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); + + if (!pti) + { + return -1; } + // check if range is non-zero + if (std::any_of(inbuf+0xD4+1, inbuf+0xD4+0x58, [](u8 x) { return x != 0; })) + { + return -2; + } + + // expand the seed into a xor buffer + auto xorbuf = expandSeed(pti->key, pti->code, seed); + + // The XOR the decrypt_mode implies wins over the tag table's, which is only the fallback - + // same precedence as JPCSP, and it leaves every tag that has a seed of its own alone. + const u8 *xor1 = inbuf[0x7C] == PRX_DECRYPT_MODE_SPRX ? xor_91E0A9AD : pti->seed; + + // construct the header format for a type 2 prx + PRXType5 type5(inbuf); + type5.decrypt(pti->code, xor1, seed); + + return pspFinishType5(kirk, inbuf, outbuf, size, type5, xorbuf, pti->code); + } + +// Decrypt mode 18: a game patch installed through the DRM BB install utility, like the one +// Killzone: Liberation's DLC installs. Type 5 with the first step through KIRK command 3, so it only +// decrypts with the Fuse ID of the PSP that did the install. The secure install ID the game passes +// to sceKernelLoadModuleDNAS is the second XOR. JPCSP's PRX.DecryptPRX, uofw's mesgled.h. +static int pspDecryptType3(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size, const u8 *secureId) +{ + const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); + if (!pti || pti->type != 3 || !secureId) + { + return -1; + } + INFO_LOG(Log::Loader, "Decrypting tag %08X", (u32)*(u32_le *)&inbuf[0xD0]); + + auto xorbuf = expandSeed(pti->key, pti->code, secureId); + PRXType5 type5(inbuf); + if (!type5.decrypt(pti->code, pti->seed, secureId, kirk, inbuf + 0xEC)) + { + return -6; + } + return pspFinishType5(kirk, inbuf, outbuf, size, type5, xorbuf, pti->code); +} + static int pspDecryptType6(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) { INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); @@ -1247,7 +1302,7 @@ static int pspDecryptType9(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz return decryptSize; } -int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) +int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed, const u32 *fuseId) { // Every type below reads the tag at 0xD0, the size at 0xB0 and key data as far as 0x150, and // writes a KIRK header into outbuf at a fixed offset derived from sizeof(PSP_Header). Without @@ -1260,7 +1315,17 @@ int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) } KirkState kirk{}; - kirk_init(&kirk); + if (fuseId) { + kirk_init2(&kirk, (u8 *)"PrxDecrypter", 12, fuseId[0], fuseId[1]); + } else { + kirk_init(&kirk); + } + + // Type 3 tags are only ever type 3, and the others would fail on its header anyway. + const TAG_INFO2 *pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); + if (pti && pti->type == 3) { + return pspDecryptType3(&kirk, inbuf, outbuf, size, seed); + } // this would be significantly better if we had a log of the tags // and their appropriate prx types @@ -1294,3 +1359,74 @@ int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) // and then fail on the hash, so trying it earlier would shadow the real answer. return pspDecryptType9(&kirk, inbuf, outbuf, size); } + +bool pspUnwrapDNASPGD(std::vector &data, const u8 *secureId, const u32 *fuseId) +{ + if (data.size() < 0x90 || memcmp(data.data(), "\0PGD", 4) != 0) { + return false; + } + + KirkState kirk{}; + if (fuseId) { + kirk_init2(&kirk, (u8 *)"PrxDecrypter", 12, fuseId[0], fuseId[1]); + } else { + kirk_init(&kirk); + } + + // The PGD of an installed patch uses the flag 1 fixed key; keep 2 (the usual NPDRM one) as a fallback. + u8 header[0x90]; + PGD_DESC *pgd = nullptr; + for (int flag : { 1, 2 }) { + memcpy(header, data.data(), sizeof(header)); + pgd = pgd_open(&kirk, header, flag, const_cast(secureId)); + if (pgd) { + break; + } + } + if (!pgd) { + return false; + } + + const u64 blocksSize = (u64)pgd->block_nr * pgd->block_size; + if ((u64)pgd->data_offset + blocksSize > data.size() || pgd->data_size > blocksSize) { + pgd_close(pgd); + return false; + } + std::vector plain((size_t)blocksSize); + for (u32 block = 0; block < pgd->block_nr; block++) { + memcpy(pgd->block_buf, data.data() + pgd->data_offset + (size_t)block * pgd->block_size, pgd->block_size); + pgd_decrypt_block(&kirk, pgd, block); + memcpy(plain.data() + (size_t)block * pgd->block_size, pgd->block_buf, pgd->block_size); + } + plain.resize(pgd->data_size); + pgd_close(pgd); + data = std::move(plain); + return true; +} + +bool pspParseFuseId(std::string_view text, u32 fuseId[2]) +{ + if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + text.remove_prefix(2); + } + if (text.size() != 12 && text.size() != 16) { + return false; + } + u64 value = 0; + for (char c : text) { + int digit; + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (c >= 'a' && c <= 'f') { + digit = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + digit = c - 'A' + 10; + } else { + return false; + } + value = (value << 4) | digit; + } + fuseId[0] = (u32)value; + fuseId[1] = (u32)(value >> 32); + return true; +} diff --git a/Core/ELF/PrxDecrypter.h b/Core/ELF/PrxDecrypter.h index 3ccffabe7a..5f8caade58 100644 --- a/Core/ELF/PrxDecrypter.h +++ b/Core/ELF/PrxDecrypter.h @@ -17,6 +17,9 @@ #pragma once +#include +#include + #include "Common/Common.h" #include "Common/CommonTypes.h" #include "Common/Swap.h" // PSP_Header below is all _le types. @@ -65,4 +68,14 @@ typedef struct #pragma pack(pop) #endif -int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed = nullptr); +// fuseId is the PSP's Fuse ID, the words at 0xBC100090 and 0xBC100094. Only a game patch installed +// on a PSP needs it (seed is its secure install ID then). +int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed = nullptr, const u32 *fuseId = nullptr); + +// A module installed by the DRM BB install utility (sceKernelLoadModuleDNAS) is a PGD around a type 3 +// PRX, both keyed to the console by its Fuse ID and to the game by the secure install ID it passes. +// Replaces data with the PGD's contents; false if it doesn't open with these. +bool pspUnwrapDNASPGD(std::vector &data, const u8 *secureId, const u32 *fuseId); + +// A Fuse ID as PSP tools show it, 12 (or 16) hex digits with the 0xBC100094 word first. +bool pspParseFuseId(std::string_view text, u32 fuseId[2]); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 3407b8389a..5b0e59f195 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -72,8 +72,6 @@ #include "Core/HLE/scePsmf.h" #include "Core/HLE/sceAtrac.h" #include "Core/HLE/sceIo.h" -#include "Core/HLE/sceChnnlsv.h" -#include "ext/libkirk/amctrl.h" #include "Core/HLE/KernelWaitHelpers.h" #include "Core/ELF/ParamSFO.h" @@ -1502,7 +1500,7 @@ static void ApplyVshHomebrewPatches(u32 start, u32 end) { discId ? "patched" : "not found", headerType ? "patched" : "not found", headerBytesRetargeted ? "patched" : "not found"); } -static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 loadAddress, bool fromTop, std::string *error_string, u32 *magic, std::string_view filename, u32 &error, const u8 *prxSeed = nullptr) { +static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 loadAddress, bool fromTop, std::string *error_string, u32 *magic, std::string_view filename, u32 &error, const u8 *prxSeed = nullptr, const u32 *fuseId = nullptr) { // The magic reads below need four bytes, and the ~SCE branch another four after that. Everything // downstream checks its own sizes; this is just so we can look at the magic at all. The PBP path // in __KernelLoadModule computes elfSize from two offsets in the file and doesn't floor it. @@ -1575,7 +1573,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load newptr = new u8[maxElfSize]; elfSize = maxElfSize; ptr = newptr; - int decryptedSize = pspDecryptPRX(in, (u8*)ptr, head->psp_size, prxSeed); + int decryptedSize = pspDecryptPRX(in, (u8*)ptr, head->psp_size, prxSeed, fuseId); // If decryption got us nowhere, the PRX may simply not be encrypted - in which case the ELF // starts right after the header. Check the source buffer, not the destination: on the paths // where decryption bails early nothing has been written to newptr yet, so this used to read @@ -2558,7 +2556,7 @@ int sceKernelLoadExec(const char *filename, u32 paramPtr) { return hleLogDebug(Log::sceModule, 0); } -static u32 KernelLoadModuleFromData(const char *name, std::vector &fileData, u32 flags, const SceKernelLMOption *lmoption, bool *tryLoadExec); +static u32 KernelLoadModuleFromData(const char *name, std::vector &fileData, u32 flags, const SceKernelLMOption *lmoption, bool *tryLoadExec, const u8 *secureId = nullptr, const u32 *fuseId = nullptr); // What sceKernelLoadModule does once the file has been read: decrypt the module, place it in memory // and link it. Returns the module's UID or an error. Logs directly rather than through hleLog*, @@ -2577,7 +2575,7 @@ static u32 KernelLoadModuleFromFile(const char *name, u32 flags, const SceKernel return KernelLoadModuleFromData(name, fileData, flags, lmoption, tryLoadExec); } -static u32 KernelLoadModuleFromData(const char *name, std::vector &fileData, u32 flags, const SceKernelLMOption *lmoption, bool *tryLoadExec) { +static u32 KernelLoadModuleFromData(const char *name, std::vector &fileData, u32 flags, const SceKernelLMOption *lmoption, bool *tryLoadExec, const u8 *secureId, const u32 *fuseId) { // A .sprx installed by a PKG game update comes wrapped in an NPDRM "\0PSPEDAT" container: a // 0x90-byte header naming the content ID, then the payload at the offset in its u16 at 0x0C. // The payload is an ordinary ~PSP PRX, so stepping over the header is enough to get it to the @@ -2616,7 +2614,7 @@ static u32 KernelLoadModuleFromData(const char *name, std::vector &file u32 magic; u32 error; std::string error_string; - module = __KernelLoadELFFromPtr(fileData.data(), fileData.size(), 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, name, error, havePrxSeed ? prxSeed : nullptr); + module = __KernelLoadELFFromPtr(fileData.data(), fileData.size(), 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, name, error, havePrxSeed ? prxSeed : secureId, fuseId); if (!module) { if (magic == 0x46535000) { @@ -3366,39 +3364,12 @@ static u32 sceKernelLoadModuleByID(u32 id, u32 flags, u32 lmoptionPtr) { return hleNoLog(module->GetUID()); } -// Replaces a PGD's contents with its decrypted payload. False if the key or the PGD type is one we -// can't open. -static bool UnwrapPGD(std::vector &fileData, u8 *key) { - KirkState *kirk = __ChnnlsvKirkState(); - PGD_DESC *pgd = pgd_open(kirk, fileData.data(), 2, key); - if (!pgd) { - return false; - } - const size_t dataEnd = (size_t)pgd->data_offset + (size_t)pgd->block_nr * pgd->block_size; - if (dataEnd > fileData.size() || pgd->data_size > (u64)pgd->block_nr * pgd->block_size) { - pgd_close(pgd); - return false; - } - std::vector plain; - plain.reserve((size_t)pgd->block_nr * pgd->block_size); - for (u32 block = 0; block < pgd->block_nr; block++) { - memcpy(pgd->block_buf, fileData.data() + pgd->data_offset + block * pgd->block_size, pgd->block_size); - pgd_decrypt_block(kirk, pgd, block); - plain.insert(plain.end(), pgd->block_buf, pgd->block_buf + pgd->block_size); - } - plain.resize(pgd->data_size); - pgd_close(pgd); - fileData = std::move(plain); - return true; -} - // Loads a module installed by the DRM BB install utility (the psheet driver), like the patch -// Killzone: Liberation's Chapter 5 DLC installs as ms0:/PSP/GAME//KZL.PRX. Such a file is a PGD -// around a PRX, and both layers end up keyed to the PSP that installed it: the install runs the PRX -// through KIRK command 2, whose key (AES keyslot 3) isn't known. So a file copied from a PSP won't -// load here. A PGD we can open with the caller's key is unwrapped, and a module that's already -// decrypted loads as is - anything else fails, and Killzone's bootloader takes any negative result -// as "no patch" and loads its stock module from the disc. +// Killzone: Liberation's Chapter 5 DLC installs as ms0:/PSP/GAME//KZL.PRX. That's a PGD around a +// type 3 PRX, both keyed to the PSP that installed it (KIRK commands 8 and 3) and to the game by the +// secure install ID passed here, so it only loads with that PSP's Fuse ID in the FuseID setting. +// Killzone's bootloader takes any negative result as "no patch" and loads its stock module from the +// disc. A module that isn't encrypted at all loads as is. static u32 sceKernelLoadModuleDNAS(const char *name, u32 keyAddr, u32 flags, u32 optionAddr) { if (!name) { return hleLogError(Log::Loader, SCE_KERNEL_ERROR_ILLEGAL_ADDR, "bad filename"); @@ -3427,19 +3398,28 @@ static u32 sceKernelLoadModuleDNAS(const char *name, u32 keyAddr, u32 flags, u32 const int fileSize = (int)fileData.size(); const int loadUs = __IoOpenDelayUs(name) + __IoReadDelayUs(fileSize) + 1000 + fileSize / 34; + u32 fuseId[2]; + const bool haveFuseId = pspParseFuseId(g_Config.sFuseID, fuseId); + if (!haveFuseId && !g_Config.sFuseID.empty()) { + WARN_LOG(Log::Loader, "FuseID setting '%s' isn't 12 hex digits", g_Config.sFuseID.c_str()); + } + u32 result; - if (fileData.size() >= 0x90 && !memcmp(fileData.data(), "\0PGD", 4) && !UnwrapPGD(fileData, key)) { + if (fileData.size() >= 0x90 && !memcmp(fileData.data(), "\0PGD", 4) && !pspUnwrapDNASPGD(fileData, key, haveFuseId ? fuseId : nullptr)) { result = SCE_ERROR_PGD_INVALID_HEADER; } else { bool tryLoadExec = false; - result = KernelLoadModuleFromData(name, fileData, flags, lmoption, &tryLoadExec); + result = KernelLoadModuleFromData(name, fileData, flags, lmoption, &tryLoadExec, key, haveFuseId ? fuseId : nullptr); } if ((s32)result < 0) { auto err = GetI18NCategory(I18NCat::ERRORS); - g_OSD.Show(OSDType::MESSAGE_WARNING, err->T("DNASModuleUndecryptable", "Can't load a game update installed on a real PSP. Running without it."), "dnasmodule"); - return hleDelayResult(hleLogError(Log::Loader, result, "can't decrypt %s (key %08x%08x%08x%08x)", name, - bswap32(*(u32 *)&key[0]), bswap32(*(u32 *)&key[4]), bswap32(*(u32 *)&key[8]), bswap32(*(u32 *)&key[12])), "module loaded", loadUs); + if (haveFuseId) { + g_OSD.Show(OSDType::MESSAGE_WARNING, err->T("DNASModuleWrongFuseID", "This game update doesn't decrypt with the Fuse ID that's set. Running without it."), "dnasmodule"); + } else { + g_OSD.Show(OSDType::MESSAGE_WARNING, err->T("DNASModuleNeedsFuseID", "This game update was installed on a real PSP and needs that PSP's Fuse ID. Running without it."), "dnasmodule"); + } + return hleDelayResult(hleLogError(Log::Loader, result, "can't decrypt %s%s", name, haveFuseId ? "" : " (no Fuse ID set)"), "module loaded", loadUs); } return hleDelayResult(hleLogInfo(Log::Loader, result), "module loaded", loadUs); } diff --git a/docs/dnas-patches.md b/docs/dnas-patches.md new file mode 100644 index 0000000000..0a1b8861fd --- /dev/null +++ b/docs/dnas-patches.md @@ -0,0 +1,68 @@ +# Game patches installed on a PSP (DNAS) + +Some early games shipped their updates and DLC as an "installer" package that the PSP's install +utility turns into a module only that PSP can load. Killzone: Liberation's Chapter 5 DLC is the known +case (issues #12343, #12344). PPSSPP can load such a patch once it has been installed on a real PSP, +given that PSP's Fuse ID. It can't install one itself. + +## What the download is + +`PSP/GAME/UCES00279/` (UCUS98646 for the US game) gets two files: + +- `EBOOT.PBP`, 8.5 MB, `CATEGORY=MG`, `BOOTABLE=0`, title "Killzone: Liberation Installer". Its + `DATA.PSP` is the encrypted package the install utility unpacks. The XMB refuses to start it. +- `CHAPTER5.BIN`, 68 MB, the chapter's data. The patched game module reads it as it is. + +## Installing it on a PSP + +The install is done by the game's own boot program (`disc0:/PSP_GAME/SYSDIR/EBOOT.BIN`), which +imports `sceUtilityInstall*`. It only installs when started with the argument `-i`, and then only if +`ms0:/PSP/GAME/UCES00279/EBOOT.PBP` is larger than 1 MB. The game passes `-i` itself, after the +online download in its menus (from `kzl.killzone.com`, long gone), so there's no way to trigger it +from inside the game today. + +Starting the boot program with `-i` from PSPLink works. `modexec` doesn't (the LoadExec is refused), +but a tiny kernel-mode PRX calling the CFW's `sctrlKernelLoadExecVSHDisc` (`SystemCtrlForKernel`, +NID `0x577AF198`) with `key = "game"` and the argument block `"disc0:/PSP_GAME/SYSDIR/EBOOT.BIN\0-i\0"` +brings up the system's install screen. Afterwards the folder holds `KZL.PRX` (8.6 MB, the patch), +and the 8.5 MB `EBOOT.PBP` has been replaced by a 10 KB one for the XMB. + +The Fuse ID is the two words at `0xBC100090` and `0xBC100094`, which only kernel code can read. +PSP tools show it as 12 hex digits, the `0x94` word first. + +## Loading it + +The boot program calls `sceKernelLoadModuleDNAS(path, secureId, 0, 0)` with a 16-byte secure +install ID from its data (`81A19A6F0FA9CFBAE378F73FFD504C7F` for UCES00279), and treats any negative +result as "no patch", falling back to `disc0:/PSP_GAME/USRDIR/KZL.PRX`. Two layers: + +1. **A PGD** (key index 1, DRM type 0), opened by `amctrl` with the secure install ID and the "flag 1" + DNAS key. Its MAC and cipher use KIRK commands 5 and 8, which are AES under the per-console key + with seed 1. +2. **A type 3 PRX**, decrypt mode 18, tag `0x89742B04`. This is type 5 with the first step done by + KIRK command 3 instead of 7: a small command 3 block whose header is at `0xEC` and whose 0x50-byte + body is the usual kirk header + SHA1 pieces, XORed with the tag's key and the secure install ID. + The install produced that block with KIRK command 2, re-encrypting under the per-console key with + seed 0. Command 2's own key (AES keyslot 3) isn't known, which is why the download can't be + installed here. + +The per-console keys come from the Fuse ID: the key mesh on psdevwiki's Kirk page, and the final +step from the KIRK ROM's `aes_set_perconsole_key` (uofw `src/kirk/kirk-rom.c`). The type 3 layout is +JPCSP's `crypto/PRX.java`. The tag's key and XOR are in its `KeyVault.java`, and uofw's `mesgled.h` +documents the decrypt mode. + +In PPSSPP: + +- `kirk_individual_key()`, `kirk_CMD3/5/8` in `ext/libkirk/kirk_engine.c`. +- `pspDecryptType3()` and `pspUnwrapDNASPGD()` in `Core/ELF/PrxDecrypter.cpp`. +- `sceKernelLoadModuleDNAS()` in `Core/HLE/sceKernelModule.cpp` takes the Fuse ID from the `FuseID` + setting (`[SystemParam]` in `ppsspp.ini`) or `--fuse-id`. + +A wrong Fuse ID fails the PGD's MAC, so the game just runs unpatched. + +To decrypt one offline: + +``` +PPSSPPHeadless --re-decrypt KZL.PRX --re-decrypt-out KZL.elf \ + --re-decrypt-key 81a19a6f0fa9cfbae378f73ffd504c7f --fuse-id <12 hex digits> +``` diff --git a/ext/libkirk/kirk_engine.c b/ext/libkirk/kirk_engine.c index 59e2aff2bd..cc0ad95c8f 100644 --- a/ext/libkirk/kirk_engine.c +++ b/ext/libkirk/kirk_engine.c @@ -657,6 +657,117 @@ int kirk_CMD17(KirkState *kirk, const u8 * inbuff, int insize) { } } +// The per-console key KIRK derives from the Fuse ID for a service: seed 0 for commands 2 and 3, +// 1 for 5 and 8. The key mesh is psdevwiki's Kirk page (the same steps as decrypt_kirk16_private, +// which is seed 3), the final step the KIRK ROM's aes_set_perconsole_key (uofw src/kirk/kirk-rom.c). +void kirk_individual_key(KirkState *kirk, int seed, u8 *key_out) +{ + u8 fuseid[8], subkey_1[0x10], subkey_2[0x10], mesh[0x30]; + rijndael_ctx aes_ctx; + int i, k; + + fuseid[7] = kirk->g_fuse90 & 0xFF; + fuseid[6] = (kirk->g_fuse90 >> 8) & 0xFF; + fuseid[5] = (kirk->g_fuse90 >> 16) & 0xFF; + fuseid[4] = (kirk->g_fuse90 >> 24) & 0xFF; + fuseid[3] = kirk->g_fuse94 & 0xFF; + fuseid[2] = (kirk->g_fuse94 >> 8) & 0xFF; + fuseid[1] = (kirk->g_fuse94 >> 16) & 0xFF; + fuseid[0] = (kirk->g_fuse94 >> 24) & 0xFF; + + rijndael_set_key(&aes_ctx, kirk16_key, 128); + for (i = 0; i < 0x10; i++) + subkey_2[i] = subkey_1[i] = fuseid[i % 8]; + for (i = 0; i < 3; i++) + { + rijndael_encrypt(&aes_ctx, subkey_1, subkey_1); + rijndael_decrypt(&aes_ctx, subkey_2, subkey_2); + } + rijndael_set_key(&aes_ctx, subkey_1, 128); + for (i = 0; i < 3; i++) + { + for (k = 0; k < 3; k++) + rijndael_encrypt(&aes_ctx, subkey_2, subkey_2); + memcpy(&mesh[i * 0x10], subkey_2, 0x10); + } + + memcpy(key_out, (seed & 1) ? &mesh[0x10] : &mesh[0], 0x10); + rijndael_set_key(&aes_ctx, &mesh[0x20], 128); + for (i = 0; i < (seed >> 1) + 1; i++) + rijndael_encrypt(&aes_ctx, key_out, key_out); +} + +// Command 3 decrypts what command 2 produced. It's command 1 with the header keys encrypted with +// the per-console key. The CMACs are checked (they catch a wrong Fuse ID); ECDSA signatures aren't, +// since command 3's public key isn't known. +int kirk_CMD3(KirkState *kirk, u8* outbuff, const u8* inbuff, int size) +{ + const KIRK_CMD1_HEADER* header = (const KIRK_CMD1_HEADER*)inbuff; + header_keys keys; + u8 key[16], cmac_hash[16]; + AES_ctx k; + int chk_size; + + if(size < 0x90) return KIRK_INVALID_SIZE; + if(kirk->is_kirk_initialized == 0) return KIRK_NOT_INITIALIZED; + if(header->mode != KIRK_MODE_CMD3) return KIRK_INVALID_MODE; + if(header->data_size == 0) return KIRK_DATA_SIZE_ZERO; + if(header->data_offset > (u32)size || header->data_size > (u32)size - sizeof(KIRK_CMD1_HEADER) - header->data_offset) return KIRK_INVALID_SIZE; + + kirk_individual_key(kirk, 0, key); + AES_set_key(&k, key, 128); + AES_cbc_decrypt(&k, inbuff, (u8*)&keys, 16*2); + + if(!(header->ecdsa_hash & 1)) + { + chk_size = (header->data_size + 15) & ~15; + if(header->data_offset + chk_size > (u32)size - sizeof(KIRK_CMD1_HEADER)) return KIRK_INVALID_SIZE; + AES_set_key(&k, keys.CMAC, 128); + AES_CMAC(&k, (u8*)inbuff+0x60, 0x30, cmac_hash); + if(memcmp(cmac_hash, header->CMAC_header_hash, 16) != 0) return KIRK_HEADER_HASH_INVALID; + AES_CMAC(&k, (u8*)inbuff+0x60, 0x30 + chk_size + header->data_offset, cmac_hash); + if(memcmp(cmac_hash, header->CMAC_data_hash, 16) != 0) return KIRK_DATA_HASH_INVALID; + } + + AES_set_key(&k, keys.AES, 128); + AES_cbc_decrypt(&k, inbuff+sizeof(KIRK_CMD1_HEADER)+header->data_offset, outbuff, header->data_size); + return KIRK_OPERATION_SUCCESS; +} + +// Commands 5 and 8 are 4 and 7 with the per-console key (KIRK ROM kirk_cmd5_encrypt_perconsole and +// kirk_cmd8_decrypt_perconsole, which only accept keyseed 0x100). +int kirk_CMD5(KirkState *kirk, u8* outbuff, const u8* inbuff, int size) +{ + const KIRK_AES128CBC_HEADER *header = (const KIRK_AES128CBC_HEADER*)inbuff; + u8 key[16]; + AES_ctx aesKey; + + if(kirk->is_kirk_initialized == 0) return KIRK_NOT_INITIALIZED; + if(header->mode != KIRK_MODE_ENCRYPT_CBC || header->keyseed != 0x100) return KIRK_INVALID_MODE; + if(header->data_size == 0) return KIRK_DATA_SIZE_ZERO; + + kirk_individual_key(kirk, 1, key); + AES_set_key(&aesKey, key, 128); + AES_cbc_encrypt(&aesKey, inbuff+sizeof(KIRK_AES128CBC_HEADER), outbuff+sizeof(KIRK_AES128CBC_HEADER), header->data_size); + return KIRK_OPERATION_SUCCESS; +} + +int kirk_CMD8(KirkState *kirk, u8* outbuff, const u8* inbuff, int size) +{ + const KIRK_AES128CBC_HEADER *header = (const KIRK_AES128CBC_HEADER*)inbuff; + u8 key[16]; + AES_ctx aesKey; + + if(kirk->is_kirk_initialized == 0) return KIRK_NOT_INITIALIZED; + if(header->mode != KIRK_MODE_DECRYPT_CBC || header->keyseed != 0x100) return KIRK_INVALID_MODE; + if(header->data_size == 0) return KIRK_DATA_SIZE_ZERO; + + kirk_individual_key(kirk, 1, key); + AES_set_key(&aesKey, key, 128); + AES_cbc_decrypt(&aesKey, inbuff+sizeof(KIRK_AES128CBC_HEADER), outbuff, header->data_size); + return KIRK_OPERATION_SUCCESS; +} + int kirk_init(KirkState *kirk) { return kirk_init2(kirk, (u8*)"Lazy Dev should have initialized!",33,0xBABEF00D, 0xDEADBEEF ); @@ -729,6 +840,9 @@ int kirk_sceUtilsBufferCopyWithRange(KirkState *kirk, u8* outbuff, int outsize, { case KIRK_CMD_DECRYPT_PRIVATE: return kirk_CMD1(kirk, outbuff, inbuff, insize); break; // NOTE: I think this actually trashes inbuff case KIRK_CMD_ENCRYPT_IV_0: return kirk_CMD4(kirk, outbuff, inbuff, insize); break; + case KIRK_CMD_3: return kirk_CMD3(kirk, outbuff, inbuff, insize); break; + case KIRK_CMD_ENCRYPT_IV_FUSE: return kirk_CMD5(kirk, outbuff, inbuff, insize); break; + case KIRK_CMD_DECRYPT_IV_FUSE: return kirk_CMD8(kirk, outbuff, inbuff, insize); break; case KIRK_CMD_DECRYPT_IV_0: return kirk_CMD7(kirk, outbuff, inbuff, insize); break; case KIRK_CMD_PRIV_SIGN_CHECK: return kirk_CMD10(kirk, inbuff, insize); break; case KIRK_CMD_SHA1_HASH: return kirk_CMD11(kirk, outbuff, inbuff, insize); break; diff --git a/ext/libkirk/kirk_engine.h b/ext/libkirk/kirk_engine.h index 153e8c0b34..2eb60d067e 100644 --- a/ext/libkirk/kirk_engine.h +++ b/ext/libkirk/kirk_engine.h @@ -216,7 +216,10 @@ int kirk_CMD0(KirkState *kirk, u8* outbuff, const u8* inbuff, int size, int gene int kirk_CMD1(KirkState *kirk, u8* outbuff, u8* inbuff, int size); int kirk_CMD4(KirkState *kirk, u8* outbuff, const u8* inbuff, int size); +int kirk_CMD3(KirkState *kirk, u8* outbuff, const u8* inbuff, int size); +int kirk_CMD5(KirkState *kirk, u8* outbuff, const u8* inbuff, int size); int kirk_CMD7(KirkState *kirk, u8* outbuff, const u8* inbuff, int size); +int kirk_CMD8(KirkState *kirk, u8* outbuff, const u8* inbuff, int size); int kirk_CMD10(KirkState *kirk, u8* inbuff, int insize); int kirk_CMD11(KirkState *kirk, u8* outbuff, const u8* inbuff, int size); int kirk_CMD12(KirkState *kirk, u8* outbuff, int outsize); @@ -234,6 +237,7 @@ void kirk7(u8* outbuff, const u8* inbuff, size_t size, int keyId); //helper funcs const u8* kirk_4_7_get_key(int key_type); +void kirk_individual_key(KirkState *kirk, int seed, u8 *key_out); //kirk "ex" functions int kirk_CMD1_ex(KirkState *kirk, u8* outbuff, u8* inbuff, int size, KIRK_CMD1_HEADER* header); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 7ef0590fde..9d13985a7d 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -1210,7 +1210,8 @@ int main(int argc, const char* argv[]) { // Placed here rather than with the other early-exit subcommands above, because resolving a // "flash0:/kd/foo.prx" module path needs nandRootDirectory, which is only settled just above. if (cmdLineOptions.reDecrypt.has_value()) { - return RunDecryptFile(cmdLineOptions.reDecrypt.value(), cmdLineOptions.reDecryptOut.value_or("decrypted.bin")); + return RunDecryptFile(cmdLineOptions.reDecrypt.value(), cmdLineOptions.reDecryptOut.value_or("decrypted.bin"), + cmdLineOptions.reDecryptKey.value_or(""), cmdLineOptions.fuseId.value_or("")); } if (cmdLineOptions.dumpFile.has_value()) { if (cmdLineOptions.bootFilenames.empty()) { diff --git a/headless/ReverseEngineer.cpp b/headless/ReverseEngineer.cpp index fa3ab9f19a..c4d0cf0ad6 100644 --- a/headless/ReverseEngineer.cpp +++ b/headless/ReverseEngineer.cpp @@ -26,6 +26,7 @@ // separate parser would drift from the one that actually runs. #include +#include #include #include #include @@ -312,9 +313,24 @@ void WriteRegEvidence(FILE *f, const FuncInfo &func) { fprintf(f, "; looking at what the callees do with it.\n"); } +// Exactly size bytes' worth of hex digits. +static bool ParseHexBytes(const std::string &hex, u8 *out, size_t size) { + if (hex.size() != size * 2) { + return false; + } + for (size_t i = 0; i < size; i++) { + unsigned int value; + if (!std::isxdigit((unsigned char)hex[i * 2]) || !std::isxdigit((unsigned char)hex[i * 2 + 1]) || sscanf(hex.c_str() + i * 2, "%2x", &value) != 1) { + return false; + } + out[i] = (u8)value; + } + return true; +} + } // namespace -int RunDecryptFile(const std::string &inPath, const std::string &outPath) { +int RunDecryptFile(const std::string &inPath, const std::string &outPath, const std::string &secureIdHex, const std::string &fuseIdHex) { const Path in = ResolveModulePath(inPath); std::string data; if (!File::ReadBinaryFileToString(in, &data)) { @@ -324,6 +340,35 @@ int RunDecryptFile(const std::string &inPath, const std::string &outPath) { std::vector out; int outSize; + u8 secureId[16]; + const bool haveSecureId = !secureIdHex.empty(); + if (haveSecureId) { + if (!ParseHexBytes(secureIdHex, secureId, sizeof(secureId))) { + fprintf(stderr, "re-decrypt: the key has to be 32 hex digits\n"); + return 1; + } + } + u32 fuseId[2]; + const bool haveFuseId = !fuseIdHex.empty(); + if (haveFuseId && !pspParseFuseId(fuseIdHex, fuseId)) { + fprintf(stderr, "re-decrypt: the Fuse ID has to be 12 hex digits\n"); + return 1; + } + + if (data.size() >= 4 && !memcmp(data.data(), "\0PGD", 4)) { + if (!haveSecureId) { + fprintf(stderr, "re-decrypt: %s is a PGD; it needs --re-decrypt-key\n", in.c_str()); + return 1; + } + std::vector pgd(data.begin(), data.end()); + if (!pspUnwrapDNASPGD(pgd, secureId, haveFuseId ? fuseId : nullptr)) { + fprintf(stderr, "re-decrypt: %s doesn't open with that key%s\n", in.c_str(), haveFuseId ? " and Fuse ID" : " (no --fuse-id)"); + return 1; + } + printf("re-decrypt: %s: PGD, %d -> %d bytes\n", in.c_str(), (int)data.size(), (int)pgd.size()); + data.assign(pgd.begin(), pgd.end()); + } + bool inputIsKL3E = false; if (IsKL4EMagic((const u8 *)data.data(), data.size(), &inputIsKL3E)) { // Already-decrypted input, e.g. the second stream this tool splits out of an ME image. @@ -343,7 +388,7 @@ int RunDecryptFile(const std::string &inPath, const std::string &outPath) { // Decrypts in place on the PSP too, but keep the input around so a failure leaves it readable. out.resize(data.size()); - outSize = pspDecryptPRX((const u8 *)data.data(), out.data(), (u32)data.size()); + outSize = pspDecryptPRX((const u8 *)data.data(), out.data(), (u32)data.size(), haveSecureId ? secureId : nullptr, haveFuseId ? fuseId : nullptr); if (outSize <= 0) { fprintf(stderr, "re-decrypt: no key for tag %08X, or the data didn't decrypt (%d)\n", tag, outSize); return 1; diff --git a/headless/ReverseEngineer.h b/headless/ReverseEngineer.h index 6ab7c099b9..821494a266 100644 --- a/headless/ReverseEngineer.h +++ b/headless/ReverseEngineer.h @@ -48,8 +48,10 @@ int RunReverseEngineer(const ReverseEngineerOptions &opts); // Decrypts one encrypted PSP file and writes the plaintext, without loading anything. // Handles any container pspDecryptPRX() knows a tag for - including the ME images in // flash0:/kd/resource, which are ordinary tagged containers with the signature blanked, so the -// normal module loader won't touch them. Returns a process exit code. -int RunDecryptFile(const std::string &inPath, const std::string &outPath); +// normal module loader won't touch them. A game patch installed on a PSP (a PGD around a type 3 +// PRX) also needs the secure install ID the game loads it with and that PSP's Fuse ID, as hex. +// Returns a process exit code. +int RunDecryptFile(const std::string &inPath, const std::string &outPath, const std::string &secureIdHex = "", const std::string &fuseIdHex = ""); // Copies one file out of a disc image (ISO/CSO/...) as-is, e.g. "disc0:/PSP_GAME/USRDIR/MOVIE.PMF". // Returns a process exit code.