diff --git a/source/fs/filetype.c b/source/fs/filetype.c index 5255f97..f94c78d 100644 --- a/source/fs/filetype.c +++ b/source/fs/filetype.c @@ -8,41 +8,51 @@ u32 IdentifyFileType(const char* path) { const u8 firm_magic[] = { FIRM_MAGIC }; u8 header[0x200] __attribute__((aligned(32))); // minimum required size size_t fsize = FileGetSize(path); - if (FileGetData(path, header, 0x200, 0) != 0x200) return 0; + char* fname = strrchr(path, '/'); + if (fname == NULL) return 0; // not a proper path + fname++; + if (FileGetData(path, header, 0x200, 0) < fsize) return 0; - if ((getbe32(header + 0x100) == 0x4E435344) && (getbe64(header + 0x110) == (u64) 0x0104030301000000) && - (getbe64(header + 0x108) == (u64) 0) && (fsize >= 0x8FC8000)) { - return IMG_NAND; // NAND image - } else if (ValidateFatHeader(header) == 0) { - return IMG_FAT; // FAT image file - } else if (ValidateMbrHeader((MbrHeader*) (void*) header) == 0) { - MbrHeader* mbr = (MbrHeader*) (void*) header; - MbrPartitionInfo* partition0 = mbr->partitions; - if ((partition0->sector + partition0->count) <= (fsize / 0x200)) // size check - return IMG_FAT; // possibly an MBR -> also treat as FAT image - } else if (ValidateCiaHeader((CiaHeader*) (void*) header) == 0) { - // this only works because these functions ignore CIA content index - CiaInfo info; - GetCiaInfo(&info, (CiaHeader*) header); - if (fsize >= info.size_cia) - return GAME_CIA; // CIA file - } else if (ValidateNcsdHeader((NcsdHeader*) (void*) header) == 0) { - NcsdHeader* ncsd = (NcsdHeader*) (void*) header; - if (fsize >= GetNcsdTrimmedSize(ncsd)) - return GAME_NCSD; // NCSD (".3DS") file - } else if (ValidateNcchHeader((NcchHeader*) (void*) header) == 0) { - NcchHeader* ncch = (NcchHeader*) (void*) header; - if (fsize >= (ncch->size * NCCH_MEDIA_UNIT)) - return GAME_NCCH; // NCSD (".3DS") file - } else if (ValidateExeFsHeader((ExeFsHeader*) (void*) header, fsize) == 0) { - return GAME_EXEFS; // ExeFS file - } else if (memcmp(header, romfs_magic, sizeof(romfs_magic)) == 0) { - return GAME_ROMFS; // RomFS file (check could be better) - } else if (strncmp(TMD_ISSUER, (char*) (header + 0x140), 0x40) == 0) { - if (fsize >= TMD_SIZE_N(getbe16(header + 0x1DE))) - return GAME_TMD; // TMD file - } else if (memcmp(header, firm_magic, sizeof(firm_magic)) == 0) { - return SYS_FIRM; // FIRM file + if (fsize >= 0x200) { + if ((getbe32(header + 0x100) == 0x4E435344) && (getbe64(header + 0x110) == (u64) 0x0104030301000000) && + (getbe64(header + 0x108) == (u64) 0) && (fsize >= 0x8FC8000)) { + return IMG_NAND; // NAND image + } else if (ValidateFatHeader(header) == 0) { + return IMG_FAT; // FAT image file + } else if (ValidateMbrHeader((MbrHeader*) (void*) header) == 0) { + MbrHeader* mbr = (MbrHeader*) (void*) header; + MbrPartitionInfo* partition0 = mbr->partitions; + if ((partition0->sector + partition0->count) <= (fsize / 0x200)) // size check + return IMG_FAT; // possibly an MBR -> also treat as FAT image + } else if (ValidateCiaHeader((CiaHeader*) (void*) header) == 0) { + // this only works because these functions ignore CIA content index + CiaInfo info; + GetCiaInfo(&info, (CiaHeader*) header); + if (fsize >= info.size_cia) + return GAME_CIA; // CIA file + } else if (ValidateNcsdHeader((NcsdHeader*) (void*) header) == 0) { + NcsdHeader* ncsd = (NcsdHeader*) (void*) header; + if (fsize >= GetNcsdTrimmedSize(ncsd)) + return GAME_NCSD; // NCSD (".3DS") file + } else if (ValidateNcchHeader((NcchHeader*) (void*) header) == 0) { + NcchHeader* ncch = (NcchHeader*) (void*) header; + if (fsize >= (ncch->size * NCCH_MEDIA_UNIT)) + return GAME_NCCH; // NCCH (".APP") file + } else if (ValidateExeFsHeader((ExeFsHeader*) (void*) header, fsize) == 0) { + return GAME_EXEFS; // ExeFS file (false positives possible) + } else if (memcmp(header, romfs_magic, sizeof(romfs_magic)) == 0) { + return GAME_ROMFS; // RomFS file (check could be better) + } else if (strncmp(TMD_ISSUER, (char*) (header + 0x140), 0x40) == 0) { + if (fsize >= TMD_SIZE_N(getbe16(header + 0x1DE))) + return GAME_TMD; // TMD file + } else if (memcmp(header, firm_magic, sizeof(firm_magic)) == 0) { + return SYS_FIRM; // FIRM file + } + } + if ((fsize > sizeof(NcchInfoHeader)) && + (GetNcchInfoVersion((NcchInfoHeader*) (void*) header)) && + (strncasecmp(fname, NCCHINFO_NAME, 32) == 0)) { + return MISC_NINFO; // ncchinfo.bin file } return 0; diff --git a/source/fs/filetype.h b/source/fs/filetype.h index 551f56f..d04de1c 100644 --- a/source/fs/filetype.h +++ b/source/fs/filetype.h @@ -14,11 +14,14 @@ #define SYS_FIRM (1<<8) +#define MISC_NINFO (1<<9) + #define FTYPE_MOUNTABLE (IMG_FAT|IMG_NAND|GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_EXEFS|GAME_ROMFS|SYS_FIRM) #define FYTPE_VERIFICABLE (IMG_NAND|GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_TMD|SYS_FIRM) #define FYTPE_DECRYPTABLE (GAME_CIA|GAME_NCSD|GAME_NCCH|SYS_FIRM) #define FTYPE_BUILDABLE (GAME_NCSD|GAME_NCCH|GAME_TMD) #define FTYPE_BUILDABLE_L (FTYPE_BUILDABLE&(GAME_TMD)) #define FTYPE_RESTORABLE (IMG_NAND) +#define FTYPE_XORPAD (MISC_NINFO) u32 IdentifyFileType(const char* path); diff --git a/source/game/game.h b/source/game/game.h index a2f760e..bca1b5c 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -6,3 +6,4 @@ #include "exefs.h" #include "romfs.h" #include "firm.h" +#include "ncchinfo.h" diff --git a/source/game/gameutil.c b/source/game/gameutil.c index 0094811..c359fae 100644 --- a/source/game/gameutil.c +++ b/source/game/gameutil.c @@ -1128,3 +1128,51 @@ u32 BuildCiaFromGameFile(const char* path, bool force_legit) { return ret; } + +u32 BuildNcchInfoXorpads(const char* destdir, const char* path) { + FIL fp_info; + FIL fp_xorpad; + UINT bt; + + if (!CheckWritePermissions(destdir)) return 1; + + NcchInfoHeader info; + u32 version = 0; + u32 entry_size = 0; + u32 ret = 0; + if (fvx_open(&fp_info, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + fvx_lseek(&fp_info, 0); + if ((fvx_read(&fp_info, &info, sizeof(NcchInfoHeader), &bt) != FR_OK) || + (bt != sizeof(NcchInfoHeader))) { + fvx_close(&fp_info); + return 1; + } + version = GetNcchInfoVersion(&info); + entry_size = (version == 3) ? NCCHINFO_V3_SIZE : sizeof(NcchInfoEntry); + if (!version) ret = 1; + for (u32 i = 0; (i < info.n_entries) && (ret == 0); i++) { + NcchInfoEntry entry; + if ((fvx_read(&fp_info, &entry, entry_size, &bt) != FR_OK) || + (bt != entry_size)) ret = 1; + if (FixNcchInfoEntry(&entry, version) != 0) ret = 1; + if (ret != 0) break; + + char dest[256]; // 256 is the maximum length of a full path + snprintf(dest, 256, "%s/%s", destdir, entry.filename); + if (fvx_open(&fp_xorpad, dest, FA_WRITE | FA_CREATE_ALWAYS) != FR_OK) + ret = 1; + if (!ShowProgress(0, 0, entry.filename)) ret = 1; + for (u64 p = 0; (p < entry.size_b) && (ret == 0); p += MAIN_BUFFER_SIZE) { + UINT create_bytes = min(MAIN_BUFFER_SIZE, entry.size_b - p); + if (BuildNcchInfoXorpad(MAIN_BUFFER, &entry, create_bytes, p) != 0) ret = 1; + if (fvx_write(&fp_xorpad, MAIN_BUFFER, create_bytes, &bt) != FR_OK) ret = 1; + if (!ShowProgress(p + create_bytes, entry.size_b, entry.filename)) ret = 1; + } + fvx_close(&fp_xorpad); + if (ret != 0) f_unlink(dest); // get rid of the borked file + } + + fvx_close(&fp_info); + return ret; +} diff --git a/source/game/gameutil.h b/source/game/gameutil.h index c7b68db..fcd5f2f 100644 --- a/source/game/gameutil.h +++ b/source/game/gameutil.h @@ -6,3 +6,4 @@ u32 VerifyGameFile(const char* path); u32 CheckEncryptedGameFile(const char* path); u32 DecryptGameFile(const char* path, bool inplace); u32 BuildCiaFromGameFile(const char* path, bool force_legit); +u32 BuildNcchInfoXorpads(const char* destdir, const char* path); diff --git a/source/game/ncch.h b/source/game/ncch.h index c57d146..134bd06 100644 --- a/source/game/ncch.h +++ b/source/game/ncch.h @@ -60,6 +60,7 @@ typedef struct { } __attribute__((packed, aligned(16))) NcchHeader; u32 ValidateNcchHeader(NcchHeader* header); +u32 SetNcchKey(NcchHeader* ncch, u32 keyid); u32 SetupNcchCrypto(NcchHeader* ncch); u32 DecryptNcch(u8* data, u32 offset, u32 size, NcchHeader* ncch, ExeFsHeader* exefs); u32 DecryptNcchSequential(u8* data, u32 offset, u32 size); diff --git a/source/game/ncchinfo.c b/source/game/ncchinfo.c new file mode 100644 index 0000000..0f1ad18 --- /dev/null +++ b/source/game/ncchinfo.c @@ -0,0 +1,59 @@ +#include "ncchinfo.h" +#include "ncch.h" +#include "aes.h" + +u32 GetNcchInfoVersion(NcchInfoHeader* info) { + if (!info->n_entries) return 0; // cannot be empty + if (info->ncch_info_version == NCCHINFO_V3_MAGIC) return 3; + else if (info->ncch_info_version == NCCHINFO_V4_MAGIC) return 4; + else return 0; +} + +u32 FixNcchInfoEntry(NcchInfoEntry* entry, u32 version) { + // convert ncchinfo if v3 + if (version == 3) { // ncchinfo v3 + u8* entry_data = (u8*) (entry); + memmove(entry_data + 56, entry_data + 48, 112); + memset(entry_data + 48, 0, 8); // zero out nonexistent title id + } else if (version != 4) { // !ncchinfo v4.0/v4.1/v4.2 + return 1; + } + + // poor man's UTF-16 -> UTF-8 + if (entry->filename[1] == 0x00) { + for (u32 i = 1; i < (sizeof(entry->filename) / 2); i++) + entry->filename[i] = entry->filename[i*2]; + } + + // fix sdmc: prefix + if (memcmp(entry->filename, "sdmc:", 5) == 0) + memmove(entry->filename, entry->filename + 5, 112 - 5); + + // workaround (1) for older (v4.0) ncchinfo.bin + // this combination means seed crypto rather than FixedKey + if ((entry->ncchFlag7 == 0x01) && entry->ncchFlag3) + entry->ncchFlag7 = 0x20; + + // workaround (2) for older (v4.1) ncchinfo.bin + if (!entry->size_b) entry->size_b = entry->size_mb * 1024 * 1024; + + return 0; +} + +u32 BuildNcchInfoXorpad(u8* buffer, NcchInfoEntry* entry, u32 size, u32 offset) { + // set NCCH key + // build faux NCCH header from entry + NcchHeader ncch = { 0 }; + memcpy(ncch.signature, entry->keyY, 16); + ncch.flags[3] = (u8) entry->ncchFlag3; + ncch.flags[7] = (u8) (entry->ncchFlag7 & ~0x04); + ncch.programId = ncch.partitionId = entry->titleId; + if (SetNcchKey(&ncch, 1) != 0) + return 1; + + // write xorpad + memset(buffer, 0, size); + ctr_decrypt_byte(buffer, buffer, size, offset, AES_CNT_CTRNAND_MODE, entry->ctr); + + return 0; +} diff --git a/source/game/ncchinfo.h b/source/game/ncchinfo.h new file mode 100644 index 0000000..cec7fca --- /dev/null +++ b/source/game/ncchinfo.h @@ -0,0 +1,30 @@ +#pragma once + +#include "common.h" + +#define NCCHINFO_NAME "ncchinfo.bin" +#define NCCHINFO_V3_MAGIC 0xF0000003 +#define NCCHINFO_V4_MAGIC 0xF0000004 +#define NCCHINFO_V3_SIZE 160 + +typedef struct { + u8 ctr[16]; + u8 keyY[16]; + u32 size_mb; + u32 size_b; // this is only used if it is non-zero + u32 ncchFlag7; + u32 ncchFlag3; + u64 titleId; + char filename[112]; +} __attribute__((packed)) NcchInfoEntry; + +typedef struct { + u32 padding; + u32 ncch_info_version; + u32 n_entries; + u8 reserved[4]; +} __attribute__((packed, aligned(16))) NcchInfoHeader; + +u32 GetNcchInfoVersion(NcchInfoHeader* info); +u32 FixNcchInfoEntry(NcchInfoEntry* entry, u32 version); +u32 BuildNcchInfoXorpad(u8* buffer, NcchInfoEntry* entry, u32 size, u32 offset); diff --git a/source/godmode.c b/source/godmode.c index 3d4059f..4464c93 100644 --- a/source/godmode.c +++ b/source/godmode.c @@ -12,6 +12,7 @@ #include "nand.h" #include "virtual.h" #include "vcart.h" +#include "ncchinfo.h" #include "image.h" #define N_PANES 2 @@ -577,6 +578,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur u32 filetype = IdentifyFileType(curr_entry->path); u32 drvtype = DriveType(curr_entry->path); + bool in_output_path = (strncmp(current_path, OUTPUT_PATH, 256) == 0); + // special stuff, only available for known filetypes (see int special below) bool mountable = ((filetype & FTYPE_MOUNTABLE) && !(drvtype & DRV_IMAGE)); bool verificable = (filetype & FYTPE_VERIFICABLE); @@ -585,8 +588,9 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur bool buildable = (filetype & FTYPE_BUILDABLE); bool buildable_legit = (filetype & FTYPE_BUILDABLE_L); bool restorable = (CheckA9lh() && (filetype & FTYPE_RESTORABLE) && !(drvtype & DRV_SYSNAND)); + bool xorpadable = (filetype & FTYPE_XORPAD); bool special_opt = mountable || verificable || decryptable || decryptable_inplace || - buildable || buildable_legit || restorable; + buildable || buildable_legit || restorable || xorpadable; char pathstr[32 + 1]; TruncateString(pathstr, curr_entry->path, 32, 8); @@ -602,7 +606,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur int special = (special_opt) ? ++n_opt : -1; int hexviewer = ++n_opt; int calcsha = ++n_opt; - int copystd = (strncmp(current_path, OUTPUT_PATH, 256) != 0) ? ++n_opt : -1; + int copystd = (!in_output_path) ? ++n_opt : -1; int inject = ((clipboard->n_entries == 1) && (clipboard->entry[0].type == T_FILE) && (drvtype & DRV_FAT) && @@ -618,7 +622,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur (filetype == GAME_EXEFS) ? "Mount as EXEFS image" : (filetype == GAME_ROMFS) ? "Mount as ROMFS image" : (filetype == GAME_TMD) ? "TMD file options..." : - (filetype == SYS_FIRM) ? "FIRM image options..." : "???"; + (filetype == SYS_FIRM) ? "FIRM image options..." : + (filetype == MISC_NINFO) ? "NCCHinfo options..." : "???"; optionstr[hexviewer-1] = "Show in Hexeditor"; optionstr[calcsha-1] = "Calculate SHA-256"; if (copystd > 0) optionstr[copystd-1] = "Copy to " OUTPUT_PATH; @@ -691,6 +696,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur int build = (buildable) ? ++n_opt : -1; int build_legit = (buildable_legit) ? ++n_opt : -1; int verify = (verificable) ? ++n_opt : -1; + int xorpad = (xorpadable) ? ++n_opt : -1; + int xorpad_inplace = (xorpadable) ? ++n_opt : -1; if (mount > 0) optionstr[mount-1] = "Mount image to drive"; if (restore > 0) optionstr[restore-1] = "Restore SysNAND (safe)"; if (decrypt > 0) optionstr[decrypt-1] = "Decrypt file (SD output)"; @@ -698,6 +705,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur if (build > 0) optionstr[build-1] = (build_legit < 0) ? "Build CIA from file" : "Build CIA (standard)"; if (build_legit > 0) optionstr[build_legit-1] = "Build CIA (legit)"; if (verify > 0) optionstr[verify-1] = "Verify file"; + if (xorpad > 0) optionstr[xorpad-1] = "Build XORpads (SD output)"; + if (xorpad_inplace > 0) optionstr[xorpad_inplace-1] = "Build XORpads (inplace)"; // auto select when there is only one option user_select = (n_opt > 1) ? (int) ShowSelectPrompt(n_opt, optionstr, pathstr) : n_opt; @@ -787,10 +796,12 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur n_success, n_marked, n_other, n_marked); else ShowPrompt(false, "%lu/%lu CIAs built ok", n_success, n_marked); if (n_success) ShowPrompt(false, "%lu files written to %s", n_success, OUTPUT_PATH); + if (n_success && in_output_path) GetDirContents(current_dir, current_path); } else { - if (BuildCiaFromGameFile(curr_entry->path, force_legit) == 0) + if (BuildCiaFromGameFile(curr_entry->path, force_legit) == 0) { ShowPrompt(false, "%s\nCIA built to %s", pathstr, OUTPUT_PATH); - else ShowPrompt(false, "%s\nCIA build failed", pathstr); + if (in_output_path) GetDirContents(current_dir, current_path); + } else ShowPrompt(false, "%s\nCIA build failed", pathstr); } return 0; } else if (user_select == verify) { // -> verify game / nand file @@ -832,6 +843,21 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur } else if (user_select == restore) { // -> restore SysNAND (A9LH preserving) ShowPrompt(false, "%s\nNAND restore %s", pathstr, (SafeRestoreNandDump(curr_entry->path) == 0) ? "success" : "failed"); + } else if ((user_select == xorpad) || (user_select == xorpad_inplace)) { + bool inplace = (user_select == xorpad_inplace); + bool success = (BuildNcchInfoXorpads((inplace) ? current_path : OUTPUT_PATH, curr_entry->path) == 0); + ShowPrompt(false, "%s\nNCCHinfo padgen %s%s", pathstr, + (success) ? "success" : "failed", + (!success || inplace) ? "" : "\nOutput dir: " OUTPUT_PATH); + GetDirContents(current_dir, current_path); + for (; *cursor < current_dir->n_entries; (*cursor)++) { + curr_entry = &(current_dir->entry[*cursor]); + if (strncasecmp(curr_entry->name, NCCHINFO_NAME, 32) == 0) break; + } + if (*cursor >= current_dir->n_entries) { + *scroll = 0; + *cursor = 1; + } } return 1;