2016-12-10 15:40:36 +01:00
|
|
|
#include "gameutil.h"
|
2020-07-22 00:20:16 +02:00
|
|
|
#include "nandcmac.h"
|
2018-02-21 01:57:35 +01:00
|
|
|
#include "disadiff.h"
|
2016-12-08 22:06:47 +01:00
|
|
|
#include "game.h"
|
2018-07-16 01:07:59 +02:00
|
|
|
#include "nand.h" // so that we can trim NAND images
|
2017-03-29 02:23:06 +02:00
|
|
|
#include "hid.h"
|
2016-12-08 22:06:47 +01:00
|
|
|
#include "ui.h"
|
2017-11-03 16:11:29 +01:00
|
|
|
#include "fs.h"
|
2017-02-26 13:35:37 +01:00
|
|
|
#include "unittype.h"
|
2016-12-08 22:06:47 +01:00
|
|
|
#include "aes.h"
|
|
|
|
#include "sha.h"
|
|
|
|
|
2017-01-31 16:16:26 +01:00
|
|
|
// use NCCH crypto defines for everything
|
|
|
|
#define CRYPTO_DECRYPT NCCH_NOCRYPTO
|
|
|
|
#define CRYPTO_ENCRYPT NCCH_STDCRYPTO
|
|
|
|
|
2020-01-02 20:58:57 +01:00
|
|
|
u32 GetNcchHeaders(NcchHeader* ncch, NcchExtHeader* exthdr, ExeFsHeader* exefs, FIL* file, bool nocrypto) {
|
2017-01-16 22:32:32 +01:00
|
|
|
u32 offset_ncch = fvx_tell(file);
|
2016-12-08 22:06:47 +01:00
|
|
|
UINT btr;
|
2020-01-02 20:58:57 +01:00
|
|
|
|
|
|
|
if (fvx_read(file, ncch, sizeof(NcchHeader), &btr) != FR_OK) return 1;
|
|
|
|
if (nocrypto) {
|
|
|
|
ncch->flags[3] = 0x00;
|
|
|
|
ncch->flags[7] = (ncch->flags[7] & ~0x21) | 0x04;
|
|
|
|
}
|
|
|
|
if (ValidateNcchHeader(ncch) != 0) return 1;
|
2016-12-08 22:06:47 +01:00
|
|
|
|
2016-12-19 01:33:30 +01:00
|
|
|
if (exthdr) {
|
2016-12-19 19:17:03 +01:00
|
|
|
if (!ncch->size_exthdr) return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(file, offset_ncch + NCCH_EXTHDR_OFFSET);
|
|
|
|
if ((fvx_read(file, exthdr, NCCH_EXTHDR_SIZE, &btr) != FR_OK) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(DecryptNcch((u8*) exthdr, NCCH_EXTHDR_OFFSET, NCCH_EXTHDR_SIZE, ncch, NULL) != 0))
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (exefs) {
|
|
|
|
if (!ncch->size_exefs) return 1;
|
2016-12-08 22:06:47 +01:00
|
|
|
u32 offset_exefs = offset_ncch + (ncch->offset_exefs * NCCH_MEDIA_UNIT);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(file, offset_exefs);
|
|
|
|
if ((fvx_read(file, exefs, sizeof(ExeFsHeader), &btr) != FR_OK) ||
|
2016-12-08 22:06:47 +01:00
|
|
|
(DecryptNcch((u8*) exefs, ncch->offset_exefs * NCCH_MEDIA_UNIT, sizeof(ExeFsHeader), ncch, NULL) != 0) ||
|
|
|
|
(ValidateExeFsHeader(exefs, ncch->size_exefs * NCCH_MEDIA_UNIT) != 0))
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
u32 CheckNcchHash(u8* expected, FIL* file, u32 size_data, u32 offset_ncch, NcchHeader* ncch, ExeFsHeader* exefs) {
|
2017-01-16 22:32:32 +01:00
|
|
|
u32 offset_data = fvx_tell(file) - offset_ncch;
|
2016-12-08 22:06:47 +01:00
|
|
|
u8 hash[32];
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
u8* buffer = (u8*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!buffer) return 1;
|
|
|
|
|
2016-12-08 22:06:47 +01:00
|
|
|
sha_init(SHA256_MODE);
|
2018-01-24 23:32:06 +01:00
|
|
|
for (u32 i = 0; i < size_data; i += STD_BUFFER_SIZE) {
|
|
|
|
u32 read_bytes = min(STD_BUFFER_SIZE, (size_data - i));
|
2016-12-08 22:06:47 +01:00
|
|
|
UINT bytes_read;
|
2018-01-24 23:32:06 +01:00
|
|
|
fvx_read(file, buffer, read_bytes, &bytes_read);
|
|
|
|
DecryptNcch(buffer, offset_data + i, read_bytes, ncch, exefs);
|
|
|
|
sha_update(buffer, read_bytes);
|
2016-12-08 22:06:47 +01:00
|
|
|
}
|
|
|
|
sha_get(hash);
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(buffer);
|
|
|
|
|
2016-12-08 22:06:47 +01:00
|
|
|
return (memcmp(hash, expected, 32) == 0) ? 0 : 1;
|
|
|
|
}
|
|
|
|
|
2016-12-19 01:33:30 +01:00
|
|
|
u32 LoadNcchHeaders(NcchHeader* ncch, NcchExtHeader* exthdr, ExeFsHeader* exefs, const char* path, u32 offset) {
|
|
|
|
FIL file;
|
|
|
|
|
|
|
|
// open file, get NCCH header
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset);
|
2020-01-02 20:58:57 +01:00
|
|
|
if (GetNcchHeaders(ncch, exthdr, exefs, &file, false) != 0) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-19 01:33:30 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-11 16:28:51 +01:00
|
|
|
u32 LoadNcsdHeader(NcsdHeader* ncsd, const char* path) {
|
|
|
|
FIL file;
|
|
|
|
UINT btr;
|
|
|
|
|
|
|
|
// open file, get NCSD header
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, 0);
|
|
|
|
if ((fvx_read(&file, ncsd, sizeof(NcsdHeader), &btr) != FR_OK) ||
|
2016-12-11 16:28:51 +01:00
|
|
|
(ValidateNcsdHeader(ncsd) != 0)) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 LoadCiaStub(CiaStub* stub, const char* path) {
|
|
|
|
FIL file;
|
|
|
|
UINT btr;
|
|
|
|
CiaInfo info;
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
|
|
|
|
|
|
|
// first 0x20 byte of CIA header
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, 0);
|
|
|
|
if ((fvx_read(&file, stub, 0x20, &btr) != FR_OK) || (btr != 0x20) ||
|
2016-12-11 16:28:51 +01:00
|
|
|
(ValidateCiaHeader(&(stub->header)) != 0)) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
GetCiaInfo(&info, &(stub->header));
|
|
|
|
|
|
|
|
// everything up till content offset
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, 0);
|
|
|
|
if ((fvx_read(&file, stub, info.offset_content, &btr) != FR_OK) || (btr != info.offset_content)) {
|
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-09-08 03:12:04 +02:00
|
|
|
u32 LoadExeFsFile(void* data, const char* path, u32 offset, const char* name, u32 size_max, u32* bytes_read) {
|
2016-12-15 11:46:00 +01:00
|
|
|
NcchHeader ncch;
|
|
|
|
ExeFsHeader exefs;
|
|
|
|
FIL file;
|
|
|
|
UINT btr;
|
|
|
|
u32 ret = 0;
|
|
|
|
|
|
|
|
// open file, get NCCH, ExeFS header
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset);
|
2020-01-02 20:58:57 +01:00
|
|
|
if ((GetNcchHeaders(&ncch, NULL, &exefs, &file, false) != 0) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(!ncch.size_exefs)) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-19 01:33:30 +01:00
|
|
|
// load file from exefs
|
|
|
|
ExeFsFileHeader* exefile = NULL;
|
|
|
|
for (u32 i = 0; i < 10; i++) {
|
|
|
|
u32 size = exefs.files[i].size;
|
|
|
|
if (!size || (size > size_max)) continue;
|
|
|
|
char* exename = exefs.files[i].name;
|
|
|
|
if (strncmp(name, exename, 8) == 0) {
|
|
|
|
exefile = exefs.files + i;
|
|
|
|
break;
|
2016-12-15 11:46:00 +01:00
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
}
|
2017-03-29 02:23:06 +02:00
|
|
|
|
2016-12-19 01:33:30 +01:00
|
|
|
if (exefile) {
|
|
|
|
u32 size_exefile = exefile->size;
|
|
|
|
u32 offset_exefile = (ncch.offset_exefs * NCCH_MEDIA_UNIT) + sizeof(ExeFsHeader) + exefile->offset;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset + offset_exefile); // offset to file
|
|
|
|
if ((fvx_read(&file, data, size_exefile, &btr) != FR_OK) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(DecryptNcch(data, offset_exefile, size_exefile, &ncch, &exefs) != 0) ||
|
|
|
|
(btr != size_exefile)) {
|
|
|
|
ret = 1;
|
2016-12-15 11:46:00 +01:00
|
|
|
}
|
|
|
|
} else ret = 1;
|
|
|
|
|
2017-09-08 03:12:04 +02:00
|
|
|
if (bytes_read) *bytes_read = btr;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-15 11:46:00 +01:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2016-12-19 01:33:30 +01:00
|
|
|
u32 LoadNcchMeta(CiaMeta* meta, const char* path, u64 offset) {
|
|
|
|
NcchHeader ncch;
|
|
|
|
NcchExtHeader exthdr;
|
|
|
|
|
|
|
|
// get dependencies from exthdr, icon from exeFS
|
|
|
|
if ((LoadNcchHeaders(&ncch, &exthdr, NULL, path, offset) != 0) ||
|
|
|
|
(BuildCiaMeta(meta, &exthdr, NULL) != 0) ||
|
2017-09-08 03:12:04 +02:00
|
|
|
(LoadExeFsFile(meta->smdh, path, offset, "icon", sizeof(meta->smdh), NULL)))
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-11 16:28:51 +01:00
|
|
|
u32 LoadTmdFile(TitleMetaData* tmd, const char* path) {
|
2020-03-09 22:52:45 +01:00
|
|
|
// first part (TMD only) (we need to read the content count first)
|
|
|
|
if ((fvx_qread(path, tmd, 0, TMD_SIZE_MIN, NULL) != FR_OK) ||
|
|
|
|
(ValidateTmd(tmd) != 0))
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
// second part (read full size)
|
|
|
|
if (fvx_qread(path, tmd, 0, TMD_SIZE_N(getbe16(tmd->content_count)), NULL) != FR_OK)
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
|
|
|
|
2017-02-06 02:57:21 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-01-23 00:29:11 +00:00
|
|
|
u32 LoadCdnTicketFile(Ticket** ticket, const char* path_cnt) {
|
|
|
|
if(!ticket) return 1;
|
2017-02-10 16:12:00 +01:00
|
|
|
// path points to CDN content file
|
2017-02-06 02:57:21 +01:00
|
|
|
char path_cetk[256];
|
2017-02-10 16:12:00 +01:00
|
|
|
strncpy(path_cetk, path_cnt, 256);
|
2018-05-24 00:14:37 +02:00
|
|
|
path_cetk[255] = '\0';
|
2017-02-10 16:12:00 +01:00
|
|
|
char* name_cetk = strrchr(path_cetk, '/');
|
2017-02-06 02:57:21 +01:00
|
|
|
if (!name_cetk) return 1; // will not happen
|
2017-02-10 16:12:00 +01:00
|
|
|
char* ext_cetk = strrchr(++name_cetk, '.');
|
|
|
|
ext_cetk = (ext_cetk) ? ext_cetk + 1 : name_cetk;
|
|
|
|
snprintf(ext_cetk, 256 - (ext_cetk - path_cetk), "cetk");
|
2017-02-06 02:57:21 +01:00
|
|
|
|
|
|
|
// load and check ticket
|
2020-01-23 00:29:11 +00:00
|
|
|
TicketMinimum tmp;
|
2017-02-06 02:57:21 +01:00
|
|
|
UINT br;
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((fvx_qread(path_cetk, &tmp, 0, TICKET_MINIMUM_SIZE, &br) != FR_OK) || (br != TICKET_MINIMUM_SIZE) ||
|
|
|
|
ValidateTicket((Ticket*)&tmp) != 0) return 1;
|
|
|
|
|
|
|
|
u32 tik_size = GetTicketSize((Ticket*)&tmp);
|
|
|
|
|
|
|
|
Ticket* tik = (Ticket*)malloc(tik_size);
|
|
|
|
if (!tik) return 1;
|
|
|
|
|
|
|
|
if ((fvx_qread(path_cetk, tik, 0, tik_size, &br) != FR_OK) || (br != tik_size)) {
|
|
|
|
free(tik);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
*ticket = tik;
|
2016-12-11 16:28:51 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-05-15 22:59:12 +02:00
|
|
|
u32 GetTmdContentPath(char* path_content, const char* path_tmd) {
|
|
|
|
// get path to TMD first content
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 dlc_tid_high[] = { DLC_TID_HIGH };
|
2017-05-15 22:59:12 +02:00
|
|
|
|
|
|
|
// content path string
|
|
|
|
char* name_content;
|
|
|
|
strncpy(path_content, path_tmd, 256);
|
2018-05-24 00:14:37 +02:00
|
|
|
path_content[255] = '\0';
|
2017-05-15 22:59:12 +02:00
|
|
|
name_content = strrchr(path_content, '/');
|
|
|
|
if (!name_content) return 1; // will not happen
|
|
|
|
name_content++;
|
|
|
|
|
|
|
|
// load TMD file
|
2018-01-24 23:32:06 +01:00
|
|
|
TitleMetaData* tmd = (TitleMetaData*) malloc(TMD_SIZE_MAX);
|
|
|
|
TmdContentChunk* chunk = (TmdContentChunk*) (tmd + 1);
|
|
|
|
if (!tmd) return 1;
|
|
|
|
if ((LoadTmdFile(tmd, path_tmd) != 0) || !getbe16(tmd->content_count)) {
|
|
|
|
free(tmd);
|
2017-05-15 22:59:12 +02:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2017-05-15 22:59:12 +02:00
|
|
|
snprintf(name_content, 256 - (name_content - path_content),
|
|
|
|
(memcmp(tmd->title_id, dlc_tid_high, sizeof(dlc_tid_high)) == 0) ? "00000000/%08lx.app" : "%08lx.app", getbe32(chunk->id));
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(tmd);
|
2017-05-15 22:59:12 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
u32 WriteCiaStub(CiaStub* stub, const char* path) {
|
|
|
|
FIL file;
|
|
|
|
UINT btw;
|
|
|
|
CiaInfo info;
|
|
|
|
|
|
|
|
GetCiaInfo(&info, &(stub->header));
|
|
|
|
|
|
|
|
// everything up till content offset
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_WRITE | FA_OPEN_ALWAYS) != FR_OK)
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, 0);
|
|
|
|
if ((fvx_write(&file, stub, info.offset_content, &btw) != FR_OK) || (btw != info.offset_content)) {
|
|
|
|
fvx_close(&file);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-11 16:28:51 +01:00
|
|
|
u32 VerifyTmdContent(const char* path, u64 offset, TmdContentChunk* chunk, const u8* titlekey) {
|
|
|
|
u8 hash[32];
|
|
|
|
u8 ctr[16];
|
|
|
|
FIL file;
|
|
|
|
|
|
|
|
u8* expected = chunk->hash;
|
|
|
|
u64 size = getbe64(chunk->size);
|
|
|
|
bool encrypted = getbe16(chunk->type) & 0x1;
|
|
|
|
|
|
|
|
if (!ShowProgress(0, 0, path)) return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (offset + size > fvx_size(&file)) {
|
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset);
|
2016-12-11 16:28:51 +01:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
u8* buffer = (u8*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!buffer) {
|
|
|
|
fvx_close(&file);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-11 16:28:51 +01:00
|
|
|
GetTmdCtr(ctr, chunk);
|
|
|
|
sha_init(SHA256_MODE);
|
2018-01-24 23:32:06 +01:00
|
|
|
for (u32 i = 0; i < size; i += STD_BUFFER_SIZE) {
|
|
|
|
u32 read_bytes = min(STD_BUFFER_SIZE, (size - i));
|
2016-12-11 16:28:51 +01:00
|
|
|
UINT bytes_read;
|
2018-01-24 23:32:06 +01:00
|
|
|
fvx_read(&file, buffer, read_bytes, &bytes_read);
|
|
|
|
if (encrypted) DecryptCiaContentSequential(buffer, read_bytes, ctr, titlekey);
|
|
|
|
sha_update(buffer, read_bytes);
|
2016-12-11 16:28:51 +01:00
|
|
|
if (!ShowProgress(i + read_bytes, size, path)) break;
|
|
|
|
}
|
|
|
|
sha_get(hash);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(buffer);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-11 16:28:51 +01:00
|
|
|
|
|
|
|
return memcmp(hash, expected, 32);
|
|
|
|
}
|
|
|
|
|
2016-12-08 22:06:47 +01:00
|
|
|
u32 VerifyNcchFile(const char* path, u32 offset, u32 size) {
|
2020-01-02 20:58:57 +01:00
|
|
|
bool cryptofix = false;
|
2016-12-08 22:06:47 +01:00
|
|
|
NcchHeader ncch;
|
2018-02-07 02:16:31 +01:00
|
|
|
NcchExtHeader exthdr;
|
2016-12-08 22:06:47 +01:00
|
|
|
ExeFsHeader exefs;
|
|
|
|
FIL file;
|
|
|
|
|
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
|
|
|
// open file, get NCCH, ExeFS header
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-08 22:06:47 +01:00
|
|
|
return 1;
|
|
|
|
|
2020-01-02 20:58:57 +01:00
|
|
|
// fetch and check NCCH header
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset);
|
2020-01-02 20:58:57 +01:00
|
|
|
if (GetNcchHeaders(&ncch, NULL, NULL, &file, cryptofix) != 0) {
|
2016-12-08 22:06:47 +01:00
|
|
|
if (!offset) ShowPrompt(false, "%s\nError: Not a NCCH file", pathstr);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-08 22:06:47 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2020-01-02 20:58:57 +01:00
|
|
|
|
|
|
|
// check NCCH size
|
|
|
|
if (!size) size = fvx_size(&file) - offset;
|
|
|
|
if ((fvx_size(&file) < offset) || (size < ncch.size * NCCH_MEDIA_UNIT)) {
|
|
|
|
if (!offset) ShowPrompt(false, "%s\nError: File is too small", pathstr);
|
2018-09-03 22:55:37 +02:00
|
|
|
fvx_close(&file);
|
|
|
|
return 1;
|
|
|
|
}
|
2016-12-08 22:06:47 +01:00
|
|
|
|
2020-01-02 20:58:57 +01:00
|
|
|
// fetch and check ExeFS header
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset);
|
2020-01-02 20:58:57 +01:00
|
|
|
if (ncch.size_exefs && (GetNcchHeaders(&ncch, NULL, &exefs, &file, cryptofix) != 0)) {
|
|
|
|
bool borkedflags = false;
|
|
|
|
if (ncch.size_exefs && NCCH_ENCRYPTED(&ncch)) {
|
|
|
|
// disable crypto, try again
|
|
|
|
cryptofix = true;
|
|
|
|
fvx_lseek(&file, offset);
|
|
|
|
if ((GetNcchHeaders(&ncch, NULL, &exefs, &file, cryptofix) == 0) &&
|
|
|
|
ShowPrompt(true, "%s\nError: Bad crypto flags\n \nAttempt to fix?", pathstr))
|
|
|
|
borkedflags = true;
|
|
|
|
}
|
|
|
|
if (!borkedflags) {
|
|
|
|
if (!offset) ShowPrompt(false, "%s\nError: Bad ExeFS header", pathstr);
|
|
|
|
fvx_close(&file);
|
|
|
|
return 1;
|
|
|
|
}
|
2016-12-19 19:17:03 +01:00
|
|
|
}
|
2020-01-02 20:58:57 +01:00
|
|
|
|
|
|
|
// fetch and check ExtHeader
|
|
|
|
fvx_lseek(&file, offset);
|
|
|
|
if (ncch.size_exthdr && (GetNcchHeaders(&ncch, &exthdr, NULL, &file, cryptofix) != 0)) {
|
|
|
|
if (!offset) ShowPrompt(false, "%s\nError: Missing ExtHeader", pathstr);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-08 22:06:47 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2020-01-02 20:58:57 +01:00
|
|
|
|
2016-12-08 22:06:47 +01:00
|
|
|
// check / setup crypto
|
2017-01-30 22:06:26 +01:00
|
|
|
if (SetupNcchCrypto(&ncch, NCCH_NOCRYPTO) != 0) {
|
2016-12-08 22:06:47 +01:00
|
|
|
if (!offset) ShowPrompt(false, "%s\nError: Crypto not set up", pathstr);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-08 22:06:47 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 ver_exthdr = 0;
|
|
|
|
u32 ver_exefs = 0;
|
|
|
|
u32 ver_romfs = 0;
|
|
|
|
|
|
|
|
// base hash check for extheader
|
|
|
|
if (ncch.size_exthdr > 0) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset + NCCH_EXTHDR_OFFSET);
|
2016-12-15 11:46:00 +01:00
|
|
|
ver_exthdr = CheckNcchHash(ncch.hash_exthdr, &file, 0x400, offset, &ncch, NULL);
|
2016-12-08 22:06:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// base hash check for exefs
|
|
|
|
if (ncch.size_exefs > 0) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset + (ncch.offset_exefs * NCCH_MEDIA_UNIT));
|
2016-12-15 11:46:00 +01:00
|
|
|
ver_exefs = CheckNcchHash(ncch.hash_exefs, &file, ncch.size_exefs_hash * NCCH_MEDIA_UNIT, offset, &ncch, &exefs);
|
2016-12-08 22:06:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// base hash check for romfs
|
|
|
|
if (ncch.size_romfs > 0) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset + (ncch.offset_romfs * NCCH_MEDIA_UNIT));
|
2016-12-15 11:46:00 +01:00
|
|
|
ver_romfs = CheckNcchHash(ncch.hash_romfs, &file, ncch.size_romfs_hash * NCCH_MEDIA_UNIT, offset, &ncch, NULL);
|
2016-12-08 22:06:47 +01:00
|
|
|
}
|
|
|
|
|
2018-02-07 02:16:31 +01:00
|
|
|
// thorough exefs verification (workaround for Process9)
|
2018-09-25 01:42:52 +02:00
|
|
|
if (!ShowProgress(0, 0, path)) return 1;
|
2018-02-07 02:16:31 +01:00
|
|
|
if ((ncch.size_exefs > 0) && (memcmp(exthdr.name, "Process9", 8) != 0)) {
|
2016-12-08 22:06:47 +01:00
|
|
|
for (u32 i = 0; !ver_exefs && (i < 10); i++) {
|
|
|
|
ExeFsFileHeader* exefile = exefs.files + i;
|
|
|
|
u8* hash = exefs.hashes[9 - i];
|
|
|
|
if (!exefile->size) continue;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, offset + (ncch.offset_exefs * NCCH_MEDIA_UNIT) + 0x200 + exefile->offset);
|
2016-12-15 11:46:00 +01:00
|
|
|
ver_exefs = CheckNcchHash(hash, &file, exefile->size, offset, &ncch, &exefs);
|
2016-12-08 22:06:47 +01:00
|
|
|
}
|
|
|
|
}
|
2018-09-25 01:42:52 +02:00
|
|
|
|
|
|
|
// thorough romfs verification
|
|
|
|
if (!ver_romfs && (ncch.size_romfs > 0)) {
|
|
|
|
UINT btr;
|
|
|
|
|
|
|
|
// load ivfc header
|
|
|
|
RomFsIvfcHeader ivfc;
|
|
|
|
fvx_lseek(&file, offset + (ncch.offset_romfs * NCCH_MEDIA_UNIT));
|
|
|
|
if ((fvx_read(&file, &ivfc, sizeof(RomFsIvfcHeader), &btr) != FR_OK) ||
|
|
|
|
(DecryptNcch((u8*) &ivfc, ncch.offset_romfs * NCCH_MEDIA_UNIT, sizeof(RomFsIvfcHeader), &ncch, NULL) != 0) )
|
|
|
|
ver_romfs = 1;
|
|
|
|
|
|
|
|
// load data
|
|
|
|
u64 lvl1_size = 0;
|
|
|
|
u64 lvl2_size = 0;
|
|
|
|
u8* masterhash = NULL;
|
|
|
|
u8* lvl1_data = NULL;
|
|
|
|
u8* lvl2_data = NULL;
|
|
|
|
if (!ver_romfs && (ValidateRomFsHeader(&ivfc, ncch.size_romfs * NCCH_MEDIA_UNIT) == 0)) {
|
|
|
|
// load masterhash(es)
|
|
|
|
masterhash = malloc(ivfc.size_masterhash);
|
|
|
|
if (masterhash) {
|
|
|
|
u64 offset_add = (ncch.offset_romfs * NCCH_MEDIA_UNIT) + sizeof(RomFsIvfcHeader);
|
|
|
|
fvx_lseek(&file, offset + offset_add);
|
|
|
|
if ((fvx_read(&file, masterhash, ivfc.size_masterhash, &btr) != FR_OK) ||
|
|
|
|
(DecryptNcch(masterhash, offset_add, ivfc.size_masterhash, &ncch, NULL) != 0))
|
|
|
|
ver_romfs = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// load lvl1
|
|
|
|
lvl1_size = align(ivfc.size_lvl1, 1 << ivfc.log_lvl1);
|
|
|
|
lvl1_data = malloc(lvl1_size);
|
|
|
|
if (lvl1_data) {
|
|
|
|
u64 offset_add = (ncch.offset_romfs * NCCH_MEDIA_UNIT) + GetRomFsLvOffset(&ivfc, 1);
|
|
|
|
fvx_lseek(&file, offset + offset_add);
|
|
|
|
if ((fvx_read(&file, lvl1_data, lvl1_size, &btr) != FR_OK) ||
|
|
|
|
(DecryptNcch(lvl1_data, offset_add, lvl1_size, &ncch, NULL) != 0))
|
|
|
|
ver_romfs = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// load lvl2
|
|
|
|
lvl2_size = align(ivfc.size_lvl2, 1 << ivfc.log_lvl2);
|
|
|
|
lvl2_data = malloc(lvl2_size);
|
|
|
|
if (lvl2_data) {
|
|
|
|
u64 offset_add = (ncch.offset_romfs * NCCH_MEDIA_UNIT) + GetRomFsLvOffset(&ivfc, 2);
|
|
|
|
fvx_lseek(&file, offset + offset_add);
|
|
|
|
if ((fvx_read(&file, lvl2_data, lvl2_size, &btr) != FR_OK) ||
|
|
|
|
(DecryptNcch(lvl2_data, offset_add, lvl2_size, &ncch, NULL) != 0))
|
|
|
|
ver_romfs = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check mallocs
|
|
|
|
if (!masterhash || !lvl1_data || !lvl2_data)
|
|
|
|
ver_romfs = 1; // should never happen
|
|
|
|
}
|
|
|
|
|
|
|
|
// actual verification
|
|
|
|
if (!ver_romfs) {
|
|
|
|
// verify lvl1
|
|
|
|
u32 n_blocks = lvl1_size >> ivfc.log_lvl1;
|
|
|
|
u32 block_log = ivfc.log_lvl1;
|
|
|
|
for (u32 i = 0; !ver_romfs && (i < n_blocks); i++)
|
|
|
|
ver_romfs = (u32) sha_cmp(masterhash + (i*0x20), lvl1_data + (i<<block_log), 1<<block_log, SHA256_MODE);
|
|
|
|
|
|
|
|
// verify lvl2
|
|
|
|
n_blocks = lvl2_size >> ivfc.log_lvl2;
|
|
|
|
block_log = ivfc.log_lvl2;
|
|
|
|
for (u32 i = 0; !ver_romfs && (i < n_blocks); i++) {
|
|
|
|
ver_romfs = sha_cmp(lvl1_data + (i*0x20), lvl2_data + (i<<block_log), 1<<block_log, SHA256_MODE);
|
|
|
|
}
|
|
|
|
|
|
|
|
// lvl3 verification (this will take long)
|
|
|
|
u64 offset_add = (ncch.offset_romfs * NCCH_MEDIA_UNIT) + GetRomFsLvOffset(&ivfc, 3);
|
|
|
|
n_blocks = align(ivfc.size_lvl3, 1 << ivfc.log_lvl3) >> ivfc.log_lvl3;
|
|
|
|
block_log = ivfc.log_lvl3;
|
|
|
|
fvx_lseek(&file, offset + offset_add);
|
|
|
|
for (u32 i = 0; !ver_romfs && (i < n_blocks); i++) {
|
|
|
|
ver_romfs = CheckNcchHash(lvl2_data + (i*0x20), &file, 1 << block_log, offset, &ncch, NULL);
|
|
|
|
offset_add += 1 << block_log;
|
|
|
|
if (!(i % 16) && !ShowProgress(i+1, n_blocks, path)) ver_romfs = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (masterhash) free(masterhash);
|
|
|
|
if (lvl1_data) free(lvl1_data);
|
|
|
|
if (lvl2_data) free(lvl2_data);
|
|
|
|
}
|
2016-12-08 22:06:47 +01:00
|
|
|
|
|
|
|
if (!offset && (ver_exthdr|ver_exefs|ver_romfs)) { // verification summary
|
|
|
|
ShowPrompt(false, "%s\nNCCH verification failed:\nExtHdr/ExeFS/RomFS: %s/%s/%s", pathstr,
|
|
|
|
(!ncch.size_exthdr) ? "-" : (ver_exthdr == 0) ? "ok" : "fail",
|
|
|
|
(!ncch.size_exefs) ? "-" : (ver_exefs == 0) ? "ok" : "fail",
|
|
|
|
(!ncch.size_romfs) ? "-" : (ver_romfs == 0) ? "ok" : "fail");
|
|
|
|
}
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2020-01-02 20:58:57 +01:00
|
|
|
if (cryptofix) fvx_qwrite(path, &ncch, offset, sizeof(NcchHeader), NULL);
|
2016-12-08 22:06:47 +01:00
|
|
|
return ver_exthdr|ver_exefs|ver_romfs;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 VerifyNcsdFile(const char* path) {
|
|
|
|
NcsdHeader ncsd;
|
|
|
|
|
|
|
|
// path string
|
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
|
|
|
// load NCSD header
|
|
|
|
if (LoadNcsdHeader(&ncsd, path) != 0) {
|
|
|
|
ShowPrompt(false, "%s\nError: Not a NCSD file", pathstr);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// validate NCSD contents
|
|
|
|
for (u32 i = 0; i < 8; i++) {
|
|
|
|
NcchPartition* partition = ncsd.partitions + i;
|
|
|
|
u32 offset = partition->offset * NCSD_MEDIA_UNIT;
|
|
|
|
u32 size = partition->size * NCSD_MEDIA_UNIT;
|
|
|
|
if (!size) continue;
|
|
|
|
if (VerifyNcchFile(path, offset, size) != 0) {
|
|
|
|
ShowPrompt(false, "%s\nContent%lu (%08lX@%08lX):\nVerification failed",
|
|
|
|
pathstr, i, size, offset, i);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 VerifyCiaFile(const char* path) {
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
2016-12-08 22:06:47 +01:00
|
|
|
CiaInfo info;
|
|
|
|
u8 titlekey[16];
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!cia) return 1;
|
|
|
|
|
2016-12-08 22:06:47 +01:00
|
|
|
// path string
|
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
|
|
|
// load CIA stub
|
|
|
|
if ((LoadCiaStub(cia, path) != 0) ||
|
|
|
|
(GetCiaInfo(&info, &(cia->header)) != 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0)) {
|
2016-12-08 22:06:47 +01:00
|
|
|
ShowPrompt(false, "%s\nError: Probably not a CIA file", pathstr);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-08 22:06:47 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2018-09-05 23:53:45 +02:00
|
|
|
|
|
|
|
// verify TMD
|
|
|
|
if (VerifyTmd(&(cia->tmd)) != 0) {
|
|
|
|
ShowPrompt(false, "%s\nError: TMD probably corrupted", pathstr);
|
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
2016-12-08 22:06:47 +01:00
|
|
|
|
|
|
|
// verify contents
|
|
|
|
u32 content_count = getbe16(cia->tmd.content_count);
|
|
|
|
u64 next_offset = info.offset_content;
|
2018-08-10 14:59:23 +02:00
|
|
|
u8* cnt_index = cia->header.content_index;
|
2016-12-19 13:50:03 +01:00
|
|
|
for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
2016-12-08 22:06:47 +01:00
|
|
|
TmdContentChunk* chunk = &(cia->content_list[i]);
|
2018-08-10 14:59:23 +02:00
|
|
|
u16 index = getbe16(chunk->index);
|
|
|
|
if (!(cnt_index[index/8] & (1 << (7-(index%8))))) continue; // don't check missing contents
|
2016-12-09 15:33:04 +01:00
|
|
|
if (VerifyTmdContent(path, next_offset, chunk, titlekey) != 0) {
|
2016-12-08 22:06:47 +01:00
|
|
|
ShowPrompt(false, "%s\nID %08lX (%08llX@%08llX)\nVerification failed",
|
|
|
|
pathstr, getbe32(chunk->id), getbe64(chunk->size), next_offset, i);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-08 22:06:47 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
next_offset += getbe64(chunk->size);
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-08 22:06:47 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-02-06 02:57:21 +01:00
|
|
|
u32 VerifyTmdFile(const char* path, bool cdn) {
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 dlc_tid_high[] = { DLC_TID_HIGH };
|
2016-12-09 15:33:04 +01:00
|
|
|
|
|
|
|
// path string
|
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
|
|
|
// content path string
|
|
|
|
char path_content[256];
|
|
|
|
char* name_content;
|
|
|
|
strncpy(path_content, path, 256);
|
2018-05-24 00:14:37 +02:00
|
|
|
path_content[255] = '\0';
|
2016-12-09 15:33:04 +01:00
|
|
|
name_content = strrchr(path_content, '/');
|
|
|
|
if (!name_content) return 1; // will not happen
|
|
|
|
name_content++;
|
|
|
|
|
|
|
|
// load TMD file
|
2018-01-24 23:32:06 +01:00
|
|
|
TitleMetaData* tmd = (TitleMetaData*) malloc(TMD_SIZE_MAX);
|
|
|
|
TmdContentChunk* content_list = (TmdContentChunk*) (tmd + 1);
|
2018-09-05 23:53:45 +02:00
|
|
|
if ((LoadTmdFile(tmd, path) != 0) || (VerifyTmd(tmd) != 0)) {
|
2016-12-09 15:33:04 +01:00
|
|
|
ShowPrompt(false, "%s\nError: TMD probably corrupted", pathstr);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(tmd);
|
2016-12-09 15:33:04 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
u8 titlekey[0x10] = { 0xFF };
|
2017-02-06 02:57:21 +01:00
|
|
|
if (cdn) { // load / build ticket (for titlekey / CDN only)
|
2020-01-23 00:29:11 +00:00
|
|
|
Ticket* ticket = NULL;
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!((LoadCdnTicketFile(&ticket, path) == 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
((ticket = (Ticket*)malloc(TICKET_COMMON_SIZE), ticket != NULL) &&
|
|
|
|
(BuildFakeTicket(ticket, tmd->title_id) == 0) &&
|
|
|
|
(FindTitleKey(ticket, tmd->title_id) == 0))) ||
|
|
|
|
(GetTitleKey(titlekey, ticket) != 0)) {
|
2017-02-06 02:57:21 +01:00
|
|
|
ShowPrompt(false, "%s\nError: CDN titlekey not found", pathstr);
|
2020-01-23 00:29:11 +00:00
|
|
|
free(ticket);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(tmd);
|
2017-02-06 02:57:21 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2020-01-23 00:29:11 +00:00
|
|
|
free(ticket);
|
2017-02-06 02:57:21 +01:00
|
|
|
}
|
|
|
|
|
2016-12-09 15:33:04 +01:00
|
|
|
// verify contents
|
|
|
|
u32 content_count = getbe16(tmd->content_count);
|
2017-02-06 02:57:21 +01:00
|
|
|
bool dlc = !cdn && (memcmp(tmd->title_id, dlc_tid_high, sizeof(dlc_tid_high)) == 0);
|
2016-12-19 13:50:03 +01:00
|
|
|
for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
2016-12-09 15:33:04 +01:00
|
|
|
TmdContentChunk* chunk = &(content_list[i]);
|
2017-02-06 02:57:21 +01:00
|
|
|
if (!cdn) chunk->type[1] &= ~0x01; // remove crypto flag
|
2016-12-21 00:30:46 +01:00
|
|
|
snprintf(name_content, 256 - (name_content - path_content),
|
2017-02-06 02:57:21 +01:00
|
|
|
(cdn) ? "%08lx" : (dlc) ? "00000000/%08lx.app" : "%08lx.app", getbe32(chunk->id));
|
2016-12-09 15:33:04 +01:00
|
|
|
TruncateString(pathstr, path_content, 32, 8);
|
2017-02-06 02:57:21 +01:00
|
|
|
if (VerifyTmdContent(path_content, 0, chunk, titlekey) != 0) {
|
2016-12-09 15:33:04 +01:00
|
|
|
ShowPrompt(false, "%s\nVerification failed", pathstr);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(tmd);
|
2016-12-09 15:33:04 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(tmd);
|
2016-12-09 15:33:04 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-22 01:35:35 +01:00
|
|
|
u32 VerifyFirmFile(const char* path) {
|
2017-02-03 02:21:16 +01:00
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
void* firm_buffer = (void*) malloc(FIRM_MAX_SIZE);
|
|
|
|
if (!firm_buffer) return 1;
|
|
|
|
|
|
|
|
// load the whole FIRM into memory
|
|
|
|
u32 firm_size = fvx_qsize(path);
|
|
|
|
if ((firm_size > FIRM_MAX_SIZE) || (fvx_qread(path, firm_buffer, 0, firm_size, NULL) != FR_OK) ||
|
|
|
|
(ValidateFirmHeader(firm_buffer, firm_size) != 0)) {
|
|
|
|
free(firm_buffer);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// hash verify all available sections
|
2018-01-24 23:32:06 +01:00
|
|
|
FirmHeader header;
|
|
|
|
memcpy(&header, firm_buffer, sizeof(FirmHeader));
|
2019-10-04 18:26:08 +02:00
|
|
|
for (u32 i = 0; i < 4; i++) {
|
2018-01-24 23:32:06 +01:00
|
|
|
FirmSectionHeader* sct = header.sections + i;
|
|
|
|
void* section = ((u8*) firm_buffer) + sct->offset;
|
|
|
|
if (!(sct->size)) continue;
|
|
|
|
if (sha_cmp(sct->hash, section, sct->size, SHA256_MODE) != 0) {
|
2016-12-22 01:35:35 +01:00
|
|
|
ShowPrompt(false, "%s\nSection %u hash mismatch", pathstr, i);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(firm_buffer);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-31 15:48:14 +02:00
|
|
|
// no arm11 / arm9 entrypoints?
|
|
|
|
if (!header.entry_arm9) {
|
|
|
|
ShowPrompt(false, "%s\nARM9 entrypoint is missing", pathstr);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(firm_buffer);
|
2017-02-03 02:21:16 +01:00
|
|
|
return 1;
|
2017-05-31 15:48:14 +02:00
|
|
|
} else if (!header.entry_arm11) {
|
|
|
|
ShowPrompt(false, "%s\nWarning: ARM11 entrypoint is missing", pathstr);
|
2017-02-03 02:21:16 +01:00
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(firm_buffer);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-01-30 20:28:49 +01:00
|
|
|
u32 VerifyBossFile(const char* path) {
|
2018-01-24 23:32:06 +01:00
|
|
|
BossHeader boss;
|
2017-01-30 20:28:49 +01:00
|
|
|
u32 payload_size;
|
|
|
|
bool encrypted = false;
|
2017-05-30 01:58:18 +02:00
|
|
|
FIL file;
|
|
|
|
UINT btr;
|
2017-01-30 20:28:49 +01:00
|
|
|
|
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
2018-12-08 18:12:25 +01:00
|
|
|
// read file header
|
2017-05-30 01:58:18 +02:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
|
|
|
return 1;
|
|
|
|
fvx_lseek(&file, 0);
|
2018-01-24 23:32:06 +01:00
|
|
|
if ((fvx_read(&file, &boss, sizeof(BossHeader), &btr) != FR_OK) ||
|
|
|
|
(btr != sizeof(BossHeader)) || (ValidateBossHeader(&boss, 0) != 0)) {
|
2017-01-30 20:28:49 +01:00
|
|
|
ShowPrompt(false, "%s\nError: Not a BOSS file", pathstr);
|
2017-05-30 01:58:18 +02:00
|
|
|
fvx_close(&file);
|
2017-01-30 20:28:49 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// get / check size
|
2018-01-24 23:32:06 +01:00
|
|
|
payload_size = getbe32(boss.filesize) - sizeof(BossHeader);
|
2017-05-30 01:58:18 +02:00
|
|
|
if (!payload_size) {
|
|
|
|
fvx_close(&file);
|
2017-01-30 20:28:49 +01:00
|
|
|
return 1;
|
2017-05-30 01:58:18 +02:00
|
|
|
}
|
2017-01-30 20:28:49 +01:00
|
|
|
|
|
|
|
// check if encrypted, decrypt if required
|
2018-01-24 23:32:06 +01:00
|
|
|
encrypted = (CheckBossEncrypted(&boss) == 0);
|
|
|
|
if (encrypted) CryptBoss((void*) &boss, 0, sizeof(BossHeader), &boss);
|
|
|
|
|
|
|
|
// set up a buffer
|
|
|
|
u8* buffer = (u8*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!buffer) {
|
|
|
|
fvx_close(&file);
|
|
|
|
return 1;
|
|
|
|
}
|
2017-01-30 20:28:49 +01:00
|
|
|
|
|
|
|
// actual hash calculation & compare
|
|
|
|
u8 hash[32];
|
2017-05-30 01:58:18 +02:00
|
|
|
sha_init(SHA256_MODE);
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
GetBossPayloadHashHeader(buffer, &boss);
|
|
|
|
u32 read_bytes = min((STD_BUFFER_SIZE - BOSS_SIZE_PAYLOAD_HEADER), payload_size);
|
|
|
|
fvx_read(&file, buffer + BOSS_SIZE_PAYLOAD_HEADER, read_bytes, &btr);
|
|
|
|
if (encrypted) CryptBoss(buffer + BOSS_SIZE_PAYLOAD_HEADER, sizeof(BossHeader), read_bytes, &boss);
|
|
|
|
sha_update(buffer, read_bytes + BOSS_SIZE_PAYLOAD_HEADER);
|
|
|
|
|
|
|
|
for (u32 i = read_bytes; i < payload_size; i += STD_BUFFER_SIZE) {
|
|
|
|
read_bytes = min(STD_BUFFER_SIZE, (payload_size - i));
|
|
|
|
fvx_read(&file, buffer, read_bytes, &btr);
|
|
|
|
if (encrypted) CryptBoss(buffer, sizeof(BossHeader) + i, read_bytes, &boss);
|
|
|
|
sha_update(buffer, read_bytes);
|
2017-05-30 01:58:18 +02:00
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
2017-05-30 01:58:18 +02:00
|
|
|
sha_get(hash);
|
2018-01-24 23:32:06 +01:00
|
|
|
fvx_close(&file);
|
|
|
|
free(buffer);
|
|
|
|
|
|
|
|
if (memcmp(hash, boss.hash_payload, 0x20) != 0) {
|
2018-12-08 18:12:25 +01:00
|
|
|
if (ShowPrompt(true, "%s\nBOSS payload hash mismatch.\n \nTry to fix it?", pathstr)) {
|
|
|
|
// fix hash, reencrypt BOSS header if required, write to file
|
|
|
|
memcpy(boss.hash_payload, hash, 0x20);
|
|
|
|
if (encrypted) CryptBoss((void*) &boss, 0, sizeof(BossHeader), &boss);
|
|
|
|
if (!CheckWritePermissions(path) ||
|
|
|
|
(fvx_qwrite(path, &boss, 0, sizeof(BossHeader), NULL) != FR_OK))
|
|
|
|
return 1;
|
|
|
|
} else return 1;
|
2017-01-30 20:28:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-08 22:06:47 +01:00
|
|
|
u32 VerifyGameFile(const char* path) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
if (filetype & GAME_CIA)
|
2016-12-08 22:06:47 +01:00
|
|
|
return VerifyCiaFile(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & GAME_NCSD)
|
2016-12-08 22:06:47 +01:00
|
|
|
return VerifyNcsdFile(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & GAME_NCCH)
|
2016-12-08 22:06:47 +01:00
|
|
|
return VerifyNcchFile(path, 0, 0);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & GAME_TMD)
|
2017-02-06 02:57:21 +01:00
|
|
|
return VerifyTmdFile(path, filetype & FLAG_NUSCDN);
|
2017-01-30 20:28:49 +01:00
|
|
|
else if (filetype & GAME_BOSS)
|
|
|
|
return VerifyBossFile(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & SYS_FIRM)
|
2016-12-22 01:35:35 +01:00
|
|
|
return VerifyFirmFile(path);
|
2016-12-08 22:06:47 +01:00
|
|
|
else return 1;
|
|
|
|
}
|
2016-12-13 00:20:00 +01:00
|
|
|
|
|
|
|
u32 CheckEncryptedNcchFile(const char* path, u32 offset) {
|
|
|
|
NcchHeader ncch;
|
2016-12-22 01:35:35 +01:00
|
|
|
if (LoadNcchHeaders(&ncch, NULL, NULL, path, offset) != 0)
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
|
|
|
return (NCCH_ENCRYPTED(&ncch)) ? 0 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 CheckEncryptedNcsdFile(const char* path) {
|
|
|
|
NcsdHeader ncsd;
|
|
|
|
|
|
|
|
// load NCSD header
|
|
|
|
if (LoadNcsdHeader(&ncsd, path) != 0)
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
// check for encryption in NCSD contents
|
|
|
|
for (u32 i = 0; i < 8; i++) {
|
|
|
|
NcchPartition* partition = ncsd.partitions + i;
|
|
|
|
u32 offset = partition->offset * NCSD_MEDIA_UNIT;
|
|
|
|
if (!partition->size) continue;
|
|
|
|
if (CheckEncryptedNcchFile(path, offset) == 0)
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 CheckEncryptedCiaFile(const char* path) {
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
2016-12-13 00:20:00 +01:00
|
|
|
CiaInfo info;
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!cia) return 1;
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
// load CIA stub
|
|
|
|
if ((LoadCiaStub(cia, path) != 0) ||
|
2018-01-24 23:32:06 +01:00
|
|
|
(GetCiaInfo(&info, &(cia->header)) != 0)) {
|
|
|
|
free(cia);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-13 00:20:00 +01:00
|
|
|
|
|
|
|
// check for encryption in CIA contents
|
|
|
|
u32 content_count = getbe16(cia->tmd.content_count);
|
|
|
|
u64 next_offset = info.offset_content;
|
2016-12-19 13:50:03 +01:00
|
|
|
for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
2016-12-13 00:20:00 +01:00
|
|
|
TmdContentChunk* chunk = &(cia->content_list[i]);
|
2018-01-24 23:32:06 +01:00
|
|
|
if ((getbe16(chunk->type) & 0x1) || (CheckEncryptedNcchFile(path, next_offset) == 0)) {
|
|
|
|
free(cia);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 0; // encryption found
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-13 00:20:00 +01:00
|
|
|
next_offset += getbe64(chunk->size);
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-22 01:35:35 +01:00
|
|
|
u32 CheckEncryptedFirmFile(const char* path) {
|
|
|
|
FirmHeader header;
|
|
|
|
FIL file;
|
|
|
|
UINT btr;
|
|
|
|
|
|
|
|
// open file, get FIRM header
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, 0);
|
|
|
|
if ((fvx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) ||
|
2017-02-03 02:21:16 +01:00
|
|
|
(ValidateFirmHeader(&header, fvx_size(&file)) != 0)) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check ARM9 binary for ARM9 loader
|
|
|
|
FirmSectionHeader* arm9s = FindFirmArm9Section(&header);
|
|
|
|
if (arm9s) {
|
|
|
|
FirmA9LHeader a9l;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&file, arm9s->offset);
|
|
|
|
if ((fvx_read(&file, &a9l, sizeof(FirmA9LHeader), &btr) == FR_OK) &&
|
2016-12-22 01:35:35 +01:00
|
|
|
(ValidateFirmA9LHeader(&a9l) == 0)) {
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&file);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-01-30 20:28:49 +01:00
|
|
|
u32 CheckEncryptedBossFile(const char* path) {
|
2018-01-24 23:32:06 +01:00
|
|
|
// get boss header, check if encrypted
|
|
|
|
BossHeader boss;
|
|
|
|
if (fvx_qread(path, &boss, 0, sizeof(BossHeader), NULL) != FR_OK) return 1;
|
|
|
|
return CheckBossEncrypted(&boss);
|
2017-01-30 20:28:49 +01:00
|
|
|
}
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
u32 CheckEncryptedGameFile(const char* path) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
if (filetype & GAME_CIA)
|
2016-12-13 00:20:00 +01:00
|
|
|
return CheckEncryptedCiaFile(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & GAME_NCSD)
|
2016-12-13 00:20:00 +01:00
|
|
|
return CheckEncryptedNcsdFile(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & GAME_NCCH)
|
2016-12-13 00:20:00 +01:00
|
|
|
return CheckEncryptedNcchFile(path, 0);
|
2017-01-30 20:28:49 +01:00
|
|
|
else if (filetype & GAME_BOSS)
|
|
|
|
return CheckEncryptedBossFile(path);
|
2017-01-27 15:29:53 +01:00
|
|
|
else if (filetype & SYS_FIRM)
|
2016-12-22 01:35:35 +01:00
|
|
|
return CheckEncryptedFirmFile(path);
|
2017-02-07 23:18:25 +01:00
|
|
|
else if (filetype & GAME_NUSCDN)
|
|
|
|
return 0; // these should always be encrypted
|
2016-12-13 00:20:00 +01:00
|
|
|
else return 1;
|
|
|
|
}
|
|
|
|
|
2017-01-31 16:16:26 +01:00
|
|
|
u32 CryptNcchNcsdBossFirmFile(const char* orig, const char* dest, u32 mode, u16 crypto,
|
2016-12-13 00:20:00 +01:00
|
|
|
u32 offset, u32 size, TmdContentChunk* chunk, const u8* titlekey) { // this line only for CIA contents
|
|
|
|
// this will do a simple copy for unencrypted files
|
|
|
|
bool inplace = (strncmp(orig, dest, 256) == 0);
|
|
|
|
FIL ofile;
|
|
|
|
FIL dfile;
|
|
|
|
FIL* ofp = &ofile;
|
|
|
|
FIL* dfp = (inplace) ? &ofile : &dfile;
|
|
|
|
FSIZE_t fsize;
|
|
|
|
|
2017-01-31 16:16:26 +01:00
|
|
|
// FIRM encryption is not possible (yet)
|
|
|
|
if ((mode & SYS_FIRM) && (crypto != CRYPTO_DECRYPT))
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
// check for BOSS crypto
|
|
|
|
bool crypt_boss = ((mode & GAME_BOSS) && (CheckEncryptedBossFile(orig) == 0));
|
|
|
|
crypt_boss = ((mode & GAME_BOSS) && (crypt_boss == (crypto == CRYPTO_DECRYPT)));
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
// open file(s)
|
|
|
|
if (inplace) {
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(ofp, orig, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(ofp, offset);
|
2016-12-13 00:20:00 +01:00
|
|
|
} else {
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(ofp, orig, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(dfp, dest, FA_WRITE | (offset ? FA_OPEN_ALWAYS : FA_CREATE_ALWAYS)) != FR_OK) {
|
|
|
|
fvx_close(ofp);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(ofp, offset);
|
|
|
|
fvx_lseek(dfp, offset);
|
2016-12-13 00:20:00 +01:00
|
|
|
}
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
fsize = fvx_size(ofp); // for progress bar
|
2017-02-07 23:18:25 +01:00
|
|
|
if (fsize < offset) return 1;
|
|
|
|
if (!size) size = fsize - offset;
|
2016-12-13 00:20:00 +01:00
|
|
|
|
2017-12-28 03:46:57 +01:00
|
|
|
// ensure free space in destination
|
|
|
|
if (!inplace) {
|
|
|
|
if ((fvx_lseek(dfp, offset + size) != FR_OK) ||
|
|
|
|
(fvx_tell(dfp) != offset + size) ||
|
|
|
|
(fvx_lseek(dfp, offset) != FR_OK)) {
|
|
|
|
fvx_close(ofp);
|
|
|
|
fvx_close(dfp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
// set up buffer
|
|
|
|
u8* buffer = (u8*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!buffer) {
|
|
|
|
fvx_close(ofp);
|
|
|
|
fvx_close(dfp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
u32 ret = 0;
|
2016-12-22 01:35:35 +01:00
|
|
|
if (!ShowProgress(offset, fsize, dest)) ret = 1;
|
2017-05-17 01:16:24 +02:00
|
|
|
if (mode & (GAME_NCCH|GAME_NCSD|GAME_BOSS|SYS_FIRM|GAME_NDS)) { // for NCCH / NCSD / BOSS / FIRM files
|
2018-01-24 23:32:06 +01:00
|
|
|
for (u64 i = 0; (i < size) && (ret == 0); i += STD_BUFFER_SIZE) {
|
|
|
|
u32 read_bytes = min(STD_BUFFER_SIZE, (size - i));
|
2016-12-13 00:20:00 +01:00
|
|
|
UINT bytes_read, bytes_written;
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_read(ofp, buffer, read_bytes, &bytes_read) != FR_OK) ret = 1;
|
|
|
|
if (((mode & GAME_NCCH) && (CryptNcchSequential(buffer, i, read_bytes, crypto) != 0)) ||
|
|
|
|
((mode & GAME_NCSD) && (CryptNcsdSequential(buffer, i, read_bytes, crypto) != 0)) ||
|
|
|
|
((mode & GAME_BOSS) && crypt_boss && (CryptBossSequential(buffer, i, read_bytes) != 0)) ||
|
|
|
|
((mode & SYS_FIRM) && (DecryptFirmSequential(buffer, i, read_bytes) != 0)))
|
2016-12-13 00:20:00 +01:00
|
|
|
ret = 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (inplace) fvx_lseek(ofp, fvx_tell(ofp) - read_bytes);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_write(dfp, buffer, read_bytes, &bytes_written) != FR_OK) ret = 1;
|
2016-12-13 00:20:00 +01:00
|
|
|
if ((read_bytes != bytes_read) || (bytes_read != bytes_written)) ret = 1;
|
|
|
|
if (!ShowProgress(offset + i + read_bytes, fsize, dest)) ret = 1;
|
|
|
|
}
|
2017-02-07 23:18:25 +01:00
|
|
|
} else if (mode & (GAME_CIA|GAME_NUSCDN)) { // for NCCHs inside CIAs
|
2016-12-13 00:20:00 +01:00
|
|
|
bool cia_crypto = getbe16(chunk->type) & 0x1;
|
|
|
|
bool ncch_crypto; // find out by decrypting the NCCH header
|
|
|
|
UINT bytes_read, bytes_written;
|
|
|
|
u8 ctr[16];
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
NcchHeader* ncch = (NcchHeader*) (void*) buffer;
|
2016-12-13 00:20:00 +01:00
|
|
|
GetTmdCtr(ctr, chunk); // NCCH crypto?
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_read(ofp, buffer, sizeof(NcchHeader), &bytes_read) != FR_OK) ret = 1;
|
|
|
|
if (cia_crypto) DecryptCiaContentSequential(buffer, sizeof(NcchHeader), ctr, titlekey);
|
2017-01-31 16:16:26 +01:00
|
|
|
ncch_crypto = ((ValidateNcchHeader(ncch) == 0) && (NCCH_ENCRYPTED(ncch) || !(crypto & NCCH_NOCRYPTO)));
|
|
|
|
if (ncch_crypto && (SetupNcchCrypto(ncch, crypto) != 0))
|
2016-12-15 11:46:00 +01:00
|
|
|
ret = 1;
|
2016-12-13 00:20:00 +01:00
|
|
|
|
|
|
|
GetTmdCtr(ctr, chunk);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(ofp, offset);
|
2016-12-13 00:20:00 +01:00
|
|
|
sha_init(SHA256_MODE);
|
2018-01-24 23:32:06 +01:00
|
|
|
for (u64 i = 0; (i < size) && (ret == 0); i += STD_BUFFER_SIZE) {
|
|
|
|
u32 read_bytes = min(STD_BUFFER_SIZE, (size - i));
|
|
|
|
if (fvx_read(ofp, buffer, read_bytes, &bytes_read) != FR_OK) ret = 1;
|
|
|
|
if (cia_crypto && (DecryptCiaContentSequential(buffer, read_bytes, ctr, titlekey) != 0)) ret = 1;
|
|
|
|
if (ncch_crypto && (CryptNcchSequential(buffer, i, read_bytes, crypto) != 0)) ret = 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (inplace) fvx_lseek(ofp, fvx_tell(ofp) - read_bytes);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_write(dfp, buffer, read_bytes, &bytes_written) != FR_OK) ret = 1;
|
|
|
|
sha_update(buffer, read_bytes);
|
2016-12-13 00:20:00 +01:00
|
|
|
if ((read_bytes != bytes_read) || (bytes_read != bytes_written)) ret = 1;
|
|
|
|
if (!ShowProgress(offset + i + read_bytes, fsize, dest)) ret = 1;
|
|
|
|
}
|
|
|
|
sha_get(chunk->hash);
|
|
|
|
chunk->type[1] &= ~0x01;
|
|
|
|
}
|
|
|
|
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(ofp);
|
|
|
|
if (!inplace) fvx_close(dfp);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (buffer) free(buffer);
|
2016-12-13 00:20:00 +01:00
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2017-01-31 16:16:26 +01:00
|
|
|
u32 CryptCiaFile(const char* orig, const char* dest, u16 crypto) {
|
2016-12-13 00:20:00 +01:00
|
|
|
bool inplace = (strncmp(orig, dest, 256) == 0);
|
|
|
|
CiaInfo info;
|
|
|
|
u8 titlekey[16];
|
|
|
|
|
|
|
|
// start operation
|
|
|
|
if (!ShowProgress(0, 0, orig)) return 1;
|
|
|
|
|
|
|
|
// if not inplace: clear destination
|
|
|
|
if (!inplace) f_unlink(dest);
|
|
|
|
|
|
|
|
// load CIA stub from origin
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
|
|
|
if (!cia) return 1;
|
2016-12-13 00:20:00 +01:00
|
|
|
if ((LoadCiaStub(cia, orig) != 0) ||
|
|
|
|
(GetCiaInfo(&info, &(cia->header)) != 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0)) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// decrypt CIA contents
|
|
|
|
u32 content_count = getbe16(cia->tmd.content_count);
|
|
|
|
u64 next_offset = info.offset_content;
|
2018-08-10 14:59:23 +02:00
|
|
|
u8* cnt_index = cia->header.content_index;
|
2016-12-19 13:50:03 +01:00
|
|
|
for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
2016-12-13 00:20:00 +01:00
|
|
|
TmdContentChunk* chunk = &(cia->content_list[i]);
|
|
|
|
u64 size = getbe64(chunk->size);
|
2018-08-10 14:59:23 +02:00
|
|
|
u16 index = getbe16(chunk->index);
|
|
|
|
if (!(cnt_index[index/8] & (1 << (7-(index%8))))) continue; // don't crypt missing contents
|
2018-01-24 23:32:06 +01:00
|
|
|
if (CryptNcchNcsdBossFirmFile(orig, dest, GAME_CIA, crypto, next_offset, size, chunk, titlekey) != 0) {
|
|
|
|
free(cia);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-13 00:20:00 +01:00
|
|
|
next_offset += size;
|
|
|
|
}
|
|
|
|
|
2017-09-09 13:07:29 +02:00
|
|
|
// if not inplace: take over CIA metadata
|
|
|
|
if (!inplace && (info.size_meta == CIA_META_SIZE)) {
|
|
|
|
CiaMeta* meta = (CiaMeta*) (void*) (cia + 1);
|
|
|
|
if ((fvx_qread(orig, meta, info.offset_meta, CIA_META_SIZE, NULL) != FR_OK) ||
|
2018-01-24 23:32:06 +01:00
|
|
|
(fvx_qwrite(dest, meta, info.offset_meta, CIA_META_SIZE, NULL) != FR_OK)) {
|
|
|
|
free(cia);
|
2017-09-09 13:07:29 +02:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2017-09-09 13:07:29 +02:00
|
|
|
}
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
// fix TMD hashes, write CIA stub to destination
|
2018-01-24 23:32:06 +01:00
|
|
|
if ((FixTmdHashes(&(cia->tmd)) != 0) || (WriteCiaStub(cia, dest) != 0)) {
|
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
2016-12-13 00:20:00 +01:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-13 00:20:00 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-22 01:35:35 +01:00
|
|
|
u32 DecryptFirmFile(const char* orig, const char* dest) {
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 dec_magic[] = { 'D', 'E', 'C', '\0' }; // insert to decrypted firms
|
2018-01-24 23:32:06 +01:00
|
|
|
void* firm_buffer = (void*) malloc(FIRM_MAX_SIZE);
|
|
|
|
if (!firm_buffer) return 1;
|
2016-12-22 01:35:35 +01:00
|
|
|
|
2019-11-04 23:44:11 +01:00
|
|
|
// load the whole FIRM into memory & decrypt it
|
|
|
|
ShowProgress(0, 2, dest);
|
2018-01-24 23:32:06 +01:00
|
|
|
u32 firm_size = fvx_qsize(orig);
|
|
|
|
if ((firm_size > FIRM_MAX_SIZE) || (fvx_qread(orig, firm_buffer, 0, firm_size, NULL) != FR_OK) ||
|
|
|
|
(DecryptFirmFull(firm_buffer, firm_size) != 0)) {
|
|
|
|
free(firm_buffer);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
// add the decrypted magic
|
|
|
|
FirmHeader* firm = (FirmHeader*) firm_buffer;
|
|
|
|
memcpy(firm->dec_magic, dec_magic, sizeof(dec_magic));
|
2016-12-22 01:35:35 +01:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
// write decrypted FIRM to the destination file
|
2019-11-04 23:44:11 +01:00
|
|
|
ShowProgress(1, 2, dest);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_qwrite(dest, firm_buffer, 0, firm_size, NULL) != FR_OK) {
|
|
|
|
free(firm_buffer);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2019-11-04 23:44:11 +01:00
|
|
|
ShowProgress(2, 2, dest);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(firm_buffer);
|
2016-12-22 01:35:35 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
u32 CryptCdnFileBuffered(const char* orig, const char* dest, u16 crypto, void* buffer) {
|
2017-02-07 23:18:25 +01:00
|
|
|
bool inplace = (strncmp(orig, dest, 256) == 0);
|
2018-01-24 23:32:06 +01:00
|
|
|
TitleMetaData* tmd = (TitleMetaData*) buffer;
|
2017-02-07 23:18:25 +01:00
|
|
|
TmdContentChunk* content_list = (TmdContentChunk*) (tmd + 1);
|
2017-02-10 16:12:00 +01:00
|
|
|
|
|
|
|
// get name
|
|
|
|
char* fname;
|
|
|
|
fname = strrchr(orig, '/');
|
|
|
|
if (!fname) return 1; // will not happen
|
|
|
|
fname++;
|
2017-02-07 23:18:25 +01:00
|
|
|
|
|
|
|
// try to load TMD file
|
|
|
|
char path_tmd[256];
|
2017-02-10 16:12:00 +01:00
|
|
|
if (!strrchr(fname, '.')) {
|
|
|
|
char* name_tmd;
|
|
|
|
strncpy(path_tmd, orig, 256);
|
2018-05-24 00:14:37 +02:00
|
|
|
path_tmd[255] = '\0';
|
2017-02-10 16:12:00 +01:00
|
|
|
name_tmd = strrchr(path_tmd, '/');
|
|
|
|
if (!name_tmd) return 1; // will not happen
|
|
|
|
name_tmd++;
|
|
|
|
snprintf(name_tmd, 256 - (name_tmd - path_tmd), "tmd");
|
|
|
|
if (LoadTmdFile(tmd, path_tmd) != 0) tmd = NULL;
|
|
|
|
} else tmd = NULL;
|
2017-02-07 23:18:25 +01:00
|
|
|
|
|
|
|
// load or build ticket
|
2020-01-23 00:29:11 +00:00
|
|
|
Ticket* ticket = NULL;
|
2018-01-24 23:32:06 +01:00
|
|
|
if (LoadCdnTicketFile(&ticket, orig) != 0) {
|
2020-01-23 00:29:11 +00:00
|
|
|
if (!tmd || (ticket = (Ticket*)malloc(TICKET_COMMON_SIZE), !ticket)) return 1;
|
|
|
|
if ((BuildFakeTicket(ticket, tmd->title_id) != 0)) {
|
|
|
|
free(ticket);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (FindTitleKey(ticket, tmd->title_id) != 0) {
|
|
|
|
free(ticket);
|
|
|
|
return 1;
|
|
|
|
}
|
2017-02-07 23:18:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// get titlekey
|
2018-01-24 23:32:06 +01:00
|
|
|
u8 titlekey[0x10] = { 0xFF };
|
2020-01-23 00:29:11 +00:00
|
|
|
if (GetTitleKey(titlekey, ticket) != 0) {
|
|
|
|
free(ticket);
|
2017-02-07 23:18:25 +01:00
|
|
|
return 1;
|
2020-01-23 00:29:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
free(ticket);
|
2017-02-07 23:18:25 +01:00
|
|
|
|
|
|
|
// find (build fake) content chunk
|
|
|
|
TmdContentChunk* chunk = NULL;
|
|
|
|
if (!tmd) {
|
|
|
|
chunk = content_list;
|
|
|
|
memset(chunk, 0, sizeof(TmdContentChunk));
|
|
|
|
chunk->type[1] = 0x01; // encrypted
|
|
|
|
} else {
|
|
|
|
u32 content_count = getbe16(tmd->content_count);
|
2017-02-10 16:12:00 +01:00
|
|
|
u32 content_id = 0;
|
|
|
|
if (sscanf(fname, "%08lx", &content_id) != 1) return 1;
|
2017-02-07 23:18:25 +01:00
|
|
|
for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
|
|
|
chunk = &(content_list[i]);
|
2017-02-10 16:12:00 +01:00
|
|
|
if (getbe32(chunk->id) == content_id) break;
|
2017-02-07 23:18:25 +01:00
|
|
|
chunk = NULL;
|
|
|
|
}
|
|
|
|
if (!chunk || !(getbe16(chunk->type) & 0x01)) return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// actual crypto
|
|
|
|
if (CryptNcchNcsdBossFirmFile(orig, dest, GAME_NUSCDN, crypto, 0, 0, chunk, titlekey) != 0)
|
|
|
|
return 1;
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
if (inplace && tmd) { // in that case, write the change to the TMD file, too
|
2017-02-07 23:18:25 +01:00
|
|
|
u32 offset = ((u8*) chunk) - ((u8*) tmd);
|
2018-01-24 23:32:06 +01:00
|
|
|
fvx_qwrite(path_tmd, chunk, offset, sizeof(TmdContentChunk), NULL);
|
2017-02-07 23:18:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
u32 CryptCdnFile(const char* orig, const char* dest, u16 crypto) {
|
|
|
|
void* buffer = (void*) malloc(TMD_SIZE_MAX);
|
|
|
|
if (!buffer) return 1;
|
|
|
|
|
|
|
|
u32 ret = CryptCdnFileBuffered(orig, dest, crypto, buffer);
|
|
|
|
|
|
|
|
free(buffer);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2017-01-31 16:16:26 +01:00
|
|
|
u32 CryptGameFile(const char* path, bool inplace, bool encrypt) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2017-01-31 16:16:26 +01:00
|
|
|
u16 crypto = encrypt ? CRYPTO_ENCRYPT : CRYPTO_DECRYPT;
|
2016-12-13 00:20:00 +01:00
|
|
|
char dest[256];
|
|
|
|
char* destptr = (char*) path;
|
|
|
|
u32 ret = 0;
|
|
|
|
|
2017-05-09 17:07:49 +02:00
|
|
|
if (!inplace) { // build output name
|
2017-05-15 20:00:26 +02:00
|
|
|
// build output name
|
|
|
|
snprintf(dest, 256, OUTPUT_PATH "/");
|
|
|
|
char* dname = dest + strnlen(dest, 256);
|
2017-05-16 16:08:38 +02:00
|
|
|
if ((strncmp(path + 1, ":/title/", 8) != 0) || (GetGoodName(dname, path, false) != 0)) {
|
2017-05-15 20:00:26 +02:00
|
|
|
char* name = strrchr(path, '/');
|
|
|
|
if (!name) return 1;
|
|
|
|
snprintf(dest, 256, "%s/%s", OUTPUT_PATH, ++name);
|
|
|
|
}
|
2016-12-13 00:20:00 +01:00
|
|
|
destptr = dest;
|
|
|
|
}
|
|
|
|
|
2016-12-13 14:48:44 +01:00
|
|
|
if (!CheckWritePermissions(destptr))
|
|
|
|
return 1;
|
|
|
|
|
2017-06-06 21:14:19 +02:00
|
|
|
if (!inplace) { // ensure the output dir exists
|
|
|
|
if (fvx_rmkdir(OUTPUT_PATH) != FR_OK)
|
2016-12-13 14:48:44 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-13 00:20:00 +01:00
|
|
|
if (filetype & GAME_CIA)
|
2017-01-31 16:16:26 +01:00
|
|
|
ret = CryptCiaFile(path, destptr, crypto);
|
2017-02-07 23:18:25 +01:00
|
|
|
else if (filetype & GAME_NUSCDN)
|
|
|
|
ret = CryptCdnFile(path, destptr, crypto);
|
2016-12-22 01:35:35 +01:00
|
|
|
else if (filetype & SYS_FIRM)
|
|
|
|
ret = DecryptFirmFile(path, destptr);
|
2017-01-30 20:28:49 +01:00
|
|
|
else if (filetype & (GAME_NCCH|GAME_NCSD|GAME_BOSS))
|
2017-01-31 16:16:26 +01:00
|
|
|
ret = CryptNcchNcsdBossFirmFile(path, destptr, filetype, crypto, 0, 0, NULL, NULL);
|
2016-12-13 00:20:00 +01:00
|
|
|
else ret = 1;
|
|
|
|
|
|
|
|
if (!inplace && (ret != 0))
|
|
|
|
f_unlink(dest); // try to get rid of the borked file
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2016-12-15 11:46:00 +01:00
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
u32 GetInstallAppPath(char* path, const char* drv, const u8* title_id, const u8* content_id) {
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 dlc_tid_high[] = { DLC_TID_HIGH };
|
2020-07-22 00:20:16 +02:00
|
|
|
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
|
2020-07-24 10:41:43 +02:00
|
|
|
if (!ShowProgress(1, 5, "TMD/CMD")) return 1;
|
2020-07-22 00:20:16 +02:00
|
|
|
|
|
|
|
// 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
|
2020-07-24 10:41:43 +02:00
|
|
|
if (!ShowProgress(2, 5, "Savegame")) return 1;
|
2020-07-22 00:20:16 +02:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2020-07-24 10:41:02 +02:00
|
|
|
// generate the save file, first check if it already exists
|
|
|
|
if (fvx_qsize(path_save) != exthdr->savedata_size) {
|
|
|
|
static const u8 zeroes[0x20] = { 0x00 };
|
|
|
|
UINT bw;
|
|
|
|
FIL save;
|
|
|
|
fvx_rmkpath(path_save);
|
|
|
|
if (fvx_open(&save, path_save, FA_WRITE | FA_CREATE_ALWAYS) != 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;
|
|
|
|
}
|
2020-07-22 00:20:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// progress update
|
2020-07-24 10:41:43 +02:00
|
|
|
if (!ShowProgress(3, 5, "TitleDB update")) return 1;
|
2020-07-22 00:20:16 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
// title database
|
|
|
|
if (!InitImgFS(path_titledb) ||
|
|
|
|
((AddTitleInfoEntryToDB("D:/partitionA.bin", title_id, &tie, true)) != 0)) {
|
|
|
|
InitImgFS(path_bak);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-07-24 10:41:43 +02:00
|
|
|
// progress update
|
|
|
|
if (!ShowProgress(4, 5, "TicketDB update")) return 1;
|
|
|
|
|
|
|
|
// ticket database
|
|
|
|
if (!InitImgFS(path_ticketdb) ||
|
|
|
|
((AddTicketToDB("D:/partitionA.bin", title_id, (Ticket*) ticket, true)) != 0)) {
|
|
|
|
InitImgFS(path_bak);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
u32 InsertCiaContent(const char* path_cia, const char* path_content, u32 offset, u32 size,
|
2017-02-06 23:56:12 +01:00
|
|
|
TmdContentChunk* chunk, const u8* titlekey, bool force_legit, bool cxi_fix, bool cdn_decrypt) {
|
|
|
|
// crypto types / ctr
|
|
|
|
bool ncch_decrypt = !force_legit;
|
|
|
|
bool cia_encrypt = (force_legit && (getbe16(chunk->type) & 0x01));
|
|
|
|
if (!cia_encrypt) chunk->type[1] &= ~0x01; // remove crypto flag
|
2016-12-15 11:46:00 +01:00
|
|
|
|
|
|
|
// open file(s)
|
|
|
|
FIL ofile;
|
|
|
|
FIL dfile;
|
|
|
|
FSIZE_t fsize;
|
|
|
|
UINT bytes_read, bytes_written;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&ofile, path_content, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&ofile, offset);
|
|
|
|
fsize = fvx_size(&ofile);
|
2016-12-19 01:33:30 +01:00
|
|
|
if (offset > fsize) return 1;
|
|
|
|
if (!size) size = fsize - offset;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&dfile, path_cia, FA_WRITE | FA_OPEN_APPEND) != FR_OK) {
|
|
|
|
fvx_close(&ofile);
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-12-28 03:46:57 +01:00
|
|
|
// ensure free space for destination file
|
2018-01-01 22:40:09 +01:00
|
|
|
UINT offset_dest = fvx_size(&dfile);
|
|
|
|
if ((fvx_lseek(&dfile, offset_dest + size) != FR_OK) ||
|
|
|
|
(fvx_tell(&dfile) != offset_dest + size) ||
|
2017-12-28 03:46:57 +01:00
|
|
|
(fvx_lseek(&dfile, offset_dest) != FR_OK)) {
|
|
|
|
fvx_close(&ofile);
|
|
|
|
fvx_close(&dfile);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
// check if NCCH crypto is available
|
2017-02-06 23:56:12 +01:00
|
|
|
if (ncch_decrypt) {
|
2016-12-15 11:46:00 +01:00
|
|
|
NcchHeader ncch;
|
2017-02-06 23:56:12 +01:00
|
|
|
u8 ctr[16];
|
|
|
|
GetTmdCtr(ctr, chunk);
|
2017-01-16 22:32:32 +01:00
|
|
|
if ((fvx_read(&ofile, &ncch, sizeof(NcchHeader), &bytes_read) != FR_OK) ||
|
2017-02-06 23:56:12 +01:00
|
|
|
(cdn_decrypt && (DecryptCiaContentSequential((u8*) &ncch, 0x200, ctr, titlekey) != 0)) ||
|
2016-12-15 11:46:00 +01:00
|
|
|
(ValidateNcchHeader(&ncch) != 0) ||
|
2017-01-30 22:06:26 +01:00
|
|
|
(SetupNcchCrypto(&ncch, NCCH_NOCRYPTO) != 0))
|
2017-02-06 23:56:12 +01:00
|
|
|
ncch_decrypt = false;
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_lseek(&ofile, offset);
|
2016-12-15 11:46:00 +01:00
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
// allocate buffer
|
|
|
|
u8* buffer = (u8*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!buffer) {
|
|
|
|
fvx_close(&ofile);
|
|
|
|
fvx_close(&dfile);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
// main loop starts here
|
2017-02-06 23:56:12 +01:00
|
|
|
u8 ctr_in[16];
|
|
|
|
u8 ctr_out[16];
|
2016-12-15 11:46:00 +01:00
|
|
|
u32 ret = 0;
|
2017-02-06 23:56:12 +01:00
|
|
|
GetTmdCtr(ctr_in, chunk);
|
|
|
|
GetTmdCtr(ctr_out, chunk);
|
2016-12-15 11:46:00 +01:00
|
|
|
if (!ShowProgress(0, 0, path_content)) ret = 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
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 (cdn_decrypt && (DecryptCiaContentSequential(buffer, read_bytes, ctr_in, titlekey) != 0)) ret = 1;
|
|
|
|
if (ncch_decrypt && (DecryptNcchSequential(buffer, i, read_bytes) != 0)) ret = 1;
|
|
|
|
if ((i == 0) && cxi_fix && (SetNcchSdFlag(buffer) != 0)) ret = 1;
|
2016-12-19 01:33:30 +01:00
|
|
|
if (i == 0) sha_init(SHA256_MODE);
|
2018-01-24 23:32:06 +01:00
|
|
|
sha_update(buffer, read_bytes);
|
|
|
|
if (cia_encrypt && (EncryptCiaContentSequential(buffer, read_bytes, ctr_out, titlekey) != 0)) ret = 1;
|
|
|
|
if (fvx_write(&dfile, buffer, read_bytes, &bytes_written) != FR_OK) ret = 1;
|
2016-12-15 11:46:00 +01:00
|
|
|
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);
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(buffer);
|
2017-01-16 22:32:32 +01:00
|
|
|
fvx_close(&ofile);
|
|
|
|
fvx_close(&dfile);
|
2016-12-15 11:46:00 +01:00
|
|
|
|
|
|
|
// 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;
|
2017-01-16 22:32:32 +01:00
|
|
|
if (fvx_open(&file, path_cia, FA_WRITE | FA_OPEN_APPEND) != FR_OK)
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
2017-01-16 22:32:32 +01:00
|
|
|
bool res = ((fvx_write(&file, meta, CIA_META_SIZE, &btw) == FR_OK) && (btw == CIA_META_SIZE));
|
|
|
|
fvx_close(&file);
|
2016-12-15 11:46:00 +01:00
|
|
|
return (res) ? 0 : 1;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
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) {
|
2016-12-21 00:30:46 +01:00
|
|
|
const u8 dlc_tid_high[] = { DLC_TID_HIGH };
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) buffer;
|
2016-12-15 11:46:00 +01:00
|
|
|
|
2016-12-16 03:34:49 +01:00
|
|
|
// Init progress bar
|
|
|
|
if (!ShowProgress(0, 0, path_tmd)) return 1;
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
// build the CIA stub
|
|
|
|
memset(cia, 0, sizeof(CiaStub));
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((BuildCiaHeader(&(cia->header), TICKET_COMMON_SIZE) != 0) ||
|
2016-12-15 11:46:00 +01:00
|
|
|
(LoadTmdFile(&(cia->tmd), path_tmd) != 0) ||
|
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
|
|
|
(BuildCiaCert(cia->cert) != 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(BuildFakeTicket((Ticket*)&(cia->ticket), cia->tmd.title_id) != 0)) {
|
2016-12-15 11:46:00 +01:00
|
|
|
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;
|
2017-03-15 12:52:12 +01:00
|
|
|
bool dlc = (memcmp(tmd->title_id, dlc_tid_high, sizeof(dlc_tid_high)) == 0);
|
2016-12-15 11:46:00 +01:00
|
|
|
if (!content_count) return 1;
|
|
|
|
|
|
|
|
// get (legit) ticket
|
2020-01-23 00:29:11 +00:00
|
|
|
Ticket* ticket = (Ticket*)&(cia->ticket);
|
2016-12-16 03:34:49 +01:00
|
|
|
bool src_emunand = ((*path_tmd == 'B') || (*path_tmd == '4'));
|
|
|
|
if (force_legit) {
|
2020-01-23 00:29:11 +00:00
|
|
|
Ticket* ticket_tmp = NULL;
|
2020-05-11 18:02:26 +02:00
|
|
|
bool copy = true;
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((cdn && (LoadCdnTicketFile(&ticket_tmp, path_tmd) != 0)) ||
|
|
|
|
(!cdn && (FindTicket(&ticket_tmp, title_id, true, src_emunand) != 0))) {
|
2020-05-11 18:02:26 +02:00
|
|
|
static bool use_generic = false;
|
|
|
|
if (!use_generic) {
|
|
|
|
use_generic = ShowPrompt(true, "ID %016llX\nLegit ticket not found.\n \nFallback to generic as default?", getbe64(title_id));
|
|
|
|
if (!use_generic) {
|
|
|
|
free(ticket_tmp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
ShowProgress(0, 0, path_tmd);
|
|
|
|
}
|
|
|
|
if (use_generic) {
|
|
|
|
FindTitleKey(ticket, title_id);
|
|
|
|
copy = false;
|
|
|
|
}
|
2016-12-16 03:34:49 +01:00
|
|
|
}
|
2020-01-23 00:29:11 +00:00
|
|
|
// either, it's a ticket without ways to check ownership data, smaller sized
|
|
|
|
// or, it's title ticket with > 1024 contents, of which can't make it work with current CiaStub
|
2020-05-11 18:02:26 +02:00
|
|
|
if (copy && GetTicketSize(ticket_tmp) != TICKET_COMMON_SIZE) {
|
2020-01-23 00:29:11 +00:00
|
|
|
ShowPrompt(false, "ID %016llX\nLegit ticket of unsupported size.", getbe64(title_id));
|
|
|
|
free(ticket_tmp);
|
|
|
|
return 1;
|
|
|
|
}
|
2020-05-11 18:02:26 +02:00
|
|
|
// check the tickets' console id, warn if it isn't zero
|
|
|
|
if (copy && getbe32(ticket_tmp->console_id)) {
|
2018-10-24 00:21:39 +02:00
|
|
|
static u32 default_action = 0;
|
2020-07-23 18:41:42 +02:00
|
|
|
static const char* optionstr[2] =
|
2020-05-13 18:45:10 +02:00
|
|
|
{"Use generic ticket (not legit)", "Use personalized ticket (legit)"};
|
2018-10-24 00:21:39 +02:00
|
|
|
if (!default_action) {
|
|
|
|
default_action = ShowSelectPrompt(2, optionstr,
|
2020-05-13 18:45:10 +02:00
|
|
|
"ID %016llX\nLegit ticket is personalized.\nUsing this is not recommended.\nChoose default action:", getbe64(title_id));
|
2018-10-24 00:21:39 +02:00
|
|
|
ShowProgress(0, 0, path_tmd);
|
|
|
|
}
|
2020-01-23 00:29:11 +00:00
|
|
|
if (!default_action) {
|
|
|
|
free(ticket_tmp);
|
|
|
|
return 1;
|
|
|
|
}
|
2020-05-13 18:45:10 +02:00
|
|
|
else if (default_action == 1) {
|
2020-01-23 00:29:11 +00:00
|
|
|
memcpy(ticket->titlekey, ticket_tmp->titlekey, 0x10);
|
|
|
|
ticket->commonkey_idx = ticket_tmp->commonkey_idx;
|
|
|
|
copy = false;
|
2018-10-24 00:21:39 +02:00
|
|
|
}
|
|
|
|
}
|
2020-01-23 00:29:11 +00:00
|
|
|
if (copy) memcpy(ticket, ticket_tmp, TICKET_COMMON_SIZE);
|
|
|
|
free(ticket_tmp);
|
2017-02-06 23:56:12 +01:00
|
|
|
} else if (cdn) {
|
2020-01-23 00:29:11 +00:00
|
|
|
Ticket* ticket_tmp = NULL;
|
|
|
|
if ((LoadCdnTicketFile(&ticket_tmp, path_tmd) != 0) &&
|
|
|
|
(FindTitleKey(ticket_tmp, title_id) != 0)) {
|
2017-02-06 23:56:12 +01:00
|
|
|
ShowPrompt(false, "ID %016llX\nTitlekey not found.", getbe64(title_id));
|
2020-01-23 00:29:11 +00:00
|
|
|
free(ticket_tmp);
|
2017-02-06 23:56:12 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2020-01-23 00:29:11 +00:00
|
|
|
free(ticket_tmp);
|
2016-12-16 03:34:49 +01:00
|
|
|
} else {
|
2020-01-23 00:29:11 +00:00
|
|
|
Ticket* ticket_tmp;
|
2016-12-20 13:10:07 +01:00
|
|
|
if ((FindTitleKey(ticket, title_id) != 0) &&
|
2018-10-24 00:21:39 +02:00
|
|
|
(FindTicket(&ticket_tmp, title_id, false, src_emunand) == 0)) {
|
|
|
|
// we just copy the titlekey from a valid ticket (if we can)
|
2020-01-23 00:29:11 +00:00
|
|
|
memcpy(ticket->titlekey, ticket_tmp->titlekey, 0x10);
|
|
|
|
ticket->commonkey_idx = ticket_tmp->commonkey_idx;
|
2016-12-15 11:46:00 +01:00
|
|
|
}
|
2020-01-23 00:29:11 +00:00
|
|
|
free(ticket_tmp);
|
2016-12-15 11:46:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// content path string
|
|
|
|
char path_content[256];
|
|
|
|
char* name_content;
|
|
|
|
strncpy(path_content, path_tmd, 256);
|
2018-05-24 00:14:37 +02:00
|
|
|
path_content[255] = '\0';
|
2016-12-15 11:46:00 +01:00
|
|
|
name_content = strrchr(path_content, '/');
|
|
|
|
if (!name_content) return 1; // will not happen
|
|
|
|
name_content++;
|
|
|
|
|
2020-02-02 21:30:14 +00:00
|
|
|
u8 present[(TMD_MAX_CONTENTS + 7) / 8];
|
|
|
|
memset(present, 0xFF, sizeof(present));
|
2018-07-06 16:47:37 -07:00
|
|
|
|
2017-03-15 12:52:12 +01:00
|
|
|
// DLC? Check for missing contents first!
|
2018-07-06 16:47:37 -07:00
|
|
|
if (dlc) for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
2017-03-15 12:52:12 +01:00
|
|
|
FILINFO fno;
|
|
|
|
TmdContentChunk* chunk = &(content_list[i]);
|
2020-02-03 22:06:44 +00:00
|
|
|
TicketRightsCheck rights_ctx;
|
|
|
|
TicketRightsCheck_InitContext(&rights_ctx, (Ticket*)&(cia->ticket));
|
2017-03-15 12:52:12 +01:00
|
|
|
snprintf(name_content, 256 - (name_content - path_content),
|
|
|
|
(cdn) ? "%08lx" : (dlc && !cdn) ? "00000000/%08lx.app" : "%08lx.app", getbe32(chunk->id));
|
2020-02-03 22:06:44 +00:00
|
|
|
if ((fvx_stat(path_content, &fno) != FR_OK) || (fno.fsize != (u32) getbe64(chunk->size)) ||
|
|
|
|
!TicketRightsCheck_CheckIndex(&rights_ctx, getbe16(chunk->index))) {
|
2020-02-02 21:30:14 +00:00
|
|
|
present[i / 8] ^= 1 << (i % 8);
|
2018-07-06 16:47:37 -07:00
|
|
|
|
|
|
|
u16 index = getbe16(chunk->index);
|
|
|
|
cia->header.size_content -= getbe64(chunk->size);
|
|
|
|
cia->header.content_index[index/8] &= ~(1 << (7-(index%8)));
|
|
|
|
}
|
2017-03-15 12:52:12 +01:00
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
// insert / install contents
|
2016-12-15 11:46:00 +01:00
|
|
|
u8 titlekey[16] = { 0xFF };
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0) && force_legit) return 1;
|
2020-07-22 00:20:16 +02:00
|
|
|
if (!install && (WriteCiaStub(cia, path_dest) != 0)) return 1;
|
2016-12-19 13:50:03 +01:00
|
|
|
for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) {
|
2016-12-15 11:46:00 +01:00
|
|
|
TmdContentChunk* chunk = &(content_list[i]);
|
2020-02-02 21:30:14 +00:00
|
|
|
if (present[i / 8] & (1 << (i % 8))) {
|
2018-07-06 16:47:37 -07:00
|
|
|
snprintf(name_content, 256 - (name_content - path_content),
|
|
|
|
(cdn) ? "%08lx" : (dlc && !cdn) ? "00000000/%08lx.app" : "%08lx.app", getbe32(chunk->id));
|
2020-07-22 00:20:16 +02:00
|
|
|
if (!install && (InsertCiaContent(path_dest, path_content, 0, (u32) getbe64(chunk->size),
|
|
|
|
chunk, titlekey, force_legit, false, cdn) != 0)) {
|
2018-07-06 16:47:37 -07:00
|
|
|
ShowPrompt(false, "ID %016llX.%08lX\nInsert content failed", getbe64(title_id), getbe32(chunk->id));
|
|
|
|
return 1;
|
|
|
|
}
|
2020-07-22 00:20:16 +02:00
|
|
|
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;
|
|
|
|
}
|
2016-12-15 11:46:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-16 15:26:19 +01:00
|
|
|
// try to build & insert meta, but ignore result
|
2020-07-22 00:20:16 +02:00
|
|
|
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))
|
2018-01-24 23:32:06 +01:00
|
|
|
cia->header.size_meta = CIA_META_SIZE;
|
|
|
|
}
|
2020-07-22 00:20:16 +02:00
|
|
|
free(meta);
|
2017-02-06 23:56:12 +01:00
|
|
|
}
|
2016-12-16 15:26:19 +01:00
|
|
|
}
|
2016-12-15 11:46:00 +01:00
|
|
|
|
|
|
|
// write the CIA stub (take #2)
|
2020-07-22 00:20:16 +02:00
|
|
|
if ((FixTmdHashes(tmd) != 0) ||
|
|
|
|
(install && (WriteCiaStub(cia, path_dest) != 0)) ||
|
|
|
|
(!install && (InstallCiaSystemData(cia, path_dest) != 0)))
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
u32 InstallFromTmdFile(const char* path_tmd, const char* path_dest) {
|
2018-01-24 23:32:06 +01:00
|
|
|
void* buffer = (void*) malloc(sizeof(CiaStub));
|
|
|
|
if (!buffer) return 1;
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
u32 ret = BuildInstallFromTmdFileBuffered(path_tmd, path_dest, false, true, buffer, true);
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(buffer);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
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) {
|
2018-01-24 23:32:06 +01:00
|
|
|
NcchExtHeader exthdr;
|
2016-12-19 01:33:30 +01:00
|
|
|
NcchHeader ncch;
|
|
|
|
u8 title_id[8];
|
|
|
|
u32 save_size = 0;
|
2018-01-24 23:32:06 +01:00
|
|
|
bool has_exthdr = false;
|
2016-12-19 01:33:30 +01:00
|
|
|
|
|
|
|
// Init progress bar
|
|
|
|
if (!ShowProgress(0, 0, path_ncch)) return 1;
|
|
|
|
|
|
|
|
// load NCCH header / extheader, get save size && title id
|
2018-01-24 23:32:06 +01:00
|
|
|
if (LoadNcchHeaders(&ncch, &exthdr, NULL, path_ncch, 0) == 0) {
|
2018-03-27 00:28:24 +02:00
|
|
|
save_size = (u32) exthdr.savedata_size;
|
2018-01-24 23:32:06 +01:00
|
|
|
has_exthdr = true;
|
|
|
|
} else if (LoadNcchHeaders(&ncch, NULL, NULL, path_ncch, 0) != 0) {
|
|
|
|
return 1;
|
2016-12-19 01:33:30 +01:00
|
|
|
}
|
|
|
|
for (u32 i = 0; i < 8; i++)
|
|
|
|
title_id[i] = (ncch.programId >> ((7-i)*8)) & 0xFF;
|
|
|
|
|
|
|
|
// build the CIA stub
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
|
|
|
if (!cia) return 1;
|
2016-12-19 01:33:30 +01:00
|
|
|
memset(cia, 0, sizeof(CiaStub));
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((BuildCiaHeader(&(cia->header), TICKET_COMMON_SIZE) != 0) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(BuildCiaCert(cia->cert) != 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(BuildFakeTicket((Ticket*)&(cia->ticket), title_id) != 0) ||
|
2019-03-20 02:07:34 +01:00
|
|
|
(BuildFakeTmd(&(cia->tmd), title_id, 1, save_size, 0)) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
2020-07-22 00:20:16 +02:00
|
|
|
(!install && (WriteCiaStub(cia, path_dest) != 0))) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
// insert / install NCCH content
|
2016-12-19 01:33:30 +01:00
|
|
|
TmdContentChunk* chunk = cia->content_list;
|
|
|
|
memset(chunk, 0, sizeof(TmdContentChunk)); // nothing else to do
|
2020-07-22 00:20:16 +02:00
|
|
|
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))) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
|
|
|
|
// optional stuff (proper titlekey / meta data)
|
2020-07-22 00:20:16 +02:00
|
|
|
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);
|
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
|
|
|
|
// write the CIA stub (take #2)
|
2020-01-23 00:29:11 +00:00
|
|
|
FindTitleKey((Ticket*)(&cia->ticket), title_id);
|
2016-12-19 01:33:30 +01:00
|
|
|
if ((FixTmdHashes(&(cia->tmd)) != 0) ||
|
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
2020-07-22 00:20:16 +02:00
|
|
|
(!install && (WriteCiaStub(cia, path_dest) != 0)) ||
|
|
|
|
(install && (InstallCiaSystemData(cia, path_dest) != 0))) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
u32 BuildInstallFromNcsdFile(const char* path_ncsd, const char* path_dest, bool install) {
|
2018-01-24 23:32:06 +01:00
|
|
|
NcchExtHeader exthdr;
|
2016-12-19 01:33:30 +01:00
|
|
|
NcsdHeader ncsd;
|
|
|
|
NcchHeader ncch;
|
|
|
|
u8 title_id[8];
|
|
|
|
u32 save_size = 0;
|
|
|
|
|
|
|
|
// Init progress bar
|
|
|
|
if (!ShowProgress(0, 0, path_ncsd)) return 1;
|
|
|
|
|
|
|
|
// load NCSD header, get content count, title id
|
|
|
|
u32 content_count = 0;
|
|
|
|
if (LoadNcsdHeader(&ncsd, path_ncsd) != 0) return 1;
|
|
|
|
for (u32 i = 0; i < 3; i++)
|
|
|
|
if (ncsd.partitions[i].size) content_count++;
|
|
|
|
for (u32 i = 0; i < 8; i++)
|
|
|
|
title_id[i] = (ncsd.mediaId >> ((7-i)*8)) & 0xFF;
|
|
|
|
|
|
|
|
// load first content NCCH / extheader
|
2018-01-24 23:32:06 +01:00
|
|
|
if (LoadNcchHeaders(&ncch, &exthdr, NULL, path_ncsd, NCSD_CNT0_OFFSET) != 0)
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
2018-03-27 00:28:24 +02:00
|
|
|
save_size = (u32) exthdr.savedata_size;
|
2016-12-19 01:33:30 +01:00
|
|
|
|
|
|
|
// build the CIA stub
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
|
|
|
if (!cia) return 1;
|
2016-12-19 01:33:30 +01:00
|
|
|
memset(cia, 0, sizeof(CiaStub));
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((BuildCiaHeader(&(cia->header), TICKET_COMMON_SIZE) != 0) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(BuildCiaCert(cia->cert) != 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(BuildFakeTicket((Ticket*)&(cia->ticket), title_id) != 0) ||
|
2019-03-20 02:07:34 +01:00
|
|
|
(BuildFakeTmd(&(cia->tmd), title_id, content_count, save_size, 0)) ||
|
2016-12-19 01:33:30 +01:00
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
2020-07-22 00:20:16 +02:00
|
|
|
(!install && (WriteCiaStub(cia, path_dest) != 0))) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
// insert / install NCSD content
|
2016-12-19 01:33:30 +01:00
|
|
|
TmdContentChunk* chunk = cia->content_list;
|
2020-07-22 00:20:16 +02:00
|
|
|
for (u32 i = 0, idx = 0; i < 3; i++) {
|
2016-12-19 01:33:30 +01:00
|
|
|
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));
|
2020-07-22 00:20:16 +02:00
|
|
|
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))) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// optional stuff (proper titlekey / meta data)
|
2020-07-22 00:20:16 +02:00
|
|
|
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);
|
|
|
|
}
|
2020-05-06 17:22:47 +02:00
|
|
|
|
|
|
|
// update title version from cart header (yeah, that's a bit hacky)
|
|
|
|
u16 title_version;
|
|
|
|
if (fvx_qread(path_ncsd, &title_version, 0x310, 2, NULL) == FR_OK) {
|
|
|
|
u8 title_version_le[2];
|
|
|
|
title_version_le[0] = (title_version >> 8) & 0xFF;
|
|
|
|
title_version_le[1] = title_version & 0xFF;
|
|
|
|
memcpy((cia->tmd).title_version, title_version_le, 2);
|
|
|
|
memcpy((cia->ticket).ticket_version, title_version_le, 2);
|
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
|
|
|
|
// write the CIA stub (take #2)
|
2020-01-23 00:29:11 +00:00
|
|
|
FindTitleKey((Ticket*)&(cia->ticket), title_id);
|
2016-12-19 01:33:30 +01:00
|
|
|
if ((FixTmdHashes(&(cia->tmd)) != 0) ||
|
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
2020-07-22 00:20:16 +02:00
|
|
|
(!install && (WriteCiaStub(cia, path_dest) != 0)) ||
|
|
|
|
(install && (InstallCiaSystemData(cia, path_dest) != 0))) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2016-12-19 01:33:30 +01:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2016-12-19 01:33:30 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
u32 BuildInstallFromNdsFile(const char* path_nds, const char* path_dest, bool install) {
|
2019-03-20 02:07:34 +01:00
|
|
|
TwlHeader twl;
|
|
|
|
u8 title_id[8];
|
|
|
|
u32 save_size = 0;
|
|
|
|
u32 privsave_size = 0;
|
|
|
|
|
|
|
|
// Init progress bar
|
|
|
|
if (!ShowProgress(0, 0, path_nds)) return 1;
|
|
|
|
|
|
|
|
// load TWL header, get save sizes && title id
|
|
|
|
if (fvx_qread(path_nds, &twl, 0, sizeof(TwlHeader), NULL) != FR_OK)
|
|
|
|
return 1;
|
|
|
|
for (u32 i = 0; i < 8; i++)
|
|
|
|
title_id[i] = (twl.title_id >> ((7-i)*8)) & 0xFF;
|
|
|
|
save_size = twl.pubsav_size;
|
|
|
|
privsave_size = twl.prvsav_size;
|
|
|
|
|
|
|
|
// some basic sanity checks
|
|
|
|
// see: https://problemkaputt.de/gbatek.htm#dsicartridgeheader
|
|
|
|
// (gamecart dumps are not allowed)
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 tidhigh_dsiware[4] = { 0x00, 0x03, 0x00, 0x04 };
|
2019-03-20 02:07:34 +01:00
|
|
|
if ((memcmp(title_id, tidhigh_dsiware, 3) != 0) || !title_id[3])
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
// convert DSi title ID to 3DS title ID
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 tidhigh_3ds[4] = { 0x00, 0x04, 0x80, 0x04 };
|
2019-03-20 02:07:34 +01:00
|
|
|
memcpy(title_id, tidhigh_3ds, 3);
|
|
|
|
|
|
|
|
// build the CIA stub
|
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
|
|
|
if (!cia) return 1;
|
|
|
|
memset(cia, 0, sizeof(CiaStub));
|
2020-01-23 00:29:11 +00:00
|
|
|
if ((BuildCiaHeader(&(cia->header), TICKET_COMMON_SIZE) != 0) ||
|
2019-03-20 02:07:34 +01:00
|
|
|
(BuildCiaCert(cia->cert) != 0) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(BuildFakeTicket((Ticket*)&(cia->ticket), title_id) != 0) ||
|
2019-03-20 02:07:34 +01:00
|
|
|
(BuildFakeTmd(&(cia->tmd), title_id, 1, save_size, privsave_size)) ||
|
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
2020-07-22 00:20:16 +02:00
|
|
|
(!install && (WriteCiaStub(cia, path_dest) != 0))) {
|
2019-03-20 02:07:34 +01:00
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
// insert / install NDS content
|
2019-03-20 02:07:34 +01:00
|
|
|
TmdContentChunk* chunk = cia->content_list;
|
|
|
|
memset(chunk, 0, sizeof(TmdContentChunk)); // nothing else to do
|
2020-07-22 00:20:16 +02:00
|
|
|
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))) {
|
2019-03-20 02:07:34 +01:00
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// write the CIA stub (take #2)
|
2020-01-23 00:29:11 +00:00
|
|
|
FindTitleKey((Ticket*)(&cia->ticket), title_id);
|
2019-03-20 02:07:34 +01:00
|
|
|
if ((FixTmdHashes(&(cia->tmd)) != 0) ||
|
|
|
|
(FixCiaHeaderForTmd(&(cia->header), &(cia->tmd)) != 0) ||
|
2020-07-22 00:20:16 +02:00
|
|
|
(!install && (WriteCiaStub(cia, path_dest) != 0)) ||
|
|
|
|
(install && (InstallCiaSystemData(cia, path_dest) != 0))) {
|
2019-03-20 02:07:34 +01:00
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
free(cia);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
u32 BuildCiaFromGameFile(const char* path, bool force_legit) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2016-12-15 11:46:00 +01:00
|
|
|
char dest[256];
|
|
|
|
u32 ret = 0;
|
|
|
|
|
2017-05-09 17:07:49 +02:00
|
|
|
// build output name
|
2017-05-15 20:00:26 +02:00
|
|
|
snprintf(dest, 256, OUTPUT_PATH "/");
|
|
|
|
char* dname = dest + strnlen(dest, 256);
|
|
|
|
if (!((filetype & GAME_TMD) || (strncmp(path + 1, ":/title/", 8) == 0)) ||
|
2017-05-16 16:08:38 +02:00
|
|
|
(GetGoodName(dname, path, false) != 0)) {
|
2017-05-15 20:00:26 +02:00
|
|
|
char* name = strrchr(path, '/');
|
|
|
|
if (!name) return 1;
|
|
|
|
snprintf(dest, 256, "%s/%s", OUTPUT_PATH, ++name);
|
|
|
|
}
|
2017-05-09 17:07:49 +02:00
|
|
|
// replace extension
|
|
|
|
char* dot = strrchr(dest, '.');
|
|
|
|
if (!dot || (dot < strrchr(dest, '/')))
|
|
|
|
dot = dest + strnlen(dest, 256);
|
|
|
|
snprintf(dot, 16, ".%s", force_legit ? "legit.cia" : "cia");
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
if (!CheckWritePermissions(dest)) return 1;
|
2016-12-19 01:33:30 +01:00
|
|
|
f_unlink(dest); // remove the file if it already exists
|
2016-12-15 11:46:00 +01:00
|
|
|
|
|
|
|
// ensure the output dir exists
|
2017-06-06 21:14:19 +02:00
|
|
|
if (fvx_rmkdir(OUTPUT_PATH) != FR_OK)
|
2016-12-15 11:46:00 +01:00
|
|
|
return 1;
|
|
|
|
|
|
|
|
// build CIA from game file
|
|
|
|
if (filetype & GAME_TMD)
|
2017-02-06 23:56:12 +01:00
|
|
|
ret = BuildCiaFromTmdFile(path, dest, force_legit, filetype & FLAG_NUSCDN);
|
2016-12-19 01:33:30 +01:00
|
|
|
else if (filetype & GAME_NCCH)
|
2020-07-22 00:20:16 +02:00
|
|
|
ret = BuildInstallFromNcchFile(path, dest, false);
|
2016-12-19 01:33:30 +01:00
|
|
|
else if (filetype & GAME_NCSD)
|
2020-07-22 00:20:16 +02:00
|
|
|
ret = BuildInstallFromNcsdFile(path, dest, false);
|
2019-03-20 02:07:34 +01:00
|
|
|
else if ((filetype & GAME_NDS) && (filetype & FLAG_DSIW))
|
2020-07-22 00:20:16 +02:00
|
|
|
ret = BuildInstallFromNdsFile(path, dest, false);
|
2016-12-15 11:46:00 +01:00
|
|
|
else ret = 1;
|
|
|
|
|
2016-12-16 15:26:19 +01:00
|
|
|
if (ret != 0) // try to get rid of the borked file
|
|
|
|
f_unlink(dest);
|
2016-12-15 11:46:00 +01:00
|
|
|
|
2020-07-22 00:20:16 +02:00
|
|
|
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) ...
|
|
|
|
|
2016-12-15 11:46:00 +01:00
|
|
|
return ret;
|
|
|
|
}
|
2017-01-25 14:46:29 +01:00
|
|
|
|
2017-05-17 01:16:24 +02:00
|
|
|
// this has very limited uses right now
|
|
|
|
u32 DumpCxiSrlFromTmdFile(const char* path) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = 0;
|
2017-05-17 01:16:24 +02:00
|
|
|
char path_cxi[256];
|
|
|
|
char dest[256];
|
|
|
|
|
|
|
|
// prepare output name
|
|
|
|
snprintf(dest, 256, OUTPUT_PATH "/");
|
|
|
|
char* dname = dest + strnlen(dest, 256);
|
2017-09-08 20:58:20 +02:00
|
|
|
if (!CheckWritePermissions(dest)) return 1;
|
2017-05-29 01:50:35 +02:00
|
|
|
|
|
|
|
// ensure the output dir exists
|
2017-06-06 21:14:19 +02:00
|
|
|
if (fvx_rmkdir(OUTPUT_PATH) != FR_OK)
|
2017-05-29 01:50:35 +02:00
|
|
|
return 1;
|
2017-05-17 01:16:24 +02:00
|
|
|
|
|
|
|
// get path to CXI/SRL and decrypt (if encrypted)
|
|
|
|
if ((strncmp(path + 1, ":/title/", 8) != 0) ||
|
|
|
|
(GetTmdContentPath(path_cxi, path) != 0) ||
|
|
|
|
(!((filetype = IdentifyFileType(path_cxi)) & (GAME_NCCH|GAME_NDS))) ||
|
|
|
|
(GetGoodName(dname, path_cxi, false) != 0) ||
|
2017-12-28 03:46:57 +01:00
|
|
|
(CryptNcchNcsdBossFirmFile(path_cxi, dest, filetype, CRYPTO_DECRYPT, 0, 0, NULL, NULL) != 0)) {
|
|
|
|
if (*dname) fvx_unlink(dest);
|
2017-05-17 01:16:24 +02:00
|
|
|
return 1;
|
2017-12-28 03:46:57 +01:00
|
|
|
}
|
2017-05-17 01:16:24 +02:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-10-16 02:02:24 +02:00
|
|
|
u32 ExtractCodeFromCxiFile(const char* path, const char* path_out, char* extstr) {
|
2018-04-17 00:48:50 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2018-01-24 23:32:06 +01:00
|
|
|
char dest[256];
|
|
|
|
if (!path_out && (fvx_rmkdir(OUTPUT_PATH) != FR_OK)) return 1;
|
|
|
|
strncpy(dest, path_out ? path_out : OUTPUT_PATH, 256);
|
2018-05-24 00:14:37 +02:00
|
|
|
dest[255] = '\0';
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!CheckWritePermissions(dest)) return 1;
|
2017-09-08 03:12:04 +02:00
|
|
|
|
2018-04-17 00:48:50 +02:00
|
|
|
// NCSD handling
|
|
|
|
u32 ncch_offset = 0;
|
|
|
|
if (filetype & GAME_NCSD) {
|
|
|
|
NcsdHeader ncsd;
|
|
|
|
if (LoadNcsdHeader(&ncsd, path) == 0)
|
|
|
|
ncch_offset = ncsd.partitions[0].offset * NCSD_MEDIA_UNIT;
|
|
|
|
else return 1;
|
|
|
|
}
|
|
|
|
|
2018-02-08 00:49:34 +01:00
|
|
|
// load all required headers
|
2017-09-08 03:12:04 +02:00
|
|
|
NcchHeader ncch;
|
|
|
|
NcchExtHeader exthdr;
|
2018-02-08 00:49:34 +01:00
|
|
|
ExeFsHeader exefs;
|
2018-04-17 00:48:50 +02:00
|
|
|
if (LoadNcchHeaders(&ncch, &exthdr, &exefs, path, ncch_offset) != 0) return 1;
|
2018-02-08 00:49:34 +01:00
|
|
|
|
|
|
|
// find ".code" or ".firm" inside the ExeFS header
|
|
|
|
u32 code_size = 0;
|
|
|
|
u32 code_offset = 0;
|
|
|
|
for (u32 i = 0; i < 10; i++) {
|
|
|
|
if (exefs.files[i].size &&
|
|
|
|
((strncmp(exefs.files[i].name, EXEFS_CODE_NAME, 8) == 0) ||
|
|
|
|
(strncmp(exefs.files[i].name, ".firm", 8) == 0))) {
|
|
|
|
code_size = exefs.files[i].size;
|
|
|
|
code_offset = (ncch.offset_exefs * NCCH_MEDIA_UNIT) + sizeof(ExeFsHeader) + exefs.files[i].offset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if code is compressed: find decompressed size
|
|
|
|
u32 code_max_size = code_size;
|
|
|
|
if (exthdr.flag & 0x1) {
|
|
|
|
u8 footer[8];
|
|
|
|
if (code_size < 8) return 1;
|
2018-04-17 00:48:50 +02:00
|
|
|
if ((fvx_qread(path, footer, ncch_offset + code_offset + code_size - 8, 8, NULL) != FR_OK) ||
|
2018-02-08 00:49:34 +01:00
|
|
|
(DecryptNcch(footer, code_offset + code_size - 8, 8, &ncch, &exefs) != 0))
|
|
|
|
return 1;
|
|
|
|
u32 unc_size = GetCodeLzssUncompressedSize(footer, code_size);
|
|
|
|
code_max_size = max(code_size, unc_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
// allocate memory
|
2018-01-24 23:32:06 +01:00
|
|
|
u8* code = (u8*) malloc(code_max_size);
|
|
|
|
if (!code) {
|
|
|
|
ShowPrompt(false, "Out of memory.");
|
|
|
|
return 1;
|
|
|
|
}
|
2017-09-08 03:12:04 +02:00
|
|
|
|
2018-02-08 00:49:34 +01:00
|
|
|
// load .code
|
2018-04-17 00:48:50 +02:00
|
|
|
if ((fvx_qread(path, code, ncch_offset + code_offset, code_size, NULL) != FR_OK) ||
|
2018-02-08 00:49:34 +01:00
|
|
|
(DecryptNcch(code, code_offset, code_size, &ncch, &exefs) != 0)) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(code);
|
2017-09-08 03:12:04 +02:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2017-09-08 03:12:04 +02:00
|
|
|
|
|
|
|
// decompress code (only if required)
|
2018-01-24 23:32:06 +01:00
|
|
|
if ((exthdr.flag & 0x1) && (DecompressCodeLzss(code, &code_size, code_max_size) != 0)) {
|
|
|
|
free(code);
|
2017-09-08 03:12:04 +02:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
2017-09-08 03:12:04 +02:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
// finalize output path (if not already final)
|
2017-10-16 02:02:24 +02:00
|
|
|
char* ext = EXEFS_CODE_NAME;
|
|
|
|
if (code_size >= 0x200) {
|
|
|
|
if (ValidateFirmHeader((FirmHeader*)(void*) code, code_size) == 0) ext = ".firm";
|
|
|
|
else if (ValidateAgbHeader((AgbHeader*)(void*) code) == 0) ext = ".gba";
|
|
|
|
}
|
|
|
|
if (extstr) strncpy(extstr, ext, 7);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!path_out) snprintf(dest, 256, OUTPUT_PATH "/%016llX%s%s", ncch.programId, (exthdr.flag & 0x1) ? ".dec" : "", ext);
|
2017-09-08 03:12:04 +02:00
|
|
|
|
|
|
|
// write output file
|
|
|
|
fvx_unlink(dest);
|
|
|
|
if (fvx_qwrite(dest, code, 0, code_size, NULL) != FR_OK) {
|
|
|
|
fvx_unlink(dest);
|
2018-01-24 23:32:06 +01:00
|
|
|
free(code);
|
2017-09-08 03:12:04 +02:00
|
|
|
return 1;
|
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(code);
|
2017-09-08 03:12:04 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-04-22 12:28:37 -05:00
|
|
|
u32 CompressCode(const char* path, const char* path_out) {
|
|
|
|
char dest[256];
|
|
|
|
|
|
|
|
strncpy(dest, path_out ? path_out : OUTPUT_PATH, 255);
|
|
|
|
if (!CheckWritePermissions(dest)) return 1;
|
|
|
|
if (!path_out && (fvx_rmkdir(OUTPUT_PATH) != FR_OK)) return 1;
|
|
|
|
|
|
|
|
// allocate memory
|
|
|
|
u32 code_dec_size = fvx_qsize(path);
|
|
|
|
u8* code_dec = (u8*) malloc(code_dec_size);
|
|
|
|
u32 code_cmp_size = code_dec_size;
|
|
|
|
u8* code_cmp = (u8*) malloc(code_cmp_size);
|
|
|
|
if (!code_dec || !code_cmp) {
|
|
|
|
if (code_dec != NULL) free(code_dec);
|
|
|
|
if (code_cmp != NULL) free(code_cmp);
|
|
|
|
ShowPrompt(false, "Out of memory.");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// load code.bin and compress code
|
|
|
|
if ((fvx_qread(path, code_dec, 0, code_dec_size, NULL) != FR_OK) ||
|
|
|
|
(!CompressCodeLzss(code_dec, code_dec_size, code_cmp, &code_cmp_size))) {
|
|
|
|
free(code_dec);
|
|
|
|
free(code_cmp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// write output file
|
|
|
|
fvx_unlink(dest);
|
|
|
|
free(code_dec);
|
|
|
|
if (fvx_qwrite(dest, code_cmp, 0, code_cmp_size, NULL) != FR_OK) {
|
|
|
|
fvx_unlink(dest);
|
|
|
|
free(code_cmp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
free(code_cmp);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-07-16 01:07:59 +02:00
|
|
|
u64 GetGameFileTrimmedSize(const char* path) {
|
|
|
|
u64 filetype = IdentifyFileType(path);
|
|
|
|
u64 trimsize = 0;
|
|
|
|
|
|
|
|
if (filetype & GAME_NDS) {
|
|
|
|
TwlHeader hdr;
|
|
|
|
if (fvx_qread(path, &hdr, 0, sizeof(TwlHeader), NULL) != FR_OK)
|
|
|
|
return 0;
|
|
|
|
if (hdr.unit_code != 0x00) // DSi or NDS+DSi
|
|
|
|
trimsize = hdr.ntr_twl_rom_size;
|
|
|
|
else trimsize = hdr.ntr_rom_size; // regular NDS
|
|
|
|
} else {
|
|
|
|
u8 hdr[0x200];
|
|
|
|
if (fvx_qread(path, &hdr, 0, 0x200, NULL) != FR_OK)
|
|
|
|
return 0;
|
|
|
|
if (filetype & IMG_NAND)
|
|
|
|
trimsize = GetNandNcsdMinSizeSectors((NandNcsdHeader*) (void*) hdr) * 0x200;
|
|
|
|
else if (filetype & SYS_FIRM)
|
|
|
|
trimsize = GetFirmSize((FirmHeader*) (void*) hdr);
|
|
|
|
else if (filetype & GAME_NCSD)
|
|
|
|
trimsize = GetNcsdTrimmedSize((NcsdHeader*) (void*) hdr);
|
|
|
|
else if (filetype & GAME_NCCH)
|
|
|
|
trimsize = ((NcchHeader*) (void*) hdr)->size * NCCH_MEDIA_UNIT;
|
|
|
|
}
|
|
|
|
|
|
|
|
// safety check for file size
|
|
|
|
if (trimsize > fvx_qsize(path))
|
|
|
|
trimsize = 0;
|
|
|
|
|
|
|
|
return trimsize;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 TrimGameFile(const char* path) {
|
|
|
|
u64 trimsize = GetGameFileTrimmedSize(path);
|
|
|
|
if (!trimsize) return 1;
|
|
|
|
|
|
|
|
// actual truncate routine - FAT only
|
|
|
|
FIL fp;
|
|
|
|
if (fx_open(&fp, path, FA_WRITE | FA_OPEN_EXISTING) != FR_OK)
|
|
|
|
return 1;
|
|
|
|
if ((f_lseek(&fp, (u32) trimsize) != FR_OK) || (f_truncate(&fp) != FR_OK)) {
|
|
|
|
fx_close(&fp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
fx_close(&fp);
|
|
|
|
|
|
|
|
// all done
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-05-10 21:59:22 +02:00
|
|
|
u32 LoadSmdhFromGameFile(const char* path, Smdh* smdh) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2017-05-10 21:59:22 +02:00
|
|
|
|
|
|
|
if (filetype & GAME_SMDH) { // SMDH file
|
|
|
|
UINT btr;
|
|
|
|
if ((fvx_qread(path, smdh, 0, sizeof(Smdh), &btr) == FR_OK) || (btr == sizeof(Smdh))) return 0;
|
|
|
|
} else if (filetype & GAME_NCCH) { // NCCH file
|
2017-09-08 03:12:04 +02:00
|
|
|
if (LoadExeFsFile(smdh, path, 0, "icon", sizeof(Smdh), NULL) == 0) return 0;
|
2017-05-10 21:59:22 +02:00
|
|
|
} else if (filetype & GAME_NCSD) { // NCSD file
|
2017-09-08 03:12:04 +02:00
|
|
|
if (LoadExeFsFile(smdh, path, NCSD_CNT0_OFFSET, "icon", sizeof(Smdh), NULL) == 0) return 0;
|
2017-05-10 21:59:22 +02:00
|
|
|
} else if (filetype & GAME_CIA) { // CIA file
|
|
|
|
CiaInfo info;
|
|
|
|
|
2017-10-29 15:32:25 +01:00
|
|
|
if ((fvx_qread(path, &info, 0, 0x20, NULL) != FR_OK) ||
|
2017-05-10 21:59:22 +02:00
|
|
|
(GetCiaInfo(&info, (CiaHeader*) &info) != 0)) return 1;
|
2017-10-29 15:32:25 +01:00
|
|
|
if ((info.offset_meta) && (fvx_qread(path, smdh, info.offset_meta + 0x400, sizeof(Smdh), NULL) == FR_OK)) return 0;
|
2017-09-08 03:12:04 +02:00
|
|
|
else if (LoadExeFsFile(smdh, path, info.offset_content, "icon", sizeof(Smdh), NULL) == 0) return 0;
|
2017-05-10 21:59:22 +02:00
|
|
|
} else if (filetype & GAME_TMD) {
|
|
|
|
char path_content[256];
|
2017-05-15 22:59:12 +02:00
|
|
|
if (GetTmdContentPath(path_content, path) != 0) return 1;
|
2017-05-10 21:59:22 +02:00
|
|
|
return LoadSmdhFromGameFile(path_content, smdh);
|
2017-10-29 15:32:25 +01:00
|
|
|
} else if (filetype & GAME_3DSX) {
|
|
|
|
ThreedsxHeader threedsx;
|
|
|
|
if ((fvx_qread(path, &threedsx, 0, sizeof(ThreedsxHeader), NULL) != FR_OK) ||
|
|
|
|
(!threedsx.offset_smdh || (threedsx.size_smdh != sizeof(Smdh))) ||
|
|
|
|
(fvx_qread(path, smdh, threedsx.offset_smdh, sizeof(Smdh), NULL) != FR_OK))
|
|
|
|
return 1;
|
|
|
|
return 0;
|
2017-05-10 21:59:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2019-09-30 22:00:52 +02:00
|
|
|
u32 ShowSmdhTitleInfo(Smdh* smdh, u16* screen) {
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u8 smdh_magic[] = { SMDH_MAGIC };
|
2017-03-29 02:23:06 +02:00
|
|
|
const u32 lwrap = 24;
|
2019-05-26 00:03:02 -03:00
|
|
|
u16 icon[SMDH_SIZE_ICON_BIG / sizeof(u16)];
|
2018-03-17 16:38:09 +01:00
|
|
|
char desc_l[SMDH_SIZE_DESC_LONG+1];
|
|
|
|
char desc_s[SMDH_SIZE_DESC_SHORT+1];
|
|
|
|
char pub[SMDH_SIZE_PUBLISHER+1];
|
2018-01-23 01:59:06 +01:00
|
|
|
if ((memcmp(smdh->magic, smdh_magic, 4) != 0) ||
|
|
|
|
(GetSmdhIconBig(icon, smdh) != 0) ||
|
2017-03-29 02:23:06 +02:00
|
|
|
(GetSmdhDescLong(desc_l, smdh) != 0) ||
|
|
|
|
(GetSmdhDescShort(desc_s, smdh) != 0) ||
|
|
|
|
(GetSmdhPublisher(pub, smdh) != 0))
|
|
|
|
return 1;
|
|
|
|
WordWrapString(desc_l, lwrap);
|
|
|
|
WordWrapString(desc_s, lwrap);
|
|
|
|
WordWrapString(pub, lwrap);
|
2019-09-30 22:00:52 +02:00
|
|
|
ShowIconStringF(screen, icon, SMDH_DIM_ICON_BIG, SMDH_DIM_ICON_BIG, "%s\n%s\n%s", desc_l, desc_s, pub);
|
2017-03-29 02:23:06 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-09-30 22:00:52 +02:00
|
|
|
u32 ShowTwlIconTitleInfo(TwlIconData* twl_icon, u16* screen) {
|
2017-03-31 03:14:07 +02:00
|
|
|
const u32 lwrap = 24;
|
2019-05-26 00:03:02 -03:00
|
|
|
u16 icon[TWLICON_SIZE_ICON / sizeof(u16)];
|
2018-03-17 16:38:09 +01:00
|
|
|
char desc[TWLICON_SIZE_DESC+1];
|
2017-11-17 02:29:52 +01:00
|
|
|
if ((GetTwlIcon(icon, twl_icon) != 0) ||
|
2017-03-31 03:14:07 +02:00
|
|
|
(GetTwlTitle(desc, twl_icon) != 0))
|
|
|
|
return 1;
|
|
|
|
WordWrapString(desc, lwrap);
|
2019-09-30 22:00:52 +02:00
|
|
|
ShowIconStringF(screen, icon, TWLICON_DIM_ICON, TWLICON_DIM_ICON, "%s", desc);
|
2017-03-31 03:14:07 +02:00
|
|
|
return 0;
|
2017-11-17 02:29:52 +01:00
|
|
|
}
|
|
|
|
|
2019-09-30 22:00:52 +02:00
|
|
|
u32 ShowGbaFileTitleInfo(const char* path, u16* screen) {
|
2017-11-17 02:29:52 +01:00
|
|
|
AgbHeader agb;
|
|
|
|
if ((fvx_qread(path, &agb, 0, sizeof(AgbHeader), NULL) != FR_OK) ||
|
|
|
|
(ValidateAgbHeader(&agb) != 0)) return 1;
|
2019-09-30 22:00:52 +02:00
|
|
|
ShowStringF(screen, "%.12s (AGB-%.4s)\n%s", agb.game_title, agb.game_code, AGB_DESTSTR(agb.game_code));
|
2017-11-17 02:29:52 +01:00
|
|
|
return 0;
|
2017-03-31 03:14:07 +02:00
|
|
|
}
|
|
|
|
|
2019-09-30 22:00:52 +02:00
|
|
|
u32 ShowGameFileTitleInfoF(const char* path, u16* screen, bool clear) {
|
2017-05-15 22:59:12 +02:00
|
|
|
char path_content[256];
|
2017-11-17 02:29:52 +01:00
|
|
|
u64 itype = IdentifyFileType(path); // initial type
|
|
|
|
if (itype & GAME_TMD) {
|
2017-05-15 22:59:12 +02:00
|
|
|
if (GetTmdContentPath(path_content, path) != 0) return 1;
|
|
|
|
path = path_content;
|
|
|
|
}
|
2019-10-18 12:30:13 -03:00
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
void* buffer = (void*) malloc(max(sizeof(Smdh), sizeof(TwlIconData)));
|
|
|
|
Smdh* smdh = (Smdh*) buffer;
|
|
|
|
TwlIconData* twl_icon = (TwlIconData*) buffer;
|
2019-10-18 12:30:13 -03:00
|
|
|
|
2017-10-16 02:02:24 +02:00
|
|
|
// try loading SMDH, then try NDS / GBA
|
2018-01-24 23:32:06 +01:00
|
|
|
u32 ret = 1;
|
2017-05-10 21:59:22 +02:00
|
|
|
if (LoadSmdhFromGameFile(path, smdh) == 0)
|
2019-09-30 22:00:52 +02:00
|
|
|
ret = ShowSmdhTitleInfo(smdh, screen);
|
2017-11-17 02:29:52 +01:00
|
|
|
else if ((LoadTwlMetaData(path, NULL, twl_icon) == 0) ||
|
2017-11-17 02:48:01 +01:00
|
|
|
((itype & GAME_TAD) && (fvx_qread(path, twl_icon, TAD_BANNER_OFFSET, sizeof(TwlIconData), NULL) == FR_OK)))
|
2019-09-30 22:00:52 +02:00
|
|
|
ret = ShowTwlIconTitleInfo(twl_icon, screen);
|
|
|
|
else ret = ShowGbaFileTitleInfo(path, screen);
|
2019-10-18 12:30:13 -03:00
|
|
|
|
|
|
|
if (!ret && clear) {
|
2019-09-30 22:00:52 +02:00
|
|
|
while(!(InputWait(0) & (BUTTON_A | BUTTON_B)));
|
|
|
|
ClearScreen(screen, COLOR_STD_BG);
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
free(buffer);
|
|
|
|
return ret;
|
2017-03-29 02:23:06 +02:00
|
|
|
}
|
|
|
|
|
2019-09-30 22:00:52 +02:00
|
|
|
u32 ShowGameFileTitleInfo(const char* path) {
|
|
|
|
return ShowGameFileTitleInfoF(path, MAIN_SCREEN, true);
|
|
|
|
}
|
|
|
|
|
2018-08-13 00:08:56 +02:00
|
|
|
u32 ShowCiaCheckerInfo(const char* path) {
|
2020-01-23 00:59:32 +00:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
2018-08-13 00:08:56 +02:00
|
|
|
if (!cia) return 1;
|
|
|
|
|
|
|
|
// path string
|
|
|
|
char pathstr[32 + 1];
|
|
|
|
TruncateString(pathstr, path, 32, 8);
|
|
|
|
|
|
|
|
// load CIA stub
|
|
|
|
if (LoadCiaStub(cia, path) != 0) {
|
|
|
|
ShowPrompt(false, "%s\nError: Probably not a CIA file", pathstr);
|
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// states: 0 -> invalid / 1 -> valid / badsig / 2 -> valid / goodsig
|
|
|
|
u32 state_ticket = 0;
|
|
|
|
u32 state_tmd = 0;
|
|
|
|
u32 content_count = getbe16(cia->tmd.content_count);
|
|
|
|
u32 content_found = 0;
|
|
|
|
u64 title_id = getbe64(cia->ticket.title_id);
|
|
|
|
u32 console_id = getbe32(cia->ticket.console_id);
|
|
|
|
bool missing_first = false;
|
|
|
|
bool is_dlc = ((title_id >> 32) == 0x0004008C);
|
|
|
|
|
|
|
|
// check ticket
|
2020-01-23 00:29:11 +00:00
|
|
|
if (ValidateTicket((Ticket*)&(cia->ticket)) == 0)
|
2020-01-23 00:59:32 +00:00
|
|
|
state_ticket = (ValidateTicketSignature((Ticket*)&(cia->ticket)) == 0) ? 2 : 1;
|
2018-08-13 00:08:56 +02:00
|
|
|
|
|
|
|
// check tmd
|
|
|
|
if (ValidateTmd(&(cia->tmd)) == 0)
|
2020-01-23 00:59:32 +00:00
|
|
|
state_tmd = (ValidateTmdSignature(&(cia->tmd)) == 0) ? 2 : 1;
|
2018-08-13 00:08:56 +02:00
|
|
|
|
|
|
|
// check for available contents
|
|
|
|
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]);
|
|
|
|
u16 index = getbe16(chunk->index);
|
|
|
|
if (cnt_index[index/8] & (1 << (7-(index%8)))) content_found++;
|
|
|
|
else if (i == 0) missing_first = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// CIA type string
|
|
|
|
char typestr[32];
|
|
|
|
if (!state_ticket || !state_tmd || missing_first || (!is_dlc && (content_found != content_count)))
|
2020-01-23 00:59:32 +00:00
|
|
|
snprintf(typestr, 32, "Possibly Broken");
|
2018-08-13 00:08:56 +02:00
|
|
|
else snprintf(typestr, 32, "%s %s%s",
|
2020-01-23 00:59:32 +00:00
|
|
|
console_id ? "Personal" : "Universal",
|
|
|
|
((state_ticket == 2) && (state_tmd == 2)) ? "Legit" :
|
2019-04-17 00:44:38 +02:00
|
|
|
(state_tmd == 2) ? "Pirate Legit" : "Custom",
|
2020-01-23 00:59:32 +00:00
|
|
|
is_dlc ? " DLC" : "");
|
2018-08-13 00:08:56 +02:00
|
|
|
|
|
|
|
// output results
|
|
|
|
s32 state_verify = -1;
|
2020-01-23 00:59:32 +00:00
|
|
|
while (true) {
|
|
|
|
if (!ShowPrompt(state_verify < 0, "%s\n%s CIA File\n \nTitle ID: %016llX\nConsole ID: %08lX\nContents in CIA: %lu/%lu\nTicket/TMD: %s/%s\nVerification: %s",
|
|
|
|
pathstr, typestr, title_id, console_id,
|
|
|
|
content_found, content_count,
|
|
|
|
(state_ticket == 0) ? "invalid" : (state_ticket == 2) ? "legit" : "illegit",
|
|
|
|
(state_tmd == 0) ? "invalid" : (state_tmd == 2) ? "legit" : "illegit",
|
|
|
|
(state_verify < 0) ? "pending\n \nProceed with verification?" : (state_verify == 0) ? "passed" : "failed") ||
|
|
|
|
(state_verify >= 0)) break;
|
|
|
|
state_verify = VerifyCiaFile(path);
|
|
|
|
}
|
2018-08-13 00:08:56 +02:00
|
|
|
|
2020-01-22 20:44:38 +00:00
|
|
|
free(cia);
|
2020-01-23 00:59:32 +00:00
|
|
|
return (state_ticket && state_tmd) ? 0 : 1;
|
2018-08-13 00:08:56 +02:00
|
|
|
}
|
|
|
|
|
2017-01-25 14:46:29 +01:00
|
|
|
u32 BuildNcchInfoXorpads(const char* destdir, const char* path) {
|
|
|
|
FIL fp_info;
|
|
|
|
FIL fp_xorpad;
|
|
|
|
UINT bt;
|
|
|
|
|
|
|
|
if (!CheckWritePermissions(destdir)) return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
// warning: this will only build output dirs in the root dir (!)
|
2017-02-28 15:47:49 +01:00
|
|
|
if ((f_stat(destdir, NULL) != FR_OK) && (f_mkdir(destdir) != FR_OK))
|
|
|
|
return 1;
|
2017-01-25 14:46:29 +01:00
|
|
|
|
|
|
|
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;
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
u8* buffer = (u8*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!buffer) ret = 1;
|
2017-01-25 14:46:29 +01:00
|
|
|
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);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_open(&fp_xorpad, dest, FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) {
|
|
|
|
if (!ShowProgress(0, 0, entry.filename)) ret = 1;
|
|
|
|
for (u64 p = 0; (p < entry.size_b) && (ret == 0); p += STD_BUFFER_SIZE) {
|
|
|
|
UINT create_bytes = min(STD_BUFFER_SIZE, entry.size_b - p);
|
|
|
|
if (BuildNcchInfoXorpad(buffer, &entry, create_bytes, p) != 0) ret = 1;
|
|
|
|
if (fvx_write(&fp_xorpad, buffer, create_bytes, &bt) != FR_OK) ret = 1;
|
|
|
|
if (!ShowProgress(p + create_bytes, entry.size_b, entry.filename)) ret = 1;
|
|
|
|
}
|
|
|
|
fvx_close(&fp_xorpad);
|
|
|
|
} else ret = 1;
|
2017-01-25 14:46:29 +01:00
|
|
|
if (ret != 0) f_unlink(dest); // get rid of the borked file
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
if (buffer) free(buffer);
|
2017-01-25 14:46:29 +01:00
|
|
|
fvx_close(&fp_info);
|
|
|
|
return ret;
|
|
|
|
}
|
2017-01-30 01:49:01 +01:00
|
|
|
|
2017-04-05 02:24:32 +02:00
|
|
|
u32 GetHealthAndSafetyPaths(const char* drv, char* path_cxi, char* path_bak) {
|
2020-07-23 18:41:42 +02:00
|
|
|
static const u32 tidlow_hs_o3ds[] = { 0x00020300, 0x00021300, 0x00022300, 0, 0x00026300, 0x00027300, 0x00028300 };
|
|
|
|
static const u32 tidlow_hs_n3ds[] = { 0x20020300, 0x20021300, 0x20022300, 0, 0, 0x20027300, 0 };
|
2017-01-30 01:49:01 +01:00
|
|
|
|
|
|
|
// get H&S title id low
|
|
|
|
u32 tidlow_hs = 0;
|
|
|
|
for (char secchar = 'C'; secchar >= 'A'; secchar--) {
|
|
|
|
char path_secinfo[32];
|
|
|
|
u8 secinfo[0x111];
|
|
|
|
u32 region = 0xFF;
|
|
|
|
UINT br;
|
2017-04-05 02:24:32 +02:00
|
|
|
snprintf(path_secinfo, 32, "%s/rw/sys/SecureInfo_%c", drv, secchar);
|
2017-01-30 01:49:01 +01:00
|
|
|
if ((fvx_qread(path_secinfo, secinfo, 0, 0x111, &br) != FR_OK) ||
|
|
|
|
(br != 0x111))
|
|
|
|
continue;
|
|
|
|
region = secinfo[0x100];
|
|
|
|
if (region >= sizeof(tidlow_hs_o3ds) / sizeof(u32)) continue;
|
2017-02-26 13:35:37 +01:00
|
|
|
tidlow_hs = (IS_O3DS) ?
|
2017-01-30 01:49:01 +01:00
|
|
|
tidlow_hs_o3ds[region] : tidlow_hs_n3ds[region];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!tidlow_hs) return 1;
|
|
|
|
|
|
|
|
// build paths
|
2017-04-05 02:24:32 +02:00
|
|
|
if (path_cxi) *path_cxi = '\0';
|
|
|
|
if (path_bak) *path_bak = '\0';
|
2018-01-24 23:32:06 +01:00
|
|
|
TitleMetaData* tmd = (TitleMetaData*) malloc(TMD_SIZE_MAX);
|
|
|
|
TmdContentChunk* chunk = (TmdContentChunk*) (tmd + 1);
|
2017-02-28 04:10:23 +01:00
|
|
|
for (u32 i = 0; i < 8; i++) { // 8 is an arbitrary number
|
|
|
|
char path_tmd[64];
|
2017-04-05 02:24:32 +02:00
|
|
|
snprintf(path_tmd, 64, "%s/title/00040010/%08lx/content/%08lx.tmd", drv, tidlow_hs, i);
|
2017-02-28 04:10:23 +01:00
|
|
|
if (LoadTmdFile(tmd, path_tmd) != 0) continue;
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!getbe16(tmd->content_count)) break;
|
2017-04-05 02:24:32 +02:00
|
|
|
if (path_cxi) snprintf(path_cxi, 64, "%s/title/00040010/%08lx/content/%08lx.app", drv, tidlow_hs, getbe32(chunk->id));
|
|
|
|
if (path_bak) snprintf(path_bak, 64, "%s/title/00040010/%08lx/content/%08lx.bak", drv, tidlow_hs, getbe32(chunk->id));
|
2017-02-28 04:10:23 +01:00
|
|
|
break;
|
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
free(tmd);
|
2017-04-05 02:24:32 +02:00
|
|
|
|
|
|
|
return ((path_cxi && !*path_cxi) || (path_bak && !*path_bak)) ? 1 : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 CheckHealthAndSafetyInject(const char* hsdrv) {
|
|
|
|
char path_bak[64] = { 0 };
|
|
|
|
return ((GetHealthAndSafetyPaths(hsdrv, NULL, path_bak) == 0) &&
|
|
|
|
(f_stat(path_bak, NULL) == FR_OK)) ? 0 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 InjectHealthAndSafety(const char* path, const char* destdrv) {
|
|
|
|
NcchHeader ncch;
|
2018-03-27 00:28:24 +02:00
|
|
|
NcchExtHeader exthdr;
|
2017-04-05 02:24:32 +02:00
|
|
|
|
|
|
|
// write permissions
|
|
|
|
if (!CheckWritePermissions(destdrv))
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
// legacy stuff - remove mark file
|
|
|
|
char path_mrk[32] = { 0 };
|
|
|
|
snprintf(path_mrk, 32, "%s/%s", destdrv, "__gm9_hsbak.pth");
|
|
|
|
f_unlink(path_mrk);
|
|
|
|
|
|
|
|
// get H&S paths
|
|
|
|
char path_cxi[64] = { 0 };
|
|
|
|
char path_bak[64] = { 0 };
|
|
|
|
if (GetHealthAndSafetyPaths(destdrv, path_cxi, path_bak) != 0) return 1;
|
2017-01-30 01:49:01 +01:00
|
|
|
|
2017-02-28 04:10:23 +01:00
|
|
|
if (!path) { // if path == NULL -> restore H&S from backup
|
|
|
|
if (f_stat(path_bak, NULL) != FR_OK) return 1;
|
|
|
|
f_unlink(path_cxi);
|
|
|
|
f_rename(path_bak, path_cxi);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check input file / crypto
|
2018-03-27 00:28:24 +02:00
|
|
|
if ((LoadNcchHeaders(&ncch, &exthdr, NULL, path, 0) != 0) ||
|
2017-02-28 04:10:23 +01:00
|
|
|
!(NCCH_IS_CXI(&ncch)) || (SetupNcchCrypto(&ncch, NCCH_NOCRYPTO) != 0))
|
|
|
|
return 1;
|
2017-01-30 01:49:01 +01:00
|
|
|
|
|
|
|
// check crypto, get sig
|
2018-03-27 00:28:24 +02:00
|
|
|
if ((LoadNcchHeaders(&ncch, &exthdr, NULL, path_cxi, 0) != 0) ||
|
2017-04-05 02:24:32 +02:00
|
|
|
(SetupNcchCrypto(&ncch, NCCH_NOCRYPTO) != 0) || !(NCCH_IS_CXI(&ncch)))
|
2017-01-30 01:49:01 +01:00
|
|
|
return 1;
|
2017-04-05 02:24:32 +02:00
|
|
|
u8 sig[0x100];
|
2017-01-30 01:49:01 +01:00
|
|
|
memcpy(sig, ncch.signature, 0x100);
|
2017-04-05 02:24:32 +02:00
|
|
|
u16 crypto = NCCH_GET_CRYPTO(&ncch);
|
|
|
|
u64 tid_hs = ncch.programId;
|
2017-01-30 01:49:01 +01:00
|
|
|
|
|
|
|
// make a backup copy if there is not already one (point of no return)
|
|
|
|
if (f_stat(path_bak, NULL) != FR_OK) {
|
|
|
|
if (f_rename(path_cxi, path_bak) != FR_OK) return 1;
|
|
|
|
} else f_unlink(path_cxi);
|
|
|
|
|
2017-01-30 20:28:49 +01:00
|
|
|
// copy / decrypt the source CXI
|
2017-01-30 01:49:01 +01:00
|
|
|
u32 ret = 0;
|
2017-01-31 16:16:26 +01:00
|
|
|
if (CryptNcchNcsdBossFirmFile(path, path_cxi, GAME_NCCH, CRYPTO_DECRYPT, 0, 0, NULL, NULL) != 0)
|
2017-01-30 01:49:01 +01:00
|
|
|
ret = 1;
|
|
|
|
|
2018-04-13 17:12:17 +02:00
|
|
|
// fix up the injected H&S NCCH header / extheader (copy H&S signature, title ID to multiple locations)
|
|
|
|
// also set savedata size to zero (thanks @TurdPooCharger)
|
2018-03-27 00:28:24 +02:00
|
|
|
if ((ret == 0) && (LoadNcchHeaders(&ncch, &exthdr, NULL, path_cxi, 0) == 0)) {
|
2017-01-30 01:49:01 +01:00
|
|
|
ncch.programId = tid_hs;
|
|
|
|
ncch.partitionId = tid_hs;
|
2018-03-27 00:28:24 +02:00
|
|
|
exthdr.jump_id = tid_hs;
|
|
|
|
exthdr.aci_title_id = tid_hs;
|
|
|
|
exthdr.aci_limit_title_id = tid_hs;
|
2018-04-13 17:12:17 +02:00
|
|
|
exthdr.savedata_size = 0;
|
2017-01-30 01:49:01 +01:00
|
|
|
memcpy(ncch.signature, sig, 0x100);
|
2018-03-27 00:28:24 +02:00
|
|
|
sha_quick(ncch.hash_exthdr, &exthdr, 0x400, SHA256_MODE);
|
|
|
|
if ((fvx_qwrite(path_cxi, &ncch, 0, sizeof(NcchHeader), NULL) != FR_OK) ||
|
|
|
|
(fvx_qwrite(path_cxi, &exthdr, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), NULL) != FR_OK))
|
2017-01-30 01:49:01 +01:00
|
|
|
ret = 1;
|
|
|
|
} else ret = 1;
|
|
|
|
|
|
|
|
// encrypt the CXI in place
|
2017-01-30 20:28:49 +01:00
|
|
|
if (CryptNcchNcsdBossFirmFile(path_cxi, path_cxi, GAME_NCCH, crypto, 0, 0, NULL, NULL) != 0)
|
2017-01-30 01:49:01 +01:00
|
|
|
ret = 1;
|
|
|
|
|
2017-01-30 20:28:49 +01:00
|
|
|
if (ret != 0) { // in case of failure: try recover
|
2017-01-30 01:49:01 +01:00
|
|
|
f_unlink(path_cxi);
|
|
|
|
f_rename(path_bak, path_cxi);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2017-04-08 14:17:58 +02:00
|
|
|
|
|
|
|
u32 BuildTitleKeyInfo(const char* path, bool dec, bool dump) {
|
2018-01-24 23:32:06 +01:00
|
|
|
static TitleKeysInfo* tik_info = NULL;
|
2017-04-08 14:17:58 +02:00
|
|
|
const char* path_out = (dec) ? OUTPUT_PATH "/" TIKDB_NAME_DEC : OUTPUT_PATH "/" TIKDB_NAME_ENC;
|
|
|
|
const char* path_in = path;
|
|
|
|
|
2017-04-17 23:28:40 +02:00
|
|
|
// write permissions
|
|
|
|
if (!CheckWritePermissions(path_out))
|
|
|
|
return 1;
|
|
|
|
|
2017-04-08 14:17:58 +02:00
|
|
|
if (!path_in && !dump) { // no input path given - initialize
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!tik_info) tik_info = (TitleKeysInfo*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!tik_info) return 1;
|
2017-04-08 14:17:58 +02:00
|
|
|
memset(tik_info, 0, 16);
|
2018-01-24 23:32:06 +01:00
|
|
|
|
2017-04-08 14:17:58 +02:00
|
|
|
if ((fvx_stat(path_out, NULL) == FR_OK) &&
|
|
|
|
(ShowPrompt(true, "%s\nOutput file already exists.\nUpdate this?", path_out)))
|
|
|
|
path_in = path_out;
|
|
|
|
else return 0;
|
|
|
|
}
|
|
|
|
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = path_in ? IdentifyFileType(path_in) : 0;
|
2017-04-08 14:17:58 +02:00
|
|
|
if (filetype & GAME_TICKET) {
|
2020-01-23 00:29:11 +00:00
|
|
|
TicketCommon ticket;
|
|
|
|
if ((fvx_qread(path_in, &ticket, 0, TICKET_COMMON_SIZE, NULL) != FR_OK) ||
|
2018-01-24 23:32:06 +01:00
|
|
|
(TIKDB_SIZE(tik_info) + 32 > STD_BUFFER_SIZE) ||
|
2020-01-23 00:29:11 +00:00
|
|
|
(AddTicketToInfo(tik_info, (Ticket*)&ticket, dec) != 0)) {
|
2018-01-24 23:32:06 +01:00
|
|
|
return 1;
|
|
|
|
}
|
2017-04-08 14:17:58 +02:00
|
|
|
} else if (filetype & SYS_TICKDB) {
|
2020-06-11 12:22:38 -04:00
|
|
|
if (!InitImgFS(path_in))
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
DIR dir;
|
|
|
|
FILINFO fno;
|
|
|
|
TicketCommon ticket;
|
|
|
|
char tik_path[64];
|
|
|
|
|
|
|
|
if (fvx_opendir(&dir, "T:/eshop") != FR_OK) {
|
|
|
|
InitImgFS(NULL);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
while ((fvx_readdir(&dir, &fno) == FR_OK) && *(fno.fname)) {
|
|
|
|
snprintf(tik_path, 64, "T:/eshop/%s", fno.fname);
|
|
|
|
if (fvx_qread(tik_path, &ticket, 0, TICKET_COMMON_SIZE, NULL) != FR_OK)
|
|
|
|
continue;
|
|
|
|
if (TIKDB_SIZE(tik_info) + 32 > STD_BUFFER_SIZE) break; // no error message
|
|
|
|
AddTicketToInfo(tik_info, (Ticket*) &ticket, dec); // ignore result
|
|
|
|
}
|
|
|
|
|
|
|
|
fvx_closedir(&dir);
|
2018-01-24 23:32:06 +01:00
|
|
|
|
2020-06-11 12:22:38 -04:00
|
|
|
if (fvx_opendir(&dir, "T:/system") != FR_OK) {
|
|
|
|
InitImgFS(NULL);
|
2018-01-24 23:32:06 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-06-11 12:22:38 -04:00
|
|
|
while ((fvx_readdir(&dir, &fno) == FR_OK) && *(fno.fname)) {
|
|
|
|
snprintf(tik_path, 64, "T:/system/%s", fno.fname);
|
|
|
|
if ((fvx_qread(tik_path, &ticket, 0, TICKET_COMMON_SIZE, NULL) != FR_OK) ||
|
|
|
|
(ValidateTicketSignature((Ticket*) &ticket) != 0))
|
|
|
|
continue;
|
2018-02-21 01:57:35 +01:00
|
|
|
if (TIKDB_SIZE(tik_info) + 32 > STD_BUFFER_SIZE) break; // no error message
|
2020-06-11 12:22:38 -04:00
|
|
|
AddTicketToInfo(tik_info, (Ticket*) &ticket, dec); // ignore result
|
2017-04-08 14:17:58 +02:00
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
2020-06-11 12:22:38 -04:00
|
|
|
fvx_closedir(&dir);
|
|
|
|
|
|
|
|
InitImgFS(NULL);
|
2017-04-08 14:17:58 +02:00
|
|
|
} else if (filetype & BIN_TIKDB) {
|
2018-01-24 23:32:06 +01:00
|
|
|
TitleKeysInfo* tik_info_merge = (TitleKeysInfo*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!tik_info_merge) return 1;
|
|
|
|
|
|
|
|
UINT br;
|
|
|
|
if ((fvx_qread(path_in, tik_info_merge, 0, STD_BUFFER_SIZE, &br) != FR_OK) ||
|
|
|
|
(TIKDB_SIZE(tik_info_merge) != br)) {
|
|
|
|
free(tik_info_merge);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-04-08 14:17:58 +02:00
|
|
|
// merge and rebuild TitleKeyInfo
|
|
|
|
u32 n_entries = tik_info_merge->n_entries;
|
|
|
|
TitleKeyEntry* tik = tik_info_merge->entries;
|
|
|
|
for (u32 i = 0; i < n_entries; i++, tik++) {
|
2018-01-24 23:32:06 +01:00
|
|
|
if (TIKDB_SIZE(tik_info) + 32 > STD_BUFFER_SIZE) break; // no error message
|
2018-01-10 02:08:29 +01:00
|
|
|
AddTitleKeyToInfo(tik_info, tik, !(filetype & FLAG_ENC), dec, false); // ignore result
|
2017-04-08 14:17:58 +02:00
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(tik_info_merge);
|
2017-04-08 14:17:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (dump) {
|
|
|
|
u32 dump_size = TIKDB_SIZE(tik_info);
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
if (dump_size > 16) {
|
|
|
|
if (fvx_rmkdir(OUTPUT_PATH) != FR_OK) // ensure the output dir exists
|
|
|
|
return 1;
|
|
|
|
f_unlink(path_out);
|
|
|
|
if ((dump_size <= 16) || (fvx_qwrite(path_out, tik_info, 0, dump_size, NULL) != FR_OK))
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
free(tik_info);
|
|
|
|
tik_info = NULL;
|
2017-04-08 14:17:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2017-04-12 00:27:02 +02:00
|
|
|
|
|
|
|
u32 BuildSeedInfo(const char* path, bool dump) {
|
2018-01-24 23:32:06 +01:00
|
|
|
static SeedInfo* seed_info = NULL;
|
2017-04-12 00:27:02 +02:00
|
|
|
const char* path_out = OUTPUT_PATH "/" SEEDDB_NAME;
|
|
|
|
const char* path_in = path;
|
|
|
|
u32 inputtype = 0; // 0 -> none, 1 -> seeddb.bin, 2 -> seed system save
|
|
|
|
|
2017-04-17 23:28:40 +02:00
|
|
|
// write permissions
|
|
|
|
if (!CheckWritePermissions(path_out))
|
|
|
|
return 1;
|
|
|
|
|
2017-04-12 00:27:02 +02:00
|
|
|
if (!path_in && !dump) { // no input path given - initialize
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!seed_info) seed_info = (SeedInfo*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!seed_info) return 1;
|
2017-04-12 00:27:02 +02:00
|
|
|
memset(seed_info, 0, 16);
|
2018-01-24 23:32:06 +01:00
|
|
|
|
2017-04-12 00:27:02 +02:00
|
|
|
if ((fvx_stat(path_out, NULL) == FR_OK) &&
|
|
|
|
(ShowPrompt(true, "%s\nOutput file already exists.\nUpdate this?", path_out))) {
|
|
|
|
path_in = path_out;
|
|
|
|
inputtype = 1;
|
|
|
|
} else return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
// seed info has to be allocated at this point
|
|
|
|
if (!seed_info) return 1;
|
|
|
|
|
2017-04-12 00:27:02 +02:00
|
|
|
char path_str[128];
|
|
|
|
if (path_in && (strnlen(path_in, 16) == 2)) { // when only a drive is given...
|
|
|
|
// grab the key Y from movable.sed
|
2019-05-08 00:18:34 +02:00
|
|
|
u8 movable_keyy[16] __attribute__((aligned(4)));
|
2017-04-12 00:27:02 +02:00
|
|
|
snprintf(path_str, 128, "%s/private/movable.sed", path_in);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (fvx_qread(path_str, movable_keyy, 0x110, 0x10, NULL) != FR_OK)
|
2017-04-12 00:27:02 +02:00
|
|
|
return 1;
|
|
|
|
// build the seed save path
|
|
|
|
u32 sha256sum[8];
|
|
|
|
sha_quick(sha256sum, movable_keyy, 0x10, SHA256_MODE);
|
|
|
|
snprintf(path_str, 128, "%s/data/%08lX%08lX%08lX%08lX/sysdata/0001000F/00000000",
|
|
|
|
path_in, sha256sum[0], sha256sum[1], sha256sum[2], sha256sum[3]);
|
|
|
|
path_in = path_str;
|
|
|
|
inputtype = 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputtype == 1) { // seeddb.bin input
|
2018-01-24 23:32:06 +01:00
|
|
|
SeedInfo* seed_info_merge = (SeedInfo*) malloc(STD_BUFFER_SIZE);
|
|
|
|
if (!seed_info_merge) return 1;
|
|
|
|
|
|
|
|
UINT br;
|
|
|
|
if ((fvx_qread(path_in, seed_info_merge, 0, STD_BUFFER_SIZE, &br) != FR_OK) ||
|
|
|
|
(SEEDDB_SIZE(seed_info_merge) != br)) {
|
|
|
|
free(seed_info_merge);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-04-12 00:27:02 +02:00
|
|
|
// merge and rebuild SeedInfo
|
|
|
|
u32 n_entries = seed_info_merge->n_entries;
|
|
|
|
SeedInfoEntry* seed = seed_info_merge->entries;
|
|
|
|
for (u32 i = 0; i < n_entries; i++, seed++) {
|
2018-01-24 23:32:06 +01:00
|
|
|
if (SEEDDB_SIZE(seed_info) + 32 > STD_BUFFER_SIZE) break; // no error message
|
2017-04-12 00:27:02 +02:00
|
|
|
AddSeedToDb(seed_info, seed); // ignore result
|
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(seed_info_merge);
|
2017-04-12 00:27:02 +02:00
|
|
|
} else if (inputtype == 2) { // seed system save input
|
2018-02-21 01:57:35 +01:00
|
|
|
u8* seedsave = (u8*) malloc(SEEDSAVE_AREA_SIZE);
|
2018-01-24 23:32:06 +01:00
|
|
|
if (!seedsave) return 1;
|
|
|
|
|
2018-02-21 01:57:35 +01:00
|
|
|
if (ReadDisaDiffIvfcLvl4(path_in, NULL, SEEDSAVE_AREA_OFFSET, SEEDSAVE_AREA_SIZE, seedsave) != SEEDSAVE_AREA_SIZE) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(seedsave);
|
2017-04-12 00:27:02 +02:00
|
|
|
return 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
}
|
|
|
|
|
2018-02-21 01:57:35 +01:00
|
|
|
SeedInfoEntry seed = { 0 };
|
|
|
|
for (u32 s = 0; s < SEEDSAVE_MAX_ENTRIES; s++) {
|
|
|
|
seed.titleId = getle64(seedsave + (s*8));
|
|
|
|
memcpy(seed.seed, seedsave + (SEEDSAVE_MAX_ENTRIES*8) + (s*16), 16);
|
|
|
|
if (((seed.titleId >> 32) != 0x00040000) ||
|
|
|
|
(!getle64(seed.seed) && !getle64(seed.seed + 8))) continue;
|
|
|
|
if (SEEDDB_SIZE(seed_info) + 32 > STD_BUFFER_SIZE) break; // no error message
|
|
|
|
AddSeedToDb(seed_info, &seed); // ignore result
|
2017-04-12 00:27:02 +02:00
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(seedsave);
|
2017-04-12 00:27:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (dump) {
|
|
|
|
u32 dump_size = SEEDDB_SIZE(seed_info);
|
2018-03-19 01:11:48 +01:00
|
|
|
u32 ret = 0;
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
if (dump_size > 16) {
|
|
|
|
if (fvx_rmkdir(OUTPUT_PATH) != FR_OK) // ensure the output dir exists
|
2018-03-19 01:11:48 +01:00
|
|
|
ret = 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
f_unlink(path_out);
|
|
|
|
if (fvx_qwrite(path_out, seed_info, 0, dump_size, NULL) != FR_OK)
|
2018-03-19 01:11:48 +01:00
|
|
|
ret = 1;
|
|
|
|
} else ret = 1;
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(seed_info);
|
|
|
|
seed_info = NULL;
|
2018-03-19 01:11:48 +01:00
|
|
|
return ret;
|
2017-04-12 00:27:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2017-05-10 21:59:22 +02:00
|
|
|
|
2017-05-15 20:00:26 +02:00
|
|
|
u32 LoadNcchFromGameFile(const char* path, NcchHeader* ncch) {
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 filetype = IdentifyFileType(path);
|
2017-05-10 21:59:22 +02:00
|
|
|
|
2017-05-15 20:00:26 +02:00
|
|
|
if (filetype & GAME_NCCH) {
|
2018-01-24 23:32:06 +01:00
|
|
|
if ((fvx_qread(path, ncch, 0, sizeof(NcchHeader), NULL) == FR_OK) &&
|
|
|
|
(ValidateNcchHeader(ncch) == 0)) return 0;
|
2017-05-10 21:59:22 +02:00
|
|
|
} else if (filetype & GAME_NCSD) {
|
2018-01-24 23:32:06 +01:00
|
|
|
if ((fvx_qread(path, ncch, NCSD_CNT0_OFFSET, sizeof(NcchHeader), NULL) == FR_OK) &&
|
|
|
|
(ValidateNcchHeader(ncch) == 0)) return 0;
|
2017-05-10 21:59:22 +02:00
|
|
|
} else if (filetype & GAME_CIA) {
|
2018-01-24 23:32:06 +01:00
|
|
|
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
|
2017-05-10 21:59:22 +02:00
|
|
|
CiaInfo info;
|
2017-05-15 20:00:26 +02:00
|
|
|
|
|
|
|
// load CIA stub from path
|
|
|
|
if ((LoadCiaStub(cia, path) != 0) ||
|
|
|
|
(GetCiaInfo(&info, &(cia->header)) != 0)) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
2017-05-15 20:00:26 +02:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// decrypt / load NCCH header from first CIA content
|
2018-01-24 23:32:06 +01:00
|
|
|
u32 ret = 1;
|
2017-05-15 20:00:26 +02:00
|
|
|
if (getbe16(cia->tmd.content_count)) {
|
|
|
|
TmdContentChunk* chunk = cia->content_list;
|
|
|
|
if ((getbe64(chunk->size) < sizeof(NcchHeader)) ||
|
2018-01-24 23:32:06 +01:00
|
|
|
(fvx_qread(path, ncch, info.offset_content, sizeof(NcchHeader), NULL) != FR_OK)) {
|
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
2017-05-15 20:00:26 +02:00
|
|
|
if (getbe16(chunk->type) & 0x1) { // decrypt first content header
|
|
|
|
u8 titlekey[16];
|
|
|
|
u8 ctr[16];
|
|
|
|
GetTmdCtr(ctr, chunk);
|
2020-01-23 00:29:11 +00:00
|
|
|
if (GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0) {
|
2018-01-24 23:32:06 +01:00
|
|
|
free(cia);
|
|
|
|
return 1;
|
|
|
|
}
|
2017-05-15 20:00:26 +02:00
|
|
|
DecryptCiaContentSequential((void*) ncch, sizeof(NcchHeader), ctr, titlekey);
|
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
if (ValidateNcchHeader(ncch) == 0) ret = 0;
|
2017-05-15 20:00:26 +02:00
|
|
|
}
|
2018-01-24 23:32:06 +01:00
|
|
|
|
|
|
|
free(cia);
|
|
|
|
return ret;
|
2017-05-15 20:00:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 GetGoodName(char* name, const char* path, bool quick) {
|
|
|
|
// name should be 128+1 byte
|
|
|
|
// name scheme (CTR+SMDH): <title_id> <title_name> (<product_code>) (<region>).<extension>
|
|
|
|
// name scheme (CTR): <title_id> (<product_code>).<extension>
|
|
|
|
// name scheme (NTR+ICON): <title_name> (<product_code>).<extension>
|
|
|
|
// name scheme (TWL+ICON): <title_id> <title_name> (<product_code>) (<DSi unitcode>) (<region>).<extension>
|
|
|
|
// name scheme (NTR): <name_short> (<product_code>).<extension>
|
|
|
|
// name scheme (TWL): <title_id> (<product_code>).<extension>
|
2017-10-16 02:02:24 +02:00
|
|
|
// name scheme (AGB): <name_short> (<product_code>).<extension>
|
2017-05-15 20:00:26 +02:00
|
|
|
|
|
|
|
const char* path_donor = path;
|
2017-10-16 02:02:24 +02:00
|
|
|
u64 type_donor = IdentifyFileType(path);
|
2017-05-15 20:00:26 +02:00
|
|
|
char* ext =
|
|
|
|
(type_donor & GAME_CIA) ? "cia" :
|
|
|
|
(type_donor & GAME_NCSD) ? "3ds" :
|
|
|
|
(type_donor & GAME_NCCH) ? ((type_donor & FLAG_CXI) ? "cxi" : "cfa") :
|
|
|
|
(type_donor & GAME_NDS) ? "nds" :
|
2017-10-16 02:02:24 +02:00
|
|
|
(type_donor & GAME_GBA) ? "gba" :
|
2017-05-15 20:00:26 +02:00
|
|
|
(type_donor & GAME_TMD) ? "tmd" : "";
|
|
|
|
if (!*ext) return 1;
|
|
|
|
|
|
|
|
char appid_str[1 + 8 + 1] = { 0 }; // handling for NCCH / NDS in "?:/title" paths
|
|
|
|
if ((type_donor & (GAME_NCCH|GAME_NDS)) && (strncmp(path + 1, ":/title/", 8) == 0)) {
|
|
|
|
char* name = strrchr(path, '/');
|
|
|
|
if (name && (strnlen(++name, 16) >= 8))
|
|
|
|
*appid_str = '.';
|
|
|
|
strncpy(appid_str + 1, name, 8);
|
2017-05-10 21:59:22 +02:00
|
|
|
}
|
|
|
|
|
2017-05-15 20:00:26 +02:00
|
|
|
char path_content[256];
|
|
|
|
if (type_donor & GAME_TMD) {
|
2017-05-15 22:59:12 +02:00
|
|
|
if (GetTmdContentPath(path_content, path) != 0) return 1;
|
2017-05-15 20:00:26 +02:00
|
|
|
path_donor = path_content;
|
|
|
|
type_donor = IdentifyFileType(path_donor);
|
|
|
|
}
|
|
|
|
|
2017-10-16 02:02:24 +02:00
|
|
|
if (type_donor & GAME_GBA) { // AGB
|
2018-01-24 23:32:06 +01:00
|
|
|
AgbHeader agb;
|
|
|
|
if (fvx_qread(path_donor, &agb, 0, sizeof(AgbHeader), NULL) != FR_OK) return 1;
|
|
|
|
snprintf(name, 128, "%.12s (AGB-%.4s).%s", agb.game_title, agb.game_code, ext);
|
2017-10-16 02:02:24 +02:00
|
|
|
} else if (type_donor & GAME_NDS) { // NTR or TWL
|
2018-01-24 23:32:06 +01:00
|
|
|
TwlHeader twl;
|
|
|
|
TwlIconData icon;
|
|
|
|
if (LoadTwlMetaData(path_donor, &twl, quick ? NULL : &icon) != 0) return 1;
|
2017-05-15 20:00:26 +02:00
|
|
|
if (quick) {
|
2018-01-24 23:32:06 +01:00
|
|
|
if (twl.unit_code & 0x02) { // TWL
|
|
|
|
snprintf(name, 128, "%016llX (TWL-%.4s).%s", twl.title_id, twl.game_code, ext);
|
2017-05-15 20:00:26 +02:00
|
|
|
} else { // NTR
|
2018-01-24 23:32:06 +01:00
|
|
|
snprintf(name, 128, "%.12s (NTR-%.4s).%s", twl.game_title, twl.game_code, ext);
|
2017-05-15 20:00:26 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
char title_name[0x80+1] = { 0 };
|
2018-01-24 23:32:06 +01:00
|
|
|
if (GetTwlTitle(title_name, &icon) != 0) return 1;
|
2017-05-15 20:00:26 +02:00
|
|
|
char* linebrk = strchr(title_name, '\n');
|
|
|
|
if (linebrk) *linebrk = '\0';
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
if (twl.unit_code & 0x02) { // TWL
|
2017-05-15 20:00:26 +02:00
|
|
|
char region[8] = { 0 };
|
2018-01-24 23:32:06 +01:00
|
|
|
if (twl.region_flags == TWL_REGION_FREE) snprintf(region, 8, "W");
|
2017-05-15 20:00:26 +02:00
|
|
|
snprintf(region, 8, "%s%s%s%s%s",
|
2018-01-24 23:32:06 +01:00
|
|
|
(twl.region_flags & REGION_MASK_JPN) ? "J" : "",
|
|
|
|
(twl.region_flags & REGION_MASK_USA) ? "U" : "",
|
|
|
|
(twl.region_flags & REGION_MASK_EUR) ? "E" : "",
|
|
|
|
(twl.region_flags & REGION_MASK_CHN) ? "C" : "",
|
|
|
|
(twl.region_flags & REGION_MASK_KOR) ? "K" : "");
|
2017-05-15 20:00:26 +02:00
|
|
|
if (strncmp(region, "JUECK", 8) == 0) snprintf(region, 8, "W");
|
|
|
|
if (!*region) snprintf(region, 8, "UNK");
|
|
|
|
|
2018-01-24 23:32:06 +01:00
|
|
|
char* unit_str = (twl.unit_code == TWL_UNITCODE_TWLNTR) ? "DSi Enhanced" : "DSi Exclusive";
|
2017-05-15 20:00:26 +02:00
|
|
|
snprintf(name, 128, "%016llX %s (TWL-%.4s) (%s) (%s).%s",
|
2018-01-24 23:32:06 +01:00
|
|
|
twl.title_id, title_name, twl.game_code, unit_str, region, ext);
|
2017-05-15 20:00:26 +02:00
|
|
|
} else { // NTR
|
2018-01-24 23:32:06 +01:00
|
|
|
snprintf(name, 128, "%s (NTR-%.4s).%s", title_name, twl.game_code, ext);
|
2017-05-15 20:00:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (type_donor & (GAME_CIA|GAME_NCSD|GAME_NCCH)) { // CTR (data from NCCH)
|
2018-01-24 23:32:06 +01:00
|
|
|
NcchHeader ncch;
|
|
|
|
Smdh smdh; // pretty big for the stack, but should be okay here
|
|
|
|
if (LoadNcchFromGameFile(path_donor, &ncch) != 0) return 1;
|
|
|
|
if (quick || (LoadSmdhFromGameFile(path_donor, &smdh) != 0)) {
|
|
|
|
snprintf(name, 128, "%016llX%s (%.16s).%s", ncch.programId, appid_str, ncch.productcode, ext);
|
2017-05-15 20:00:26 +02:00
|
|
|
} else {
|
|
|
|
char title_name[0x40+1] = { 0 };
|
2018-01-24 23:32:06 +01:00
|
|
|
if (GetSmdhDescShort(title_name, &smdh) != 0) return 1;
|
2017-05-15 20:00:26 +02:00
|
|
|
|
|
|
|
char region[8] = { 0 };
|
2018-01-24 23:32:06 +01:00
|
|
|
if (smdh.region_lockout == SMDH_REGION_FREE) snprintf(region, 8, "W");
|
|
|
|
else snprintf(region, 8, "%s%s%s%s%s%s",
|
|
|
|
(smdh.region_lockout & REGION_MASK_JPN) ? "J" : "",
|
|
|
|
(smdh.region_lockout & REGION_MASK_USA) ? "U" : "",
|
|
|
|
(smdh.region_lockout & REGION_MASK_EUR) ? "E" : "",
|
|
|
|
(smdh.region_lockout & REGION_MASK_CHN) ? "C" : "",
|
|
|
|
(smdh.region_lockout & REGION_MASK_KOR) ? "K" : "",
|
|
|
|
(smdh.region_lockout & REGION_MASK_TWN) ? "T" : "");
|
2017-05-15 20:00:26 +02:00
|
|
|
if (strncmp(region, "JUECKT", 8) == 0) snprintf(region, 8, "W");
|
|
|
|
if (!*region) snprintf(region, 8, "UNK");
|
|
|
|
|
|
|
|
snprintf(name, 128, "%016llX%s %s (%.16s) (%s).%s",
|
2018-01-24 23:32:06 +01:00
|
|
|
ncch.programId, appid_str, title_name, ncch.productcode, region, ext);
|
2017-05-15 20:00:26 +02:00
|
|
|
}
|
|
|
|
} else return 1;
|
|
|
|
|
|
|
|
// remove illegal chars from filename
|
|
|
|
for (char* c = name; *c; c++) {
|
2017-05-17 00:44:58 +02:00
|
|
|
if ((*c == ':') || (*c == '/') || (*c == '\\') || (*c == '"') ||
|
|
|
|
(*c == '*') || (*c == '?') || (*c == '\n') || (*c == '\r'))
|
|
|
|
*c = ' ';
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove double spaces from filename
|
|
|
|
char* s = name;
|
|
|
|
for (char* c = name; *s; c++, s++) {
|
|
|
|
while ((*c == ' ') && (*(c+1) == ' ')) c++;
|
|
|
|
*s = *c;
|
|
|
|
}
|
2017-05-15 20:00:26 +02:00
|
|
|
|
2017-05-10 21:59:22 +02:00
|
|
|
return 0;
|
|
|
|
}
|