From 974790944f99a00242b15640eb0b415c1cadc837 Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Thu, 22 Oct 2020 00:38:14 -0400 Subject: [PATCH] More changes. * Added NSP dumper PoC (SD card only atm, single-threaded). * Cert: replaced a wrong strcmp() with a proper strncmp(). * CNMT: added functions to update content info entries and generate/write Partition FS patches. * NCA: encrypt key area right after removing titlekey crypto. * NPDM/ProgramInfo: changed function names. * NPDM: check if the NCA has been modified before attempting to patch ACID data + calculate RSA-PSS signature *after* generating the PFS patch, not before. lol * PFS: restore name table size value before writing the header padding. * Tik: reworked the ticket lookup algorithm. Now uses information from ticket_list.bin to properly calculate the offset to the requested ticket in ticket.bin. * Title: changed title type strings used for filename generation. * Updated to-do list. --- .gitignore | 1 - code_templates/nsp_dumper.c | 1009 +++++++++++++++++++++++++++++++++++ source/cert.c | 2 +- source/cnmt.c | 84 +++ source/cnmt.h | 22 +- source/common.h | 8 +- source/mem.h | 5 - source/nca.c | 34 +- source/nca.h | 2 +- source/npdm.c | 23 +- source/npdm.h | 2 +- source/pfs.c | 8 +- source/program_info.h | 4 +- source/rsa.c | 4 +- source/tik.c | 320 +++++++---- source/title.c | 15 +- source/utils.c | 10 +- source/utils.h | 1 + todo.txt | 31 +- 19 files changed, 1398 insertions(+), 187 deletions(-) create mode 100644 code_templates/nsp_dumper.c diff --git a/.gitignore b/.gitignore index 1dd94d6..5388b81 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ build /*.pfs0 /*.lst /*.tar.bz2 -/code_templates/nsp_dumper.c /code_templates/tmp/* /source/main.c /*.log diff --git a/code_templates/nsp_dumper.c b/code_templates/nsp_dumper.c new file mode 100644 index 0000000..3c797b6 --- /dev/null +++ b/code_templates/nsp_dumper.c @@ -0,0 +1,1009 @@ +/* + * main.c + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "gamecard.h" +#include "title.h" +#include "cnmt.h" +#include "program_info.h" +#include "nacp.h" +#include "legal_info.h" +#include "cert.h" +#include "usb.h" + +#define BLOCK_SIZE 0x800000 +#define OUTPATH "sdmc:/nsp/" + +static const char *dump_type_strings[] = { + "dump base application", + "dump update", + "dump dlc" +}; + +static const u32 dump_type_strings_count = MAX_ELEMENTS(dump_type_strings); + +typedef struct { + char str[64]; + bool val; +} options_t; + +static options_t options[] = { + { "set download distribution type", false }, + { "remove console specific data", false }, + { "remove titlekey crypto (implies previous option)", false }, + { "change acid rsa key/sig", false } +}; + +static const u32 options_count = MAX_ELEMENTS(options); + +static void consolePrint(const char *text, ...) +{ + va_list v; + va_start(v, text); + vfprintf(stdout, text, v); + va_end(v); + consoleUpdate(NULL); +} + +static void nspDump(TitleInfo *title_info) +{ + if (!title_info || !title_info->content_count || !title_info->content_infos) return; + + consoleClear(); + + TitleApplicationMetadata *app_metadata = (title_info->app_metadata ? title_info->app_metadata : ((title_info->parent && title_info->parent->app_metadata) ? title_info->parent->app_metadata : NULL)); + + printf("%s info:\n\n", title_info->meta_key.type == NcmContentMetaType_Application ? "base application" : \ + (title_info->meta_key.type == NcmContentMetaType_Patch ? "update" : "dlc")); + printf("name: %s\n", app_metadata->lang_entry.name); + printf("publisher: %s\n", app_metadata->lang_entry.author); + printf("source storage: %s\n", title_info->storage_id == NcmStorageId_GameCard ? "gamecard" : (title_info->storage_id == NcmStorageId_BuiltInUser ? "emmc" : "sd card")); + printf("title id: %016lX\n", title_info->meta_key.id); + printf("version: %u (%u.%u.%u-%u.%u)\n", title_info->version.value, title_info->version.major, title_info->version.minor, title_info->version.micro, title_info->version.major_relstep, \ + title_info->version.minor_relstep); + printf("content count: %u\n", title_info->content_count); + printf("size: %s\n", title_info->size_str); + printf("______________________________\n\n"); + printf("dump options:\n\n"); + for(u32 i = 0; i < options_count; i++) printf("%s: %s\n", options[i].str, options[i].val ? "yes" : "no"); + printf("______________________________\n\n"); + + bool set_download_type = options[0].val, remove_console_data = options[1].val, remove_titlekey_crypto = options[2].val, change_acid_rsa = options[3].val, success = false; + + u8 *buf = NULL; + char *dump_name = NULL, *path = NULL; + FILE *fd = NULL; + + NcaContext *nca_ctx = NULL; + + NcaContext *meta_nca_ctx = NULL; + ContentMetaContext cnmt_ctx = {0}; + + ProgramInfoContext *program_info_ctx = NULL; + u32 program_idx = 0, program_count = titleGetContentCountByType(title_info, NcmContentType_Program); + + NacpContext *nacp_ctx = NULL; + u32 control_idx = 0, control_count = titleGetContentCountByType(title_info, NcmContentType_Control); + + LegalInfoContext *legal_info_ctx = NULL; + u32 legal_info_idx = 0, legal_info_count = titleGetContentCountByType(title_info, NcmContentType_LegalInformation); + + Ticket tik = {0}; + TikCommonBlock *tik_common_block = NULL; + + u8 *raw_cert_chain = NULL; + u64 raw_cert_chain_size = 0; + + PartitionFileSystemFileContext pfs_file_ctx = {0}; + pfsInitializeFileContext(&pfs_file_ctx); + + char entry_name[64] = {0}; + u64 nsp_header_size = 0, nsp_size = 0; + + Sha256Context sha256_ctx = {0}; + u8 sha256_hash[SHA256_HASH_SIZE] = {0}; + + /* Allocate memory for the dump process. */ + if (!(buf = usbAllocatePageAlignedBuffer(BLOCK_SIZE))) + { + consolePrint("buf alloc failed\n"); + goto end; + } + + /* Generate output path. */ + if (!(dump_name = titleGenerateFileName(title_info, TitleFileNameConvention_Full, TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly))) + { + consolePrint("title generate file name failed\n"); + goto end; + } + + if (!(path = utilsGeneratePath(OUTPATH, dump_name, ".nsp"))) + { + consolePrint("generate path failed\n"); + goto end; + } + + if (!(nca_ctx = calloc(title_info->content_count, sizeof(NcaContext)))) + { + consolePrint("nca ctx calloc failed\n"); + goto end; + } + + if (program_count && !(program_info_ctx = calloc(program_count, sizeof(ProgramInfoContext)))) + { + consolePrint("program info ctx calloc failed\n"); + goto end; + } + + if (control_count && !(nacp_ctx = calloc(control_count, sizeof(NacpContext)))) + { + consolePrint("nacp ctx calloc failed\n"); + goto end; + } + + if (legal_info_count && !(legal_info_ctx = calloc(legal_info_count, sizeof(LegalInfoContext)))) + { + consolePrint("legal info ctx calloc failed\n"); + goto end; + } + + // set meta nca as the last nca + meta_nca_ctx = &(nca_ctx[title_info->content_count - 1]); + + if (!ncaInitializeContext(meta_nca_ctx, title_info->storage_id, (title_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ + titleGetContentInfoByTypeAndIdOffset(title_info, NcmContentType_Meta, 0), &tik)) + { + consolePrint("Meta nca initialize ctx failed\n"); + goto end; + } + + consolePrint("Meta nca initialize ctx succeeded\n"); + + if (!cnmtInitializeContext(&cnmt_ctx, meta_nca_ctx)) + { + consolePrint("cnmt initialize ctx failed\n"); + goto end; + } + + consolePrint("cnmt initialize ctx succeeded (%s)\n", meta_nca_ctx->content_id_str); + + // initialize nca context + // initialize content type context + // generate nca patches (if needed) + // generate content type xml + for(u32 i = 0, j = 0; i < title_info->content_count; i++) + { + // skip meta nca since we already initialized it + NcmContentInfo *content_info = &(title_info->content_infos[i]); + if (content_info->content_type == NcmContentType_Meta) continue; + + NcaContext *cur_nca_ctx = &(nca_ctx[j]); + if (!ncaInitializeContext(cur_nca_ctx, title_info->storage_id, (title_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), content_info, &tik)) + { + consolePrint("%s #%u initialize nca ctx failed\n", titleGetNcmContentTypeName(content_info->content_type), content_info->id_offset); + goto end; + } + + consolePrint("%s #%u initialize nca ctx succeeded\n", titleGetNcmContentTypeName(content_info->content_type), content_info->id_offset); + + // don't go any further with this nca if we can't access its fs data because it's pointless + // to do: add preload warning + if (cur_nca_ctx->rights_id_available && !cur_nca_ctx->titlekey_retrieved) + { + j++; + continue; + } + + // set download distribution type + // has no effect if this nca uses NcaDistributionType_Download + if (set_download_type) ncaSetDownloadDistributionType(cur_nca_ctx); + + // remove titlekey crypto + // has no effect if this nca doesn't use titlekey crypto + if (remove_titlekey_crypto && !ncaRemoveTitlekeyCrypto(cur_nca_ctx)) + { + consolePrint("nca remove titlekey crypto failed\n"); + goto end; + } + + switch(content_info->content_type) + { + case NcmContentType_Program: + { + ProgramInfoContext *cur_program_info_ctx = &(program_info_ctx[program_idx]); + + if (!programInfoInitializeContext(cur_program_info_ctx, cur_nca_ctx)) + { + consolePrint("initialize program info ctx failed (%s)\n", cur_nca_ctx->content_id_str); + goto end; + } + + if (change_acid_rsa && !programInfoGenerateNcaPatch(cur_program_info_ctx)) + { + consolePrint("program info nca patch failed (%s)\n", cur_nca_ctx->content_id_str); + goto end; + } + + if (!programInfoGenerateAuthoringToolXml(cur_program_info_ctx)) + { + consolePrint("program info xml failed (%s)\n", cur_nca_ctx->content_id_str); + goto end; + } + + program_idx++; + + consolePrint("initialize program info ctx succeeded (%s)\n", cur_nca_ctx->content_id_str); + + break; + } + case NcmContentType_Control: + { + NacpContext *cur_nacp_ctx = &(nacp_ctx[control_idx]); + + if (!nacpInitializeContext(cur_nacp_ctx, cur_nca_ctx)) + { + consolePrint("initialize nacp ctx failed (%s)\n", cur_nca_ctx->content_id_str); + goto end; + } + + // add nacp mods here + + if (!nacpGenerateAuthoringToolXml(cur_nacp_ctx, title_info->version.value, cnmtGetRequiredTitleVersion(&cnmt_ctx))) + { + consolePrint("nacp xml failed (%s)\n", cur_nca_ctx->content_id_str); + goto end; + } + + control_idx++; + + consolePrint("initialize nacp ctx succeeded (%s)\n", cur_nca_ctx->content_id_str); + + break; + } + case NcmContentType_LegalInformation: + { + LegalInfoContext *cur_legal_info_ctx = &(legal_info_ctx[legal_info_idx]); + + if (!legalInfoInitializeContext(cur_legal_info_ctx, cur_nca_ctx)) + { + consolePrint("initialize legal info ctx failed (%s)\n", cur_nca_ctx->content_id_str); + goto end; + } + + legal_info_idx++; + + consolePrint("initialize legal info ctx succeeded (%s)\n", cur_nca_ctx->content_id_str); + + break; + } + default: + break; + } + + if (!ncaEncryptHeader(cur_nca_ctx)) + { + consolePrint("%s #%u encrypt nca header failed\n", titleGetNcmContentTypeName(content_info->content_type), content_info->id_offset); + goto end; + } + + j++; + } + + // generate cnmt xml right away even though we don't yet have all the data we need + // This is because we need its size to calculate the full nsp size + if (!cnmtGenerateAuthoringToolXml(&cnmt_ctx, nca_ctx, title_info->content_count)) + { + consolePrint("cnmt xml #1 failed\n"); + goto end; + } + + bool retrieve_tik_cert = (!remove_titlekey_crypto && tik.size > 0); + if (retrieve_tik_cert) + { + if (!(tik_common_block = tikGetCommonBlock(tik.data))) + { + consolePrint("tik common block failed"); + goto end; + } + + if (remove_console_data && tik_common_block->titlekey_type == TikTitleKeyType_Personalized) + { + if (!tikConvertPersonalizedTicketToCommonTicket(&tik, &raw_cert_chain, &raw_cert_chain_size)) + { + consolePrint("tik convert failed\n"); + goto end; + } + } else { + raw_cert_chain = (title_info->storage_id == NcmStorageId_GameCard ? certRetrieveRawCertificateChainFromGameCardByRightsId(&(tik_common_block->rights_id), &raw_cert_chain_size) : \ + certGenerateRawCertificateChainBySignatureIssuer(tik_common_block->issuer, &raw_cert_chain_size)); + if (!raw_cert_chain) + { + consolePrint("cert failed\n"); + goto end; + } + } + } + + // add nca info + for(u32 i = 0; i < title_info->content_count; i++) + { + NcaContext *cur_nca_ctx = &(nca_ctx[i]); + sprintf(entry_name, "%s.%s", cur_nca_ctx->content_id_str, cur_nca_ctx->content_type == NcmContentType_Meta ? "cnmt.nca" : "nca"); + + if (!pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, cur_nca_ctx->content_size, NULL)) + { + consolePrint("pfs add entry failed: %s\n", entry_name); + goto end; + } + } + + // add cnmt xml info + sprintf(entry_name, "%s.cnmt.xml", meta_nca_ctx->content_id_str); + if (!pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, cnmt_ctx.authoring_tool_xml_size, &(meta_nca_ctx->content_type_ctx_data_idx))) + { + consolePrint("pfs add entry failed: %s\n", entry_name); + goto end; + } + + // add content type ctx data info + for(u32 i = 0; i < (title_info->content_count - 1); i++) + { + bool ret = false; + NcaContext *cur_nca_ctx = &(nca_ctx[i]); + if (!cur_nca_ctx->content_type_ctx) continue; + + switch(cur_nca_ctx->content_type) + { + case NcmContentType_Program: + { + ProgramInfoContext *cur_program_info_ctx = (ProgramInfoContext*)cur_nca_ctx->content_type_ctx; + sprintf(entry_name, "%s.programinfo.xml", cur_nca_ctx->content_id_str); + ret = pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, cur_program_info_ctx->authoring_tool_xml_size, &(cur_nca_ctx->content_type_ctx_data_idx)); + break; + } + case NcmContentType_Control: + { + NacpContext *cur_nacp_ctx = (NacpContext*)cur_nca_ctx->content_type_ctx; + + for(u8 j = 0; j < cur_nacp_ctx->icon_count; j++) + { + NacpIconContext *icon_ctx = &(cur_nacp_ctx->icon_ctx[j]); + sprintf(entry_name, "%s.nx.%s.jpg", cur_nca_ctx->content_id_str, nacpGetLanguageString(icon_ctx->language)); + if (!pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, icon_ctx->icon_size, j == 0 ? &(cur_nca_ctx->content_type_ctx_data_idx) : NULL)) + { + consolePrint("pfs add entry failed: %s\n", entry_name); + goto end; + } + } + + sprintf(entry_name, "%s.nacp.xml", cur_nca_ctx->content_id_str); + ret = pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, cur_nacp_ctx->authoring_tool_xml_size, NULL); + break; + } + case NcmContentType_LegalInformation: + { + LegalInfoContext *cur_legal_info_ctx = (LegalInfoContext*)cur_nca_ctx->content_type_ctx; + sprintf(entry_name, "%s.legalinfo.xml", cur_nca_ctx->content_id_str); + ret = pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, cur_legal_info_ctx->authoring_tool_xml_size, &(cur_nca_ctx->content_type_ctx_data_idx)); + break; + } + default: + break; + } + + if (!ret) + { + consolePrint("pfs add entry failed: %s\n", entry_name); + goto end; + } + } + + // add ticket and cert info + if (retrieve_tik_cert) + { + sprintf(entry_name, "%s.tik", tik.rights_id_str); + if (!pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, tik.size, NULL)) + { + consolePrint("pfs add entry failed: %s\n", entry_name); + goto end; + } + + sprintf(entry_name, "%s.cert", tik.rights_id_str); + if (!pfsAddEntryInformationToFileContext(&pfs_file_ctx, entry_name, raw_cert_chain_size, NULL)) + { + consolePrint("pfs add entry failed: %s\n", entry_name); + goto end; + } + } + + // write buffer to memory buffer + if (!pfsWriteFileContextHeaderToMemoryBuffer(&pfs_file_ctx, buf, BLOCK_SIZE, &nsp_header_size)) + { + consolePrint("pfs write header to mem #1 failed\n"); + goto end; + } + + nsp_size = (nsp_header_size + pfs_file_ctx.fs_size); + consolePrint("nsp header size: 0x%lX | nsp size: 0x%lX\n", nsp_header_size, nsp_size); + + utilsCreateDirectoryTree(path, false); + + if (nsp_size > FAT32_FILESIZE_LIMIT && !utilsCreateConcatenationFile(path)) + { + consolePrint("create concatenation file failed\n"); + goto end; + } + + if (!(fd = fopen(path, "wb"))) + { + consolePrint("fopen failed\n"); + goto end; + } + + consolePrint("dump process started. please wait... yes, i'm too lazy to print progress here...\n"); + + time_t start = time(NULL); + + // write placeholder header + memset(buf, 0, nsp_header_size); + fwrite(buf, 1, nsp_header_size, fd); + + // write ncas + for(u32 i = 0; i < title_info->content_count; i++) + { + NcaContext *cur_nca_ctx = &(nca_ctx[i]); + u64 blksize = BLOCK_SIZE; + + memset(&sha256_ctx, 0, sizeof(Sha256Context)); + sha256ContextCreate(&sha256_ctx); + + if (cur_nca_ctx->content_type == NcmContentType_Meta && (!cnmtGenerateNcaPatch(&cnmt_ctx) || !ncaEncryptHeader(cur_nca_ctx))) + { + consolePrint("cnmt generate patch failed\n"); + goto end; + } + + bool dirty_header = ncaIsHeaderDirty(cur_nca_ctx); + + for(u64 offset = 0; offset < cur_nca_ctx->content_size; offset += blksize) + { + if ((cur_nca_ctx->content_size - offset) < blksize) blksize = (cur_nca_ctx->content_size - offset); + + // read nca chunk + if (!ncaReadContentFile(cur_nca_ctx, buf, blksize, offset)) + { + consolePrint("nca read failed at 0x%lX for \"%s\"\n", offset, cur_nca_ctx->content_id_str); + goto end; + } + + if (dirty_header) + { + // write re-encrypted headers + ncaWriteEncryptedHeaderDataToMemoryBuffer(cur_nca_ctx, buf, blksize, offset); + + if (cur_nca_ctx->content_type_ctx_patch) + { + // write content type context patch + switch(cur_nca_ctx->content_type) + { + case NcmContentType_Meta: + { + cnmtWriteNcaPatch(&cnmt_ctx, buf, blksize, offset); + break; + } + case NcmContentType_Program: + { + ProgramInfoContext *cur_program_info_ctx = (ProgramInfoContext*)cur_nca_ctx->content_type_ctx; + programInfoWriteNcaPatch(cur_program_info_ctx, buf, blksize, offset); + break; + } + case NcmContentType_Control: + // write nacp patches here + break; + default: + break; + } + } + } + + // update hash calculation + sha256ContextUpdate(&sha256_ctx, buf, blksize); + + // write nca chunk + fwrite(buf, 1, blksize, fd); + } + + // get hash + sha256ContextGetHash(&sha256_ctx, sha256_hash); + + // update content id and hash + ncaUpdateContentIdAndHash(cur_nca_ctx, sha256_hash); + + // update cnmt + if (!cnmtUpdateContentInfo(&cnmt_ctx, cur_nca_ctx)) + { + consolePrint("cnmt update content info failed\n"); + goto end; + } + + // update pfs entry name + if (!pfsUpdateEntryNameFromFileContext(&pfs_file_ctx, i, cur_nca_ctx->content_id_str)) + { + consolePrint("pfs update entry name failed for nca \"%s\"\n", cur_nca_ctx->content_id_str); + goto end; + } + } + + // regenerate cnmt xml + if (!cnmtGenerateAuthoringToolXml(&cnmt_ctx, nca_ctx, title_info->content_count)) + { + consolePrint("cnmt xml #2 failed\n"); + goto end; + } + + // write cnmt xml + fwrite(cnmt_ctx.authoring_tool_xml, 1, cnmt_ctx.authoring_tool_xml_size, fd); + + // update cnmt xml pfs entry name + if (!pfsUpdateEntryNameFromFileContext(&pfs_file_ctx, meta_nca_ctx->content_type_ctx_data_idx, meta_nca_ctx->content_id_str)) + { + consolePrint("pfs update entry name cnmt xml failed\n"); + goto end; + } + + // write content type ctx data + for(u32 i = 0; i < (title_info->content_count - 1); i++) + { + NcaContext *cur_nca_ctx = &(nca_ctx[i]); + if (!cur_nca_ctx->content_type_ctx) continue; + + char *authoring_tool_xml = NULL; + u64 authoring_tool_xml_size = 0; + u32 data_idx = cur_nca_ctx->content_type_ctx_data_idx; + + switch(cur_nca_ctx->content_type) + { + case NcmContentType_Program: + { + ProgramInfoContext *cur_program_info_ctx = (ProgramInfoContext*)cur_nca_ctx->content_type_ctx; + authoring_tool_xml = cur_program_info_ctx->authoring_tool_xml; + authoring_tool_xml_size = cur_program_info_ctx->authoring_tool_xml_size; + break; + } + case NcmContentType_Control: + { + NacpContext *cur_nacp_ctx = (NacpContext*)cur_nca_ctx->content_type_ctx; + authoring_tool_xml = cur_nacp_ctx->authoring_tool_xml; + authoring_tool_xml_size = cur_nacp_ctx->authoring_tool_xml_size; + + // loop through available icons + for(u8 j = 0; j < cur_nacp_ctx->icon_count; j++) + { + NacpIconContext *icon_ctx = &(cur_nacp_ctx->icon_ctx[j]); + + // write icon + fwrite(icon_ctx->icon_data, 1, icon_ctx->icon_size, fd); + + // update pfs entry name + if (!pfsUpdateEntryNameFromFileContext(&pfs_file_ctx, data_idx++, cur_nca_ctx->content_id_str)) + { + consolePrint("pfs update entry name failed for icon \"%s\" (%u)\n", cur_nca_ctx->content_id_str, icon_ctx->language); + goto end; + } + } + + break; + } + case NcmContentType_LegalInformation: + { + LegalInfoContext *cur_legal_info_ctx = (LegalInfoContext*)cur_nca_ctx->content_type_ctx; + authoring_tool_xml = cur_legal_info_ctx->authoring_tool_xml; + authoring_tool_xml_size = cur_legal_info_ctx->authoring_tool_xml_size; + break; + } + default: + break; + } + + // write xml + fwrite(authoring_tool_xml, 1, authoring_tool_xml_size, fd); + + // update pfs entry name + if (!pfsUpdateEntryNameFromFileContext(&pfs_file_ctx, data_idx, cur_nca_ctx->content_id_str)) + { + consolePrint("pfs update entry name failed for xml \"%s\"\n", cur_nca_ctx->content_id_str); + goto end; + } + } + + if (retrieve_tik_cert) + { + // write ticket + fwrite(tik.data, 1, tik.size, fd); + + // write cert + fwrite(raw_cert_chain, 1, raw_cert_chain_size, fd); + } + + // write new pfs0 header + rewind(fd); + + if (!pfsWriteFileContextHeaderToMemoryBuffer(&pfs_file_ctx, buf, BLOCK_SIZE, &nsp_header_size)) + { + consolePrint("pfs write header to mem #2 failed\n"); + goto end; + } + + fwrite(buf, 1, nsp_header_size, fd); + + start = (time(NULL) - start); + consolePrint("process successfully completed in %lu seconds!\n", start); + + success = true; + +end: + if (fd) + { + fclose(fd); + if (!success) utilsRemoveConcatenationFile(path); + utilsCommitFileSystemChangesByPath(path); + } + + pfsFreeFileContext(&pfs_file_ctx); + + if (raw_cert_chain) free(raw_cert_chain); + + if (legal_info_ctx) + { + for(u32 i = 0; i < legal_info_count; i++) legalInfoFreeContext(&(legal_info_ctx[i])); + free(legal_info_ctx); + } + + if (nacp_ctx) + { + for(u32 i = 0; i < control_count; i++) nacpFreeContext(&(nacp_ctx[i])); + free(nacp_ctx); + } + + if (program_info_ctx) + { + for(u32 i = 0; i < program_count; i++) programInfoFreeContext(&(program_info_ctx[i])); + free(program_info_ctx); + } + + cnmtFreeContext(&cnmt_ctx); + + if (nca_ctx) free(nca_ctx); + + if (path) free(path); + + if (dump_name) free(dump_name); + + if (buf) free(buf); +} + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + int ret = 0; + + consoleInit(NULL); + + consolePrint("initializing...\n"); + + if (!utilsInitializeResources()) + { + ret = -1; + goto out; + } + + u32 app_count = 0; + TitleApplicationMetadata **app_metadata = NULL; + TitleUserApplicationData user_app_data = {0}; + TitleInfo *title_info = NULL; + + u32 menu = 0, selected_idx = 0, scroll = 0, page_size = 30; + + u32 title_idx = 0, title_scroll = 0; + u32 type_idx = 0, type_scroll = 0; + u32 list_count = 0, list_idx = 0; + + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata || !app_count) + { + consolePrint("app metadata failed\n"); + goto out2; + } + + consolePrint("app metadata succeeded\n"); + + utilsSleep(1); + + while(true) + { + consoleClear(); + + if (menu == 2 && titleIsGameCardInfoUpdated()) + { + free(app_metadata); + + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata) + { + consolePrint("\napp metadata failed\n"); + goto out2; + } + + menu = selected_idx = scroll = 0; + + title_idx = title_scroll = 0; + type_idx = type_scroll = 0; + list_count = list_idx = 0; + + continue; + } + + printf("press b to %s.\n", menu == 0 ? "exit" : "go back"); + printf("______________________________\n\n"); + + if (menu == 0) + { + printf("title: %u / %u\n", selected_idx + 1, app_count); + printf("selected title: %016lX - %s\n", app_metadata[selected_idx]->title_id, app_metadata[selected_idx]->lang_entry.name); + } else { + printf("title info:\n\n"); + printf("name: %s\n", app_metadata[title_idx]->lang_entry.name); + printf("publisher: %s\n", app_metadata[title_idx]->lang_entry.author); + printf("title id: %016lX\n", app_metadata[title_idx]->title_id); + + if (menu == 2) + { + printf("______________________________\n\n"); + + if (title_info->previous || title_info->next) + { + printf("press zl/l and/or zr/r to change the selected title\n"); + printf("title: %u / %u\n", list_idx, list_count); + printf("______________________________\n\n"); + } + + printf("selected %s info:\n\n", title_info->meta_key.type == NcmContentMetaType_Application ? "base application" : \ + (title_info->meta_key.type == NcmContentMetaType_Patch ? "update" : "dlc")); + printf("source storage: %s\n", title_info->storage_id == NcmStorageId_GameCard ? "gamecard" : (title_info->storage_id == NcmStorageId_BuiltInUser ? "emmc" : "sd card")); + if (title_info->meta_key.type != NcmContentMetaType_Application) printf("title id: %016lX\n", title_info->meta_key.id); + printf("version: %u (%u.%u.%u-%u.%u)\n", title_info->version.value, title_info->version.major, title_info->version.minor, title_info->version.micro, title_info->version.major_relstep, \ + title_info->version.minor_relstep); + printf("content count: %u\n", title_info->content_count); + printf("size: %s\n", title_info->size_str); + } + } + + printf("______________________________\n\n"); + + u32 max_val = (menu == 0 ? app_count : (menu == 1 ? dump_type_strings_count : (1 + options_count))); + for(u32 i = scroll; i < max_val; i++) + { + if (i >= (scroll + page_size)) break; + + printf("%s", i == selected_idx ? " -> " : " "); + + if (menu == 0) + { + printf("%016lX - %s\n", app_metadata[i]->title_id, app_metadata[i]->lang_entry.name); + } else + if (menu == 1) + { + printf("%s\n", dump_type_strings[i]); + } else + if (menu == 2) + { + if (i == 0) + { + printf("start nsp dump\n"); + } else { + printf("%s: < %s >\n", options[i - 1].str, options[i - 1].val ? "yes" : "no"); + } + } + } + + printf("\n"); + + consoleUpdate(NULL); + + bool gc_update = false; + u64 btn_down = 0, btn_held = 0; + + while(true) + { + hidScanInput(); + btn_down = utilsHidKeysAllDown(); + btn_held = utilsHidKeysAllHeld(); + if (btn_down || btn_held) break; + + if (titleIsGameCardInfoUpdated()) + { + free(app_metadata); + + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata) + { + consolePrint("\napp metadata failed\n"); + goto out2; + } + + menu = selected_idx = scroll = 0; + + title_idx = title_scroll = 0; + type_idx = type_scroll = 0; + list_count = list_idx = 0; + + gc_update = true; + + break; + } + } + + if (gc_update) continue; + + if (btn_down & KEY_A) + { + bool error = false; + + if (menu == 0) + { + title_idx = selected_idx; + title_scroll = scroll; + } else + if (menu == 1) + { + type_idx = selected_idx; + type_scroll = scroll; + } + + menu++; + + if (menu == 3 && selected_idx != 0) + { + menu--; + continue; + } + + if (menu == 1) + { + if (!titleGetUserApplicationData(app_metadata[title_idx]->title_id, &user_app_data)) + { + consolePrint("\nget user application data failed!\n"); + error = true; + } + } else + if (menu == 2) + { + if ((type_idx == 0 && !user_app_data.app_info) || (type_idx == 1 && !user_app_data.patch_info) || (type_idx == 2 && !user_app_data.aoc_info)) + { + consolePrint("\nthe selected title doesn't have available %s data\n", type_idx == 0 ? "base application" : (type_idx == 1 ? "update" : "dlc")); + error = true; + } else { + title_info = (type_idx == 0 ? user_app_data.app_info : (type_idx == 1 ? user_app_data.patch_info : user_app_data.aoc_info)); + list_count = titleGetCountFromInfoBlock(title_info); + list_idx = 1; + } + } else + if (menu == 3) + { + consoleClear(); + utilsChangeHomeButtonBlockStatus(true); + nspDump(title_info); + utilsChangeHomeButtonBlockStatus(false); + } + + if (error || menu >= 3) + { + consolePrint("press any button to continue\n"); + utilsWaitForButtonPress(KEY_NONE); + menu--; + } else { + selected_idx = scroll = 0; + } + } else + if ((btn_down & KEY_DDOWN) || (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN))) + { + selected_idx++; + + if (selected_idx >= max_val) + { + if (btn_down & KEY_DDOWN) + { + selected_idx = scroll = 0; + } else { + selected_idx = (max_val - 1); + } + } else + if (selected_idx >= (scroll + (page_size / 2)) && max_val > (scroll + page_size)) + { + scroll++; + } + } else + if ((btn_down & KEY_DUP) || (btn_held & (KEY_LSTICK_UP | KEY_RSTICK_UP))) + { + selected_idx--; + + if (selected_idx == UINT32_MAX) + { + if (btn_down & KEY_DUP) + { + selected_idx = (max_val - 1); + scroll = (max_val >= page_size ? (max_val - page_size) : 0); + } else { + selected_idx = 0; + } + } else + if (selected_idx < (scroll + (page_size / 2)) && scroll > 0) + { + scroll--; + } + } else + if (btn_down & KEY_B) + { + menu--; + + if (menu == UINT32_MAX) + { + break; + } else { + selected_idx = (menu == 0 ? title_idx : type_idx); + scroll = (menu == 0 ? title_scroll : type_scroll); + } + } else + if (((btn_down & KEY_DLEFT) || (btn_down & KEY_DRIGHT)) && menu == 2 && selected_idx != 0) + { + options[selected_idx - 1].val ^= 1; + } else + if (((btn_down & KEY_L) || (btn_down & KEY_ZL)) && menu == 2 && title_info->previous) + { + title_info = title_info->previous; + list_idx--; + } else + if (((btn_down & KEY_R) || (btn_down & KEY_ZR)) && menu == 2 && title_info->next) + { + title_info = title_info->next; + list_idx++; + } + + if (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN | KEY_LSTICK_UP | KEY_RSTICK_UP)) svcSleepThread(50000000); // 50 ms + } + +out2: + if (menu != UINT32_MAX) + { + consolePrint("press any button to exit\n"); + utilsWaitForButtonPress(KEY_NONE); + } + + if (app_metadata) free(app_metadata); + +out: + utilsCloseResources(); + + consoleExit(NULL); + + return ret; +} diff --git a/source/cert.c b/source/cert.c index b44f2fc..5a5d2cd 100644 --- a/source/cert.c +++ b/source/cert.c @@ -79,7 +79,7 @@ bool certRetrieveCertificateChainBySignatureIssuer(CertificateChain *dst, const bool ret = false; size_t issuer_len = 0; - if (!dst || !issuer || !(issuer_len = strlen(issuer)) || issuer_len <= 5 || strcmp(issuer, "Root-") != 0) + if (!dst || !issuer || (issuer_len = strlen(issuer)) <= 5 || strncmp(issuer, "Root-", 5) != 0) { LOGFILE("Invalid parameters!"); goto end; diff --git a/source/cnmt.c b/source/cnmt.c index 19afe92..ed91017 100644 --- a/source/cnmt.c +++ b/source/cnmt.c @@ -247,6 +247,90 @@ end: return success; } +bool cnmtUpdateContentInfo(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx) +{ + if (!cnmtIsValidContext(cnmt_ctx) || !nca_ctx || !*(nca_ctx->content_id_str) || !*(nca_ctx->hash_str) || nca_ctx->content_type > NcmContentType_DeltaFragment || !nca_ctx->content_size) + { + LOGFILE("Invalid parameters!"); + return false; + } + + /* Return right away if we're dealing with a Meta NCA. */ + if (nca_ctx->content_type == NcmContentType_Meta) return true; + + bool success = false; + + for(u16 i = 0; i < cnmt_ctx->packaged_header->content_count; i++) + { + NcmPackagedContentInfo *packaged_content_info = &(cnmt_ctx->packaged_content_info[i]); + NcmContentInfo *content_info = &(packaged_content_info->info); + u64 content_size = 0; + + titleConvertNcmContentSizeToU64(content_info->size, &content_size); + + if (content_size == nca_ctx->content_size && content_info->content_type == nca_ctx->content_type && content_info->id_offset == nca_ctx->id_offset) + { + /* Jackpot. Copy content ID and hash to our raw CNMT. */ + memcpy(packaged_content_info->hash, nca_ctx->hash, sizeof(nca_ctx->hash)); + memcpy(&(content_info->content_id), &(nca_ctx->content_id), sizeof(NcmContentId)); + LOGFILE("Updated CNMT content record #%u (size 0x%lX, type 0x%02X, ID offset 0x%02X).", i, content_size, content_info->content_type, content_info->id_offset); + success = true; + break; + } + } + + if (!success) LOGFILE("Unable to find CNMT content info entry for \"%s\" NCA! (size 0x%lX, type 0x%02X, ID offset 0x%02X).", nca_ctx->content_id_str, nca_ctx->content_size, nca_ctx->content_type, \ + nca_ctx->id_offset); + + return success; +} + +bool cnmtGenerateNcaPatch(ContentMetaContext *cnmt_ctx) +{ + if (!cnmtIsValidContext(cnmt_ctx)) + { + LOGFILE("Invalid parameters!"); + return false; + } + + /* Check if we really need to generate this patch. */ + u8 cnmt_hash[SHA256_HASH_SIZE] = {0}; + sha256CalculateHash(cnmt_hash, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size); + if (!memcmp(cnmt_hash, cnmt_ctx->raw_data_hash, sizeof(cnmt_hash))) + { + LOGFILE("Skipping CNMT patching - no content records have been changed."); + return true; + } + + /* Generate Partition FS entry patch. */ + if (!pfsGenerateEntryPatch(&(cnmt_ctx->pfs_ctx), cnmt_ctx->pfs_entry, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size, 0, &(cnmt_ctx->nca_patch))) + { + LOGFILE("Failed to generate Partition FS entry patch!"); + return false; + } + + /* Update NCA content type context patch status. */ + cnmt_ctx->nca_ctx->content_type_ctx_patch = true; + + return true; +} + +void cnmtWriteNcaPatch(ContentMetaContext *cnmt_ctx, void *buf, u64 buf_size, u64 buf_offset) +{ + /* Using cnmtIsValidContext() here would probably take up precious CPU cycles. */ + if (!cnmt_ctx || !cnmt_ctx->nca_ctx || cnmt_ctx->nca_ctx->content_type != NcmContentType_Meta || !cnmt_ctx->nca_ctx->content_type_ctx_patch || cnmt_ctx->nca_patch.written) return; + + /* Attempt to write Partition FS entry. */ + pfsWriteEntryPatchToMemoryBuffer(&(cnmt_ctx->pfs_ctx), &(cnmt_ctx->nca_patch), buf, buf_size, buf_offset); + + /* Check if we need to update the NCA content type context patch status. */ + if (cnmt_ctx->nca_patch.written) + { + cnmt_ctx->nca_ctx->content_type_ctx_patch = false; + LOGFILE("CNMT Partition FS file entry patch successfully written to NCA \"%s\"!", cnmt_ctx->nca_ctx->content_id_str); + } +} + bool cnmtGenerateAuthoringToolXml(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx, u32 nca_ctx_count) { if (!cnmtIsValidContext(cnmt_ctx) || !nca_ctx || nca_ctx_count != ((u32)cnmt_ctx->packaged_header->content_count + 1)) diff --git a/source/cnmt.h b/source/cnmt.h index 524ca3a..0fd4ed7 100644 --- a/source/cnmt.h +++ b/source/cnmt.h @@ -241,6 +241,15 @@ typedef struct { /// Initializes a ContentMetaContext using a previously initialized NcaContext (which must belong to a Meta NCA). bool cnmtInitializeContext(ContentMetaContext *out, NcaContext *nca_ctx); +/// Updates NcmPackagedContentInfo data for the content entry with size, type and ID offset values that match the ones from the input NcaContext. +bool cnmtUpdateContentInfo(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx); + +/// Generates a Partition FS entry patch for the NcaContext pointed to by the input ContentMetaContext, using its raw CNMT data. +bool cnmtGenerateNcaPatch(ContentMetaContext *cnmt_ctx); + +/// Writes data from the Partition FS patch in the input ContentMetaContext to the provided buffer. +void cnmtWriteNcaPatch(ContentMetaContext *cnmt_ctx, void *buf, u64 buf_size, u64 buf_offset); + /// Generates an AuthoringTool-like XML using information from a previously initialized ContentMetaContext, as well as a pointer to 'nca_ctx_count' NcaContext with content information. /// If the function succeeds, XML data and size will get saved to the 'authoring_tool_xml' and 'authoring_tool_xml_size' members from the ContentMetaContext. bool cnmtGenerateAuthoringToolXml(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx, u32 nca_ctx_count); @@ -266,19 +275,6 @@ NX_INLINE bool cnmtIsValidContext(ContentMetaContext *cnmt_ctx) ((cnmt_ctx->extended_data_size && cnmt_ctx->extended_data) || (!cnmt_ctx->extended_data_size && !cnmt_ctx->extended_data)) && cnmt_ctx->digest); } -NX_INLINE bool cnmtIsNcaPatchRequired(ContentMetaContext *cnmt_ctx) -{ - if (!cnmtIsValidContext(cnmt_ctx)) return false; - u8 tmp_hash[SHA256_HASH_SIZE] = {0}; - sha256CalculateHash(tmp_hash, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size); - return (memcmp(tmp_hash, cnmt_ctx->raw_data_hash, SHA256_HASH_SIZE) != 0); -} - -NX_INLINE bool cnmtGenerateNcaPatch(ContentMetaContext *cnmt_ctx) -{ - return (cnmtIsValidContext(cnmt_ctx) && pfsGenerateEntryPatch(&(cnmt_ctx->pfs_ctx), cnmt_ctx->pfs_entry, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size, 0, &(cnmt_ctx->nca_patch))); -} - NX_INLINE u64 cnmtGetRequiredTitleId(ContentMetaContext *cnmt_ctx) { return ((cnmtIsValidContext(cnmt_ctx) && (cnmt_ctx->packaged_header->content_meta_type == NcmContentMetaType_Application || \ diff --git a/source/common.h b/source/common.h index 1003f2e..beb2da2 100644 --- a/source/common.h +++ b/source/common.h @@ -23,7 +23,13 @@ #ifndef __COMMON_H__ #define __COMMON_H__ -#define SYSTEM_UPDATE_TID (u64)0x0100000000000816 +#define FS_SYSMODULE_TID (u64)0x0100000000000000 +#define BOOT_SYSMODULE_TID (u64)0x0100000000000005 +#define SPL_SYSMODULE_TID (u64)0x0100000000000028 +#define ES_SYSMODULE_TID (u64)0x0100000000000033 +#define SYSTEM_UPDATE_TID (u64)0x0100000000000816 + +#define FAT32_FILESIZE_LIMIT (u64)0xFFFFFFFF /* 4 GiB - 1 (4294967295 bytes). */ /// Used to store version numbers expressed in dot notation: "{major}.{minor}.{micro}-{major_relstep}.{minor_relstep}". /// Referenced by multiple header files. diff --git a/source/mem.h b/source/mem.h index eda7e71..6016174 100644 --- a/source/mem.h +++ b/source/mem.h @@ -24,11 +24,6 @@ #ifndef __MEM_H__ #define __MEM_H__ -#define FS_SYSMODULE_TID (u64)0x0100000000000000 -#define BOOT_SYSMODULE_TID (u64)0x0100000000000005 -#define SPL_SYSMODULE_TID (u64)0x0100000000000028 -#define ES_SYSMODULE_TID (u64)0x0100000000000033 - typedef enum { MemoryProgramSegmentType_Text = BIT(0), MemoryProgramSegmentType_Rodata = BIT(1), diff --git a/source/nca.c b/source/nca.c index 0869629..2850b23 100644 --- a/source/nca.c +++ b/source/nca.c @@ -334,15 +334,20 @@ void ncaSetDownloadDistributionType(NcaContext *ctx) { if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !*(ctx->content_id_str) || ctx->content_type > NcmContentType_DeltaFragment || \ ctx->header.distribution_type == NcaDistributionType_Download) return; - LOGFILE("Setting download distribution type to %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str); ctx->header.distribution_type = NcaDistributionType_Download; + LOGFILE("Set download distribution type to %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str); } -void ncaRemoveTitlekeyCrypto(NcaContext *ctx) +bool ncaRemoveTitlekeyCrypto(NcaContext *ctx) { - if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !*(ctx->content_id_str) || ctx->content_type > NcmContentType_DeltaFragment || !ctx->rights_id_available || !ctx->titlekey_retrieved) return; + if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !*(ctx->content_id_str) || ctx->content_type > NcmContentType_DeltaFragment) + { + LOGFILE("Invalid parameters!"); + return false; + } - LOGFILE("Removing titlekey crypto from %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str); + /* Don't proceed if we're not dealing with a NCA with a populated rights ID field, or if we couldn't retrieve the titlekey for it. */ + if (!ctx->rights_id_available || !ctx->titlekey_retrieved) return true; /* Copy decrypted titlekey to the decrypted NCA key area. */ /* This will be reencrypted at a later stage. */ @@ -356,11 +361,22 @@ void ncaRemoveTitlekeyCrypto(NcaContext *ctx) memcpy(key_ptr, ctx->titlekey, AES_128_KEY_SIZE); } + /* Encrypt NCA key area. */ + if (!ncaEncryptKeyArea(ctx)) + { + LOGFILE("Error encrypting %s NCA \"%s\" key area!", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str); + return false; + } + /* Wipe Rights ID. */ memset(&(ctx->header.rights_id), 0, sizeof(FsRightsId)); /* Update context flags. */ ctx->rights_id_available = false; + + LOGFILE("Removed titlekey crypto from %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str); + + return true; } bool ncaEncryptHeader(NcaContext *ctx) @@ -378,13 +394,6 @@ bool ncaEncryptHeader(NcaContext *ctx) const u8 *header_key = keysGetNcaHeaderKey(); Aes128XtsContext hdr_aes_ctx = {0}, nca0_fs_header_ctx = {0}; - /* Encrypt NCA key area. */ - if (!ctx->rights_id_available && !ncaEncryptKeyArea(ctx)) - { - LOGFILE("Error encrypting NCA \"%s\" key area!", ctx->content_id_str); - return false; - } - /* Prepare AES-128-XTS contexts. */ aes128XtsContextCreate(&hdr_aes_ctx, header_key, header_key + AES_128_KEY_SIZE, true); if (ctx->format_version == NcaVersion_Nca0) aes128XtsContextCreate(&nca0_fs_header_ctx, ctx->decrypted_key_area.aes_xts_1, ctx->decrypted_key_area.aes_xts_2, true); @@ -1129,7 +1138,8 @@ static bool ncaWritePatchToMemoryBuffer(NcaContext *ctx, const void *patch, u64 memcpy((u8*)buf + buf_block_offset, (const u8*)patch + patch_block_offset, buf_block_size); - LOGFILE("Overwrote 0x%lX bytes block at offset 0x%lX from raw NCA \"%s\" buffer (size 0x%lX, NCA offset 0x%lX).", buf_block_size, buf_block_offset, ctx->content_id_str, buf_size, buf_offset); + LOGFILE("Overwrote 0x%lX bytes block at offset 0x%lX from raw %s NCA \"%s\" buffer (size 0x%lX, NCA offset 0x%lX).", buf_block_size, buf_block_offset, titleGetNcmContentTypeName(ctx->content_type), \ + ctx->content_id_str, buf_size, buf_offset); return ((patch_block_offset + buf_block_size) == patch_size); } diff --git a/source/nca.h b/source/nca.h index 4194a62..7de69ef 100644 --- a/source/nca.h +++ b/source/nca.h @@ -410,7 +410,7 @@ void ncaWriteHierarchicalIntegrityPatchToMemoryBuffer(NcaContext *ctx, NcaHierar void ncaSetDownloadDistributionType(NcaContext *ctx); /// Removes titlekey crypto dependency from a NCA context by wiping the Rights ID from the underlying NCA header and copying the decrypted titlekey to the NCA key area. -void ncaRemoveTitlekeyCrypto(NcaContext *ctx); +bool ncaRemoveTitlekeyCrypto(NcaContext *ctx); /// Encrypts NCA header and NCA FS headers. /// The 'encrypted_header' member from the NCA context and its underlying NCA FS section contexts is updated by this function. diff --git a/source/npdm.c b/source/npdm.c index 10418eb..a3ff7bc 100644 --- a/source/npdm.c +++ b/source/npdm.c @@ -258,7 +258,7 @@ end: return success; } -bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx) +bool npdmGenerateNcaPatch(NpdmContext *npdm_ctx) { NcaContext *nca_ctx = NULL; @@ -268,16 +268,16 @@ bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx) return false; } + /* Check if we really need to generate this patch. */ + if (!ncaIsHeaderDirty(nca_ctx)) + { + LOGFILE("Skipping NPDM patching - NCA header hasn't been modified."); + return true; + } + /* Update NPDM ACID public key. */ memcpy(npdm_ctx->acid_header->public_key, rsa2048GetCustomPublicKey(), RSA2048_PUBKEY_SIZE); - /* Update NCA ACID signature. */ - if (!rsa2048GenerateSha256BasedPssSignature(nca_ctx->header.acid_signature, &(nca_ctx->header.magic), NCA_ACID_SIGNATURE_AREA_SIZE)) - { - LOGFILE("Failed to generate RSA-2048-PSS NCA ACID signature!"); - return false; - } - /* Generate Partition FS entry patch. */ if (!pfsGenerateEntryPatch(npdm_ctx->pfs_ctx, npdm_ctx->pfs_entry, npdm_ctx->raw_data, npdm_ctx->raw_data_size, 0, &(npdm_ctx->nca_patch))) { @@ -285,6 +285,13 @@ bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx) return false; } + /* Update NCA ACID signature. */ + if (!rsa2048GenerateSha256BasedPssSignature(nca_ctx->header.acid_signature, &(nca_ctx->header.magic), NCA_ACID_SIGNATURE_AREA_SIZE)) + { + LOGFILE("Failed to generate RSA-2048-PSS NCA ACID signature!"); + return false; + } + /* Update NCA content type context patch status. */ nca_ctx->content_type_ctx_patch = true; diff --git a/source/npdm.h b/source/npdm.h index db544f2..bbbb972 100644 --- a/source/npdm.h +++ b/source/npdm.h @@ -555,7 +555,7 @@ typedef struct { bool npdmInitializeContext(NpdmContext *out, PartitionFileSystemContext *pfs_ctx); /// Changes the ACID public key from the NPDM in the input NpdmContext, updates the ACID signature from the NCA header in the underlying NCA context and generates a Partition FS entry patch. -bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx); +bool npdmGenerateNcaPatch(NpdmContext *npdm_ctx); /// Writes data from the Partition FS patch in the input NpdmContext to the provided buffer. void npdmWriteNcaPatch(NpdmContext *npdm_ctx, void *buf, u64 buf_size, u64 buf_offset); diff --git a/source/pfs.c b/source/pfs.c index a762640..4285f79 100644 --- a/source/pfs.c +++ b/source/pfs.c @@ -336,13 +336,12 @@ bool pfsWriteFileContextHeaderToMemoryBuffer(PartitionFileSystemFileContext *ctx return false; } - /* Update name table size in Partition FS header to make it reflect the padding. */ - header->name_table_size += padding_size; - /* Write full header. */ + header->name_table_size += padding_size; block_size = sizeof(PartitionFileSystemHeader); memcpy(buf_u8 + block_offset, header, block_size); block_offset += block_size; + header->name_table_size -= padding_size; block_size = (header->entry_count * sizeof(PartitionFileSystemEntry)); memcpy(buf_u8 + block_offset, ctx->entries, block_size); @@ -357,8 +356,5 @@ bool pfsWriteFileContextHeaderToMemoryBuffer(PartitionFileSystemFileContext *ctx /* Update output header size. */ *out_header_size = full_header_size; - /* Restore name table size in Partition FS header. */ - header->name_table_size -= padding_size; - return true; } diff --git a/source/program_info.h b/source/program_info.h index 210cfba..2cdbd63 100644 --- a/source/program_info.h +++ b/source/program_info.h @@ -70,9 +70,9 @@ NX_INLINE bool programInfoIsValidContext(ProgramInfoContext *program_info_ctx) program_info_ctx->nso_count && program_info_ctx->nso_ctx); } -NX_INLINE bool programInfoChangeAcidPublicKeyAndNcaSignature(ProgramInfoContext *program_info_ctx) +NX_INLINE bool programInfoGenerateNcaPatch(ProgramInfoContext *program_info_ctx) { - return (programInfoIsValidContext(program_info_ctx) && npdmChangeAcidPublicKeyAndNcaSignature(&(program_info_ctx->npdm_ctx))); + return (programInfoIsValidContext(program_info_ctx) && npdmGenerateNcaPatch(&(program_info_ctx->npdm_ctx))); } NX_INLINE void programInfoWriteNcaPatch(ProgramInfoContext *program_info_ctx, void *buf, u64 buf_size, u64 buf_offset) diff --git a/source/rsa.c b/source/rsa.c index 192c88c..9ce4c5e 100644 --- a/source/rsa.c +++ b/source/rsa.c @@ -93,8 +93,8 @@ bool rsa2048GenerateSha256BasedPssSignature(void *dst, const void *src, size_t s return false; } - u8 hash[SHA256_HASH_SIZE]; - u8 buf[MBEDTLS_MPI_MAX_SIZE]; + u8 hash[SHA256_HASH_SIZE] = {0}; + u8 buf[MBEDTLS_MPI_MAX_SIZE] = {0}; const char *pers = "rsa_sign_pss"; size_t olen = 0; diff --git a/source/tik.c b/source/tik.c index 8258533..ddf6fa7 100644 --- a/source/tik.c +++ b/source/tik.c @@ -32,7 +32,9 @@ #define TIK_COMMON_SAVEFILE_PATH BIS_SYSTEM_PARTITION_MOUNT_NAME "/save/80000000000000e1" #define TIK_PERSONALIZED_SAVEFILE_PATH BIS_SYSTEM_PARTITION_MOUNT_NAME "/save/80000000000000e2" -#define TIK_SAVEFILE_STORAGE_PATH "/ticket.bin" + +#define TIK_LIST_STORAGE_PATH "/ticket_list.bin" +#define TIK_DB_STORAGE_PATH "/ticket.bin" #define ETICKET_DEVKEY_PUBLIC_EXPONENT 0x10001 @@ -40,6 +42,31 @@ /* Type definitions. */ +/// Used to parse ticket_list.bin entries. +typedef struct { + FsRightsId rights_id; + u64 ticket_id; + u32 account_id; + u8 reserved[0x04]; +} TikListEntry; + +/// 9.x+ CTR key entry in ES .data segment. Used to store CTR key/IV data for encrypted volatile tickets in ticket.bin and/or encrypted entries in ticket_list.bin. +/// This is always stored in pairs. The first entry holds the key/IV for the encrypted volatile ticket, while the second entry holds the key/IV for the encrypted entry in ticket_list.bin. +/// First index in this list is always 0, and it's aligned to ES_CTRKEY_ENTRY_ALIGNMENT. +typedef struct { + u32 idx; ///< Entry index. + u8 key[AES_BLOCK_SIZE]; ///< AES-128-CTR key. + u8 ctr[AES_BLOCK_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out. +} TikEsCtrKeyEntry9x; + +/// Lookup pattern for TikEsCtrKeyEntry9x. +typedef struct { + u32 idx1; ///< Always set to 0 (first entry). + u8 ctrdata[AES_BLOCK_SIZE * 2]; + u32 idx2; ///< Always set to 1 (second entry). +} TikEsCtrKeyPattern9x; + +/// Used to parse the eTicket device key retrieved from PRODINFO via setcalGetEticketDeviceKey(). /// Everything after the AES CTR is encrypted. typedef struct { u8 ctr[0x10]; @@ -49,23 +76,7 @@ typedef struct { u8 padding[0x14]; u64 device_id; u8 ghash[0x10]; -} tikEticketDeviceKeyData; - -/// 9.x+ CTR key entry in ES .data segment. Used to store CTR key/IV data for encrypted volatile tickets in ticket.bin and/or encrypted entries in ticket_list.bin. -/// This is always stored in pairs. The first entry holds the key/IV for the encrypted volatile ticket, while the second entry holds the key/IV for the encrypted entry in ticket_list.bin. -/// First index in this list is always 0, and it's aligned to ES_CTRKEY_ENTRY_ALIGNMENT. -typedef struct { - u32 idx; ///< Entry index. - u8 key[AES_BLOCK_SIZE]; ///< AES-128-CTR key. - u8 ctr[AES_BLOCK_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out. -} tikEsCtrKeyEntry9x; - -/// Lookup pattern for tikEsCtrKeyEntry9x. -typedef struct { - u32 idx1; ///< Always set to 0 (first entry). - u8 ctrdata[AES_BLOCK_SIZE * 2]; - u32 idx2; ///< Always set to 1 (second entry). -} tikEsCtrKeyPattern9x; +} TikEticketDeviceKeyData; /* Global variables. */ @@ -101,7 +112,9 @@ static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_g static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out); static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, bool personalized); -static u8 *tikRetrieveTicketEntryFromTicketBin(allocation_table_storage_ctx_t *fat_storage, u64 ticket_bin_size, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type); + +static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 *out_offset, u8 titlekey_type); +static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 ticket_offset, u8 titlekey_type); static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size); @@ -288,67 +301,64 @@ static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRight u8 titlekey_type = 0; save_ctx_t *save_ctx = NULL; - allocation_table_storage_ctx_t fat_storage = {0}; - u64 ticket_bin_size = 0; - u64 buf_size = (SIGNED_TIK_MAX_SIZE * 0x10); - u8 *buf = NULL, *ticket_entry = NULL; + u64 buf_size = (SIGNED_TIK_MAX_SIZE * 0x100); + u8 *buf = NULL; + u64 ticket_offset = 0; bool success = false; + /* Allocate memory to retrieve the ticket. */ + if (!(buf = malloc(buf_size))) + { + LOGFILE("Unable to allocate 0x%lX bytes block for temporary read buffer!", buf_size); + return false; + } + + /* Get titlekey type. */ if (!tikGetTitleKeyTypeFromRightsId(id, &titlekey_type)) { LOGFILE("Unable to retrieve ticket titlekey type!"); - return false; + goto end; } - save_ctx = save_open_savefile(titlekey_type == TikTitleKeyType_Common ? TIK_COMMON_SAVEFILE_PATH : TIK_PERSONALIZED_SAVEFILE_PATH, 0); - if (!save_ctx) + /* Open ES common/personalized system savefile. */ + if (!(save_ctx = save_open_savefile(titlekey_type == TikTitleKeyType_Common ? TIK_COMMON_SAVEFILE_PATH : TIK_PERSONALIZED_SAVEFILE_PATH, 0))) { LOGFILE("Failed to open ES %s ticket system savefile!", g_tikTitleKeyTypeStrings[titlekey_type]); - return false; - } - - if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_SAVEFILE_STORAGE_PATH, &fat_storage, &ticket_bin_size)) - { - LOGFILE("Failed to locate \"%s\" in ES %s ticket system save!", TIK_SAVEFILE_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); goto end; } - if (ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0) + /* Get ticket entry offset from ticket_list.bin. */ + if (!tikGetTicketEntryOffsetFromTicketList(save_ctx, buf, buf_size, id, &ticket_offset, titlekey_type)) { - LOGFILE("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_SAVEFILE_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_bin_size); + LOGFILE("Unable to find an entry with a matching Rights ID in \"%s\" from ES %s ticket system save!", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); goto end; } - buf = malloc(buf_size); - if (!buf) - { - LOGFILE("Unable to allocate 0x%lX bytes block for temporary read buffer!", buf_size); - goto end; - } - - if (!(ticket_entry = tikRetrieveTicketEntryFromTicketBin(&fat_storage, ticket_bin_size, buf, buf_size, id, titlekey_type))) + /* Get ticket entry from ticket.bin. */ + if (!tikRetrieveTicketEntryFromTicketBin(save_ctx, buf, buf_size, id, ticket_offset, titlekey_type)) { LOGFILE("Unable to find a matching %s ticket entry for the provided Rights ID!", g_tikTitleKeyTypeStrings[titlekey_type]); goto end; } - if (!tikGetTicketTypeAndSize(ticket_entry, SIGNED_TIK_MAX_SIZE, &(dst->type), &(dst->size))) + /* Get ticket type and size. */ + if (!tikGetTicketTypeAndSize(buf, SIGNED_TIK_MAX_SIZE, &(dst->type), &(dst->size))) { LOGFILE("Unable to determine ticket type and size!"); goto end; } - memcpy(dst->data, ticket_entry, dst->size); + memcpy(dst->data, buf, dst->size); success = true; end: - if (buf) free(buf); - if (save_ctx) save_close_savefile(save_ctx); + if (buf) free(buf); + return success; } @@ -365,7 +375,7 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik) size_t out_keydata_size = 0; u8 out_keydata[0x100] = {0}; - tikEticketDeviceKeyData *eticket_devkey = NULL; + TikEticketDeviceKeyData *eticket_devkey = NULL; switch(tik_common_block->titlekey_type) { @@ -381,7 +391,7 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik) return false; } - eticket_devkey = (tikEticketDeviceKeyData*)g_eTicketDeviceKey.key; + eticket_devkey = (TikEticketDeviceKeyData*)g_eTicketDeviceKey.key; /* Perform a RSA-OAEP decrypt operation to get the titlekey. */ if (!rsa2048OaepDecryptAndVerify(out_keydata, 0x100, tik_common_block->titlekey_block, eticket_devkey->modulus, eticket_devkey->exponent, 0x100, g_nullHash, &out_keydata_size) || \ @@ -520,104 +530,184 @@ static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, return true; } -static u8 *tikRetrieveTicketEntryFromTicketBin(allocation_table_storage_ctx_t *fat_storage, u64 ticket_bin_size, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type) +static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 *out_offset, u8 titlekey_type) { - if (!fat_storage || ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0 || !buf || !buf_size || (buf_size % SIGNED_TIK_MAX_SIZE) != 0 || !id) + if (!save_ctx || !buf || !buf_size || (buf_size % sizeof(TikListEntry)) != 0 || !id || !out_offset) { LOGFILE("Invalid parameters!"); - return NULL; + return false; } - u64 br = 0, total_br = 0; - u8 *out_tik = NULL; + allocation_table_storage_ctx_t fat_storage = {0}; + u64 ticket_list_bin_size = 0, br = 0, total_br = 0; - Aes128CtrContext ctr_ctx = {0}; - u8 null_ctr[AES_BLOCK_SIZE] = {0}, ctr[AES_BLOCK_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0}; + u8 last_rights_id[0x10]; + memset(last_rights_id, 0xFF, sizeof(last_rights_id)); - bool is_9x = hosversionAtLeast(9, 0, 0); + bool last_entry_found = false, success = false; - if (is_9x && !memRetrieveFullProgramMemory(&g_esMemoryLocation)) + /* Get FAT storage info for the ticket_list.bin stored within the opened system savefile. */ + if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_LIST_STORAGE_PATH, &fat_storage, &ticket_list_bin_size)) { - LOGFILE("Failed to retrieve ES program memory!"); - return NULL; + LOGFILE("Failed to locate \"%s\" in ES %s ticket system save!", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); + return false; } - while(total_br < ticket_bin_size) + /* Check ticket_list.bin size. */ + if (ticket_list_bin_size < sizeof(TikListEntry) || (ticket_list_bin_size % sizeof(TikListEntry)) != 0) { - if (buf_size > (ticket_bin_size - total_br)) buf_size = (ticket_bin_size - total_br); + LOGFILE("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_list_bin_size); + return false; + } + + /* Look for an entry matching our rights ID in ticket_list.bin. */ + while(total_br < ticket_list_bin_size) + { + if (buf_size > (ticket_list_bin_size - total_br)) buf_size = (ticket_list_bin_size - total_br); - br = save_allocation_table_storage_read(fat_storage, buf, total_br, buf_size); - if (br != buf_size) + if ((br = save_allocation_table_storage_read(&fat_storage, buf, total_br, buf_size)) != buf_size) { - LOGFILE("Failed to read 0x%lX bytes chunk at offset 0x%lX from \"%s\" in ES %s ticket system save!", buf_size, total_br, TIK_SAVEFILE_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); + LOGFILE("Failed to read 0x%lX bytes chunk at offset 0x%lX from \"%s\" in ES %s ticket system save!", buf_size, total_br, TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); break; } - for(u64 i = 0; i < buf_size; i += SIGNED_TIK_MAX_SIZE) + for(u64 i = 0; i < buf_size; i += sizeof(TikListEntry)) { - if ((buf_size - i) < SIGNED_TIK_MIN_SIZE) break; + if ((buf_size - i) < sizeof(TikListEntry)) break; - u8 *cur_tik = (buf + i); - u64 tik_offset = (total_br + i); - TikCommonBlock *tik_common_block = tikGetCommonBlock(cur_tik); + u64 entry_offset = (total_br + i); + TikListEntry *entry = (TikListEntry*)(buf + i); - if (!tik_common_block) + /* Check if we found the last entry. */ + if (!memcmp(entry->rights_id.c, last_rights_id, sizeof(last_rights_id))) { - /* Check if we're dealing with a padding block. */ - if (!memcmp(cur_tik, null_ctr, sizeof(null_ctr))) continue; - - /* We're most likely dealing with an encrypted ticket. Don't proceed if HOS version isn't at least 9.0.0. */ - if (!is_9x) continue; - - /* Sad path. We need to retrieve the CTR key/IV from ES program memory in order to decrypt this ticket. */ - for(u64 j = 0; j < g_esMemoryLocation.data_size; j += ES_CTRKEY_ENTRY_ALIGNMENT) - { - if ((g_esMemoryLocation.data_size - j) < (sizeof(tikEsCtrKeyEntry9x) * 2)) break; - - /* Check if the key indexes are valid. idx2 should always be an odd number.*/ - tikEsCtrKeyPattern9x *pattern = (tikEsCtrKeyPattern9x*)(g_esMemoryLocation.data + j); - if (pattern->idx2 != (pattern->idx1 + 1) || !(pattern->idx2 & 1)) continue; - - /* Seems like indexes are valid. Check if the key is not null and if the CTR is. */ - tikEsCtrKeyEntry9x *key_entry = (tikEsCtrKeyEntry9x*)pattern; - if (!memcmp(key_entry->key, null_ctr, sizeof(null_ctr)) || memcmp(key_entry->ctr, null_ctr, sizeof(null_ctr)) != 0) continue; - - /* Check if we can decrypt the current ticket with this data. */ - memset(&ctr_ctx, 0, sizeof(Aes128CtrContext)); - aes128CtrInitializePartialCtr(ctr, key_entry->ctr, tik_offset); - aes128CtrContextCreate(&ctr_ctx, key_entry->key, ctr); - aes128CtrCrypt(&ctr_ctx, dec_tik, cur_tik, SIGNED_TIK_MAX_SIZE); - - if ((tik_common_block = tikGetCommonBlock(dec_tik)) != NULL && !strncmp(tik_common_block->issuer, "Root", 4)) - { - /* Ticket successfully decrypted. */ - memcpy(cur_tik, dec_tik, SIGNED_TIK_MAX_SIZE); - tik_common_block = tikGetCommonBlock(cur_tik); - break; - } - } - - /* Don't proceed if we couldn't decrypt the ticket. */ - if (!tik_common_block || strncmp(tik_common_block->issuer, "Root", 4) != 0) continue; + last_entry_found = true; + break; } - /* Check if the rights ID from the ticket common block matches the one we're looking for. */ - if (!memcmp(tik_common_block->rights_id.c, id->c, 0x10)) + /* Check if this is the entry we're looking for. */ + if (!memcmp(entry->rights_id.c, id->c, sizeof(id->c))) { /* Jackpot. */ - out_tik = cur_tik; + *out_offset = (entry_offset << 5); /* (entry_offset / 0x20) * 0x400 */ + success = true; break; } } total_br += br; - if (out_tik) break; + if (last_entry_found || success) break; } - if (is_9x) memFreeMemoryLocation(&g_esMemoryLocation); + return success; +} + +static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 ticket_offset, u8 titlekey_type) +{ + if (!save_ctx || !buf || buf_size < SIGNED_TIK_MAX_SIZE || !id || (ticket_offset % SIGNED_TIK_MAX_SIZE) != 0) + { + LOGFILE("Invalid parameters!"); + return false; + } - return out_tik; + allocation_table_storage_ctx_t fat_storage = {0}; + u64 ticket_bin_size = 0, br = 0; + + TikCommonBlock *tik_common_block = NULL; + + Aes128CtrContext ctr_ctx = {0}; + u8 null_ctr[AES_BLOCK_SIZE] = {0}, ctr[AES_BLOCK_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0}; + + bool is_volatile = false, success = false; + + /* Get FAT storage info for the ticket.bin stored within the opened system savefile. */ + if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_DB_STORAGE_PATH, &fat_storage, &ticket_bin_size)) + { + LOGFILE("Failed to locate \"%s\" in ES %s ticket system save!", TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); + return false; + } + + /* Check ticket.bin size. */ + if (ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0 || ticket_bin_size < (ticket_offset + SIGNED_TIK_MAX_SIZE)) + { + LOGFILE("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_bin_size); + return false; + } + + /* Read ticket data. */ + if ((br = save_allocation_table_storage_read(&fat_storage, buf, ticket_offset, SIGNED_TIK_MAX_SIZE)) != SIGNED_TIK_MAX_SIZE) + { + LOGFILE("Failed to read 0x%lX bytes long ticket at offset 0x%lX from \"%s\" in ES %s ticket system save!", SIGNED_TIK_MAX_SIZE, ticket_offset, TIK_DB_STORAGE_PATH, \ + g_tikTitleKeyTypeStrings[titlekey_type]); + return false; + } + + /* Check if we're dealing with a volatile (encrypted) ticket. */ + if (!(tik_common_block = tikGetCommonBlock(buf)) || strncmp(tik_common_block->issuer, "Root", 4) != 0) + { + tik_common_block = NULL; + is_volatile = true; + + /* Don't proceed if HOS version isn't at least 9.0.0. */ + if (!hosversionAtLeast(9, 0, 0)) + { + LOGFILE("Unable to retrieve ES key entry for volatile tickets under HOS versions below 9.0.0!"); + return false; + } + + /* Retrieve ES program memory. */ + if (!memRetrieveFullProgramMemory(&g_esMemoryLocation)) + { + LOGFILE("Failed to retrieve ES program memory!"); + return false; + } + + /* Retrieve the CTR key/IV from ES program memory in order to decrypt this ticket. */ + for(u64 i = 0; i < g_esMemoryLocation.data_size; i += ES_CTRKEY_ENTRY_ALIGNMENT) + { + if ((g_esMemoryLocation.data_size - i) < (sizeof(TikEsCtrKeyEntry9x) * 2)) break; + + /* Check if the key indexes are valid. idx2 should always be an odd number equal to idx + 1. */ + TikEsCtrKeyPattern9x *pattern = (TikEsCtrKeyPattern9x*)(g_esMemoryLocation.data + i); + if (pattern->idx2 != (pattern->idx1 + 1) || !(pattern->idx2 & 1)) continue; + + /* Check if the key is not null and if the CTR is. */ + TikEsCtrKeyEntry9x *key_entry = (TikEsCtrKeyEntry9x*)pattern; + if (!memcmp(key_entry->key, null_ctr, sizeof(null_ctr)) || memcmp(key_entry->ctr, null_ctr, sizeof(null_ctr)) != 0) continue; + + /* Check if we can decrypt the current ticket with this data. */ + memset(&ctr_ctx, 0, sizeof(Aes128CtrContext)); + aes128CtrInitializePartialCtr(ctr, key_entry->ctr, ticket_offset); + aes128CtrContextCreate(&ctr_ctx, key_entry->key, ctr); + aes128CtrCrypt(&ctr_ctx, dec_tik, buf, SIGNED_TIK_MAX_SIZE); + + /* Check if we successfully decrypted this ticket. */ + if ((tik_common_block = tikGetCommonBlock(dec_tik)) != NULL && !strncmp(tik_common_block->issuer, "Root", 4)) + { + memcpy(buf, dec_tik, SIGNED_TIK_MAX_SIZE); + tik_common_block = tikGetCommonBlock(buf); + break; + } + + tik_common_block = NULL; + } + + /* Check if we were able to decrypt the ticket. */ + if (!tik_common_block) + { + LOGFILE("Unable to decrypt volatile ticket at offset 0x%lX in \"%s\" from ES %s ticket system save!", ticket_offset, TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); + goto end; + } + } + + /* Check if the rights ID from the ticket common block matches the one we're looking for. */ + if (!(success = (memcmp(tik_common_block->rights_id.c, id->c, 0x10) == 0))) LOGFILE("Retrieved ticket doesn't hold a matching Rights ID!"); + +end: + if (is_volatile) memFreeMemoryLocation(&g_esMemoryLocation); + + return success; } static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size) @@ -673,7 +763,7 @@ static bool tikRetrieveEticketDeviceKey(void) Result rc = 0; u32 public_exponent = 0; - tikEticketDeviceKeyData *eticket_devkey = NULL; + TikEticketDeviceKeyData *eticket_devkey = NULL; Aes128CtrContext eticket_aes_ctx = {0}; rc = setcalGetEticketDeviceKey(&g_eTicketDeviceKey); @@ -684,9 +774,9 @@ static bool tikRetrieveEticketDeviceKey(void) } /* Decrypt eTicket RSA key. */ - eticket_devkey = (tikEticketDeviceKeyData*)g_eTicketDeviceKey.key; + eticket_devkey = (TikEticketDeviceKeyData*)g_eTicketDeviceKey.key; aes128CtrContextCreate(&eticket_aes_ctx, keysGetEticketRsaKek(g_eTicketDeviceKey.generation > 0), eticket_devkey->ctr); - aes128CtrCrypt(&eticket_aes_ctx, &(eticket_devkey->exponent), &(eticket_devkey->exponent), sizeof(tikEticketDeviceKeyData) - 0x10); + aes128CtrCrypt(&eticket_aes_ctx, &(eticket_devkey->exponent), &(eticket_devkey->exponent), sizeof(TikEticketDeviceKeyData) - 0x10); /* Public exponent value must be 0x10001. */ /* It is stored using big endian byte order. */ diff --git a/source/title.c b/source/title.c index 1406baa..af856f8 100644 --- a/source/title.c +++ b/source/title.c @@ -73,6 +73,13 @@ static const char *g_titleNcmContentMetaTypeNames[] = { [NcmContentMetaType_Delta - 0x7A] = "Delta" }; +static const char *g_filenameTypeStrings[] = { + [NcmContentMetaType_Application - 0x80] = "BASE", + [NcmContentMetaType_Patch - 0x80] = "UPD", + [NcmContentMetaType_AddOnContent - 0x80] = "DLC", + [NcmContentMetaType_Delta - 0x80] = "DELTA" +}; + /* Info retrieved from https://switchbrew.org/wiki/Title_list. */ /* Titles bundled with the kernel are excluded. */ static const SystemTitleName g_systemTitles[] = { @@ -735,13 +742,15 @@ char *titleGenerateFileName(const TitleInfo *title_info, u8 name_convention, u8 char title_name[0x400] = {0}; TitleApplicationMetadata *app_metadata = NULL; - if (!title_info || name_convention > TitleFileNameConvention_IdAndVersionOnly || \ + if (!title_info || title_info->meta_key.type < NcmContentMetaType_Application || title_info->meta_key.type > NcmContentMetaType_Delta || name_convention > TitleFileNameConvention_IdAndVersionOnly || \ (name_convention == TitleFileNameConvention_Full && illegal_char_replace_type > TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly)) { LOGFILE("Invalid parameters!"); goto end; } + u8 type = (title_info->meta_key.type - 0x80); + /* Retrieve application metadata. */ /* System titles and user applications: just retrieve the app_metadata pointer from the input TitleInfo. */ /* Patches and add-on contents: retrieve the app_metadata pointer from the parent TitleInfo if it's available. */ @@ -756,11 +765,11 @@ char *titleGenerateFileName(const TitleInfo *title_info, u8 name_convention, u8 if (illegal_char_replace_type) utilsReplaceIllegalCharacters(title_name, illegal_char_replace_type == TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly); } - sprintf(title_name + strlen(title_name), "[%016lX][v%u][%s]", title_info->meta_key.id, title_info->meta_key.version, titleGetNcmContentMetaTypeName(title_info->meta_key.type)); + sprintf(title_name + strlen(title_name), "[%016lX][v%u][%s]", title_info->meta_key.id, title_info->meta_key.version, g_filenameTypeStrings[type]); } else if (name_convention == TitleFileNameConvention_IdAndVersionOnly) { - sprintf(title_name, "%016lX_v%u_%s", title_info->meta_key.id, title_info->meta_key.version, titleGetNcmContentMetaTypeName(title_info->meta_key.type)); + sprintf(title_name, "%016lX_v%u_%s", title_info->meta_key.id, title_info->meta_key.version, g_filenameTypeStrings[type]); } /* Duplicate generated filename. */ diff --git a/source/utils.c b/source/utils.c index 8d6d9e1..5f09ad9 100644 --- a/source/utils.c +++ b/source/utils.c @@ -633,6 +633,13 @@ bool utilsCheckIfFileExists(const char *path) return false; } +void utilsRemoveConcatenationFile(const char *path) +{ + if (!path || !*path) return; + remove(path); + fsdevDeleteDirectoryRecursively(path); +} + bool utilsCreateConcatenationFile(const char *path) { if (!path || !*path) @@ -642,8 +649,7 @@ bool utilsCreateConcatenationFile(const char *path) } /* Safety check: remove any existant file/directory at the destination path. */ - remove(path); - fsdevDeleteDirectoryRecursively(path); + utilsRemoveConcatenationFile(path); /* Create ConcatenationFile */ /* If the call succeeds, the caller function will be able to operate on this file using stdio calls. */ diff --git a/source/utils.h b/source/utils.h index 4e99631..aa095d1 100644 --- a/source/utils.h +++ b/source/utils.h @@ -111,6 +111,7 @@ bool utilsCommitSdCardFileSystemChanges(void); bool utilsCheckIfFileExists(const char *path); +void utilsRemoveConcatenationFile(const char *path); bool utilsCreateConcatenationFile(const char *path); void utilsCreateDirectoryTree(const char *path, bool create_last_element); diff --git a/todo.txt b/todo.txt index cbd0ad8..79109b6 100644 --- a/todo.txt +++ b/todo.txt @@ -2,7 +2,7 @@ reminder: list of top level functions designed to alter nca data in order of (possible) usage: - out of loop: + out of dump loop: * ncaSetDownloadDistributionType (instead of always using it like legacy, offer it as an option) * ncaRemoveTitlekeyCrypto (can be used with digital titles + game updates in gamecards) @@ -10,17 +10,11 @@ list of top level functions designed to alter nca data in order of (possible) us * cnmtGenerateNcaPatch (Meta) * calls pfsGenerateEntryPatch * calls ncaGenerateHierarchicalSha256Patch - * cnmtIsNcaPatchRequired -> not sure if i'll keep this - * missing wrapper for pfsWriteEntryPatchToMemoryBuffer !!! - * programInfoChangeAcidPublicKeyAndNcaSignature (Program) + * programInfoGenerateNcaPatch (Program) * calls npdmChangeAcidPublicKeyAndNcaSignature * calls pfsGenerateEntryPatch * calls ncaGenerateHierarchicalSha256Patch - * needs programInfoWriteNcaPatch to write patched data - * calls npdmWriteNcaPatch - * calls pfsWriteEntryPatchToMemoryBuffer - * calls ncaWriteHierarchicalSha256PatchToMemoryBuffer * nacpGenerateNcaPatch (Control) * calls romfsGenerateFileEntryPatch @@ -29,15 +23,26 @@ list of top level functions designed to alter nca data in order of (possible) us * missing wrapper for romfsWriteFileEntryPatchToMemoryBuffer !!! * missing functions for nacp mods !!! - * ncaIsHeaderDirty (doesn't modify anything per se, but it's used to check if any of the functions above has been used, basically) * ncaEncryptHeader (doesn't modify anything per se, but it's used to generate new encrypted header data if needed) - inside loop: - * ncaWriteEncryptedHeaderDataToMemoryBuffer + inside dump loop: + * ncaIsHeaderDirty (doesn't modify anything per se, but it's used to check if any of the functions above has been used, basically - and by extension, if the functions below need to be used) + + * ncaWriteEncryptedHeaderDataToMemoryBuffer (write encrypted nca header data) + + * cnmtUpdateContentInfo (used to update content entry info in the raw cnmt copy after dumping each one) + + * cnmtWriteNcaPatch (writes cnmt patch) + * calls pfsWriteEntryPatchToMemoryBuffer + * calls ncaWriteHierarchicalSha256PatchToMemoryBuffer + + * programInfoWriteNcaPatch (writes ndpm patch) + * calls npdmWriteNcaPatch + * calls pfsWriteEntryPatchToMemoryBuffer + * calls ncaWriteHierarchicalSha256PatchToMemoryBuffer * pfsWriteEntryPatchToMemoryBuffer * calls ncaWriteHierarchicalSha256PatchToMemoryBuffer - * missing cnmt, program wrappers * romfsWriteFileEntryPatchToMemoryBuffer * calls ncaWriteHierarchicalSha256PatchToMemoryBuffer / ncaWriteHierarchicalIntegrityPatchToMemoryBuffer @@ -60,8 +65,6 @@ todo: nca: support for compressed fs sections? nca: support for sparse sections? - tik: option to wipe elicense property mask (otherwise, the console will attempt to connect to the Internet to perform elicense verification before launching the title the ticket belongs to) - tik: option to wipe volatile property mask (otherwise, the imported ticket will use an additional aes-ctr crypto layer in ticket.bin) tik: automatically dump tickets to the SD card? tik: use dumped tickets when the original ones can't be found in the ES savefile?