diff --git a/arm9/source/filesys/filetype.h b/arm9/source/filesys/filetype.h index 89fbb97..4ecefaf 100644 --- a/arm9/source/filesys/filetype.h +++ b/arm9/source/filesys/filetype.h @@ -50,6 +50,7 @@ #define FTYPE_ENCRYPTABLE(tp) (tp&(GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_BOSS|BIN_KEYDB)) #define FTYPE_CIABUILD(tp) ((tp&(GAME_NCSD|GAME_NCCH|GAME_TMD)) || ((tp&GAME_NDS)&&(tp&(FLAG_DSIW)))) #define FTYPE_CIABUILD_L(tp) (FTYPE_CIABUILD(tp) && (tp&(GAME_TMD))) +#define FTYPE_CIAINSTALL(tp) ((tp&(GAME_NCSD|GAME_NCCH|GAME_CIA)) || ((tp&GAME_NDS)&&(tp&(FLAG_DSIW))) || (tp&(GAME_TMD)&&(tp&(FLAG_NUSCDN)))) #define FTYPE_CXIDUMP(tp) (tp&(GAME_TMD)) #define FTYPE_TIKBUILD(tp) (tp&(GAME_TICKET|SYS_TICKDB|BIN_TIKDB)) #define FTYPE_KEYBUILD(tp) (tp&(BIN_KEYDB|BIN_LEGKEY)) diff --git a/arm9/source/game/bdri.h b/arm9/source/game/bdri.h index 3aa8374..0dc4c65 100644 --- a/arm9/source/game/bdri.h +++ b/arm9/source/game/bdri.h @@ -2,29 +2,9 @@ #include "common.h" #include "ticket.h" - -// There's probably a better place to put this -#define SD_TITLEDB_PATH(emu) ((emu) ? "B:/dbs/title.db" : "A:/dbs/title.db") +#include "tie.h" // https://www.3dbrew.org/wiki/Inner_FAT -// https://www.3dbrew.org/wiki/Title_Database - -typedef struct { - u64 title_size; - u32 title_type; // usually == 0x40 - u32 title_version; - u8 flags_0[4]; - u32 tmd_content_id; - u32 cmd_content_id; - u8 flags_1[4]; - u32 extdata_id_low; // 0 if the title doesn't use extdata - u8 reserved1[4]; - u8 flags_2[8]; - char product_code[16]; - u8 reserved2[16]; - u8 unknown[4]; // appears to not matter what's here - u8 reserved3[44]; -} __attribute__((packed)) TitleInfoEntry; u32 GetNumTitleInfoEntries(const char* path); u32 GetNumTickets(const char* path); diff --git a/arm9/source/game/cmd.c b/arm9/source/game/cmd.c index 62f4684..34c3fc8 100644 --- a/arm9/source/game/cmd.c +++ b/arm9/source/game/cmd.c @@ -9,3 +9,42 @@ u32 CheckCmdSize(CmdHeader* cmd, u64 fsize) { return (fsize == cmdsize) ? 0 : 1; } + +u32 BuildCmdData(CmdHeader* cmd, TitleMetaData* tmd) { + u32 content_count = getbe16(tmd->content_count); + + // header basic info + cmd->cmd_id = 0x1; + cmd->n_entries = content_count; + cmd->n_cmacs = content_count; + cmd->unknown = 0x0; // this means no CMACs, only valid for NAND + + // copy content ids + u32* cnt_id = (u32*) (cmd + 1); + u32* cnt_id_cpy = cnt_id + content_count; + TmdContentChunk* chunk = (TmdContentChunk*) (tmd + 1); + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++, chunk++) { + cnt_id[i] = getbe32(chunk->id); + cnt_id_cpy[i] = cnt_id[i]; + } + + // bubble sort the second content id list + u32 b = 0; + while ((b < content_count) && (b < TMD_MAX_CONTENTS)) { + for (b = 1; (b < content_count) && (b < TMD_MAX_CONTENTS); b++) { + if (cnt_id_cpy[b] < cnt_id_cpy[b-1]) { + u32 swp = cnt_id_cpy[b]; + cnt_id_cpy[b] = cnt_id_cpy[b-1]; + cnt_id_cpy[b-1] = swp; + } + } + } + + // set CMACs to 0xFF + u8* cnt_cmac = (u8*) (cnt_id + (2*cmd->n_entries)); + memset(cmd->cmac, 0xFF, 0x10); + memset(cnt_cmac, 0xFF, 0x10 * content_count); + + // we still need to fix / set the CMACs inside the CMD file! + return 0; +} diff --git a/arm9/source/game/cmd.h b/arm9/source/game/cmd.h index b286cbc..d425ae9 100644 --- a/arm9/source/game/cmd.h +++ b/arm9/source/game/cmd.h @@ -1,6 +1,10 @@ #pragma once #include "common.h" +#include "tmd.h" + +#define CMD_SIZE_N(n) (sizeof(CmdHeader) + ((n)*(sizeof(u32)+sizeof(u32)+0x10))) +#define CMD_SIZE_NS(n) (sizeof(CmdHeader) + ((n)*(sizeof(u32)+sizeof(u32)))) // from: http://3dbrew.org/wiki/Titles#Data_Structure @@ -16,3 +20,4 @@ typedef struct { } __attribute__((packed, aligned(4))) CmdHeader; u32 CheckCmdSize(CmdHeader* cmd, u64 fsize); +u32 BuildCmdData(CmdHeader* cmd, TitleMetaData* tmd); diff --git a/arm9/source/game/game.h b/arm9/source/game/game.h index ccbe7dd..24a534f 100644 --- a/arm9/source/game/game.h +++ b/arm9/source/game/game.h @@ -14,6 +14,9 @@ #include "tad.h" #include "3dsx.h" #include "tmd.h" +#include "ticket.h" +#include "tie.h" #include "cmd.h" +#include "bdri.h" #include "ticketdb.h" #include "ncchinfo.h" diff --git a/arm9/source/game/tie.c b/arm9/source/game/tie.c new file mode 100644 index 0000000..1d1db74 --- /dev/null +++ b/arm9/source/game/tie.c @@ -0,0 +1,92 @@ +#include "tie.h" +#include "cmd.h" + +#define CMD_SIZE_ALIGN(sd) (sd ? 0x8000 : 0x4000) + + +u32 BuildTitleInfoEntryTmd(TitleInfoEntry* tie, TitleMetaData* tmd, bool sd) { + u64 title_id = getbe64(tmd->title_id); + u32 has_id1 = false; + + // set basic values + memset(tie, 0x00, sizeof(TitleInfoEntry)); + tie->title_type = 0x40; + + // title version, product code, cmd id + tie->title_version = getbe16(tmd->title_version); + tie->cmd_content_id = 0x01; + memcpy(tie->unknown, "GM9", 4); // GM9 install magic number + + // calculate base title size + // align size: 0x4000 for TWL and CTRNAND, 0x8000 for SD + u32 align_size = CMD_SIZE_ALIGN(sd); + u32 content_count = getbe16(tmd->content_count); + TmdContentChunk* chunk = (TmdContentChunk*) (tmd + 1); + tie->title_size = + (align_size * 3) + // base folder + 'content' + 'cmd' + align(TMD_SIZE_N(content_count), align_size) + // TMD + align(CMD_SIZE_N(content_count), align_size); // CMD + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++, chunk++) { + if (getbe32(chunk->id) == 1) has_id1 = true; // will be useful later + tie->title_size += align(getbe64(chunk->size), align_size); + } + + // manual? (we need to properly check this later) + if (has_id1 && (((title_id >> 32) == 0x00040000) || ((title_id >> 32) == 0x00040010))) { + tie->flags_0[0] = 0x1; // this may have a manual + } + + return 0; +} + +u32 BuildTitleInfoEntryTwl(TitleInfoEntry* tie, TitleMetaData* tmd, TwlHeader* twl) { + u64 title_id = getbe64(tmd->title_id); + + if (ValidateTwlHeader(twl) != 0) return 1; + if (BuildTitleInfoEntryTmd(tie, tmd, false) != 0) return 1; + + // product code + memcpy(tie->product_code, twl->game_title, 0x0A); + + // specific flags + // see: http://3dbrew.org/wiki/Titles + if ((title_id >> 32) == 0x00048004) { // TWL app / game + tie->flags_2[0] = 0x01; + tie->flags_2[4] = 0x01; + tie->flags_2[5] = 0x01; + } + + return 0; +} + +u32 BuildTitleInfoEntryNcch(TitleInfoEntry* tie, TitleMetaData* tmd, NcchHeader* ncch, NcchExtHeader* exthdr, bool sd) { + u64 title_id = getbe64(tmd->title_id); + + if (ValidateNcchHeader(ncch) != 0) return 1; + if (BuildTitleInfoEntryTmd(tie, tmd, sd) != 0) return 1; + + // product code, extended title version + memcpy(tie->product_code, ncch->productcode, 0x10); + tie->title_version |= (ncch->version << 16); + + // specific flags + // see: http://3dbrew.org/wiki/Titles + if (!((title_id >> 32) & 0x10)) // not a system title + tie->flags_2[4] = 0x01; + + // stuff from extheader + if (exthdr) { + // add save data size to title size + if (exthdr->savedata_size) { + u32 align_size = CMD_SIZE_ALIGN(sd); + tie->title_size += + align_size + // 'data' folder + align(exthdr->savedata_size, align_size); // savegame + tie->flags_1[0] = 0x01; // has SD save + }; + // extdata ID low (hacky) + tie->extdata_id_low = getle32(exthdr->aci_data + 0x30 - 0x0C + 0x04); + } else tie->flags_0[0] = 0x00; // no manual + + return 0; +} diff --git a/arm9/source/game/tie.h b/arm9/source/game/tie.h new file mode 100644 index 0000000..f6de4dc --- /dev/null +++ b/arm9/source/game/tie.h @@ -0,0 +1,31 @@ +#pragma once + +#include "common.h" +#include "tmd.h" +#include "ncch.h" +#include "nds.h" + +// There's probably a better place to put this +#define SD_TITLEDB_PATH(emu) ((emu) ? "B:/dbs/title.db" : "A:/dbs/title.db") + + +// see: https://www.3dbrew.org/wiki/Title_Database +typedef struct { + u64 title_size; + u32 title_type; // usually == 0x40 + u32 title_version; + u8 flags_0[4]; + u32 tmd_content_id; + u32 cmd_content_id; + u8 flags_1[4]; + u32 extdata_id_low; // 0 if the title doesn't use extdata + u8 reserved1[4]; + u8 flags_2[8]; + char product_code[16]; + u8 reserved2[16]; + u8 unknown[4]; // appears to not matter what's here + u8 reserved3[44]; +} __attribute__((packed)) TitleInfoEntry; + +u32 BuildTitleInfoEntryTwl(TitleInfoEntry* tie, TitleMetaData* tmd, TwlHeader* twl); +u32 BuildTitleInfoEntryNcch(TitleInfoEntry* tie, TitleMetaData* tmd, NcchHeader* ncch, NcchExtHeader* exthdr, bool sd); diff --git a/arm9/source/godmode.c b/arm9/source/godmode.c index 3ab341f..2e1d83a 100644 --- a/arm9/source/godmode.c +++ b/arm9/source/godmode.c @@ -1091,9 +1091,12 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan bool cryptable_inplace = ((encryptable||decryptable) && !in_output_path && (drvtype & DRV_FAT)); bool cia_buildable = (FTYPE_CIABUILD(filetype)); bool cia_buildable_legit = (FTYPE_CIABUILD_L(filetype)); + bool cia_installable = (FTYPE_CIAINSTALL(filetype)) && !(drvtype & DRV_CTRNAND) && + !(drvtype & DRV_TWLNAND) && !(drvtype & DRV_ALIAS); bool cxi_dumpable = (FTYPE_CXIDUMP(filetype)); bool tik_buildable = (FTYPE_TIKBUILD(filetype)) && !in_output_path; - bool key_buildable = (FTYPE_KEYBUILD(filetype)) && !in_output_path && !((drvtype & DRV_VIRTUAL) && (drvtype & DRV_SYSNAND)); + bool key_buildable = (FTYPE_KEYBUILD(filetype)) && !in_output_path && + !((drvtype & DRV_VIRTUAL) && (drvtype & DRV_SYSNAND)); bool titleinfo = (FTYPE_TITLEINFO(filetype)); bool ciacheckable = (FTYPE_CIACHECK(filetype)); bool renamable = (FTYPE_RENAMABLE(filetype)); @@ -1308,6 +1311,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan int cia_build = (cia_buildable) ? ++n_opt : -1; int cia_build_legit = (cia_buildable_legit) ? ++n_opt : -1; int cxi_dump = (cxi_dumpable) ? ++n_opt : -1; + int cia_install = (cia_installable) ? ++n_opt : -1; int tik_build_enc = (tik_buildable) ? ++n_opt : -1; int tik_build_dec = (tik_buildable) ? ++n_opt : -1; int key_build = (key_buildable) ? ++n_opt : -1; @@ -1340,6 +1344,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan if (cia_build > 0) optionstr[cia_build-1] = (cia_build_legit < 0) ? "Build CIA from file" : "Build CIA (standard)"; if (cia_build_legit > 0) optionstr[cia_build_legit-1] = "Build CIA (legit)"; if (cxi_dump > 0) optionstr[cxi_dump-1] = "Dump CXI/NDS file"; + if (cia_install > 0) optionstr[cia_install-1] = "Install game file"; if (tik_build_enc > 0) optionstr[tik_build_enc-1] = "Build " TIKDB_NAME_ENC; if (tik_build_dec > 0) optionstr[tik_build_dec-1] = "Build " TIKDB_NAME_DEC; if (key_build > 0) optionstr[key_build-1] = "Build " KEYDB_NAME; @@ -1546,6 +1551,53 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan } return 0; } + else if (user_select == cia_install) { // -> install game file + bool to_emunand = false; + if (CheckVirtualDrive("E:")) { + optionstr[0] = "Install to SysNAND"; + optionstr[1] = "Install to EmuNAND"; + user_select = (int) ShowSelectPrompt(2, optionstr, (n_marked > 1) ? + "%s\n%(%lu files selected)" : "%s", pathstr, n_marked); + if (!user_select) return 0; + else to_emunand = (user_select == 2); + } + if ((n_marked > 1) && ShowPrompt(true, "Try to install all %lu selected files?", n_marked)) { + u32 n_success = 0; + u32 n_other = 0; + ShowString("Trying to install %lu files...", n_marked); + for (u32 i = 0; i < current_dir->n_entries; i++) { + const char* path = current_dir->entry[i].path; + if (!current_dir->entry[i].marked) + continue; + if (!(IdentifyFileType(path) & filetype & TYPE_BASE)) { + n_other++; + continue; + } + DrawDirContents(current_dir, (*cursor = i), scroll); + if (InstallGameFile(path, to_emunand, false)) n_success++; + else { // on failure: show error, continue + char lpathstr[32+1]; + TruncateString(lpathstr, path, 32, 8); + if (ShowPrompt(true, "%s\nInstall failed\n \nContinue?", lpathstr)) continue; + else break; + } + current_dir->entry[i].marked = false; + } + if (n_other) { + ShowPrompt(false, "%lu/%lu files installed ok\n%lu/%lu not of same type", + n_success, n_marked, n_other, n_marked); + } else ShowPrompt(false, "%lu/%lu files installed ok", n_success, n_marked); + } else { + u32 ret = InstallGameFile(file_path, to_emunand, false); + ShowPrompt(false, "%s\nInstall %s", pathstr, (ret == 0) ? "success" : "failed"); + if ((ret != 0) && (filetype & (GAME_NCCH|GAME_NCSD)) && + ShowPrompt(true, "%s\nfile failed install.\n \nVerify now?", pathstr)) { + ShowPrompt(false, "%s\nVerification %s", pathstr, + (VerifyGameFile(file_path) == 0) ? "success" : "failed"); + } + } + return 0; + } else if (user_select == verify) { // -> verify game / nand file if ((n_marked > 1) && ShowPrompt(true, "Try to verify all %lu selected files?", n_marked)) { u32 n_success = 0; diff --git a/arm9/source/utils/gameutil.c b/arm9/source/utils/gameutil.c index 7aaf04a..7157f31 100644 --- a/arm9/source/utils/gameutil.c +++ b/arm9/source/utils/gameutil.c @@ -1,4 +1,5 @@ #include "gameutil.h" +#include "nandcmac.h" #include "disadiff.h" #include "game.h" #include "nand.h" // so that we can trim NAND images @@ -1228,6 +1229,269 @@ u32 CryptGameFile(const char* path, bool inplace, bool encrypt) { return ret; } +u32 GetInstallAppPath(char* path, const char* drv, const u8* title_id, const u8* content_id) { + const u8 dlc_tid_high[] = { DLC_TID_HIGH }; + bool dlc = (memcmp(title_id, dlc_tid_high, sizeof(dlc_tid_high)) == 0); + u32 tid_high = getbe32(title_id); + u32 tid_low = getbe32(title_id + 4); + + if ((*drv == '2') || (*drv == '5')) // TWL titles need TWL title ID + tid_high = 0x00030000 | (tid_high&0xFF); + + if (!content_id) { // just the base title path in that case + snprintf(path, 256, "%2.2s/title/%08lx/%08lx", + drv, tid_high, tid_low); + } else { // full app path + snprintf(path, 256, "%2.2s/title/%08lx/%08lx/content/%s%08lx.app", + drv, tid_high, tid_low, dlc ? "00000000/" : "", getbe32(content_id)); + } + + return 0; +} + +u32 InstallCiaContent(const char* drv, const char* path_content, u32 offset, u32 size, + TmdContentChunk* chunk, const u8* title_id, const u8* titlekey, bool cxi_fix) { + char dest[256]; + + // create destination path and ensure it exists + GetInstallAppPath(dest, drv, title_id, chunk->id); + fvx_rmkpath(dest); + + // open file(s) + FIL ofile; + FIL dfile; + FSIZE_t fsize; + UINT bytes_read, bytes_written; + if (fvx_open(&ofile, path_content, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + fvx_lseek(&ofile, offset); + fsize = fvx_size(&ofile); + if (offset > fsize) return 1; + if (!size) size = fsize - offset; + if (fvx_open(&dfile, dest, FA_WRITE | FA_CREATE_ALWAYS) != FR_OK) { + fvx_close(&ofile); + return 1; + } + + // ensure free space for destination file + if ((fvx_lseek(&dfile, size) != FR_OK) || + (fvx_tell(&dfile) != size) || + (fvx_lseek(&dfile, 0) != FR_OK)) { + fvx_close(&ofile); + fvx_close(&dfile); + fvx_unlink(dest); + return 1; + } + + // allocate buffer + u8* buffer = (u8*) malloc(STD_BUFFER_SIZE); + if (!buffer) { + fvx_close(&ofile); + fvx_close(&dfile); + fvx_unlink(dest); + return 1; + } + + // main loop starts here + u8 ctr_in[16]; + u8 ctr_out[16]; + u32 ret = 0; + bool cia_crypto = getbe16(chunk->type) & 0x1; + GetTmdCtr(ctr_in, chunk); + GetTmdCtr(ctr_out, chunk); + if (!ShowProgress(0, 0, path_content)) ret = 1; + for (u32 i = 0; (i < size) && (ret == 0); i += STD_BUFFER_SIZE) { + u32 read_bytes = min(STD_BUFFER_SIZE, (size - i)); + if (fvx_read(&ofile, buffer, read_bytes, &bytes_read) != FR_OK) ret = 1; + if (cia_crypto && (DecryptCiaContentSequential(buffer, read_bytes, ctr_in, titlekey) != 0)) ret = 1; + if ((i == 0) && cxi_fix && (SetNcchSdFlag(buffer) != 0)) ret = 1; + if (i == 0) sha_init(SHA256_MODE); + sha_update(buffer, read_bytes); + if (fvx_write(&dfile, buffer, read_bytes, &bytes_written) != FR_OK) ret = 1; + if ((read_bytes != bytes_read) || (bytes_read != bytes_written)) ret = 1; + if (!ShowProgress(offset + i + read_bytes, fsize, path_content)) ret = 1; + } + u8 hash[0x20]; + sha_get(hash); + + free(buffer); + fvx_close(&ofile); + fvx_close(&dfile); + + // did something go wrong? + if (ret != 0) fvx_unlink(dest); + + // chunk size / chunk hash + for (u32 i = 0; i < 8; i++) chunk->size[i] = (u8) (size >> (8*(7-i))); + memcpy(chunk->hash, hash, 0x20); + + return ret; +} + +u32 InstallCiaSystemData(CiaStub* cia, const char* drv) { + // this assumes contents already installed(!) + // we use hardcoded IDs for CMD (0x1), TMD (0x0), save (0x1/0x0) + TitleInfoEntry tie; + CmdHeader* cmd; + TicketCommon* ticket = &(cia->ticket); + TitleMetaData* tmd = &(cia->tmd); + TmdContentChunk* content_list = cia->content_list; + bool syscmd = ((*drv == '1') || (*drv == '4')); + bool sdtie = ((*drv == 'A') || (*drv == 'B')); + u32 content_count = getbe16(tmd->content_count); + u8* title_id = ticket->title_id; + u32 tid_high = getbe32(title_id); + u32 tid_low = getbe32(title_id + 4); + + char path_titledb[32]; + char path_ticketdb[32]; + char path_tmd[64]; + char path_cmd[64]; + char path_save[128]; + + // sanity checks + if (content_count == 0) return 1; + if ((*drv != '1') && (*drv != '2') && (*drv != 'A') && + (*drv != '4') && (*drv != '5') && (*drv != 'B')) + return 1; + + // TWL titles need TWL title ID high + if ((*drv == '2') || (*drv == '5')) + tid_high = 0x00030000 | (tid_high&0xFF); + + // progress update + if (!ShowProgress(0, 0, "TMD/CMD/TiE/Ticket/Save")) return 1; + + // collect data for title info entry + char path_cnt0[256]; + u8 hdr_cnt0[0x600]; // we don't need more + NcchHeader* ncch = NULL; + NcchExtHeader* exthdr = NULL; + GetInstallAppPath(path_cnt0, drv, title_id, content_list->id); + if (fvx_qread(path_cnt0, hdr_cnt0, 0, 0x600, NULL) != FR_OK) + return 1; + if (ValidateNcchHeader((void*) hdr_cnt0) == 0) { + ncch = (void*) hdr_cnt0; + exthdr = (void*) (hdr_cnt0 + sizeof(NcchHeader)); + if (!(ncch->size_exthdr) || + (DecryptNcch((u8*) exthdr, NCCH_EXTHDR_OFFSET, 0x400, ncch, NULL) != 0)) + exthdr = NULL; + } + + // build title info entry + if ((ncch && (BuildTitleInfoEntryNcch(&tie, tmd, ncch, exthdr, sdtie) != 0)) || + (!ncch && (BuildTitleInfoEntryTwl(&tie, tmd, (TwlHeader*) (void*) hdr_cnt0) != 0))) + return 1; + + // build the cmd + cmd = (CmdHeader*) malloc(CMD_SIZE_N(content_count)); + if (!cmd) return 1; + BuildCmdData(cmd, tmd); + if (!syscmd) cmd->unknown = 0xFFFFFFFE; // mark this as custom built + + // generate all the paths + snprintf(path_titledb, 32, "%2.2s/dbs/title.db", + (*drv == '2') ? "1:" : *drv == '5' ? "4:" : drv); + snprintf(path_ticketdb, 32, "%2.2s/dbs/ticket.db", + ((*drv == 'A') || (*drv == '2')) ? "1:" : + ((*drv == 'B') || (*drv == '5')) ? "4:" : drv); + snprintf(path_tmd, 64, "%2.2s/title/%08lx/%08lx/content/00000000.tmd", + drv, tid_high, tid_low); + snprintf(path_cmd, 64, "%2.2s/title/%08lx/%08lx/content/cmd/00000001.cmd", + drv, tid_high, tid_low); + + // progress update + if (!ShowProgress(1, 5, "TMD/CMD/TiE/Ticket/Save")) return 1; + + // copy tmd & cmd + fvx_rmkpath(path_tmd); + fvx_rmkpath(path_cmd); + if ((fvx_qwrite(path_tmd, tmd, 0, TMD_SIZE_N(content_count), NULL) != FR_OK) || + (fvx_qwrite(path_cmd, cmd, 0, + syscmd ? CMD_SIZE_NS(content_count) : CMD_SIZE_N(content_count), NULL) != FR_OK)) { + free(cmd); + return 1; + } + free(cmd); // we don't need this anymore + + // progress update + if (!ShowProgress(2, 5, "TMD/CMD/TiE/Ticket/Save")) return 1; + + // generate savedata + if (exthdr && (exthdr->savedata_size)) { + // generate the save path (thanks ihaveamac for system path) + if ((*drv == '1') || (*drv == '4')) { // ooof, system save + u8 sd_keyy[16] __attribute__((aligned(4))); + char path_movable[32]; + u32 sha256sum[8]; + snprintf(path_movable, 32, "%2.2s/private/movable.sed", drv); + if (fvx_qread(path_movable, sd_keyy, 0x110, 0x10, NULL) != FR_OK) return 1; + memset(sd_keyy, 0x00, 16); + sha_quick(sha256sum, sd_keyy, 0x10, SHA256_MODE); + snprintf(path_save, 128, "%2.2s/data/%08lx%08lx%08lx%08lx/sysdata/%08lx/00000000", + drv, sha256sum[0], sha256sum[1], sha256sum[2], sha256sum[3], + tid_low | 0x00020000); + } else { // SD save, simple + snprintf(path_save, 128, "%2.2s/title/%08lx/%08lx/data/00000001.sav", + drv, tid_high, tid_low); + } + + // generate the save file + u8 zeroes[0x20] = { 0x00 }; + UINT bw; + FIL save; + fvx_rmkpath(path_save); + if (fvx_open(&save, path_save, FA_WRITE | FA_CREATE_NEW) != FR_OK) + return 1; + if ((fvx_write(&save, zeroes, 0x20, &bw) != FR_OK) || (bw != 0x20)) + bw = 0; + fvx_lseek(&save, exthdr->savedata_size); + fvx_sync(&save); + fvx_close(&save); + if (bw != 0x20) return 1; + } + + // progress update + if (!ShowProgress(3, 5, "TMD/CMD/TiE/Ticket/Save")) return 1; + + // write ticket and title databases + // ensure remounting the old mount path + char path_store[256] = { 0 }; + char* path_bak = NULL; + strncpy(path_store, GetMountPath(), 256); + if (*path_store) path_bak = path_store; + + // ticket database + if (!InitImgFS(path_ticketdb) || + ((AddTicketToDB("D:/partitionA.bin", title_id, (Ticket*) ticket, true)) != 0)) { + InitImgFS(path_bak); + return 1; + } + + // progress update + if (!ShowProgress(4, 5, "TMD/CMD/TiE/Ticket/Save")) return 1; + + // title database + if (!InitImgFS(path_titledb) || + ((AddTitleInfoEntryToDB("D:/partitionA.bin", title_id, &tie, true)) != 0)) { + InitImgFS(path_bak); + return 1; + } + + // progress update + if (!ShowProgress(5, 5, "TMD/CMD/TiE/Ticket/Save")) return 1; + + // restore old mount path + InitImgFS(path_bak); + + // fix CMACs where required + if (!syscmd) FixFileCmac(path_cmd); + FixFileCmac(path_ticketdb); + FixFileCmac(path_titledb); + + return 0; +} + u32 InsertCiaContent(const char* path_cia, const char* path_content, u32 offset, u32 size, TmdContentChunk* chunk, const u8* titlekey, bool force_legit, bool cxi_fix, bool cdn_decrypt) { // crypto types / ctr @@ -1330,7 +1594,53 @@ u32 InsertCiaMeta(const char* path_cia, CiaMeta* meta) { return (res) ? 0 : 1; } -u32 BuildCiaFromTmdFileBuffered(const char* path_tmd, const char* path_cia, bool force_legit, bool cdn, void* buffer) { +u32 InstallFromCiaFile(const char* path_cia, const char* path_dest) { + CiaInfo info; + u8 titlekey[16]; + + // start operation + if (!ShowProgress(0, 0, path_cia)) return 1; + + // load CIA stub from origin + CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub)); + if (!cia) return 1; + if ((LoadCiaStub(cia, path_cia) != 0) || + (GetCiaInfo(&info, &(cia->header)) != 0) || + (GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0)) { + free(cia); + return 1; + } + + // install CIA contents + u8* title_id = cia->tmd.title_id; + u32 content_count = getbe16(cia->tmd.content_count); + u64 next_offset = info.offset_content; + u8* cnt_index = cia->header.content_index; + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { + TmdContentChunk* chunk = &(cia->content_list[i]); + u64 size = getbe64(chunk->size); + u16 index = getbe16(chunk->index); + if (!(cnt_index[index/8] & (1 << (7-(index%8))))) continue; // don't try to install missing contents + if (InstallCiaContent(path_dest, path_cia, next_offset, size, + chunk, title_id, titlekey, false) != 0) { + free(cia); + return 1; + } + next_offset += size; + } + + // fix TMD hashes, install CIA system data + if ((FixTmdHashes(&(cia->tmd)) != 0) || + (InstallCiaSystemData(cia, path_dest) != 0)) { + free(cia); + return 1; + } + + free(cia); + return 0; +} + +u32 BuildInstallFromTmdFileBuffered(const char* path_tmd, const char* path_dest, bool force_legit, bool cdn, void* buffer, bool install) { const u8 dlc_tid_high[] = { DLC_TID_HIGH }; CiaStub* cia = (CiaStub*) buffer; @@ -1456,58 +1766,78 @@ u32 BuildCiaFromTmdFileBuffered(const char* path_tmd, const char* path_cia, bool } } - // insert contents + // insert / install contents u8 titlekey[16] = { 0xFF }; if ((GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0) && force_legit) return 1; - if (WriteCiaStub(cia, path_cia) != 0) return 1; + if (!install && (WriteCiaStub(cia, path_dest) != 0)) return 1; for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(content_list[i]); if (present[i / 8] & (1 << (i % 8))) { snprintf(name_content, 256 - (name_content - path_content), (cdn) ? "%08lx" : (dlc && !cdn) ? "00000000/%08lx.app" : "%08lx.app", getbe32(chunk->id)); - if (InsertCiaContent(path_cia, path_content, 0, (u32) getbe64(chunk->size), chunk, titlekey, force_legit, false, cdn) != 0) { + if (!install && (InsertCiaContent(path_dest, path_content, 0, (u32) getbe64(chunk->size), + chunk, titlekey, force_legit, false, cdn) != 0)) { ShowPrompt(false, "ID %016llX.%08lX\nInsert content failed", getbe64(title_id), getbe32(chunk->id)); return 1; } + if (install && (InstallCiaContent(path_dest, path_content, 0, (u32) getbe64(chunk->size), + chunk, title_id, titlekey, false) != 0)) { + ShowPrompt(false, "ID %016llX.%08lX\nInstall content failed", getbe64(title_id), getbe32(chunk->id)); + return 1; + } } } // try to build & insert meta, but ignore result - CiaMeta* meta = (CiaMeta*) malloc(sizeof(CiaMeta)); - if (meta) { - if (content_count && cdn) { - if (!force_legit || !(getbe16(content_list->type) & 0x01)) { - CiaInfo info; - GetCiaInfo(&info, &(cia->header)); - if ((LoadNcchMeta(meta, path_cia, info.offset_content) == 0) && (InsertCiaMeta(path_cia, meta) == 0)) + if (!install) { + CiaMeta* meta = (CiaMeta*) malloc(sizeof(CiaMeta)); + if (meta) { + if (content_count && cdn) { + if (!force_legit || !(getbe16(content_list->type) & 0x01)) { + CiaInfo info; + GetCiaInfo(&info, &(cia->header)); + if ((LoadNcchMeta(meta, path_dest, info.offset_content) == 0) && (InsertCiaMeta(path_dest, meta) == 0)) + cia->header.size_meta = CIA_META_SIZE; + } + } else if (content_count) { + snprintf(name_content, 256 - (name_content - path_content), "%08lx.app", getbe32(content_list->id)); + if ((LoadNcchMeta(meta, path_content, 0) == 0) && (InsertCiaMeta(path_dest, meta) == 0)) cia->header.size_meta = CIA_META_SIZE; } - } else if (content_count) { - snprintf(name_content, 256 - (name_content - path_content), "%08lx.app", getbe32(content_list->id)); - if ((LoadNcchMeta(meta, path_content, 0) == 0) && (InsertCiaMeta(path_cia, meta) == 0)) - cia->header.size_meta = CIA_META_SIZE; + free(meta); } - free(meta); } // write the CIA stub (take #2) - if ((FixTmdHashes(tmd) != 0) || (WriteCiaStub(cia, path_cia) != 0)) + if ((FixTmdHashes(tmd) != 0) || + (install && (WriteCiaStub(cia, path_dest) != 0)) || + (!install && (InstallCiaSystemData(cia, path_dest) != 0))) return 1; return 0; } -u32 BuildCiaFromTmdFile(const char* path_tmd, const char* path_cia, bool force_legit, bool cdn) { +u32 InstallFromTmdFile(const char* path_tmd, const char* path_dest) { void* buffer = (void*) malloc(sizeof(CiaStub)); if (!buffer) return 1; - u32 ret = BuildCiaFromTmdFileBuffered(path_tmd, path_cia, force_legit, cdn, buffer); + u32 ret = BuildInstallFromTmdFileBuffered(path_tmd, path_dest, false, true, buffer, true); free(buffer); return ret; } -u32 BuildCiaFromNcchFile(const char* path_ncch, const char* path_cia) { +u32 BuildCiaFromTmdFile(const char* path_tmd, const char* path_dest, bool force_legit, bool cdn) { + void* buffer = (void*) malloc(sizeof(CiaStub)); + if (!buffer) return 1; + + u32 ret = BuildInstallFromTmdFileBuffered(path_tmd, path_dest, force_legit, cdn, buffer, true); + + free(buffer); + return ret; +} + +u32 BuildInstallFromNcchFile(const char* path_ncch, const char* path_dest, bool install) { NcchExtHeader exthdr; NcchHeader ncch; u8 title_id[8]; @@ -1536,32 +1866,36 @@ u32 BuildCiaFromNcchFile(const char* path_ncch, const char* path_cia) { (BuildFakeTicket((Ticket*)&(cia->ticket), title_id) != 0) || (BuildFakeTmd(&(cia->tmd), title_id, 1, save_size, 0)) || (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || - (WriteCiaStub(cia, path_cia) != 0)) { + (!install && (WriteCiaStub(cia, path_dest) != 0))) { free(cia); return 1; } - // insert NCCH content + // insert / install NCCH content TmdContentChunk* chunk = cia->content_list; memset(chunk, 0, sizeof(TmdContentChunk)); // nothing else to do - if (InsertCiaContent(path_cia, path_ncch, 0, 0, chunk, NULL, false, true, false) != 0) { + if ((!install && (InsertCiaContent(path_dest, path_ncch, 0, 0, chunk, NULL, false, true, false) != 0)) || + (install && (InstallCiaContent(path_dest, path_ncch, 0, 0, chunk, title_id, NULL, true) != 0))) { free(cia); return 1; } // optional stuff (proper titlekey / meta data) - CiaMeta* meta = (CiaMeta*) malloc(sizeof(CiaMeta)); - if (meta && has_exthdr && (BuildCiaMeta(meta, &exthdr, NULL) == 0) && - (LoadExeFsFile(meta->smdh, path_ncch, 0, "icon", sizeof(meta->smdh), NULL) == 0) && - (InsertCiaMeta(path_cia, meta) == 0)) - cia->header.size_meta = CIA_META_SIZE; - free(meta); + if (!install) { + CiaMeta* meta = (CiaMeta*) malloc(sizeof(CiaMeta)); + if (meta && has_exthdr && (BuildCiaMeta(meta, &exthdr, NULL) == 0) && + (LoadExeFsFile(meta->smdh, path_ncch, 0, "icon", sizeof(meta->smdh), NULL) == 0) && + (InsertCiaMeta(path_dest, meta) == 0)) + cia->header.size_meta = CIA_META_SIZE; + free(meta); + } // write the CIA stub (take #2) FindTitleKey((Ticket*)(&cia->ticket), title_id); if ((FixTmdHashes(&(cia->tmd)) != 0) || (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || - (WriteCiaStub(cia, path_cia) != 0)) { + (!install && (WriteCiaStub(cia, path_dest) != 0)) || + (install && (InstallCiaSystemData(cia, path_dest) != 0))) { free(cia); return 1; } @@ -1570,7 +1904,7 @@ u32 BuildCiaFromNcchFile(const char* path_ncch, const char* path_cia) { return 0; } -u32 BuildCiaFromNcsdFile(const char* path_ncsd, const char* path_cia) { +u32 BuildInstallFromNcsdFile(const char* path_ncsd, const char* path_dest, bool install) { NcchExtHeader exthdr; NcsdHeader ncsd; NcchHeader ncch; @@ -1602,33 +1936,39 @@ u32 BuildCiaFromNcsdFile(const char* path_ncsd, const char* path_cia) { (BuildFakeTicket((Ticket*)&(cia->ticket), title_id) != 0) || (BuildFakeTmd(&(cia->tmd), title_id, content_count, save_size, 0)) || (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || - (WriteCiaStub(cia, path_cia) != 0)) { + (!install && (WriteCiaStub(cia, path_dest) != 0))) { free(cia); return 1; } - // insert NCSD content + // insert / install NCSD content TmdContentChunk* chunk = cia->content_list; - for (u32 i = 0; i < 3; i++) { + for (u32 i = 0, idx = 0; i < 3; i++) { NcchPartition* partition = ncsd.partitions + i; u32 offset = partition->offset * NCSD_MEDIA_UNIT; u32 size = partition->size * NCSD_MEDIA_UNIT; if (!size) continue; memset(chunk, 0, sizeof(TmdContentChunk)); - chunk->id[3] = chunk->index[1] = i; - if (InsertCiaContent(path_cia, path_ncsd, offset, size, chunk++, NULL, false, (i == 0), false) != 0) { + chunk->id[3] = i; + chunk->index[1] = idx++; + if ((!install && (InsertCiaContent(path_dest, path_ncsd, + offset, size, chunk++, NULL, false, (i == 0), false) != 0)) || + (install && (InstallCiaContent(path_dest, path_ncsd, + offset, size, chunk, title_id, NULL, (i == 0)) != 0))) { free(cia); return 1; } } // optional stuff (proper titlekey / meta data) - CiaMeta* meta = (CiaMeta*) malloc(sizeof(CiaMeta)); - if (meta && (BuildCiaMeta(meta, &exthdr, NULL) == 0) && - (LoadExeFsFile(meta->smdh, path_ncsd, NCSD_CNT0_OFFSET, "icon", sizeof(meta->smdh), NULL) == 0) && - (InsertCiaMeta(path_cia, meta) == 0)) - cia->header.size_meta = CIA_META_SIZE; - if (meta) free(meta); + if (!install) { + CiaMeta* meta = (CiaMeta*) malloc(sizeof(CiaMeta)); + if (meta && (BuildCiaMeta(meta, &exthdr, NULL) == 0) && + (LoadExeFsFile(meta->smdh, path_ncsd, NCSD_CNT0_OFFSET, "icon", sizeof(meta->smdh), NULL) == 0) && + (InsertCiaMeta(path_dest, meta) == 0)) + cia->header.size_meta = CIA_META_SIZE; + if (meta) free(meta); + } // update title version from cart header (yeah, that's a bit hacky) u16 title_version; @@ -1644,7 +1984,8 @@ u32 BuildCiaFromNcsdFile(const char* path_ncsd, const char* path_cia) { FindTitleKey((Ticket*)&(cia->ticket), title_id); if ((FixTmdHashes(&(cia->tmd)) != 0) || (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || - (WriteCiaStub(cia, path_cia) != 0)) { + (!install && (WriteCiaStub(cia, path_dest) != 0)) || + (install && (InstallCiaSystemData(cia, path_dest) != 0))) { free(cia); return 1; } @@ -1653,7 +1994,7 @@ u32 BuildCiaFromNcsdFile(const char* path_ncsd, const char* path_cia) { return 0; } -u32 BuildCiaFromNdsFile(const char* path_nds, const char* path_cia) { +u32 BuildInstallFromNdsFile(const char* path_nds, const char* path_dest, bool install) { TwlHeader twl; u8 title_id[8]; u32 save_size = 0; @@ -1690,15 +2031,16 @@ u32 BuildCiaFromNdsFile(const char* path_nds, const char* path_cia) { (BuildFakeTicket((Ticket*)&(cia->ticket), title_id) != 0) || (BuildFakeTmd(&(cia->tmd), title_id, 1, save_size, privsave_size)) || (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || - (WriteCiaStub(cia, path_cia) != 0)) { + (!install && (WriteCiaStub(cia, path_dest) != 0))) { free(cia); return 1; } - // insert NDS content + // insert / install NDS content TmdContentChunk* chunk = cia->content_list; memset(chunk, 0, sizeof(TmdContentChunk)); // nothing else to do - if (InsertCiaContent(path_cia, path_nds, 0, 0, chunk, NULL, false, false, false) != 0) { + if ((!install && (InsertCiaContent(path_dest, path_nds, 0, 0, chunk, NULL, false, false, false) != 0)) || + (install && (InstallCiaContent(path_dest, path_nds, 0, 0, chunk, title_id, NULL, false) != 0))) { free(cia); return 1; } @@ -1707,7 +2049,8 @@ u32 BuildCiaFromNdsFile(const char* path_nds, const char* path_cia) { FindTitleKey((Ticket*)(&cia->ticket), title_id); if ((FixTmdHashes(&(cia->tmd)) != 0) || (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || - (WriteCiaStub(cia, path_cia) != 0)) { + (!install && (WriteCiaStub(cia, path_dest) != 0)) || + (install && (InstallCiaSystemData(cia, path_dest) != 0))) { free(cia); return 1; } @@ -1747,11 +2090,11 @@ u32 BuildCiaFromGameFile(const char* path, bool force_legit) { if (filetype & GAME_TMD) ret = BuildCiaFromTmdFile(path, dest, force_legit, filetype & FLAG_NUSCDN); else if (filetype & GAME_NCCH) - ret = BuildCiaFromNcchFile(path, dest); + ret = BuildInstallFromNcchFile(path, dest, false); else if (filetype & GAME_NCSD) - ret = BuildCiaFromNcsdFile(path, dest); + ret = BuildInstallFromNcsdFile(path, dest, false); else if ((filetype & GAME_NDS) && (filetype & FLAG_DSIW)) - ret = BuildCiaFromNdsFile(path, dest); + ret = BuildInstallFromNdsFile(path, dest, false); else ret = 1; if (ret != 0) // try to get rid of the borked file @@ -1760,6 +2103,73 @@ u32 BuildCiaFromGameFile(const char* path, bool force_legit) { return ret; } +u32 InstallGameFile(const char* path, bool to_emunand, bool force_nand) { + const char* drv; + u64 filetype = IdentifyFileType(path); + u32 ret = 0; + + // we need to figure out the drive based on title id + u64 title_id = 0; + if (filetype & GAME_CIA) { + CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub)); + if (!cia) return 1; + if (LoadCiaStub(cia, path) != 0) { + free(cia); + return 1; + } + title_id = getbe64(cia->tmd.title_id); + free(cia); + } else if (filetype & GAME_TMD) { + TitleMetaData* tmd = (TitleMetaData*) malloc(TMD_SIZE_MAX); + if (!tmd) return 1; + if (LoadTmdFile(tmd, path) != 0) { + free(tmd); + return 1; + } + title_id = getbe64(tmd->title_id); + free(tmd); + } else if (filetype & GAME_NCCH) { + NcchHeader ncch; + if (LoadNcchHeaders(&ncch, NULL, NULL, path, 0) != 0) + return 1; + title_id = ncch.partitionId; + } else if (filetype & GAME_NCSD) { + NcsdHeader ncsd; + if (LoadNcsdHeader(&ncsd, path) != 0) + return 1; + title_id = ncsd.mediaId; + } + + // decide the drive + if (((title_id >> 32) & 0x8000) || (filetype & GAME_NDS)) + drv = (to_emunand ? "5:" : "2:"); + else if (((title_id >> 32) & 0x10) || force_nand) + drv = (to_emunand ? "4:" : "1:"); + else + drv = (to_emunand ? "B:" : "A:"); + + // check permissions for SysNAND + if (!CheckWritePermissions(to_emunand ? "4:" : "1:")) return 1; + + // install game file + if (filetype & GAME_CIA) + ret = InstallFromCiaFile(path, drv); + else if ((filetype & GAME_TMD) && (filetype & FLAG_NUSCDN)) + ret = InstallFromTmdFile(path, drv); + else if (filetype & GAME_NCCH) + ret = BuildInstallFromNcchFile(path, drv, true); + else if (filetype & GAME_NCSD) + ret = BuildInstallFromNcsdFile(path, drv, true); + else if ((filetype & GAME_NDS) && (filetype & FLAG_DSIW)) + ret = BuildInstallFromNdsFile(path, drv, true); + else ret = 1; + + // we have no clue what to do on failure + // if (ret != 0) ... + + return ret; +} + // this has very limited uses right now u32 DumpCxiSrlFromTmdFile(const char* path) { u64 filetype = 0; diff --git a/arm9/source/utils/gameutil.h b/arm9/source/utils/gameutil.h index c315c44..14998f9 100644 --- a/arm9/source/utils/gameutil.h +++ b/arm9/source/utils/gameutil.h @@ -6,6 +6,7 @@ u32 VerifyGameFile(const char* path); u32 CheckEncryptedGameFile(const char* path); u32 CryptGameFile(const char* path, bool inplace, bool encrypt); u32 BuildCiaFromGameFile(const char* path, bool force_legit); +u32 InstallGameFile(const char* path, bool to_emunand, bool force_nand); u32 DumpCxiSrlFromTmdFile(const char* path); u32 ExtractCodeFromCxiFile(const char* path, const char* path_out, char* extstr); u32 CompressCode(const char* path, const char* path_out); diff --git a/arm9/source/utils/nandcmac.c b/arm9/source/utils/nandcmac.c index 2c9433f..c709c5a 100644 --- a/arm9/source/utils/nandcmac.c +++ b/arm9/source/utils/nandcmac.c @@ -380,6 +380,14 @@ u32 CheckFixCmdCmac(const char* path, bool fix) { } + // we abuse the unknown u32 to mark custom, unfinished CMDs + bool fix_missing = false; + if (cmd->unknown == 0xFFFFFFFE) { + fixed = true; + cmd->unknown = 0x1; + fix_missing = true; + } + // now, check the CMAC@0x10 use_aeskey(keyslot); aes_cmac(cmd_data, cmac, 1); @@ -394,7 +402,7 @@ u32 CheckFixCmdCmac(const char* path, bool fix) { } // further checking will be more complicated - // set up pointers to cmd data (pointer arithemtic is hard) + // set up pointers to cmd data (pointer arithmetic is hard) u32 n_entries = cmd->n_entries; u32* cnt_id = (u32*) (cmd + 1); u8* cnt_cmac = (u8*) (cnt_id + cmd->n_entries + cmd->n_cmacs); @@ -406,8 +414,13 @@ u32 CheckFixCmdCmac(const char* path, bool fix) { if (*cnt_id == 0xFFFFFFFF) continue; // unavailable content snprintf(name_content, 32, "%s%08lX.app", (is_dlc) ? "00000000/" : "", *cnt_id); if (fvx_qread(path_content, hashdata, 0x100, 0x100, NULL) != FR_OK) { - free(cmd_data); - return 1; // failed to read content + if (fix_missing) { + *cnt_id = 0xFFFFFFFF; + continue; + } else { + free(cmd_data); + return 1; // failed to read content + } } memcpy(hashdata + 0x100, &cnt_idx, 4); memcpy(hashdata + 0x104, cnt_id, 4);