From 2a904dc28400513696550557f35f895ec808839f Mon Sep 17 00:00:00 2001 From: d0k3 Date: Thu, 15 Dec 2016 11:46:00 +0100 Subject: [PATCH] Enable build CIA from TMD files --- source/common.h | 1 + source/crypto/aes.c | 21 +++ source/crypto/aes.h | 1 + source/fs/filetype.h | 2 + source/game/cia.c | 161 ++++++++++++++++++++--- source/game/cia.h | 14 +- source/game/gameutil.c | 285 +++++++++++++++++++++++++++++++++++++++-- source/game/gameutil.h | 1 + source/game/ncch.c | 17 ++- source/godmode.c | 38 +++++- 10 files changed, 500 insertions(+), 41 deletions(-) diff --git a/source/common.h b/source/common.h index 70f0519..15a9866 100644 --- a/source/common.h +++ b/source/common.h @@ -50,6 +50,7 @@ // buffer area defines (temporary, in use by various functions) // -> godmode.c hexviewer // -> ncch.c seed setup +// -> cia.c ticket / titlekey setup // -> gameutil.c various temporary stuff #define TEMP_BUFFER ((u8*)0x21100000) #define TEMP_BUFFER_SIZE (0x100000) diff --git a/source/crypto/aes.c b/source/crypto/aes.c index 4a19975..355ae66 100644 --- a/source/crypto/aes.c +++ b/source/crypto/aes.c @@ -171,6 +171,27 @@ void cbc_decrypt(void *inbuf, void *outbuf, size_t size, uint32_t mode, uint8_t } } +void cbc_encrypt(void *inbuf, void *outbuf, size_t size, uint32_t mode, uint8_t *ctr) +{ + size_t blocks_left = size; + size_t blocks; + uint8_t *in = inbuf; + uint8_t *out = outbuf; + uint32_t i; + + while (blocks_left) + { + set_ctr(ctr); + blocks = (blocks_left >= 0xFFFF) ? 0xFFFF : blocks_left; + aes_decrypt(in, out, blocks, mode); + for (i=0; isize_header != CIA_HEADER_SIZE) || (header->size_cert != CIA_CERT_SIZE) || @@ -50,9 +69,9 @@ u32 GetCiaContentInfo(CiaContentInfo* contents, TitleMetaData* tmd) { return 0; } -u32 GetTitleKey(u8* titlekey, Ticket* ticket) { +u32 CryptTitleKey(TitleKeyEntry* tik, bool encrypt) { // From https://github.com/profi200/Project_CTR/blob/master/makerom/pki/prod.h#L19 - static const u8 common_keyy[6][16] = { + static const u8 common_keyy[6][16] __attribute__((aligned(16))) = { {0xD0, 0x7B, 0x33, 0x7F, 0x9C, 0xA4, 0x38, 0x59, 0x32, 0xA2, 0xE2, 0x57, 0x23, 0x23, 0x2E, 0xB9} , // 0 - eShop Titles {0x0C, 0x76, 0x72, 0x30, 0xF0, 0x99, 0x8F, 0x1C, 0x46, 0x82, 0x82, 0x02, 0xFA, 0xAC, 0xBE, 0x4C} , // 1 - System Titles {0xC4, 0x75, 0xCB, 0x3A, 0xB8, 0xC7, 0x88, 0xBB, 0x57, 0x5E, 0x12, 0xA1, 0x09, 0x07, 0xB8, 0xA4} , // 2 @@ -61,7 +80,7 @@ u32 GetTitleKey(u8* titlekey, Ticket* ticket) { {0x5E, 0x66, 0x99, 0x8A, 0xB4, 0xE8, 0x93, 0x16, 0x06, 0x85, 0x0F, 0xD7, 0xA1, 0x6D, 0xD7, 0x55} , // 5 }; // From https://github.com/profi200/Project_CTR/blob/master/makerom/pki/dev.h#L21 - /* static const u8 common_key_devkit[6][16] = { // unused atm! + /* static const u8 common_key_devkit[6][16] __attribute__((aligned(16))) = { // unused atm! {0x55, 0xA3, 0xF8, 0x72, 0xBD, 0xC8, 0x0C, 0x55, 0x5A, 0x65, 0x43, 0x81, 0x13, 0x9E, 0x15, 0x3B} , // 0 - eShop Titles {0x44, 0x34, 0xED, 0x14, 0x82, 0x0C, 0xA1, 0xEB, 0xAB, 0x82, 0xC1, 0x6E, 0x7B, 0xEF, 0x0C, 0x25} , // 1 - System Titles {0xF6, 0x2E, 0x3F, 0x95, 0x8E, 0x28, 0xA2, 0x1F, 0x28, 0x9E, 0xEC, 0x71, 0xA8, 0x66, 0x29, 0xDC} , // 2 @@ -70,18 +89,29 @@ u32 GetTitleKey(u8* titlekey, Ticket* ticket) { {0xAA, 0xDA, 0x4C, 0xA8, 0xF6, 0xE5, 0xA9, 0x77, 0xE0, 0xA0, 0xF9, 0xE4, 0x76, 0xCF, 0x0D, 0x63} , // 5 };*/ - // setup key 0x3D - if (ticket->commonkey_idx >= 6) return 1; - setup_aeskeyY(0x3D, (void*) common_keyy[ticket->commonkey_idx >= 6]); - use_aeskey(0x3D); - - // grab and decrypt the titlekey + u32 mode = (encrypt) ? AES_CNT_TITLEKEY_ENCRYPT_MODE : AES_CNT_TITLEKEY_DECRYPT_MODE; u8 ctr[16] = { 0 }; - memcpy(ctr, ticket->title_id, 8); - memcpy(titlekey, ticket->titlekey, 16); - set_ctr(ctr); - aes_decrypt(titlekey, titlekey, 1, AES_CNT_TITLEKEY_DECRYPT_MODE); + // setup key 0x3D // ctr + if (tik->commonkey_idx >= 6) return 1; + setup_aeskeyY(0x3D, (void*) common_keyy[tik->commonkey_idx]); + use_aeskey(0x3D); + memcpy(ctr, tik->title_id, 8); + set_ctr(ctr); + + // decrypt / encrypt the titlekey + aes_decrypt(tik->titlekey, tik->titlekey, 1, mode); + return 0; +} + +u32 GetTitleKey(u8* titlekey, Ticket* ticket) { + TitleKeyEntry tik = { 0 }; + memcpy(tik.title_id, ticket->title_id, 8); + memcpy(tik.titlekey, ticket->titlekey, 16); + tik.commonkey_idx = ticket->commonkey_idx; + + if (CryptTitleKey(&tik, false) != 0) return 0; + memcpy(titlekey, tik.titlekey, 16); return 0; } @@ -105,6 +135,35 @@ u32 FixTmdHashes(TitleMetaData* tmd) { return 0; } +u32 FixCiaHeaderForTmd(CiaHeader* header, TitleMetaData* tmd) { + TmdContentChunk* content_list = (TmdContentChunk*) (tmd + 1); + u32 content_count = getbe16(tmd->content_count); + header->size_content = 0; + header->size_tmd = CIA_TMD_SIZE_N(content_count); + memset(header->content_index, 0, sizeof(header->content_index)); + for (u32 i = 0; i < content_count; i++) { + u16 index = getbe16(content_list[i].index); + header->size_content += getbe64(content_list[i].size); + header->content_index[index/8] |= (1 << (7-(index%8))); + } + return 0; +} + +Ticket* ParseForTicket(u8* data, u32 size, u8* title_id) { + const u8 magic[] = { CIA_SIG_TYPE }; + for (u32 i = 0; i + sizeof(Ticket) <= size; i++) { + Ticket* ticket = (Ticket*) (data + i); + if ((memcmp(ticket->sig_type, magic, sizeof(magic)) != 0) || + ((strncmp((char*) ticket->issuer, CIA_TICKET_ISSUER, 0x40) != 0) && + (strncmp((char*) ticket->issuer, CIA_TICKET_ISSUER_DEV, 0x40) != 0))) + continue; // magics not found + if (title_id && (memcmp(title_id, ticket->title_id, 8) != 0)) + continue; // title id not matching + return ticket; + } + return NULL; +} + u32 BuildCiaCert(u8* ciacert) { const u8 cert_hash_expected[0x20] = { 0xC7, 0x2E, 0x1C, 0xA5, 0x61, 0xDC, 0x9B, 0xC8, 0x05, 0x58, 0x58, 0x9C, 0x63, 0x08, 0x1C, 0x8A, @@ -137,7 +196,7 @@ u32 BuildCiaCert(u8* ciacert) { } u32 BuildFakeTicket(Ticket* ticket, u8* title_id) { - const u8 sig_type[4] = { 0x00, 0x01, 0x00, 0x04 }; // RSA_2048 SHA256 + const u8 sig_type[4] = { CIA_SIG_TYPE }; // RSA_2048 SHA256 const u8 ticket_cnt_index[] = { // whatever this is 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0xAC, 0x00, 0x00, 0x00, 0x14, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x84, @@ -155,19 +214,51 @@ u32 BuildFakeTicket(Ticket* ticket, u8* title_id) { ticket->version = 0x01; memset(ticket->titlekey, 0xFF, 16); memcpy(ticket->title_id, title_id, 8); - ticket->commonkey_idx = 0x01; + ticket->commonkey_idx = 0x00; // eshop ticket->audit = 0x01; // whatever memcpy(ticket->content_index, ticket_cnt_index, sizeof(ticket_cnt_index)); + // search for a titlekey inside encTitleKeys.bin / decTitleKeys.bin + for (u32 enc = 0; enc <= 1; enc++) { + const char* base[] = { INPUT_PATHS }; + bool found = false; + for (u32 i = 0; (i < (sizeof(base)/sizeof(char*))) && !found; i++) { + TitleKeysInfo* tikdb = (TitleKeysInfo*) (TEMP_BUFFER + (TEMP_BUFFER_SIZE/2)); + char path[64]; + FIL file; + UINT btr; + + snprintf(path, 64, "%s/%s", base[i], (enc) ? TIKDB_NAME_ENC : TIKDB_NAME_DEC); + if (f_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + continue; + f_read(&file, tikdb, TEMP_BUFFER_SIZE / 2, &btr); + f_close(&file); + if (tikdb->n_entries > (btr - 16) / 32) + continue; // filesize / titlekey db size mismatch + for (u32 t = 0; t < tikdb->n_entries; t++) { + TitleKeyEntry* tik = tikdb->entries + t; + if (memcmp(title_id, tik->title_id, 8) != 0) + continue; + if (!enc && (CryptTitleKey(tik, true) != 0)) // encrypt the key first + continue; + memcpy(ticket->titlekey, tik->titlekey, 16); + ticket->commonkey_idx = tik->commonkey_idx; + found = true; // found, inserted + break; + } + } + if (found) break; + } + return 0; } u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents) { - const u8 sig_type[4] = { 0x00, 0x01, 0x00, 0x04 }; // RSA_2048 SHA256 + const u8 sig_type[4] = { CIA_SIG_TYPE }; // safety check: number of contents - if (n_contents > 64) return 1; // !!! + if (n_contents > CIA_MAX_CONTENTS) return 1; // !!! // set TMD all zero for a clean start - memset(tmd, 0x00, sizeof(TitleMetaData) + (n_contents * sizeof(TmdContentChunk))); + memset(tmd, 0x00, CIA_TMD_SIZE_N(n_contents)); // file TMD values memcpy(tmd->sig_type, sig_type, 4); memset(tmd->signature, 0xFF, 0x100); @@ -185,6 +276,29 @@ u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents) { return 0; } +u32 BuildCiaMeta(CiaMeta* meta, u8* exthdr, u8* smdh) { + // init metadata with all zeroes and core version + memset(meta, 0x00, sizeof(CiaMeta)); + meta->core_version = 2; + // copy dependencies from extheader + if (exthdr) memcpy(meta->dependencies, exthdr + 0x40, sizeof(meta->dependencies)); + // copy smdh (icon file in exefs) + if (smdh) memcpy(meta->smdh, smdh, sizeof(meta->smdh)); + return 0; +} + +u32 BuildCiaHeader(CiaHeader* header) { + memset(header, 0, sizeof(CiaHeader)); + // sizes in header - fill only known sizes, others zero + header->size_header = sizeof(CiaHeader); + header->size_cert = CIA_CERT_SIZE; + header->size_ticket = sizeof(Ticket); + header->size_tmd = 0; + header->size_meta = 0; + header->size_content = 0; + return 0; +} + u32 DecryptCiaContentSequential(u8* data, u32 size, u8* ctr, const u8* titlekey) { // WARNING: size and offset of data have to be a multiple of 16 u8 tik[16] __attribute__((aligned(32))); @@ -195,3 +309,14 @@ u32 DecryptCiaContentSequential(u8* data, u32 size, u8* ctr, const u8* titlekey) cbc_decrypt(data, data, size / 16, mode, ctr); return 0; } + +u32 EncryptCiaContentSequential(u8* data, u32 size, u8* ctr, const u8* titlekey) { + // WARNING: size and offset of data have to be a multiple of 16 + u8 tik[16] __attribute__((aligned(32))); + u32 mode = AES_CNT_TITLEKEY_ENCRYPT_MODE; + memcpy(tik, titlekey, 16); + setup_aeskey(0x11, tik); + use_aeskey(0x11); + cbc_encrypt(data, data, size / 16, mode, ctr); + return 0; +} diff --git a/source/game/cia.h b/source/game/cia.h index 1d576f5..e9500aa 100644 --- a/source/game/cia.h +++ b/source/game/cia.h @@ -2,8 +2,10 @@ #include "common.h" -#define CIA_TICKET_ISSUER "Root-CA00000003-XS0000000c" -#define CIA_TMD_ISSUER "Root-CA00000003-CP0000000b" +#define CIA_TICKET_ISSUER "Root-CA00000003-XS0000000c" +#define CIA_TICKET_ISSUER_DEV "Root-CA00000004-XS00000009" +#define CIA_TMD_ISSUER "Root-CA00000003-CP0000000b" +#define CIA_SIG_TYPE 0x00, 0x01, 0x00, 0x04 // RSA_2048 SHA256 #define CIA_MAX_CONTENTS (100+1) // theme CIAs contain maximum 100 themes + 1 index content #define CIA_HEADER_SIZE sizeof(CiaHeader) @@ -153,12 +155,14 @@ u32 GetCiaContentInfo(CiaContentInfo* contents, TitleMetaData* tmd); u32 GetTitleKey(u8* titlekey, Ticket* ticket); u32 GetTmdCtr(u8* ctr, TmdContentChunk* chunk); u32 FixTmdHashes(TitleMetaData* tmd); +u32 FixCiaHeaderForTmd(CiaHeader* header, TitleMetaData* tmd); +Ticket* ParseForTicket(u8* data, u32 size, u8* title_id); u32 BuildCiaCert(u8* ciacert); u32 BuildFakeTicket(Ticket* ticket, u8* title_id); u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents); - -/*u32 BuildCiaMeta(Ncch ncch); -u32 InsertCiaContent(const char* path, const char* content, bool encrypt);*/ +u32 BuildCiaMeta(CiaMeta* meta, u8* exthdr, u8* smdh); +u32 BuildCiaHeader(CiaHeader* header); u32 DecryptCiaContentSequential(u8* data, u32 size, u8* ctr, const u8* titlekey); +u32 EncryptCiaContentSequential(u8* data, u32 size, u8* ctr, const u8* titlekey); diff --git a/source/game/gameutil.c b/source/game/gameutil.c index fae8281..e0abbb7 100644 --- a/source/game/gameutil.c +++ b/source/game/gameutil.c @@ -35,7 +35,7 @@ u32 GetOutputPath(char* dest, const char* path, const char* ext) { return 0; } -u32 GetNcchFileHeaders(NcchHeader* ncch, ExeFsHeader* exefs, FIL* file) { +u32 GetNcchHeaders(NcchHeader* ncch, ExeFsHeader* exefs, FIL* file) { u32 offset_ncch = f_tell(file); UINT btr; @@ -55,7 +55,7 @@ u32 GetNcchFileHeaders(NcchHeader* ncch, ExeFsHeader* exefs, FIL* file) { return 0; } -u32 CheckNcchFileHash(u8* expected, FIL* file, u32 size_data, u32 offset_ncch, NcchHeader* ncch, ExeFsHeader* exefs) { +u32 CheckNcchHash(u8* expected, FIL* file, u32 size_data, u32 offset_ncch, NcchHeader* ncch, ExeFsHeader* exefs) { u32 offset_data = f_tell(file) - offset_ncch; u8 hash[32]; @@ -118,7 +118,69 @@ u32 LoadCiaStub(CiaStub* stub, const char* path) { return 0; } +u32 LoadNcchMeta(CiaMeta* meta, const char* path, u64 offset) { + NcchHeader ncch; + ExeFsHeader exefs; + FIL file; + UINT btr; + u32 ret = 0; + + // this uses the meta builder function only in part + if (BuildCiaMeta(meta, NULL, NULL) != 0) return 1; + + // open file, get NCCH, ExeFS header + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&file, offset); + if (GetNcchHeaders(&ncch, &exefs, &file) != 0) { + fx_close(&file); + return 1; + } + + // dependencies + if (ncch.size_exthdr > 0) { + u8* dep = meta->dependencies; + u32 size_dep = sizeof(meta->dependencies); + f_lseek(&file, offset + NCCH_EXTHDR_OFFSET + 0x40); // offset to dependencies + if ((fx_read(&file, dep, size_dep, &btr) != FR_OK) || + (DecryptNcch(dep, NCCH_EXTHDR_OFFSET + 0x40, size_dep, &ncch, NULL) != 0) || + (btr != size_dep)) { + ret = 1; + } + } else ret = 1; + + // smdh from exefs + if (ncch.size_exefs > 0) { + ExeFsFileHeader* icon = NULL; + u32 size_smdh = sizeof(meta->smdh); + for (u32 i = 0; i < 10; i++) { + u32 size = exefs.files[i].size; + if (!size || (size > size_smdh)) continue; + char* name = exefs.files[i].name; + if (strncmp(name, "icon", 8) == 0) { + icon = exefs.files + i; + break; + } + } + if (icon) { + u32 size_icon = icon->size; + u32 offset_icon = (ncch.offset_exefs * NCCH_MEDIA_UNIT) + sizeof(ExeFsHeader) + icon->offset; + u8* smdh = meta->smdh; + f_lseek(&file, offset + offset_icon); // offset to icon + if ((fx_read(&file, smdh, size_icon, &btr) != FR_OK) || + (DecryptNcch(smdh, offset_icon, size_icon, &ncch, &exefs) != 0) || + (btr != size_icon)) { + ret = 1; + } + } else ret = 1; + } else ret = 1; + + fx_close(&file); + return ret; +} + u32 LoadTmdFile(TitleMetaData* tmd, const char* path) { + const u8 magic[] = { CIA_SIG_TYPE }; FIL file; UINT btr; @@ -128,6 +190,7 @@ u32 LoadTmdFile(TitleMetaData* tmd, const char* path) { // full TMD file f_lseek(&file, 0); if ((fx_read(&file, tmd, CIA_TMD_SIZE_MAX, &btr) != FR_OK) || + (memcmp(tmd->sig_type, magic, sizeof(magic)) != 0) || (btr < CIA_TMD_SIZE_N(getbe16(tmd->content_count)))) { fx_close(&file); return 1; @@ -157,6 +220,31 @@ u32 WriteCiaStub(CiaStub* stub, const char* path) { return 0; } +u32 SearchTicket(Ticket* ticket, const char* path, u8* title_id, bool force_legit) { + FIL db; + UINT btr; + + if (f_open(&db, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&db, 0); + + u32 fsize = f_size(&db); + u32 offset = 0; + Ticket* tick = NULL; + ShowProgress(0, 0, path); + while (!tick && (f_read(&db, MAIN_BUFFER, MAIN_BUFFER_SIZE, &btr) == FR_OK) && (btr > 0)) { + offset += (btr == MAIN_BUFFER_SIZE) ? MAIN_BUFFER_SIZE - (sizeof(Ticket) - 1) : btr; + if (!ShowProgress(offset, fsize, path)) break; + f_lseek(&db, offset); + tick = ParseForTicket(MAIN_BUFFER, btr, title_id); + if (tick && force_legit && (getbe64(tick->ticket_id) == 0)) + tick = NULL; + } + if (tick) memcpy(ticket, tick, sizeof(Ticket)); + + return (tick) ? 0 : 1; +} + u32 VerifyTmdContent(const char* path, u64 offset, TmdContentChunk* chunk, const u8* titlekey) { u8 hash[32]; u8 ctr[16]; @@ -204,7 +292,7 @@ u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { return 1; f_lseek(&file, offset); - if (GetNcchFileHeaders(&ncch, &exefs, &file) != 0) { + if (GetNcchHeaders(&ncch, &exefs, &file) != 0) { if (!offset) ShowPrompt(false, "%s\nError: Not a NCCH file", pathstr); fx_close(&file); return 1; @@ -232,19 +320,19 @@ u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { // base hash check for extheader if (ncch.size_exthdr > 0) { f_lseek(&file, offset + NCCH_EXTHDR_OFFSET); - ver_exthdr = CheckNcchFileHash(ncch.hash_exthdr, &file, 0x400, offset, &ncch, &exefs); + ver_exthdr = CheckNcchHash(ncch.hash_exthdr, &file, 0x400, offset, &ncch, NULL); } // base hash check for exefs if (ncch.size_exefs > 0) { f_lseek(&file, offset + (ncch.offset_exefs * NCCH_MEDIA_UNIT)); - ver_exefs = CheckNcchFileHash(ncch.hash_exefs, &file, ncch.size_exefs_hash * NCCH_MEDIA_UNIT, offset, &ncch, &exefs); + ver_exefs = CheckNcchHash(ncch.hash_exefs, &file, ncch.size_exefs_hash * NCCH_MEDIA_UNIT, offset, &ncch, &exefs); } // base hash check for romfs if (ncch.size_romfs > 0) { f_lseek(&file, offset + (ncch.offset_romfs * NCCH_MEDIA_UNIT)); - ver_romfs = CheckNcchFileHash(ncch.hash_romfs, &file, ncch.size_romfs_hash * NCCH_MEDIA_UNIT, offset, &ncch, &exefs); + ver_romfs = CheckNcchHash(ncch.hash_romfs, &file, ncch.size_romfs_hash * NCCH_MEDIA_UNIT, offset, &ncch, NULL); } // thorough exefs verification @@ -254,7 +342,7 @@ u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { u8* hash = exefs.hashes[9 - i]; if (!exefile->size) continue; f_lseek(&file, offset + (ncch.offset_exefs * NCCH_MEDIA_UNIT) + 0x200 + exefile->offset); - ver_exefs = CheckNcchFileHash(hash, &file, exefile->size, offset, &ncch, &exefs); + ver_exefs = CheckNcchHash(hash, &file, exefile->size, offset, &ncch, &exefs); } } @@ -434,7 +522,7 @@ u32 CheckEncryptedCiaFile(const char* path) { u64 next_offset = info.offset_content; for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(cia->content_list[i]); - if ((getbe16(chunk->type) & 0x1) || (CheckEncryptedNcchFile(path, next_offset))) + if ((getbe16(chunk->type) & 0x1) || (CheckEncryptedNcchFile(path, next_offset) == 0)) return 0; // encryption found next_offset += getbe64(chunk->size); } @@ -509,9 +597,8 @@ u32 DecryptNcchNcsdFile(const char* orig, const char* dest, u32 mode, if (cia_crypto) DecryptCiaContentSequential(MAIN_BUFFER, sizeof(NcchHeader), ctr, titlekey); ncch_crypto = ((ValidateNcchHeader((NcchHeader*) (void*) MAIN_BUFFER) == 0) && NCCH_ENCRYPTED((NcchHeader*) (void*) MAIN_BUFFER)); - if (ncch_crypto && (SetupNcchCrypto((NcchHeader*) (void*) MAIN_BUFFER) != 0)) { - ShowPrompt(false, "Error: Cannot set up NCCH crypto"); - }; + if (ncch_crypto && (SetupNcchCrypto((NcchHeader*) (void*) MAIN_BUFFER) != 0)) + ret = 1; GetTmdCtr(ctr, chunk); f_lseek(ofp, offset); @@ -605,3 +692,179 @@ u32 DecryptGameFile(const char* path, bool inplace) { return ret; } + +u32 InsertCiaContent(const char* path_cia, const char* path_content, u32 offset, u32 size, + TmdContentChunk* chunk, const u8* titlekey, bool force_legit) { + // crypto types + bool ncch_crypto = (!force_legit && (CheckEncryptedNcchFile(path_content, offset) == 0)); + bool cia_crypto = (force_legit && (getbe16(chunk->type) & 0x01)); + if (!cia_crypto) chunk->type[1] &= ~0x01; // remove crypto flag + + // open file(s) + FIL ofile; + FIL dfile; + FSIZE_t fsize; + UINT bytes_read, bytes_written; + if (fx_open(&ofile, path_content, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&ofile, offset); + fsize = f_size(&ofile); // for progress bar + if (fx_open(&dfile, path_cia, FA_WRITE | FA_OPEN_APPEND) != FR_OK) { + fx_close(&ofile); + return 1; + } + + // check if NCCH crypto is available + if (ncch_crypto) { + NcchHeader ncch; + if ((fx_read(&ofile, &ncch, sizeof(NcchHeader), &bytes_read) != FR_OK) || + (ValidateNcchHeader(&ncch) != 0) || + (SetupNcchCrypto(&ncch) != 0)) + ncch_crypto = false; + f_lseek(&ofile, offset); + } + + // main loop starts here + u8 ctr[16]; + u32 ret = 0; + GetTmdCtr(ctr, chunk); + sha_init(SHA256_MODE); + if (!ShowProgress(0, 0, path_content)) ret = 1; + for (u32 i = 0; (i < size) && (ret == 0); i += MAIN_BUFFER_SIZE) { + u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i)); + if (fx_read(&ofile, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1; + if (ncch_crypto && (DecryptNcchSequential(MAIN_BUFFER, i, read_bytes) != 0)) ret = 1; + sha_update(MAIN_BUFFER, read_bytes); + if (cia_crypto && (EncryptCiaContentSequential(MAIN_BUFFER, read_bytes, ctr, titlekey) != 0)) ret = 1; + if (fx_write(&dfile, MAIN_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); + + fx_close(&ofile); + fx_close(&dfile); + + // force legit? + if (force_legit && (memcmp(hash, chunk->hash, 0x20) != 0)) return 1; + if (force_legit && (getbe64(chunk->size) != size)) return 1; + + // 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 InsertCiaMeta(const char* path_cia, CiaMeta* meta) { + FIL file; + UINT btw; + if (fx_open(&file, path_cia, FA_WRITE | FA_OPEN_APPEND) != FR_OK) + return 1; + bool res = ((fx_write(&file, meta, CIA_META_SIZE, &btw) == FR_OK) && (btw == CIA_META_SIZE)); + fx_close(&file); + return (res) ? 0 : 1; +} + +u32 BuildCiaFromTmdFile(const char* path_tmd, const char* path_cia, bool force_legit) { + CiaStub* cia = (CiaStub*) TEMP_BUFFER; + CiaMeta* meta = (CiaMeta*) (TEMP_BUFFER + sizeof(CiaStub)); + + // build the CIA stub + memset(cia, 0, sizeof(CiaStub)); + if ((BuildCiaHeader(&(cia->header)) != 0) || + (LoadTmdFile(&(cia->tmd), path_tmd) != 0) || + (FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) || + (BuildCiaCert(cia->cert) != 0) || + (BuildFakeTicket(&(cia->ticket), cia->tmd.title_id) != 0) || + (WriteCiaStub(cia, path_cia) != 0)) { + return 1; + } + + // extract info from TMD + TitleMetaData* tmd = &(cia->tmd); + TmdContentChunk* content_list = cia->content_list; + u32 content_count = getbe16(tmd->content_count); + u8* title_id = tmd->title_id; + if (!content_count) return 1; + + // get (legit) ticket + const char* path_db = ((*path_tmd == 'B') || (*path_tmd == '4')) ? + "4:/dbs/ticket.db" : "1:/dbs/ticket.db"; // EmuNAND / SysNAND + const u8 titlekey_placeholder[16] = { 0xFF }; + Ticket* ticket = &(cia->ticket); + if (force_legit && (SearchTicket(ticket, path_db, title_id, true) != 0)) { + ShowPrompt(false, "ID %016llX\nLegit ticket not found.", getbe64(title_id)); + return 1; + } else if ((memcmp(ticket->titlekey, titlekey_placeholder, 16) == 0) && + (SearchTicket(ticket, path_db, title_id, false) == 0)) { + // ticket placeholder found, try to find ticket + // if ticket found: wipe private data + if (getbe32(ticket->console_id) || getbe32(ticket->eshop_id)) { + memset(ticket->console_id, 0, 4); // zero out console id + memset(ticket->eshop_id, 0, 4); // zero out eshop id + memset(ticket->ticket_id, 0, 8); // zero out ticket id + } + } + + // content path string + char path_content[256]; + char* name_content; + strncpy(path_content, path_tmd, 256); + name_content = strrchr(path_content, '/'); + if (!name_content) return 1; // will not happen + name_content++; + + // try to build metadata + if (content_count) { + snprintf(name_content, 256 - (name_content - path_content), "%08lx.app", getbe32(content_list->id)); + if (LoadNcchMeta(meta, path_content, 0) != 0) meta = NULL; + } else meta = NULL; + + // insert contents + u8 titlekey[16] = { 0xFF }; + if ((GetTitleKey(titlekey, &(cia->ticket)) != 0) && force_legit) return 1; + for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + TmdContentChunk* chunk = &(content_list[i]); + snprintf(name_content, 256 - (name_content - path_content), "%08lx.app", getbe32(chunk->id)); + if (InsertCiaContent(path_cia, path_content, 0, (u32) getbe64(chunk->size), chunk, titlekey, force_legit) != 0) { + ShowPrompt(false, "ID %016llX.%08lX\nInsert content failed", getbe64(title_id), getbe32(chunk->id)); + return 1; + } + } + + // try to insert meta, but ignore result + if (meta && (InsertCiaMeta(path_cia, meta) == 0)) + cia->header.size_meta = CIA_META_SIZE; + + // write the CIA stub (take #2) + if ((FixTmdHashes(tmd) != 0) || (WriteCiaStub(cia, path_cia) != 0)) + return 1; + + return 0; +} + +u32 BuildCiaFromGameFile(const char* path, bool force_legit) { + u32 filetype = IdentifyFileType(path); + char dest[256]; + u32 ret = 0; + + // destination path + if (GetOutputPath(dest, path, "cia") != 0) return 1; + if (!CheckWritePermissions(dest)) return 1; + + // ensure the output dir exists + if ((f_stat(OUTPUT_PATH, NULL) != FR_OK) && (f_mkdir(OUTPUT_PATH) != FR_OK)) + return 1; + + // build CIA from game file + if (filetype & GAME_TMD) + ret = BuildCiaFromTmdFile(path, dest, force_legit); + else ret = 1; + + // if (ret != 0) // try to get rid of the borked file + // f_unlink(dest); + + return ret; +} diff --git a/source/game/gameutil.h b/source/game/gameutil.h index 11f6748..c7b68db 100644 --- a/source/game/gameutil.h +++ b/source/game/gameutil.h @@ -5,3 +5,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); diff --git a/source/game/ncch.c b/source/game/ncch.c index ccfc83c..ec23d93 100644 --- a/source/game/ncch.c +++ b/source/game/ncch.c @@ -97,7 +97,7 @@ u32 GetNcchSeed(u8* seed, NcchHeader* ncch) { nand_drv[i], sha256sum[0], sha256sum[1], sha256sum[2], sha256sum[3]); // check seedsave for seed - u8* seedsave = (u8*) TEMP_BUFFER; + u8* seedsave = (u8*) (TEMP_BUFFER + (TEMP_BUFFER_SIZE/2)); if (f_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) continue; f_read(&file, seedsave, 0x200, &btr); @@ -124,11 +124,11 @@ u32 GetNcchSeed(u8* seed, NcchHeader* ncch) { // not found -> try seeddb.bin const char* base[] = { INPUT_PATHS }; for (u32 i = 0; i < (sizeof(base)/sizeof(char*)); i++) { - SeedInfo* seeddb = (SeedInfo*) TEMP_BUFFER; + SeedInfo* seeddb = (SeedInfo*) (TEMP_BUFFER + (TEMP_BUFFER_SIZE/2)); snprintf(path, 128, "%s/%s", base[i], SEEDDB_NAME); if (f_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) continue; - f_read(&file, seeddb, TEMP_BUFFER_SIZE, &btr); + f_read(&file, seeddb, TEMP_BUFFER_SIZE / 2, &btr); f_close(&file); if (seeddb->n_entries > (btr - 16) / 32) continue; // filesize / seeddb size mismatch @@ -288,20 +288,25 @@ u32 DecryptNcchSequential(u8* data, u32 offset, u32 size) { // unexpected results otherwise static NcchHeader ncch = { 0 }; static ExeFsHeader exefs = { 0 }; + static NcchHeader* ncchptr = NULL; static ExeFsHeader* exefsptr = NULL; // fetch ncch header from data if ((offset == 0) && (size >= sizeof(NcchHeader))) { memcpy(&ncch, data, sizeof(NcchHeader)); + ncchptr = (ValidateNcchHeader(&ncch) == 0) ? &ncch : NULL; exefsptr = NULL; } + // safety check, ncch pointer + if (!ncchptr) return 1; + // fetch exefs header from data if (!exefsptr) { - u32 offset_exefs = ncch.offset_exefs * NCCH_MEDIA_UNIT; + u32 offset_exefs = ncchptr->offset_exefs * NCCH_MEDIA_UNIT; if ((offset <= offset_exefs) && ((offset + size) >= offset_exefs + sizeof(ExeFsHeader))) { - if (DecryptNcch(data, offset, offset_exefs + sizeof(ExeFsHeader) - offset, &ncch, NULL) != 0) + if (DecryptNcch(data, offset, offset_exefs + sizeof(ExeFsHeader) - offset, ncchptr, NULL) != 0) return 1; memcpy(&exefs, data + offset_exefs - offset, sizeof(ExeFsHeader)); size -= offset_exefs + sizeof(ExeFsHeader) - offset; @@ -311,5 +316,5 @@ u32 DecryptNcchSequential(u8* data, u32 offset, u32 size) { } } - return DecryptNcch(data, offset, size, &ncch, exefsptr); + return DecryptNcch(data, offset, size, ncchptr, exefsptr); } diff --git a/source/godmode.c b/source/godmode.c index ddb4914..66ac6fb 100644 --- a/source/godmode.c +++ b/source/godmode.c @@ -570,6 +570,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur bool verificable = (filetype & FYTPE_VERIFICABLE); bool decryptable = (filetype & FYTPE_DECRYPTABLE); bool decryptable_inplace = (decryptable && (drvtype & (DRV_SDCARD|DRV_RAMDRIVE))); + bool buildable = (filetype & FTYPE_BUILDABLE); + bool buildable_legit = (filetype & FTYPE_BUILDABLE_L); char pathstr[32 + 1]; TruncateString(pathstr, curr_entry->path, 32, 8); @@ -596,7 +598,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur (filetype == GAME_NCCH ) ? "NCCH image options..." : (filetype == GAME_EXEFS) ? "Mount as EXEFS image" : (filetype == GAME_ROMFS) ? "Mount as ROMFS image" : - (filetype == GAME_TMD) ? "Verify TMD file" : "???"; + (filetype == GAME_TMD) ? "TMD file options..." : "???"; optionstr[hexviewer-1] = "Show in Hexeditor"; optionstr[calcsha-1] = "Calculate SHA-256"; if (inject > 0) optionstr[inject-1] = "Inject data @offset"; @@ -638,10 +640,14 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur int mount = (mountable) ? ++n_opt : -1; int decrypt = (decryptable) ? ++n_opt : -1; int decrypt_inplace = (decryptable_inplace) ? ++n_opt : -1; + int build = (buildable) ? ++n_opt : -1; + int build_legit = (buildable_legit) ? ++n_opt : -1; int verify = (verificable) ? ++n_opt : -1; if (mount > 0) optionstr[mount-1] = "Mount image to drive"; if (decrypt > 0) optionstr[decrypt-1] = "Decrypt file (SD output)"; if (decrypt_inplace > 0) optionstr[decrypt_inplace-1] = "Decrypt file (inplace)"; + 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"; u32 n_marked = 0; @@ -714,6 +720,36 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur } } return 0; + } else if ((user_select == build) || (user_select == build_legit)) { // -> build CIA + bool force_legit = (user_select == build_legit); + if ((n_marked > 1) && ShowPrompt(true, "Try to process all %lu selected files?", n_marked)) { + u32 n_success = 0; + u32 n_other = 0; + 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) { + n_other++; + continue; + } + current_dir->entry[i].marked = false; + if (BuildCiaFromGameFile(path, force_legit) == 0) n_success++; + else { // on failure: set *cursor on failed title, break; + *cursor = i; + break; + } + } + if (n_other) ShowPrompt(false, "%lu/%lu CIAs built ok\n%lu/%lu not of same type", + 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); + } else { + 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); + } + return 0; } else if (user_select == verify) { // -> verify game file if ((n_marked > 1) && ShowPrompt(true, "Try to verify all %lu selected files?", n_marked)) { u32 n_success = 0;