diff --git a/source/fs/filetype.c b/source/fs/filetype.c index b5f052f..c0f671f 100644 --- a/source/fs/filetype.c +++ b/source/fs/filetype.c @@ -41,7 +41,7 @@ u32 IdentifyFileType(const char* path) { } 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 >= CIA_TMD_SIZE_N(getbe16(header + 0x1DE))) + if (fsize >= TMD_SIZE_N(getbe16(header + 0x1DE))) return GAME_TMD; // TMD file } diff --git a/source/game/cia.c b/source/game/cia.c index cc2787e..c124c31 100644 --- a/source/game/cia.c +++ b/source/game/cia.c @@ -9,9 +9,9 @@ u32 ValidateCiaHeader(CiaHeader* header) { if ((header->size_header != CIA_HEADER_SIZE) || (header->size_cert != CIA_CERT_SIZE) || - (header->size_ticket != CIA_TICKET_SIZE) || - (header->size_tmd < CIA_TMD_SIZE_MIN) || - (header->size_tmd > CIA_TMD_SIZE_MAX) || + (header->size_ticket != TICKET_SIZE) || + (header->size_tmd < TMD_SIZE_MIN) || + (header->size_tmd > TMD_SIZE_MAX) || (header->size_content == 0) || ((header->size_meta != 0) && (header->size_meta != CIA_META_SIZE))) return 1; @@ -37,31 +37,11 @@ u32 GetCiaInfo(CiaInfo* info, CiaHeader* header) { return 0; } -u32 GetTmdCtr(u8* ctr, TmdContentChunk* chunk) { - memset(ctr, 0, 16); - memcpy(ctr, chunk->index, 2); - return 0; -} - -u32 FixTmdHashes(TitleMetaData* tmd) { - TmdContentChunk* content_list = (TmdContentChunk*) (tmd + 1); - u32 content_count = getbe16(tmd->content_count); - // recalculate content info hashes - for (u32 i = 0, kc = 0; i < 64 && kc < content_count; i++) { - TmdContentInfo* info = tmd->contentinfo + i; - u32 k = getbe16(info->cmd_count); - sha_quick(info->hash, content_list + kc, k * sizeof(TmdContentChunk), SHA256_MODE); - kc += k; - } - sha_quick(tmd->contentinfo_hash, (u8*)tmd->contentinfo, 64 * sizeof(TmdContentInfo), SHA256_MODE); - 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); + header->size_tmd = 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); @@ -102,29 +82,6 @@ u32 BuildCiaCert(u8* ciacert) { return 0; } -u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents, u32 save_size) { - const u8 sig_type[4] = { TMD_SIG_TYPE }; - // safety check: number of contents - if (n_contents > CIA_MAX_CONTENTS) return 1; // !!! - // set TMD all zero for a clean start - memset(tmd, 0x00, CIA_TMD_SIZE_N(n_contents)); - // file TMD values - memcpy(tmd->sig_type, sig_type, 4); - memset(tmd->signature, 0xFF, 0x100); - snprintf((char*) tmd->issuer, 0x40, TMD_ISSUER); - tmd->version = 0x01; - memcpy(tmd->title_id, title_id, 8); - tmd->title_type[3] = 0x40; // whatever - for (u32 i = 0; i < 4; i++) tmd->save_size[i] = (save_size >> (i*8)) & 0xFF; // little endian? - tmd->content_count[1] = (u8) n_contents; - memset(tmd->contentinfo_hash, 0xFF, 0x20); // placeholder (hash) - tmd->contentinfo[0].cmd_count[1] = (u8) n_contents; - memset(tmd->contentinfo[0].hash, 0xFF, 0x20); // placeholder (hash) - // nothing to do for content list (yet) - - return 0; -} - u32 BuildCiaMeta(CiaMeta* meta, void* exthdr, void* smdh) { // init metadata with all zeroes and core version memset(meta, 0x00, sizeof(CiaMeta)); diff --git a/source/game/cia.h b/source/game/cia.h index 5dc57a2..c57dfb2 100644 --- a/source/game/cia.h +++ b/source/game/cia.h @@ -2,20 +2,11 @@ #include "common.h" #include "ticket.h" +#include "tmd.h" -#define TICKET_ISSUER "Root-CA00000003-XS0000000c" -#define TICKET_ISSUER_DEV "Root-CA00000004-XS00000009" -#define TMD_ISSUER "Root-CA00000003-CP0000000b" -#define TMD_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) #define CIA_CERT_SIZE 0xA00 #define CIA_META_SIZE sizeof(CiaMeta) -#define CIA_TICKET_SIZE sizeof(Ticket) -#define CIA_TMD_SIZE_MIN sizeof(TitleMetaData) -#define CIA_TMD_SIZE_MAX (sizeof(TitleMetaData) + (CIA_MAX_CONTENTS*sizeof(TmdContentChunk))) -#define CIA_TMD_SIZE_N(n) (sizeof(TitleMetaData) + (n*sizeof(TmdContentChunk))) // see: https://www.3dbrew.org/wiki/CIA#Meta typedef struct { @@ -26,48 +17,6 @@ typedef struct { u8 smdh[0x36C0]; // from ExeFS } __attribute__((packed)) CiaMeta; -// from: https://github.com/profi200/Project_CTR/blob/02159e17ee225de3f7c46ca195ff0f9ba3b3d3e4/ctrtool/tmd.h#L18-L59; -typedef struct { - u8 id[4]; - u8 index[2]; - u8 type[2]; - u8 size[8]; - u8 hash[0x20]; -} __attribute__((packed)) TmdContentChunk; - -typedef struct { - u8 index[2]; - u8 cmd_count[2]; - u8 hash[0x20]; -} __attribute__((packed)) TmdContentInfo; - -typedef struct { - u8 sig_type[4]; - u8 signature[0x100]; - u8 padding[0x3C]; - u8 issuer[0x40]; - u8 version; - u8 ca_crl_version; - u8 signer_crl_version; - u8 reserved0; - u8 system_version[8]; - u8 title_id[8]; - u8 title_type[4]; - u8 group_id[2]; - u8 save_size[4]; - u8 twl_privsave_size[4]; - u8 reserved1[4]; - u8 twl_flag; - u8 reserved2[0x31]; - u8 access_rights[4]; - u8 title_version[2]; - u8 content_count[2]; - u8 boot_content[2]; - u8 reserved3[2]; - u8 contentinfo_hash[0x20]; - TmdContentInfo contentinfo[64]; -} __attribute__((packed)) TitleMetaData; - typedef struct { u32 size_header; u16 type; @@ -86,9 +35,9 @@ typedef struct { u8 cert[CIA_CERT_SIZE]; // cert is aligned and needs no padding Ticket ticket; - u8 ticket_padding[0x40 - (CIA_TICKET_SIZE % 0x40)]; + u8 ticket_padding[0x40 - (TICKET_SIZE % 0x40)]; TitleMetaData tmd; - TmdContentChunk content_list[CIA_MAX_CONTENTS]; + TmdContentChunk content_list[TMD_MAX_CONTENTS]; } __attribute__((packed)) CiaStub; typedef struct { // first 0x20 bytes are identical with CIA header @@ -113,12 +62,9 @@ typedef struct { // first 0x20 bytes are identical with CIA header u32 ValidateCiaHeader(CiaHeader* header); u32 GetCiaInfo(CiaInfo* info, CiaHeader* header); -u32 GetTmdCtr(u8* ctr, TmdContentChunk* chunk); -u32 FixTmdHashes(TitleMetaData* tmd); u32 FixCiaHeaderForTmd(CiaHeader* header, TitleMetaData* tmd); u32 BuildCiaCert(u8* ciacert); -u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents, u32 save_size); u32 BuildCiaMeta(CiaMeta* meta, void* exthdr, void* smdh); u32 BuildCiaHeader(CiaHeader* header); diff --git a/source/game/gameutil.c b/source/game/gameutil.c index 357cb73..535883c 100644 --- a/source/game/gameutil.c +++ b/source/game/gameutil.c @@ -209,9 +209,9 @@ 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) || + if ((fx_read(&file, tmd, TMD_SIZE_MAX, &btr) != FR_OK) || (memcmp(tmd->sig_type, magic, sizeof(magic)) != 0) || - (btr < CIA_TMD_SIZE_N(getbe16(tmd->content_count)))) { + (btr < TMD_SIZE_N(getbe16(tmd->content_count)))) { fx_close(&file); return 1; } @@ -401,7 +401,7 @@ u32 VerifyCiaFile(const char* path) { // verify contents u32 content_count = getbe16(cia->tmd.content_count); u64 next_offset = info.offset_content; - for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(cia->content_list[i]); if (VerifyTmdContent(path, next_offset, chunk, titlekey) != 0) { ShowPrompt(false, "%s\nID %08lX (%08llX@%08llX)\nVerification failed", @@ -438,7 +438,7 @@ u32 VerifyTmdFile(const char* path) { // verify contents u32 content_count = getbe16(tmd->content_count); - for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(content_list[i]); chunk->type[1] &= ~0x01; // remove crypto flag snprintf(name_content, 256 - (name_content - path_content), "%08lx.app", getbe32(chunk->id)); @@ -515,7 +515,7 @@ u32 CheckEncryptedCiaFile(const char* path) { // check for encryption in CIA contents u32 content_count = getbe16(cia->tmd.content_count); u64 next_offset = info.offset_content; - for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(cia->content_list[i]); if ((getbe16(chunk->type) & 0x1) || (CheckEncryptedNcchFile(path, next_offset) == 0)) return 0; // encryption found @@ -641,7 +641,7 @@ u32 DecryptCiaFile(const char* orig, const char* dest) { // decrypt CIA contents u32 content_count = getbe16(cia->tmd.content_count); u64 next_offset = info.offset_content; - for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(cia->content_list[i]); u64 size = getbe64(chunk->size); if (DecryptNcchNcsdFile(orig, dest, GAME_CIA, next_offset, size, chunk, titlekey) != 0) @@ -820,7 +820,7 @@ u32 BuildCiaFromTmdFile(const char* path_tmd, const char* path_cia, bool force_l // 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++) { + for (u32 i = 0; (i < content_count) && (i < TMD_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, false) != 0) { diff --git a/source/game/ticket.h b/source/game/ticket.h index b6c040f..c3d77c5 100644 --- a/source/game/ticket.h +++ b/source/game/ticket.h @@ -2,6 +2,8 @@ #include "common.h" +#define TICKET_SIZE sizeof(Ticket) + #define TICKET_ISSUER "Root-CA00000003-XS0000000c" #define TICKET_ISSUER_DEV "Root-CA00000004-XS00000009" #define TICKET_SIG_TYPE 0x00, 0x01, 0x00, 0x04 // RSA_2048 SHA256 diff --git a/source/game/tmd.c b/source/game/tmd.c new file mode 100644 index 0000000..11afe6a --- /dev/null +++ b/source/game/tmd.c @@ -0,0 +1,45 @@ +#include "tmd.h" +#include "sha.h" + +u32 GetTmdCtr(u8* ctr, TmdContentChunk* chunk) { + memset(ctr, 0, 16); + memcpy(ctr, chunk->index, 2); + return 0; +} + +u32 FixTmdHashes(TitleMetaData* tmd) { + TmdContentChunk* content_list = (TmdContentChunk*) (tmd + 1); + u32 content_count = getbe16(tmd->content_count); + // recalculate content info hashes + for (u32 i = 0, kc = 0; i < 64 && kc < content_count; i++) { + TmdContentInfo* info = tmd->contentinfo + i; + u32 k = getbe16(info->cmd_count); + sha_quick(info->hash, content_list + kc, k * sizeof(TmdContentChunk), SHA256_MODE); + kc += k; + } + sha_quick(tmd->contentinfo_hash, (u8*)tmd->contentinfo, 64 * sizeof(TmdContentInfo), SHA256_MODE); + return 0; +} + +u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents, u32 save_size) { + const u8 sig_type[4] = { TMD_SIG_TYPE }; + // safety check: number of contents + if (n_contents > TMD_MAX_CONTENTS) return 1; // !!! + // set TMD all zero for a clean start + memset(tmd, 0x00, TMD_SIZE_N(n_contents)); + // file TMD values + memcpy(tmd->sig_type, sig_type, 4); + memset(tmd->signature, 0xFF, 0x100); + snprintf((char*) tmd->issuer, 0x40, TMD_ISSUER); + tmd->version = 0x01; + memcpy(tmd->title_id, title_id, 8); + tmd->title_type[3] = 0x40; // whatever + for (u32 i = 0; i < 4; i++) tmd->save_size[i] = (save_size >> (i*8)) & 0xFF; // little endian? + tmd->content_count[1] = (u8) n_contents; + memset(tmd->contentinfo_hash, 0xFF, 0x20); // placeholder (hash) + tmd->contentinfo[0].cmd_count[1] = (u8) n_contents; + memset(tmd->contentinfo[0].hash, 0xFF, 0x20); // placeholder (hash) + // nothing to do for content list (yet) + + return 0; +} diff --git a/source/game/tmd.h b/source/game/tmd.h new file mode 100644 index 0000000..fc1824b --- /dev/null +++ b/source/game/tmd.h @@ -0,0 +1,58 @@ +#pragma once + +#include "common.h" + +#define TMD_MAX_CONTENTS (100+1) // theme CIAs contain maximum 100 themes + 1 index content + +#define TMD_SIZE_MIN sizeof(TitleMetaData) +#define TMD_SIZE_MAX (sizeof(TitleMetaData) + (TMD_MAX_CONTENTS*sizeof(TmdContentChunk))) +#define TMD_SIZE_N(n) (sizeof(TitleMetaData) + (n*sizeof(TmdContentChunk))) + +#define TMD_ISSUER "Root-CA00000003-CP0000000b" +#define TMD_SIG_TYPE 0x00, 0x01, 0x00, 0x04 // RSA_2048 SHA256 + +// from: https://github.com/profi200/Project_CTR/blob/02159e17ee225de3f7c46ca195ff0f9ba3b3d3e4/ctrtool/tmd.h#L18-L59; +typedef struct { + u8 id[4]; + u8 index[2]; + u8 type[2]; + u8 size[8]; + u8 hash[0x20]; +} __attribute__((packed)) TmdContentChunk; + +typedef struct { + u8 index[2]; + u8 cmd_count[2]; + u8 hash[0x20]; +} __attribute__((packed)) TmdContentInfo; + +typedef struct { + u8 sig_type[4]; + u8 signature[0x100]; + u8 padding[0x3C]; + u8 issuer[0x40]; + u8 version; + u8 ca_crl_version; + u8 signer_crl_version; + u8 reserved0; + u8 system_version[8]; + u8 title_id[8]; + u8 title_type[4]; + u8 group_id[2]; + u8 save_size[4]; + u8 twl_privsave_size[4]; + u8 reserved1[4]; + u8 twl_flag; + u8 reserved2[0x31]; + u8 access_rights[4]; + u8 title_version[2]; + u8 content_count[2]; + u8 boot_content[2]; + u8 reserved3[2]; + u8 contentinfo_hash[0x20]; + TmdContentInfo contentinfo[64]; +} __attribute__((packed)) TitleMetaData; + +u32 GetTmdCtr(u8* ctr, TmdContentChunk* chunk); +u32 FixTmdHashes(TitleMetaData* tmd); +u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents, u32 save_size); diff --git a/source/virtual/vgame.c b/source/virtual/vgame.c index 51eafa4..6caa4cf 100644 --- a/source/virtual/vgame.c +++ b/source/virtual/vgame.c @@ -293,7 +293,7 @@ bool BuildVGameCiaDir(void) { TmdContentChunk* content_list = cia->content_list; u32 content_count = getbe16(cia->tmd.content_count); u64 next_offset = info.offset_content; - for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { u64 size = getbe64(content_list[i].size); bool is_ncch = false; // is unencrypted NCCH? if (!(getbe16(content_list[i].type) & 0x1)) {