From 3a16147c5933c3ccacbd02639b91ead0605ec2b4 Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Wed, 6 Nov 2019 14:22:40 -0400 Subject: [PATCH] Update to v1.1.7. --- Makefile | 2 +- README.md | 62 +- romfs/certificate/CA00000003 | Bin 1024 -> 0 bytes romfs/certificate/XS00000020 | Bin 768 -> 0 bytes romfs/certificate/XS00000021 | Bin 768 -> 0 bytes source/dumper.c | 1015 +++++++++------ source/dumper.h | 3 + source/fatfs/ffconf.h | 16 +- source/fatfs/ffsystem.c | 35 + source/keys.c | 505 ++++---- source/keys.h | 30 +- source/main.c | 635 +++++----- source/nca.c | 221 +++- source/nca.h | 35 +- source/save.c | 1931 ++++++++++++++++++++++++++++ source/save.h | 475 +++++++ source/ui.c | 1056 +++++++++------ source/ui.h | 4 + source/util.c | 2327 ++++++++++++++++++---------------- source/util.h | 69 +- 20 files changed, 5907 insertions(+), 2514 deletions(-) delete mode 100644 romfs/certificate/CA00000003 delete mode 100644 romfs/certificate/XS00000020 delete mode 100644 romfs/certificate/XS00000021 create mode 100644 source/fatfs/ffsystem.c create mode 100644 source/save.c create mode 100644 source/save.h diff --git a/Makefile b/Makefile index e1dd3dc..a7f5a28 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ include $(DEVKITPRO)/libnx/switch_rules VERSION_MAJOR := 1 VERSION_MINOR := 1 -VERSION_MICRO := 6 +VERSION_MICRO := 7 APP_TITLE := nxdumptool APP_AUTHOR := MCMrARM, DarkMatterCore diff --git a/README.md b/README.md index 394c88c..35063b5 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,13 @@ Main features * Generates full Cartridge Image dumps (XCI) with optional certificate removal and/or trimming. * Generates installable Nintendo Submission Packages (NSP) from base applications, updates and DLCs stored in the inserted gamecard, SD card and eMMC storage devices. * The generated dumps follow the `AuditingTool` format from Scene releases. + * Capable of generating dumps without console specific information (common ticket). * Capable of generating ticket-less (standard crypto) dumps. * Capable of generating dumps from installed updates/DLCs with missing base applications (orphan titles). - * Batch mode available. + * Compatible with game pre-installs. + * Batch mode available, with customizable dump settings. +* Manual gamecard certificate dump. +* Manual ticket dump from installed SD/eMMC titles + optional removal of console specific data. * Compatible with multigame carts. * CRC32 checksum calculation for XCI/NSP dumps. * Full XCI dump verification using XML database from NSWDB.COM (NSWreleases.xml). @@ -19,15 +23,17 @@ Main features * Program NCA ExeFS/RomFS section & Data NCA RomFS section file data dumping + browser with manual file dump support. * Compatible with base applications, updates and DLCs (if available). * Supports manual RomFS directory dumping. -* Manual gamecard certificate dump. * Free SD card space checks in place. * File splitting support for all operations. * Capable of storing split XCI/NSP dumps in directories with the archive bit set. +* Sequential (multi-session) dump support, in case there's not enough storage space available for a XCI/NSP full dump. * Metadata retrieval using NCM and NS services. * Dump speed calculation, ETA calculation and progress bar. Operations related to installed SD/eMMC titles require a keys file located at "sdmc:/switch/prod.keys". Use [Lockpick_RCM](https://github.com/shchmue/Lockpick_RCM) to generate it. +Please launch the application through title override (hold R while launching a game) whenever possible to avoid memory allocation problems. + Thanks to -------------- @@ -46,9 +52,10 @@ Thanks to * The [LZ4 project](http://www.lz4.org), for the LZ4 C-code implementation (licensed under [BSD 2-Clause](https://github.com/lz4/lz4/blob/master/lib/LICENSE)). * [AnalogMan](https://github.com/AnalogMan151) and [0Liam](https://github.com/0Liam), for their constant support and ideas. * [RattletraPM](https://github.com/RattletraPM), for the awesome icon used in the application. -* The GNOME project, from which the high contrast directory/file icons for the filebrowser modes were retrieved. +* The GNOME project, from which the [high contrast icons](https://commons.wikimedia.org/wiki/GNOME_High_contrast_icons) were retrieved. * The folks from ReSwitched, for working towards the creation of a good homebrew ecosystem. * The Comfy Boyes, for being both awesome and supportive. You know who you are. +* My girlfriend, for putting up with me even though I dedicate most of my free time to this little project, and for doing her best to cheer me up and keep me going. I love you. Donate -------------- @@ -60,6 +67,55 @@ If you like my work and you'd like to support me in any way, it's not necessary, Changelog -------------- +**v1.1.7:** +* Tickets and RSA certificates are now properly parsed from their respective system savedata files, thanks to the efforts of [shchmue](https://github.com/shchmue)! + * Speeds up ticket / titlekey retrieval for NSP/ExeFS/RomFS operations. + * Removes the need to bundle RSA certificates inside the application. Yay! + * As a bonus, the new `XS00000024` personalized ticket certificate introduced in 9.0.0 is now supported. Thanks to [SimonTime](https://github.com/simontime) for providing insight on this matter! +* Added NSP dump support for pre-installed titles. + * If the selected title uses titlekey crypto and no ticket for it can be found, a prompt will be displayed, asking the user if they want to proceed anyway (even though content decryption won't be possible). + * This prompt will *not* appear in batch dump operations. The dump procedure will always go ahead. + * Sequential NSP dump operations will only display the prompt during their first run. +* Added a new Ticket submenu for SD/eMMC titles. It can be used to only dump the Ticket from a specific base application / update / DLC, without having to dump its entire NSP. + * Dumped tickets are stored in `sdmc:/switch/nxdumptool/Ticket`. + * A configurable option is also available to remove console specific data from dumped tickets. + * The encrypted + decrypted title key is displayed during the dumping process, along with the Rights ID for the title. + * Just so you know, if you want to dump tickets from base application updates bundled in gamecards, use the HFS0 browser. +* Added an option in NSP/batch dump menus to control the replacement of the NPDM RSA key/sig in Program NCAs from base applications and updates: + * Up until now, replacing both the public RSA key in the ACID section from the main.npdm file (ExeFS) and the NPDM header signature (NCA header) has been the default, non-configurable behaviour whenever Program NCA modifications were needed. + * This option is enabled by default - if Program NCA modifications are needed, disabling this option will make the output NSP require ACID patches to function properly under any CFW (but at the same time, it will make the Program NCA verifiable by PC tools). + * The rest of the possible Program NCA modifications (content distribution change and/or Rights ID removal + key area replacement) will be applied when needed, even if this option is disabled. +* Changes related to the orphan content menu (Y button): + * Parent base application name is now retrieved for orphan updates and DLCs whenever possible, and used in menus and output NSP dumps. + * Moved the orphan content hint from the orphan content menu to the SD/eMMC menu. +* Changed application behaviour regarding the Lockpick_RCM keys file existence: + * SD/eMMC menu and NSP/ExeFS/RomFS related operations are now disabled if the keys file at "sdmc:/switch/prod.keys" is not available. + * An error message telling the user to run Lockpick_RCM will be displayed in the main menu if the keys file is not available. + * Additionally, error messages related to data decryption will now also suggest the user to run Lockpick_RCM. +* Changes to the generated update NSPs (thanks to [The-4n](https://github.com/The-4n) and [suchmememanyskill](https://github.com/suchmememanyskill)): + * Delta Fragments are, again, always excluded from output NSP dumps, regardless of their source storage and the selected dump settings. + * Patch Extended Data is no longer wiped from the CNMT NCA in update NSPs - only the content records are replaced accordingly. + * Furthermore, content records from Delta Fragments are preserved as well. + * Fixed CNMT PFS0 block hash calculation when the total PFS0 size exceeds the hash block size from the PFS0 superblock in the NCA header. Removes the `0x236E02` / `2002-4535` error in Goldleaf about an invalid PFS0, triggered by update NSPs with long a CNMT PFS0 section. +* Changes to the generated NSP XMLs: + * `RequiredDownloadSystemVersion` and `IdOffset` elements from the CNMT XML are now properly retrieved from their true locations in the CNMT NCA. + * Added support for the `RuntimeParameterDelivery` NACP field (introduced in HOS 9.X). + * Added support for the `IARCGeneric` value in the `RatingAge` NACP field (introduced in HOS 9.X). + * Fixed handling of `PlayLogQueryableApplicationId` values. + * Big thanks to [0Liam](https://github.com/0Liam) for documenting these changes! +* Changes related to the application update feature: + * Added a forced update prompt if the application is already on the latest version. + * The application update option will now be disabled after a successful update. + * Removed the FS service reinitialize step after closing the application's RomFS at startup. This was done because `romfsExit()` didn't close all open file handles to the NRO when I tested it with libnx v2.2.0 some time ago, thus making the application update fail. Nonetheless, the problem has been fixed. +* Fixed UI flickering when HFS0 partition data can't be retrieved from the gamecard. + * Furthermore, a warning about `nogc` spoofing is now displayed under this particular case. +* Added an extra NSP offset validation step for sequential NSP dumps. +* Minor codestyle fixes. + +Big thanks to [FennecTECH](https://github.com/fennectech) and `Hannah (Luna)#8459` for providing with **lots** of testing for this release! + +PSA: if you downloaded any new games from the eShop after updating to 9.0.0+ and used a previous release of nxdumptool to dump NSPs **with console specific data**, please redump them - their RSA certificate chain isn't the proper one. Dumps without console specific data (or without a ticket) are not affected by this. + **v1.1.6:** * Added sequential dump support: it is now possible to start a XCI/NSP dump procedure even if there's not enough space available in the SD card! * No setting has to be modified in order to enable this feature - the application will automatically ask the user if they want to use this mode if there's not enough space for the full dump. diff --git a/romfs/certificate/CA00000003 b/romfs/certificate/CA00000003 deleted file mode 100644 index 3ae4ffa6326aa9d2380145fd0ffa9193892b00c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1024 zcmZQzWMD3Ew0OUJ@4~DZwKo}!a%HAOdiZoKYLlM0SSX_EIo~(sx}$soMt3alzL+&_ z#f!-6Su>VBf1Jhh&s1l{WY#8=|IPNNt(hC;=JOu7xm3Twb<3uoj?(E%Wn8wt?|EK* z?P~V&aFd0txzDcbyLNez)A19P(k4pgaWUny_Ot7kO?|)Md-Un%ius56?4O)zo}^IW zklg8c{98v@e9G*SYqq@+|JU9Mmzx^mlr1dr$E~yZ%88{(-E7vW zHNB-S{mlU)?km@CesgGo&D^W{leA8*x&1!k7?Xs@%{+y6qstd>-hAc4J26S7YUb?@ z=c9Av^>k`yht~$`h~?(jNv-uN_cC{>tN5p&_|R+WMWF>5YZpjSFFvg&hpbh-XrubnI$@`YFI>909D zez#^kpK&oR!R=0P@@LL$orJ2ny;q|}nU<}owK;L)Xz9$p+R99U6D&ujDhGtWlsl_t ze)3>TVBnhUSu7LQ*&X{*?y%rC_u^dk$gp$IP5l8Q*3wyJ56||OhP7R7 ziC+{a{ukKL6n4)3QglgN;FJm3zKN%O^cW}rLHYS56le!&Wps8lfB<9iOsanIK4(+q z$w%)VDjs|F#s8b-kAC)U@A9|vrzUJLZED=g7rMGgP;_B=flZacKdYDTWfJztPIoKY zWR-D-W#f#X=#v>AWgp!Rcyjakhr*{xw;pjS8Na-o{7v@y!TFz;td#3d4Zc!%x4LEL z#~tU2O+S1MVNG86YKLX=gV`?`8Rch&BqwSVD7-wwyV|2(gf~rXVRNp!pu9rHJ@+D! ziGk-ck4Z1%`+3uI!FTzPnV&Aty3|v^YSJ|Mh~fXe#^3fV<2^1Jw`kANTm|m+xoj8a zINt7kd0au%XGybm=7FBfv)1fZ49N-lZo0+|iA!JQOpLjjWLkOnb;>!H_Yt`wyH<6W RFwVAn{qAxl10xWU0RR=#gMR=3 diff --git a/romfs/certificate/XS00000020 b/romfs/certificate/XS00000020 deleted file mode 100644 index 31ba1d43bdc6337e485a6dd5d1b159ee7f822e2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 768 zcmZQzWMG*#|Aj{HvYmH>j_*xQ(ej%0^h5BQ(~}eJZ<)_jXx}?==B>E&>50ANoQ=kR zqgl^3bqLSXn;vs1p@F%*=hg>>uTp%`Ws@p6T*IUdk4bR6SW;t>q~ts0yV=Wyeqn3& z7jFM3z4x8P-5qY%_V#mac`~IZ#N^e_{c1t8>votGUOtzw?D~p;+s_*0j%rm`*+r@E zNsnH9)VTco;iA{)nAr3qKNm2sQeJ0zrGt;pJM(ALZ|OA+Ir^o$G$ZOYME{mI&+IYv z4ca)*e(n5yUtEr~R%-NanBFM1{)Em`-!(hm)=B>jnB1@QBks-TC12Fm#lIHauFs^u zIq%F%L*KQ+BAWY8rpHITW1s*8<>!~^Iy)LbfH65HF-8PKbQ>9vqaC8>lB~_M8iCAP zEOX<61)gV5sFQeig4?*pUHP$PxV=xBC;M%E4x(JwMrRx_e zH3+R-BqkPF`(1AR>C`DA1sAvQ$NjL1tP`JqIy(H&JMmlAhs$Q9oOW+bPgk0oZq(@b zXV%91BJ+4+MAz-$KH7g=`eA2Uif)EMcR;4ej`!vBE1ul_%*A_t@}Kql7aFc;o`218 z%6#{Cs^{MP5V;!7{p@Lajokk?`I_x=pJpw7J8}Ak8x}zi0wy#VzL(m)b^VFWcPCe| z=YK1c+MMJNV_)00cw<#!!LrDM2^>@T>*Ls#-m{CG(HZxv_T7SneN#)*`YTpTvTxoc LUCY1-gk%5!%?$-r diff --git a/romfs/certificate/XS00000021 b/romfs/certificate/XS00000021 deleted file mode 100644 index d8344e7037cfef1cac1175164fd13bdcb778a4f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 768 zcmZQzWMEO4+Iu!6Qa1gxj?vvMyFFj*t4%z>KC^(;^e$JEI+OeA(uuRp79IIG|J~WI zj_&{UpKOu{TPE!g;IU$T{SN(r#!~lrW~I|6+-3BCefjsq45rvqy^0=BXCF9}82f7G zx}&jNNo%e(USZ_b*yylFmqSO#ud6d*i}@$JkG?bF4l+9w>}p!pZXPOWV%A*tt6ZhI zb&}nrx{t>Hi`=Ywg4?YsMqI z@r$@SWqO{ES+&KwcphKozF@mKCUMK8v!BbR8eg(3xPBmBZf#QGVXv5a!OaX)8f!|e zS|>I0CQLkB{A$@c&TLx-3P4bPeu=KLqX7gMlVcKNL@-3Rks&$SA$k@GFOq+&|5TGt zV&UtB!m{hvuMiB3+kRQYwk-X_J+~b~Q#h4=E;$=3Cb>;6{M6>Ts>aT`Pj!U2cWPEh zitYIyuD;i$;*EuR_zo^(rw2{@R-1*3{$;uFmuy%xP^}JwNxT_)Dx6eLJ~u z-;b&Vjrtq-H*>k3j`hj?kW$WGrqsF2PGZWXA5-g%OisI4?^?Fx-gZrW|3#rY-kUo} zbgEQIDMT+#pFTUNqWXon!i@c!E%PO`xL?FPN^9m&ELNJ{xBlsc+lIQ5OrOOg=dqs9 zVdLm$-^=`Z`K;}8=Rc9yJbw@S6X!EqlIO^JeoF~|T+rE3!=yJi_1JsP?}-Ul FAT32_FILESIZE_LIMIT && isFat32 && setXciArchiveBit) { snprintf(filename, MAX_ELEMENTS(filename), "%s%s.xci", XCI_DUMP_PATH, dumpName); - if (R_FAILED(result = fsdevSetArchiveBit(filename))) + result = fsdevSetArchiveBit(filename); + if (R_FAILED(result)) { breaks += 2; uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Warning: failed to set archive bit on output directory! (0x%08X)", result); @@ -814,38 +832,29 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd bool calcCrc = nspDumpCfg->calcCrc; bool removeConsoleData = nspDumpCfg->removeConsoleData; bool tiklessDump = nspDumpCfg->tiklessDump; + bool npdmAcidRsaPatch = nspDumpCfg->npdmAcidRsaPatch; + bool preInstall = false; Result result; u32 i = 0, j = 0; - u32 written = 0; - u32 total = 0; - u32 titleCount = 0; - u32 ncmTitleIndex = 0; - u32 titleNcaCount = 0; - u32 partition = 0; FsStorageId curStorageId; + u8 filter; + u32 titleCount = 0, ncmTitleIndex = 0; + + char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; + + NcmContentRecord *titleContentRecords = NULL; + u32 titleContentRecordsCnt = 0; FsGameCardHandle handle; FsStorage gameCardStorage; memset(&gameCardStorage, 0, sizeof(FsStorage)); - NcmContentMetaDatabase ncmDb; - memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase)); - - NcmContentMetaRecordsHeader contentRecordsHeader; - memset(&contentRecordsHeader, 0, sizeof(NcmContentMetaRecordsHeader)); - - u64 contentRecordsHeaderReadSize = 0; - NcmContentStorage ncmStorage; memset(&ncmStorage, 0, sizeof(NcmContentStorage)); - NcmApplicationContentMetaKey *titleList = NULL; - NcmContentRecord *titleContentRecords = NULL; - size_t titleListSize = sizeof(NcmApplicationContentMetaKey); - cnmt_xml_program_info xml_program_info; cnmt_xml_content_info *xml_content_info = NULL; @@ -896,8 +905,6 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd Sha256Context nca_hash_ctx; sha256ContextCreate(&nca_hash_ctx); - char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; - u64 n, nca_offset; FILE *outFile = NULL; u8 splitIndex = 0; @@ -934,15 +941,17 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd return false; } - curStorageId = (selectedNspDumpType == DUMP_APP_NSP ? titleAppStorageId[titleIndex] : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchStorageId[titleIndex] : titleAddOnStorageId[titleIndex])); - - if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser) + if ((selectedNspDumpType == DUMP_APP_NSP && titleIndex >= titleAppCount) || (selectedNspDumpType == DUMP_PATCH_NSP && titleIndex >= titlePatchCount) || (selectedNspDumpType == DUMP_ADDON_NSP && titleIndex >= titleAddOnCount)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title storage ID!"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title index!"); breaks += 2; return false; } + curStorageId = (selectedNspDumpType == DUMP_APP_NSP ? titleAppStorageId[titleIndex] : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchStorageId[titleIndex] : titleAddOnStorageId[titleIndex])); + + filter = ((u8)selectedNspDumpType + META_DB_REGULAR_APPLICATION); + switch(curStorageId) { case FsStorageId_GameCard: @@ -961,22 +970,6 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd break; } - if (!titleCount) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title type count!"); - breaks += 2; - return false; - } - - if (ncmTitleIndex > (titleCount - 1)) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title index!"); - breaks += 2; - return false; - } - - titleListSize *= titleCount; - char *dumpName = generateNSPDumpName(selectedNspDumpType, titleIndex); if (!dumpName) { @@ -1031,6 +1024,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd calcCrc = false; removeConsoleData = seqNspCtx.removeConsoleData; tiklessDump = seqNspCtx.tiklessDump; + npdmAcidRsaPatch = seqNspCtx.npdmAcidRsaPatch; + preInstall = seqNspCtx.preInstall; splitIndex = seqNspCtx.partNumber; progressCtx.curOffset = ((u64)seqNspCtx.partNumber * SPLIT_FILE_SEQUENTIAL_SIZE); } else { @@ -1071,35 +1066,6 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd u64 part_size = (seqDumpMode ? SPLIT_FILE_SEQUENTIAL_SIZE : SPLIT_FILE_NSP_PART_SIZE); - // If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002 - // Also open an IStorage instance for the HFS0 Secure partition, since we may also need it if we're dealing with a Patch with titlekey crypto, in order to retrieve the tik file - if (curStorageId == FsStorageId_GameCard) - { - partition = (hfs0_partition_cnt - 1); // Select the secure partition - - workaroundPartitionZeroAccess(); - - if (!getPartitionHfs0Header(partition)) goto out; - - if (!partitionHfs0FileCount) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "The Secure HFS0 partition is empty!"); - goto out; - } - - if (R_FAILED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "GetGameCardHandle failed! (0x%08X)", result); - goto out; - } - - if (R_FAILED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "OpenGameCardStorage failed! (0x%08X)", result); - goto out; - } - } - if (!batch) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Retrieving information from encrypted NCA content files..."); @@ -1107,83 +1073,52 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd breaks++; } - titleList = calloc(1, titleListSize); - if (!titleList) + if (!retrieveNcaContentRecords(curStorageId, filter, titleCount, ncmTitleIndex, &titleContentRecords, &titleContentRecordsCnt)) goto out; + + // If we're dealing with a gamecard, open an IStorage instance for the HFS0 Secure partition. We may need it if we're dealing with a Patch with titlekey crypto, in order to retrieve the tik file + if (curStorageId == FsStorageId_GameCard) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the ApplicationContentMetaKey struct!"); - goto out; + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "GetGameCardHandle failed! (0x%08X)", result); + goto out; + } + + u32 partition = (hfs0_partition_cnt - 1); // Select the secure partition + + result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "OpenGameCardStorage failed! (0x%08X)", result); + goto out; + } } - if (R_FAILED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb))) + result = ncmOpenContentStorage(curStorageId, &ncmStorage); + if (R_FAILED(result)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentMetaDatabase failed! (0x%08X)", result); - goto out; - } - - u8 filter = ((u8)selectedNspDumpType + META_DB_REGULAR_APPLICATION); - - if (R_FAILED(result = ncmContentMetaDatabaseListApplication(&ncmDb, filter, titleList, titleListSize, &written, &total))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); - goto out; - } - - if (!written || !total) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!"); - goto out; - } - - if (written != total) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total); - goto out; - } - - if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseGet failed! (0x%08X)", result); - goto out; - } - - titleNcaCount = (u32)(contentRecordsHeader.numContentRecords); - - titleContentRecords = calloc(titleNcaCount, sizeof(NcmContentRecord)); - if (!titleContentRecords) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the ContentRecord struct!"); - goto out; - } - - if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), 0, titleContentRecords, titleNcaCount * sizeof(NcmContentRecord), &written))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentStorage failed! (0x%08X)", result); goto out; } // Fill information for our CNMT XML memset(&xml_program_info, 0, sizeof(cnmt_xml_program_info)); - xml_program_info.type = titleList[ncmTitleIndex].metaRecord.type; - xml_program_info.title_id = titleList[ncmTitleIndex].metaRecord.titleId; - xml_program_info.version = titleList[ncmTitleIndex].metaRecord.version; - xml_program_info.nca_cnt = titleNcaCount; + xml_program_info.type = filter; + xml_program_info.title_id = (selectedNspDumpType == DUMP_APP_NSP ? titleAppTitleID[titleIndex] : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchTitleID[titleIndex] : titleAddOnTitleID[titleIndex])); + xml_program_info.version = (selectedNspDumpType == DUMP_APP_NSP ? titleAppVersion[titleIndex] : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchVersion[titleIndex] : titleAddOnVersion[titleIndex])); + xml_program_info.nca_cnt = titleContentRecordsCnt; - xml_content_info = calloc(titleNcaCount, sizeof(cnmt_xml_content_info)); + xml_content_info = calloc(titleContentRecordsCnt, sizeof(cnmt_xml_content_info)); if (!xml_content_info) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the CNMT XML content info struct!"); goto out; } - if (R_FAILED(result = ncmOpenContentStorage(curStorageId, &ncmStorage))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentStorage failed! (0x%08X)", result); - goto out; - } - // Fill our CNMT XML content records, leaving the CNMT NCA at the end u32 titleRecordIndex; - for(i = 0, titleRecordIndex = 0; titleRecordIndex < titleNcaCount; i++, titleRecordIndex++) + for(i = 0, titleRecordIndex = 0; titleRecordIndex < titleContentRecordsCnt; i++, titleRecordIndex++) { if (!cnmtFound && titleContentRecords[titleRecordIndex].type == NcmContentType_CNMT) { @@ -1193,8 +1128,12 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd continue; } - // Skip Delta Fragments or any other unknown content types if we're not dealing with Patch titles dumped from installed SD/eMMC (with tiklessDump disabled) - if (titleContentRecords[titleRecordIndex].type >= NCA_CONTENT_TYPE_DELTA && (curStorageId == FsStorageId_GameCard || selectedNspDumpType != DUMP_PATCH_NSP || tiklessDump)) + // Skip Delta Fragments or any other unknown content types + // Delta Fragments are used to update from a certain version to another version without needing to install the whole update + // For any dumping purposes, they're useless, because they just increase the size of the output dump. The more updates come out for a title, the more Delta Fragments there will be available for that title + // Also, since they're basically an eShop thing, they're not available in gamecards. So in this particular case, we need to skip them anyway + // However, their content records must be kept intact in the CNMT NCA + if (titleContentRecords[titleRecordIndex].type >= NCA_CONTENT_TYPE_DELTA) { xml_program_info.nca_cnt--; i--; @@ -1210,7 +1149,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd memcpy(&ncaId, &(titleContentRecords[titleRecordIndex].ncaId), sizeof(NcmNcaId)); - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) + result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to read header from NCA \"%s\"! (0x%08X)", xml_content_info[i].nca_id_str, result); proceed = false; @@ -1225,6 +1165,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd break; } + // Check if this particular content has a populated Rights ID field bool has_rights_id = false; for(j = 0; j < 0x10; j++) @@ -1236,13 +1177,44 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } } + // Check if the missing ticket flag is enabled + // If so, we may be dealing with a preinstalled title + if (curStorageId != FsStorageId_GameCard && has_rights_id && rights_info.missing_tik && !preInstall) + { + // Only display the pre-install prompt if we're not running a batch / sequential dump operation (excluding the first run of the latter) + if (!batch && !seqDumpMode) + { + int cur_breaks = breaks; + breaks += 2; + + proceed = yesNoPrompt("This is probably a pre-installed title, which explains why a ticket for it couldn't be found (even though its Rights ID field isn't empty).\nDo you want to proceed with the dump procedure anyway?\nBear in mind that no content decryption will be possible for this title in its current status."); + if (!proceed) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled."); + break; + } else { + breaks = cur_breaks; + preInstall = true; + } + } + + // Remove the prompt / error from the screen + uiFill(0, 8 + STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - (8 + STRING_Y_POS(breaks)), BG_COLOR_RGB); + } + // Fill information for our CNMT XML xml_content_info[i].keyblob = (dec_nca_header.crypto_type2 > dec_nca_header.crypto_type ? dec_nca_header.crypto_type2 : dec_nca_header.crypto_type); if (curStorageId == FsStorageId_GameCard) { + // Modify content distribution type + // It's always set to 1 (gamecard) in Applications and AddOns bundled in gamecards + // It's always set to 0 (download) in Patches bundled in gamecards. But if we're dealing with a custom XCI mounted through SX OS, we may need to change that + dec_nca_header.distribution = 0; + if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_ADDON_NSP) { + // Application and AddOn titles don't have a populated Rights ID field when bundled in gamecards if (has_rights_id) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: Rights ID field in NCA header not empty!"); @@ -1250,11 +1222,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd break; } - // Modify distribution type - dec_nca_header.distribution = 0; - - // Patch ACID pubkey and recreate NCA NPDM signature if we're dealing with the Program NCA - if (xml_content_info[i].type == NcmContentType_Program) + // Patch ACID public RSA key and recreate the NCA NPDM signature if we're dealing with the Program NCA + if (xml_content_info[i].type == NcmContentType_Program && npdmAcidRsaPatch) { if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod)) { @@ -1265,20 +1234,23 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } else if (selectedNspDumpType == DUMP_PATCH_NSP) { - if (!has_rights_id) + // Patch titles *do* have a populated Rights ID field and a ticket + certificate chain combination when bundled in gamecards + // Depending on the dump settings, we may need to change or remove that + // If no Rights ID is available, we may be dealing with a custom XCI mounted through SX OS. In this particular case, no further modification should be needed + if (has_rights_id) { - // We could be dealing with a custom XCI mounted through SX OS, so let's change back the content distribution method - dec_nca_header.distribution = 0; - } else { + // Check if we have retrieved the ticket from the HFS0 partition in the gamecard if (!rights_info.retrieved_tik) { - // Retrieve tik file - if (!getPartitionHfs0FileByName(&gameCardStorage, rights_info.tik_filename, (u8*)(&(rights_info.tik_data)), ETICKET_TIK_FILE_SIZE)) + // Retrieve ticket. We're going to use our own certificate chain down the road, there's no need to retrieve it as well + if (!getFileFromHfs0PartitionByName(&gameCardStorage, rights_info.tik_filename, dumpBuf, ETICKET_TIK_FILE_SIZE)) { proceed = false; break; } + memcpy(&(rights_info.tik_data), dumpBuf, ETICKET_TIK_FILE_SIZE); + memcpy(rights_info.enc_titlekey, rights_info.tik_data.titlekey_block, 0x10); // Load external keys @@ -1299,10 +1271,11 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd rights_info.retrieved_tik = true; } + // Save the decrypted NCA key area keys memset(xml_content_info[i].decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); memcpy(xml_content_info[i].decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), rights_info.dec_titlekey, 0x10); - // Mess with the NCA header if we're dealing with a content with a populated rights ID field and if tiklessDump is true (removeConsoleData is ignored) + // Mess with the NCA header if we're dealing with a NCA with a populated Rights ID field and if tiklessDump is true (removeConsoleData is ignored) if (tiklessDump) { // Generate new encrypted NCA key area using titlekey @@ -1312,11 +1285,11 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd break; } - // Remove rights ID from NCA + // Remove Rights ID from NCA memset(dec_nca_header.rights_id, 0, 0x10); // Patch ACID pubkey and recreate NCA NPDM signature if we're dealing with the Program NCA - if (xml_content_info[i].type == NcmContentType_Program) + if (xml_content_info[i].type == NcmContentType_Program && npdmAcidRsaPatch) { if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod)) { @@ -1330,8 +1303,9 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } else if (curStorageId == FsStorageId_SdCard || curStorageId == FsStorageId_NandUser) { - // Only mess with the NCA header if we're dealing with a content with a populated rights ID field, and if both removeConsoleData and tiklessDump are true - if (has_rights_id && removeConsoleData && tiklessDump) + // Only mess with the NCA header if we're dealing with a content with a populated Rights ID field, and if both removeConsoleData and tiklessDump are true + // This will only be done if we were able to retrieve the ticket for this title + if (has_rights_id && rights_info.retrieved_tik && removeConsoleData && tiklessDump) { // Generate new encrypted NCA key area using titlekey if (!generateEncryptedNcaKeyAreaWithTitlekey(&dec_nca_header, xml_content_info[i].decrypted_nca_keys)) @@ -1344,7 +1318,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd memset(dec_nca_header.rights_id, 0, 0x10); // Patch ACID pubkey and recreate NCA NPDM signature if we're dealing with the Program NCA - if (xml_content_info[i].type == NcmContentType_Program) + if (xml_content_info[i].type == NcmContentType_Program && npdmAcidRsaPatch) { if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod)) { @@ -1355,39 +1329,42 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } } - // Generate programinfo.xml - if (!programInfoXml && xml_content_info[i].type == NcmContentType_Program) + if (!has_rights_id || (has_rights_id && rights_info.retrieved_tik)) { - programNcaIndex = i; - - if (!generateProgramInfoXml(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &ncaProgramMod, &programInfoXml, &programInfoXmlSize)) + // Generate programinfo.xml + if (!programInfoXml && xml_content_info[i].type == NcmContentType_Program) { - proceed = false; - break; + programNcaIndex = i; + + if (!generateProgramInfoXml(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &ncaProgramMod, &programInfoXml, &programInfoXmlSize)) + { + proceed = false; + break; + } } - } - - // Retrieve NACP data (XML and icons) - if (!nacpXml && xml_content_info[i].type == NcmContentType_Icon) - { - nacpNcaIndex = i; - if (!retrieveNacpDataFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &nacpXml, &nacpXmlSize, &nacpIcons, &nacpIconCnt)) + // Retrieve NACP data (XML and icons) + if (!nacpXml && xml_content_info[i].type == NcmContentType_Icon) { - proceed = false; - break; + nacpNcaIndex = i; + + if (!retrieveNacpDataFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &nacpXml, &nacpXmlSize, &nacpIcons, &nacpIconCnt)) + { + proceed = false; + break; + } } - } - - // Retrieve legalinfo.xml - if (!legalInfoXml && xml_content_info[i].type == NcmContentType_Info) - { - legalInfoNcaIndex = i; - if (!retrieveLegalInfoXmlFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &legalInfoXml, &legalInfoXmlSize)) + // Retrieve legalinfo.xml + if (!legalInfoXml && xml_content_info[i].type == NcmContentType_Info) { - proceed = false; - break; + legalInfoNcaIndex = i; + + if (!retrieveLegalInfoXmlFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &legalInfoXml, &legalInfoXmlSize)) + { + proceed = false; + break; + } } } @@ -1408,19 +1385,19 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } // Update NCA counter just in case we found any delta fragments - titleNcaCount = xml_program_info.nca_cnt; + titleContentRecordsCnt = xml_program_info.nca_cnt; // Fill information for our CNMT XML - xml_content_info[titleNcaCount - 1].type = titleContentRecords[cnmtNcaIndex].type; - memcpy(xml_content_info[titleNcaCount - 1].nca_id, titleContentRecords[cnmtNcaIndex].ncaId.c, SHA256_HASH_SIZE / 2); // Temporary - convertDataToHexString(titleContentRecords[cnmtNcaIndex].ncaId.c, SHA256_HASH_SIZE / 2, xml_content_info[titleNcaCount - 1].nca_id_str, SHA256_HASH_SIZE + 1); // Temporary - convertNcaSizeToU64(titleContentRecords[cnmtNcaIndex].size, &(xml_content_info[titleNcaCount - 1].size)); - convertDataToHexString(xml_content_info[titleNcaCount - 1].hash, SHA256_HASH_SIZE, xml_content_info[titleNcaCount - 1].hash_str, (SHA256_HASH_SIZE * 2) + 1); // Temporary + xml_content_info[titleContentRecordsCnt - 1].type = titleContentRecords[cnmtNcaIndex].type; + memcpy(xml_content_info[titleContentRecordsCnt - 1].nca_id, titleContentRecords[cnmtNcaIndex].ncaId.c, SHA256_HASH_SIZE / 2); // Temporary + convertDataToHexString(titleContentRecords[cnmtNcaIndex].ncaId.c, SHA256_HASH_SIZE / 2, xml_content_info[titleContentRecordsCnt - 1].nca_id_str, SHA256_HASH_SIZE + 1); // Temporary + convertNcaSizeToU64(titleContentRecords[cnmtNcaIndex].size, &(xml_content_info[titleContentRecordsCnt - 1].size)); + convertDataToHexString(xml_content_info[titleContentRecordsCnt - 1].hash, SHA256_HASH_SIZE, xml_content_info[titleContentRecordsCnt - 1].hash_str, (SHA256_HASH_SIZE * 2) + 1); // Temporary memcpy(&ncaId, &(titleContentRecords[cnmtNcaIndex].ncaId), sizeof(NcmNcaId)); // Update CNMT index - cnmtNcaIndex = (titleNcaCount - 1); + cnmtNcaIndex = (titleContentRecordsCnt - 1); cnmtNcaBuf = malloc(xml_content_info[cnmtNcaIndex].size); if (!cnmtNcaBuf) @@ -1429,14 +1406,15 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd goto out; } - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size))) + result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to read CNMT NCA \"%s\"! (0x%08X)", xml_content_info[cnmtNcaIndex].nca_id_str, result); goto out; } // Retrieve CNMT NCA data - if (!retrieveCnmtNcaData(curStorageId, selectedNspDumpType, cnmtNcaBuf, &xml_program_info, &(xml_content_info[cnmtNcaIndex]), &ncaCnmtMod, &rights_info, (removeConsoleData && tiklessDump))) goto out; + if (!retrieveCnmtNcaData(curStorageId, selectedNspDumpType, cnmtNcaBuf, &xml_program_info, xml_content_info, cnmtNcaIndex, &ncaCnmtMod, &rights_info, (removeConsoleData && tiklessDump))) goto out; // Generate a placeholder CNMT XML. It's length will be used to calculate the final output dump size /*breaks++; @@ -1454,14 +1432,14 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd generateCnmtXml(&xml_program_info, xml_content_info, cnmtXml); - bool includeTikAndCert = (rights_info.has_rights_id && !tiklessDump); + bool includeTikAndCert = (rights_info.retrieved_tik && !tiklessDump); if (includeTikAndCert) { if (curStorageId == FsStorageId_GameCard) { // Ticket files from Patch titles bundled with gamecards have a different layout - // Let's convert it to a normal "common" ticket + // Let's convert it to a "normal" common ticket memset(rights_info.tik_data.signature, 0xFF, 0x100); memset((u8*)(&(rights_info.tik_data)) + 0x190, 0, 0x130); @@ -1477,35 +1455,25 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } else if (curStorageId == FsStorageId_SdCard || curStorageId == FsStorageId_NandUser) { - // Only mess with the ticket data if removeConsoleData is true, if tiklessDump is false and if we're dealing with a personalized ticket - if (removeConsoleData && rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_PERSONALIZED) - { - memset(rights_info.tik_data.signature, 0xFF, 0x100); - - memset(rights_info.tik_data.sig_issuer, 0, 0x40); - sprintf(rights_info.tik_data.sig_issuer, "Root-CA00000003-XS00000020"); - - memset(rights_info.tik_data.titlekey_block, 0, 0x100); - memcpy(rights_info.tik_data.titlekey_block, rights_info.enc_titlekey, 0x10); - - rights_info.tik_data.titlekey_type = ETICKET_TITLEKEY_COMMON; - rights_info.tik_data.ticket_id = 0; - rights_info.tik_data.device_id = 0; - rights_info.tik_data.account_id = 0; - } + // Only mess with the ticket data if removeConsoleData is true, if tiklessDump is false and if we're dealing with a personalized ticket (checked in removeConsoleDataFromTicket()) + if (removeConsoleData) removeConsoleDataFromTicket(&rights_info); } // Retrieve cert file - if (!retrieveCertData(rights_info.cert_data, (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_PERSONALIZED))) goto out; + if (!retrieveCertData(rights_info.cert_data, (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_PERSONALIZED))) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf); + goto out; + } // File count = NCA count + CNMT XML + tik + cert - nspFileCount = (titleNcaCount + 3); + nspFileCount = (titleContentRecordsCnt + 3); // Calculate PFS0 String Table size nspPfs0StrTableSize = (((nspFileCount - 4) * NSP_NCA_FILENAME_LENGTH) + (NSP_CNMT_FILENAME_LENGTH * 2) + NSP_TIK_FILENAME_LENGTH + NSP_CERT_FILENAME_LENGTH); } else { // File count = NCA count + CNMT XML - nspFileCount = (titleNcaCount + 1); + nspFileCount = (titleContentRecordsCnt + 1); // Calculate PFS0 String Table size nspPfs0StrTableSize = (((nspFileCount - 2) * NSP_NCA_FILENAME_LENGTH) + (NSP_CNMT_FILENAME_LENGTH * 2)); @@ -1580,7 +1548,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd // Calculate total dump size progressCtx.totalSize = full_nsp_header_size; - for(i = 0; i < titleNcaCount; i++) progressCtx.totalSize += xml_content_info[i].size; + for(i = 0; i < titleContentRecordsCnt; i++) progressCtx.totalSize += xml_content_info[i].size; progressCtx.totalSize += strlen(cnmtXml); @@ -1619,7 +1587,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd { // Check if the NCA count is valid // The CNMT NCA is excluded from the hash list - if (seqNspCtx.ncaCount == (titleNcaCount - 1)) + if (seqNspCtx.ncaCount == (titleContentRecordsCnt - 1)) { // Check if the PFS0 file count is valid if (seqNspCtx.nspFileCount == nspFileCount) @@ -1627,129 +1595,186 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd // Check if the current PFS0 file index is valid if (seqNspCtx.fileIndex < nspFileCount) { - for(i = 0; i < nspFileCount; i++) + // Check if we're really dealing with a title with a missing ticket if preInstall == true + if (!seqNspCtx.preInstall || (seqNspCtx.preInstall && rights_info.missing_tik)) { - if (i < seqNspCtx.fileIndex) + // Check if the current overall offset is aligned to SPLIT_FILE_SEQUENTIAL_SIZE + u64 curNspOffset = full_nsp_header_size; + + for(i = 0; i < seqNspCtx.fileIndex; i++) { - // Exclude the CNMT NCA - if (i < (titleNcaCount - 1)) + if (i < titleContentRecordsCnt) { - // Fill information for our CNMT XML - memcpy(xml_content_info[i].nca_id, seqDumpNcaHashes + (i * SHA256_HASH_SIZE), SHA256_HASH_SIZE / 2); - convertDataToHexString(xml_content_info[i].nca_id, SHA256_HASH_SIZE / 2, xml_content_info[i].nca_id_str, SHA256_HASH_SIZE + 1); - memcpy(xml_content_info[i].hash, seqDumpNcaHashes + (i * SHA256_HASH_SIZE), SHA256_HASH_SIZE); - convertDataToHexString(xml_content_info[i].hash, SHA256_HASH_SIZE, xml_content_info[i].hash_str, (SHA256_HASH_SIZE * 2) + 1); - } - } else { - if (i < titleNcaCount) - { - // Check if the offset for the current NCA is valid - if (seqNspCtx.fileOffset < xml_content_info[i].size) - { - // Copy the SHA-256 context data, but only if we're not dealing with the CNMT NCA - // NCA ID/hash for the CNMT NCA is handled in patchCnmtNca() - if (i != cnmtNcaIndex) memcpy(&nca_hash_ctx, &(seqNspCtx.hashCtx), sizeof(Sha256Context)); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NCA offset in the sequential dump reference file!"); - proceed = false; - } + curNspOffset += xml_content_info[i].size; } else - if (i == titleNcaCount) + if (i == titleContentRecordsCnt) { - // Check if the offset for the CNMT XML is valid - if (seqNspCtx.fileOffset >= strlen(cnmtXml)) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid CNMT XML offset in the sequential dump reference file!"); - proceed = false; - } + curNspOffset += strlen(cnmtXml); + } else + if (programInfoXml && i == (titleContentRecordsCnt + 1)) + { + curNspOffset += programInfoXmlSize; + } else + if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleContentRecordsCnt + nacpIconCnt)) || (programInfoXml && i <= (titleContentRecordsCnt + 1 + nacpIconCnt)))) + { + u32 icon_idx = (!programInfoXml ? (i - (titleContentRecordsCnt + 1)) : (i - (titleContentRecordsCnt + 2))); + curNspOffset += nacpIcons[icon_idx].icon_size; + } else + if (nacpXml && ((!programInfoXml && i == (titleContentRecordsCnt + nacpIconCnt + 1)) || (programInfoXml && i == (titleContentRecordsCnt + 1 + nacpIconCnt + 1)))) + { + curNspOffset += nacpXmlSize; + } else + if (legalInfoXml && ((!includeTikAndCert && i == (nspFileCount - 1)) || (includeTikAndCert && i == (nspFileCount - 3)))) + { + curNspOffset += legalInfoXmlSize; } else { - if (programInfoXml && i == (titleNcaCount + 1)) + if (i == (nspFileCount - 2)) { - // Check if the offset for the programinfo.xml is valid - if (seqNspCtx.fileOffset >= programInfoXmlSize) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid programinfo.xml offset in the sequential dump reference file!"); - proceed = false; - } - } else - if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleNcaCount + nacpIconCnt)) || (programInfoXml && i <= (titleNcaCount + 1 + nacpIconCnt)))) + curNspOffset += ETICKET_TIK_FILE_SIZE; + } else { + curNspOffset += ETICKET_CERT_FILE_SIZE; + } + } + } + + curNspOffset += seqNspCtx.fileOffset; + + if (!(curNspOffset % SPLIT_FILE_SEQUENTIAL_SIZE)) + { + // Now check if the current PFS0 file entry offset is correct + // Probably overkill but it's better to be safe than sorry + + for(i = 0; i < nspFileCount; i++) + { + if (i < seqNspCtx.fileIndex) { - // Check if the offset for the NACP icon is valid - u32 icon_idx = (!programInfoXml ? (i - (titleNcaCount + 1)) : (i - (titleNcaCount + 2))); - if (seqNspCtx.fileOffset >= nacpIcons[icon_idx].icon_size) + // Exclude the CNMT NCA + if (i < (titleContentRecordsCnt - 1)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NACP icon offset in the sequential dump reference file!"); - proceed = false; - } - } else - if (nacpXml && ((!programInfoXml && i == (titleNcaCount + nacpIconCnt + 1)) || (programInfoXml && i == (titleNcaCount + 1 + nacpIconCnt + 1)))) - { - // Check if the offset for the NACP XML is valid - if (seqNspCtx.fileOffset >= nacpXmlSize) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NACP XML offset in the sequential dump reference file!"); - proceed = false; - } - } else - if (legalInfoXml && ((!includeTikAndCert && i == (nspFileCount - 1)) || (includeTikAndCert && i == (nspFileCount - 3)))) - { - // Check if the offset for the legalinfo.xml is valid - if (seqNspCtx.fileOffset >= legalInfoXmlSize) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid legalinfo.xml offset in the sequential dump reference file!"); - proceed = false; + // Fill information for our CNMT XML + memcpy(xml_content_info[i].nca_id, seqDumpNcaHashes + (i * SHA256_HASH_SIZE), SHA256_HASH_SIZE / 2); + convertDataToHexString(xml_content_info[i].nca_id, SHA256_HASH_SIZE / 2, xml_content_info[i].nca_id_str, SHA256_HASH_SIZE + 1); + memcpy(xml_content_info[i].hash, seqDumpNcaHashes + (i * SHA256_HASH_SIZE), SHA256_HASH_SIZE); + convertDataToHexString(xml_content_info[i].hash, SHA256_HASH_SIZE, xml_content_info[i].hash_str, (SHA256_HASH_SIZE * 2) + 1); } } else { - if (i == (nspFileCount - 2)) + if (i < titleContentRecordsCnt) { - // Check if the offset for the ticket is valid - if (seqNspCtx.fileOffset >= ETICKET_TIK_FILE_SIZE) + // Check if the offset for the current NCA is valid + if (seqNspCtx.fileOffset < xml_content_info[i].size) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid ticket offset in the sequential dump reference file!"); + // Copy the SHA-256 context data, but only if we're not dealing with the CNMT NCA + // NCA ID/hash for the CNMT NCA is handled in patchCnmtNca() + if (i != cnmtNcaIndex) memcpy(&nca_hash_ctx, &(seqNspCtx.hashCtx), sizeof(Sha256Context)); + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NCA offset in the sequential dump reference file!"); + proceed = false; + } + } else + if (i == titleContentRecordsCnt) + { + // Check if the offset for the CNMT XML is valid + if (seqNspCtx.fileOffset >= strlen(cnmtXml)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid CNMT XML offset in the sequential dump reference file!"); proceed = false; } } else { - // Check if the offset for the certificate chain is valid - if (seqNspCtx.fileOffset >= ETICKET_CERT_FILE_SIZE) + if (programInfoXml && i == (titleContentRecordsCnt + 1)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid certificate chain offset in the sequential dump reference file!"); - proceed = false; + // Check if the offset for the programinfo.xml is valid + if (seqNspCtx.fileOffset >= programInfoXmlSize) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid programinfo.xml offset in the sequential dump reference file!"); + proceed = false; + } + } else + if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleContentRecordsCnt + nacpIconCnt)) || (programInfoXml && i <= (titleContentRecordsCnt + 1 + nacpIconCnt)))) + { + // Check if the offset for the NACP icon is valid + u32 icon_idx = (!programInfoXml ? (i - (titleContentRecordsCnt + 1)) : (i - (titleContentRecordsCnt + 2))); + if (seqNspCtx.fileOffset >= nacpIcons[icon_idx].icon_size) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NACP icon offset in the sequential dump reference file!"); + proceed = false; + } + } else + if (nacpXml && ((!programInfoXml && i == (titleContentRecordsCnt + nacpIconCnt + 1)) || (programInfoXml && i == (titleContentRecordsCnt + 1 + nacpIconCnt + 1)))) + { + // Check if the offset for the NACP XML is valid + if (seqNspCtx.fileOffset >= nacpXmlSize) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NACP XML offset in the sequential dump reference file!"); + proceed = false; + } + } else + if (legalInfoXml && ((!includeTikAndCert && i == (nspFileCount - 1)) || (includeTikAndCert && i == (nspFileCount - 3)))) + { + // Check if the offset for the legalinfo.xml is valid + if (seqNspCtx.fileOffset >= legalInfoXmlSize) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid legalinfo.xml offset in the sequential dump reference file!"); + proceed = false; + } + } else { + if (i == (nspFileCount - 2)) + { + // Check if the offset for the ticket is valid + if (seqNspCtx.fileOffset >= ETICKET_TIK_FILE_SIZE) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid ticket offset in the sequential dump reference file!"); + proceed = false; + } + } else { + // Check if the offset for the certificate chain is valid + if (seqNspCtx.fileOffset >= ETICKET_CERT_FILE_SIZE) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid certificate chain offset in the sequential dump reference file!"); + proceed = false; + } + } } } + + break; } } - break; - } - } - - if (proceed) - { - // Restore the modified Program NCA header - // The NPDM signature from the NCA header is generated using cryptographically secure random numbers, so the modified header is stored during the first sequential dump session - // If needed, it must be restored in later sessions - if (ncaProgramMod.block_mod_cnt) memcpy(xml_content_info[programNcaIndex].encrypted_header_mod, &(seqNspCtx.programNcaHeaderMod), NCA_FULL_HEADER_LENGTH); - - // Inform that we are resuming an already started sequential dump operation - breaks++; - - if (curStorageId == FsStorageId_GameCard) - { - if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_ADDON_NSP) + if (proceed) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation."); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided."); + // Restore the modified Program NCA header + // The NPDM signature from the NCA header is generated using cryptographically secure random numbers, so the modified header is stored during the first sequential dump session + // If needed, it must be restored in later sessions + if (ncaProgramMod.block_mod_cnt) memcpy(xml_content_info[programNcaIndex].encrypted_header_mod, &(seqNspCtx.programNcaHeaderMod), NCA_FULL_HEADER_LENGTH); + + // Inform that we are resuming an already started sequential dump operation + breaks++; + + if (curStorageId == FsStorageId_GameCard) + { + if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_ADDON_NSP) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation."); + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided."); + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Generate ticket-less dump: %s.", (tiklessDump ? "Yes" : "No")); + } + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided."); + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Remove console specific data: %s | Generate ticket-less dump: %s.", (removeConsoleData ? "Yes" : "No"), (tiklessDump ? "Yes" : "No")); + } + breaks++; - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Generate ticket-less dump: %s.", (tiklessDump ? "Yes" : "No")); } } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided."); - breaks++; - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Remove console specific data: %s | Generate ticket-less dump: %s.", (removeConsoleData ? "Yes" : "No"), (tiklessDump ? "Yes" : "No")); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: overall NSP dump offset isn't aligned to 0x%08X in the sequential dump reference file!", (u32)SPLIT_FILE_SEQUENTIAL_SIZE); + proceed = false; } - - breaks++; + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title preinstall status in the sequential dump reference file!"); + proceed = false; } } else { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid PFS0 file index in the sequential dump reference file!"); @@ -1760,7 +1785,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd proceed = false; } } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: NCA count mismatch in the sequential dump reference file! (%u != %u)", seqNspCtx.ncaCount, titleNcaCount - 1); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: NCA count mismatch in the sequential dump reference file! (%u != %u)", seqNspCtx.ncaCount, titleContentRecordsCnt - 1); proceed = false; } } else { @@ -1774,9 +1799,9 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } else { if (progressCtx.totalSize > freeSpace) { - // Check if we have at least (SPLIT_FILE_SEQUENTIAL_SIZE + (sizeof(sequentialNspCtx) + ((titleNcaCount - 1) * SHA256_HASH_SIZE))) of free space + // Check if we have at least (SPLIT_FILE_SEQUENTIAL_SIZE + (sizeof(sequentialNspCtx) + ((titleContentRecordsCnt - 1) * SHA256_HASH_SIZE))) of free space // The CNMT NCA is excluded from the hash list - if (freeSpace >= (SPLIT_FILE_SEQUENTIAL_SIZE + (sizeof(sequentialNspCtx) + ((titleNcaCount - 1) * SHA256_HASH_SIZE)))) + if (freeSpace >= (SPLIT_FILE_SEQUENTIAL_SIZE + (sizeof(sequentialNspCtx) + ((titleContentRecordsCnt - 1) * SHA256_HASH_SIZE)))) { // Ask the user if they want to use the sequential dump mode int cur_breaks = breaks; @@ -1796,21 +1821,23 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd part_size = SPLIT_FILE_SEQUENTIAL_SIZE; seqDumpMode = true; - seqDumpFileSize = (sizeof(sequentialNspCtx) + ((titleNcaCount - 1) * SHA256_HASH_SIZE)); + seqDumpFileSize = (sizeof(sequentialNspCtx) + ((titleContentRecordsCnt - 1) * SHA256_HASH_SIZE)); // Fill information in our sequential context seqNspCtx.storageId = curStorageId; seqNspCtx.removeConsoleData = removeConsoleData; seqNspCtx.tiklessDump = tiklessDump; + seqNspCtx.npdmAcidRsaPatch = npdmAcidRsaPatch; + seqNspCtx.preInstall = preInstall; seqNspCtx.nspFileCount = nspFileCount; - seqNspCtx.ncaCount = (titleNcaCount - 1); // Exclude the CNMT NCA from the hash list + seqNspCtx.ncaCount = (titleContentRecordsCnt - 1); // Exclude the CNMT NCA from the hash list // Store the modified Program NCA header // The NPDM signature from the NCA header is generated using cryptographically secure random numbers, so we must store the modified header during the first sequential dump session if (ncaProgramMod.block_mod_cnt) memcpy(&(seqNspCtx.programNcaHeaderMod), xml_content_info[programNcaIndex].encrypted_header_mod, NCA_FULL_HEADER_LENGTH); // Allocate memory for the NCA hashes - seqDumpNcaHashes = calloc(1, (titleNcaCount - 1) * SHA256_HASH_SIZE); + seqDumpNcaHashes = calloc(1, (titleContentRecordsCnt - 1) * SHA256_HASH_SIZE); if (seqDumpNcaHashes) { // Create sequential reference file and keep the handle to it opened @@ -1830,7 +1857,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd // Update free space freeSpace -= seqDumpFileSize; } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", titleNcaCount * SHA256_HASH_SIZE, write_res); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", titleContentRecordsCnt * SHA256_HASH_SIZE, write_res); proceed = false; seqDumpFileRemove = true; } @@ -1929,6 +1956,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd breaks += 2; } + memset(dumpBuf, 0, DUMP_BUFFER_SIZE); + if (seqDumpMode) { // Skip the PFS0 header in the first part file @@ -1956,7 +1985,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd u64 startFileOffset; // Dump all NCAs excluding the CNMT NCA - for(i = startFileIndex; i < (titleNcaCount - 1); i++, startFileIndex++) + for(i = startFileIndex; i < (titleContentRecordsCnt - 1); i++, startFileIndex++) { n = DUMP_BUFFER_SIZE; @@ -1996,7 +2025,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } } - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, nca_offset, dumpBuf, n))) + result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, nca_offset, dumpBuf, n); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Failed to read %lu bytes chunk at offset 0x%016lX from NCA \"%s\"! (0x%08X)", n, nca_offset, xml_content_info[i].nca_id_str, result); proceed = false; @@ -2195,13 +2225,13 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd char ncaFileName[100] = {'\0'}; u64 cur_file_size = 0; - if (i < titleNcaCount) + if (i < titleContentRecordsCnt) { - // Always reserve the first titleNcaCount entries for our NCA contents + // Always reserve the first titleContentRecordsCnt entries for our NCA contents sprintf(ncaFileName, "%s.%s", xml_content_info[i].nca_id_str, (i == cnmtNcaIndex ? "cnmt.nca" : "nca")); cur_file_size = xml_content_info[i].size; } else - if (i == titleNcaCount) + if (i == titleContentRecordsCnt) { // Reserve the entry right after our NCA contents for the CNMT XML sprintf(ncaFileName, "%s.cnmt.xml", xml_content_info[cnmtNcaIndex].nca_id_str); @@ -2215,21 +2245,21 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd // Ticket (if available) // Certificate chain (if available) - if (programInfoXml && i == (titleNcaCount + 1)) + if (programInfoXml && i == (titleContentRecordsCnt + 1)) { // programinfo.xml entry sprintf(ncaFileName, "%s.programinfo.xml", xml_content_info[programNcaIndex].nca_id_str); cur_file_size = programInfoXmlSize; } else - if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleNcaCount + nacpIconCnt)) || (programInfoXml && i <= (titleNcaCount + 1 + nacpIconCnt)))) + if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleContentRecordsCnt + nacpIconCnt)) || (programInfoXml && i <= (titleContentRecordsCnt + 1 + nacpIconCnt)))) { // NACP icon entry // Replace the NCA ID from its filename, since it could have changed - u32 icon_idx = (!programInfoXml ? (i - (titleNcaCount + 1)) : (i - (titleNcaCount + 2))); + u32 icon_idx = (!programInfoXml ? (i - (titleContentRecordsCnt + 1)) : (i - (titleContentRecordsCnt + 2))); sprintf(ncaFileName, "%s%s", xml_content_info[nacpNcaIndex].nca_id_str, strchr(nacpIcons[icon_idx].filename, '.')); cur_file_size = nacpIcons[icon_idx].icon_size; } else - if (nacpXml && ((!programInfoXml && i == (titleNcaCount + nacpIconCnt + 1)) || (programInfoXml && i == (titleNcaCount + 1 + nacpIconCnt + 1)))) + if (nacpXml && ((!programInfoXml && i == (titleContentRecordsCnt + nacpIconCnt + 1)) || (programInfoXml && i == (titleContentRecordsCnt + 1 + nacpIconCnt + 1)))) { // NACP XML entry // If there are no icons, this will effectively make it the next entry after the CNMT XML @@ -2355,7 +2385,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd fseek(outFile, 0, SEEK_END); } - startFileIndex = ((seqDumpMode && seqNspCtx.fileIndex > (titleNcaCount - 1)) ? seqNspCtx.fileIndex : (titleNcaCount - 1)); + startFileIndex = ((seqDumpMode && seqNspCtx.fileIndex > (titleContentRecordsCnt - 1)) ? seqNspCtx.fileIndex : (titleContentRecordsCnt - 1)); // Now let's write the rest of the data, including our modified CNMT NCA for(i = startFileIndex; i < nspFileCount; i++, startFileIndex++) @@ -2367,32 +2397,32 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd char ncaFileName[100] = {'\0'}; u64 cur_file_size = 0; - if (i == (titleNcaCount - 1)) + if (i == (titleContentRecordsCnt - 1)) { // CNMT NCA sprintf(ncaFileName, "%s.cnmt.nca", xml_content_info[i].nca_id_str); cur_file_size = xml_content_info[cnmtNcaIndex].size; } else - if (i == titleNcaCount) + if (i == titleContentRecordsCnt) { // CNMT XML sprintf(ncaFileName, "%s.cnmt.xml", xml_content_info[cnmtNcaIndex].nca_id_str); cur_file_size = strlen(cnmtXml); } else { - if (programInfoXml && i == (titleNcaCount + 1)) + if (programInfoXml && i == (titleContentRecordsCnt + 1)) { // programinfo.xml entry sprintf(ncaFileName, "%s.programinfo.xml", xml_content_info[programNcaIndex].nca_id_str); cur_file_size = programInfoXmlSize; } else - if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleNcaCount + nacpIconCnt)) || (programInfoXml && i <= (titleNcaCount + 1 + nacpIconCnt)))) + if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleContentRecordsCnt + nacpIconCnt)) || (programInfoXml && i <= (titleContentRecordsCnt + 1 + nacpIconCnt)))) { // NACP icon entry - u32 icon_idx = (!programInfoXml ? (i - (titleNcaCount + 1)) : (i - (titleNcaCount + 2))); + u32 icon_idx = (!programInfoXml ? (i - (titleContentRecordsCnt + 1)) : (i - (titleContentRecordsCnt + 2))); sprintf(ncaFileName, nacpIcons[icon_idx].filename); cur_file_size = nacpIcons[icon_idx].icon_size; } else - if (nacpXml && ((!programInfoXml && i == (titleNcaCount + nacpIconCnt + 1)) || (programInfoXml && i == (titleNcaCount + 1 + nacpIconCnt + 1)))) + if (nacpXml && ((!programInfoXml && i == (titleContentRecordsCnt + nacpIconCnt + 1)) || (programInfoXml && i == (titleContentRecordsCnt + 1 + nacpIconCnt + 1)))) { // NACP XML entry sprintf(ncaFileName, "%s.nacp.xml", xml_content_info[nacpNcaIndex].nca_id_str); @@ -2443,28 +2473,28 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } // Retrieve data from its respective source - if (i == (titleNcaCount - 1)) + if (i == (titleContentRecordsCnt - 1)) { // CNMT NCA memcpy(dumpBuf, cnmtNcaBuf + nca_offset, n); } else - if (i == titleNcaCount) + if (i == titleContentRecordsCnt) { // CNMT XML memcpy(dumpBuf, cnmtXml + nca_offset, n); } else { - if (programInfoXml && i == (titleNcaCount + 1)) + if (programInfoXml && i == (titleContentRecordsCnt + 1)) { // programinfo.xml entry memcpy(dumpBuf, programInfoXml + nca_offset, n); } else - if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleNcaCount + nacpIconCnt)) || (programInfoXml && i <= (titleNcaCount + 1 + nacpIconCnt)))) + if (nacpIcons && nacpIconCnt && ((!programInfoXml && i <= (titleContentRecordsCnt + nacpIconCnt)) || (programInfoXml && i <= (titleContentRecordsCnt + 1 + nacpIconCnt)))) { // NACP icon entry - u32 icon_idx = (!programInfoXml ? (i - (titleNcaCount + 1)) : (i - (titleNcaCount + 2))); + u32 icon_idx = (!programInfoXml ? (i - (titleContentRecordsCnt + 1)) : (i - (titleContentRecordsCnt + 2))); memcpy(dumpBuf, nacpIcons[icon_idx].icon_data + nca_offset, n); } else - if (nacpXml && ((!programInfoXml && i == (titleNcaCount + nacpIconCnt + 1)) || (programInfoXml && i == (titleNcaCount + 1 + nacpIconCnt + 1)))) + if (nacpXml && ((!programInfoXml && i == (titleContentRecordsCnt + nacpIconCnt + 1)) || (programInfoXml && i == (titleContentRecordsCnt + 1 + nacpIconCnt + 1)))) { // NACP XML entry memcpy(dumpBuf, nacpXml + nca_offset, n); @@ -2755,7 +2785,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd if (!seqDumpMode && progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { snprintf(dumpPath, MAX_ELEMENTS(dumpPath), "%s%s.nsp", NSP_DUMP_PATH, dumpName); - if (R_FAILED(result = fsdevSetArchiveBit(dumpPath))) + result = fsdevSetArchiveBit(dumpPath); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Warning: failed to set archive bit on output directory! (0x%08X)", result); breaks += 2; @@ -2781,7 +2812,7 @@ out: // Copy the SHA-256 context data, but only if we're not dealing with the CNMT NCA // NCA ID/hash for the CNMT NCA is handled in patchCnmtNca() - if (seqNspCtx.fileIndex < titleNcaCount && seqNspCtx.fileIndex != cnmtNcaIndex) + if (seqNspCtx.fileIndex < titleContentRecordsCnt && seqNspCtx.fileIndex != cnmtNcaIndex) { memcpy(&(seqNspCtx.hashCtx), &nca_hash_ctx, sizeof(Sha256Context)); } else { @@ -2902,15 +2933,9 @@ out: if (programInfoXml) free(programInfoXml); - serviceClose(&(ncmStorage.s)); - if (xml_content_info) free(xml_content_info); - if (titleContentRecords) free(titleContentRecords); - - serviceClose(&(ncmDb.s)); - - if (titleList) free(titleList); + serviceClose(&(ncmStorage.s)); if (curStorageId == FsStorageId_GameCard) { @@ -2927,6 +2952,8 @@ out: } } + if (titleContentRecords) free(titleContentRecords); + if (seqDumpNcaHashes) free(seqDumpNcaHashes); if (seqDumpFile) fclose(seqDumpFile); @@ -2953,9 +2980,10 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) bool isFat32 = batchDumpCfg->isFat32; bool removeConsoleData = batchDumpCfg->removeConsoleData; bool tiklessDump = batchDumpCfg->tiklessDump; + bool npdmAcidRsaPatch = batchDumpCfg->npdmAcidRsaPatch; bool skipDumpedTitles = batchDumpCfg->skipDumpedTitles; - batchModeSourceStorage batchModeSrc = batchDumpCfg->batchModeSrc; bool rememberDumpedTitles = batchDumpCfg->rememberDumpedTitles; + batchModeSourceStorage batchModeSrc = batchDumpCfg->batchModeSrc; if ((!dumpAppTitles && !dumpPatchTitles && !dumpAddOnTitles) || (batchModeSrc == BATCH_SOURCE_ALL && ((dumpAppTitles && !titleAppCount) || (dumpPatchTitles && !titlePatchCount) || (dumpAddOnTitles && !titleAddOnCount))) || (batchModeSrc == BATCH_SOURCE_SDCARD && ((dumpAppTitles && !sdCardTitleAppCount) || (dumpPatchTitles && !sdCardTitlePatchCount) || (dumpAddOnTitles && !sdCardTitleAddOnCount))) || (batchModeSrc == BATCH_SOURCE_EMMC && ((dumpAppTitles && !nandUserTitleAppCount) || (dumpPatchTitles && !nandUserTitlePatchCount) || (dumpAddOnTitles && !nandUserTitleAddOnCount))) || batchModeSrc >= BATCH_SOURCE_CNT) { @@ -2971,7 +2999,7 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) u32 titleCount, titleIndex; char *dumpName = NULL; - char dumpPath[NAME_BUF_LEN] = {'\0'}; + char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; char summary_str[256] = {'\0'}; int initial_breaks = breaks, cur_breaks; @@ -2993,6 +3021,7 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) nspDumpCfg.calcCrc = false; nspDumpCfg.removeConsoleData = removeConsoleData; nspDumpCfg.tiklessDump = tiklessDump; + nspDumpCfg.npdmAcidRsaPatch = npdmAcidRsaPatch; // Allocate memory for the batch entries if (dumpAppTitles) maxEntryCount += (batchModeSrc == BATCH_SOURCE_ALL ? titleAppCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitleAppCount : nandUserTitleAppCount)); @@ -3074,6 +3103,9 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) totalTitleCount += totalAppCount; } + // Retrieve information for orphan entries + generateOrphanPatchOrAddOnList(); + if (dumpPatchTitles) { titleCount = (batchModeSrc == BATCH_SOURCE_ALL ? titlePatchCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitlePatchCount : nandUserTitlePatchCount)); @@ -3212,7 +3244,7 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "You have already dumped all titles matching the selected settings!"); breaks += 2; - return false; + goto out; } // Display summary controls @@ -3424,7 +3456,7 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) goto out; } - // Substract the disabled entries from the total title count + // Calculate the disabled entry count for(i = 0; i < totalTitleCount; i++) { if (!batchEntries[i].enabled) disabledEntryCount++; @@ -3471,6 +3503,9 @@ bool dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg) out: free(batchEntries); + + freeOrphanPatchOrAddOnList(); + return success; } @@ -3503,7 +3538,8 @@ bool dumpRawHfs0Partition(u32 partition, bool doSplitting) workaroundPartitionZeroAccess(); - if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_SUCCEEDED(result)) { /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "GetGameCardHandle succeeded: 0x%08X", handle.value); breaks++;*/ @@ -3517,7 +3553,8 @@ bool dumpRawHfs0Partition(u32 partition, bool doSplitting) // Oddly enough, IFileSystem instances actually point to the specified partition ID filesystem. I don't understand why it doesn't work like that for IStorage, but whatever // NOTE: Using partition == 2 returns error 0x149002, and using higher values probably do so, too - if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)))) + result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)); + if (R_SUCCEEDED(result)) { /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "OpenGameCardStorage succeeded: 0x%08X", handle.value); breaks++;*/ @@ -3584,7 +3621,8 @@ bool dumpRawHfs0Partition(u32 partition, bool doSplitting) if (n > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset); - if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset + progressCtx.curOffset, dumpBuf, n))) + result = fsStorageRead(&gameCardStorage, partitionOffset + progressCtx.curOffset, dumpBuf, n); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionOffset + progressCtx.curOffset); break; @@ -3750,13 +3788,15 @@ bool copyFileFromHfs0(u32 partition, const char* source, const char* dest, const if ((destLen + 1) < NAME_BUF_LEN) { - if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_SUCCEEDED(result)) { /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "GetGameCardHandle succeeded: 0x%08X", handle.value); breaks++;*/ // Same ugly hack from dumpRawHfs0Partition() - if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)))) + result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)); + if (R_SUCCEEDED(result)) { /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "OpenGameCardStorage succeeded: 0x%08X", handle.value); breaks++;*/ @@ -3776,7 +3816,8 @@ bool copyFileFromHfs0(u32 partition, const char* source, const char* dest, const if (n > (size - off)) n = (size - off); - if (R_FAILED(result = fsStorageRead(&gameCardStorage, file_offset + off, dumpBuf, n))) + result = fsStorageRead(&gameCardStorage, file_offset + off, dumpBuf, n); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "StorageRead failed (0x%08X) at offset 0x%016lX", result, file_offset + off); break; @@ -5544,19 +5585,22 @@ bool dumpGameCardCertificate() workaroundPartitionZeroAccess(); - if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_SUCCEEDED(result)) { /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "GetGameCardHandle succeeded: 0x%08X", handle.value); breaks++;*/ - if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, 0))) + result = fsOpenGameCardStorage(&gameCardStorage, &handle, 0); + if (R_SUCCEEDED(result)) { /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "OpenGameCardStorage succeeded: 0x%08X", handle.value); breaks++;*/ if (CERT_SIZE <= freeSpace) { - if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, CERT_OFFSET, dumpBuf, CERT_SIZE))) + result = fsStorageRead(&gameCardStorage, CERT_OFFSET, dumpBuf, CERT_SIZE); + if (R_SUCCEEDED(result)) { // Calculate CRC32 crc32(dumpBuf, CERT_SIZE, &crc); @@ -5632,3 +5676,256 @@ bool dumpGameCardCertificate() return success; } + +bool dumpTicketFromTitle(u32 titleIndex, selectedTicketType curTikType, ticketOptions *tikDumpCfg) +{ + if (!tikDumpCfg) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: invalid ticket dump configuration struct!"); + breaks += 2; + return false; + } + + bool removeConsoleData = tikDumpCfg->removeConsoleData; + + u32 i = 0; + Result result; + + FsStorageId curStorageId; + u8 filter; + u32 titleCount = 0, ncmTitleIndex = 0; + + char *dumpName = NULL; + char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; + + NcmContentRecord *titleContentRecords = NULL; + u32 titleContentRecordsCnt = 0; + + NcmNcaId ncaId; + char ncaIdStr[SHA256_HASH_SIZE + 1] = {'\0'}; + + NcmContentStorage ncmStorage; + memset(&ncmStorage, 0, sizeof(NcmContentStorage)); + + u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0}; + nca_header_t dec_nca_header; + + u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE]; + + title_rights_ctx rights_info; + memset(&rights_info, 0, sizeof(title_rights_ctx)); + + char encTitleKeyStr[0x21] = {'\0'}; + char decTitleKeyStr[0x21] = {'\0'}; + + FILE *outFile = NULL; + + bool success = false, proceed = true, foundRightsIdAndTik = false, removeFile = false; + + if (curTikType != TICKET_TYPE_APP && curTikType != TICKET_TYPE_PATCH && curTikType != TICKET_TYPE_ADDON) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: invalid ticket title type!"); + goto out; + } + + if ((curTikType == TICKET_TYPE_APP && !titleAppStorageId) || (curTikType == TICKET_TYPE_PATCH && !titlePatchStorageId) || (curTikType == TICKET_TYPE_ADDON && !titlePatchStorageId)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: title storage ID unavailable!"); + goto out; + } + + if ((curTikType == TICKET_TYPE_APP && titleIndex >= titleAppCount) || (curTikType == TICKET_TYPE_PATCH && titleIndex >= titlePatchCount) || (curTikType == TICKET_TYPE_ADDON && titleIndex >= titleAddOnCount)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: invalid title index!"); + goto out; + } + + curStorageId = (curTikType == TICKET_TYPE_APP ? titleAppStorageId[titleIndex] : (curTikType == TICKET_TYPE_PATCH ? titlePatchStorageId[titleIndex] : titleAddOnStorageId[titleIndex])); + + filter = (curTikType == TICKET_TYPE_APP ? META_DB_REGULAR_APPLICATION : (curTikType == TICKET_TYPE_PATCH ? META_DB_PATCH : META_DB_ADDON)); + + if (curStorageId == FsStorageId_GameCard) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: invalid title storage ID!"); + goto out; + } + + if (sizeof(rsa2048_sha256_ticket) > freeSpace) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: not enough free space available in the SD card!"); + goto out; + } + + switch(curStorageId) + { + case FsStorageId_SdCard: + titleCount = (curTikType == TICKET_TYPE_APP ? sdCardTitleAppCount : (curTikType == TICKET_TYPE_PATCH ? sdCardTitlePatchCount : sdCardTitleAddOnCount)); + ncmTitleIndex = titleIndex; + break; + case FsStorageId_NandUser: + if (curTikType == TICKET_TYPE_APP) + { + titleCount = nandUserTitleAppCount; + ncmTitleIndex = (titleIndex - sdCardTitleAppCount); // Substract SD card app count + } else + if (curTikType == TICKET_TYPE_PATCH) + { + titleCount = nandUserTitlePatchCount; + ncmTitleIndex = (titleIndex - sdCardTitlePatchCount); // Substract SD card patch count + } else + if (curTikType == TICKET_TYPE_ADDON) + { + titleCount = nandUserTitleAddOnCount; + ncmTitleIndex = (titleIndex - sdCardTitleAddOnCount); // Substract SD card add-on count + } + + break; + default: + break; + } + + dumpName = generateNSPDumpName((nspDumpType)curTikType, titleIndex); + if (!dumpName) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: unable to generate output dump name!"); + goto out; + } + + snprintf(dumpPath, MAX_ELEMENTS(dumpPath), "%s%s.tik", TICKET_PATH, dumpName); + + // Check if the dump already exists + if (checkIfFileExists(dumpPath)) + { + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled."); + goto out; + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - (8 + STRING_Y_POS(breaks)), BG_COLOR_RGB); + } + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Retrieving Rights ID and Ticket for the selected %s...", (curTikType == TICKET_TYPE_APP ? "base application" : (curTikType == TICKET_TYPE_PATCH ? "update" : "DLC"))); + uiRefreshDisplay(); + breaks += 2; + + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem."); + breaks += 2; + } + + if (!retrieveNcaContentRecords(curStorageId, filter, titleCount, ncmTitleIndex, &titleContentRecords, &titleContentRecordsCnt)) goto out; + + result = ncmOpenContentStorage(curStorageId, &ncmStorage); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: ncmOpenContentStorage failed! (0x%08X)", result); + goto out; + } + + for(i = 0; i < titleContentRecordsCnt; i++) + { + memcpy(&ncaId, &(titleContentRecords[i].ncaId), sizeof(NcmNcaId)); + convertDataToHexString(titleContentRecords[i].ncaId.c, SHA256_HASH_SIZE / 2, ncaIdStr, SHA256_HASH_SIZE + 1); + + result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: failed to read header from NCA \"%s\"! (0x%08X)", ncaIdStr, result); + proceed = false; + break; + } + + // Decrypt the NCA header + proceed = decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, &rights_info, decrypted_nca_keys, true); + if (!proceed) break; + + // Check if we hit the right spot + if (rights_info.has_rights_id && rights_info.retrieved_tik) + { + convertDataToHexString(rights_info.enc_titlekey, 0x10, encTitleKeyStr, 0x21); + convertDataToHexString(rights_info.dec_titlekey, 0x10, decTitleKeyStr, 0x21); + foundRightsIdAndTik = true; + break; + } + } + + if (!proceed) goto out; + + if (!foundRightsIdAndTik) + { + if (!rights_info.has_rights_id) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: the selected %s doesn't use titlekey crypto! Rights ID field is empty in all the NCAs!", (curTikType == TICKET_TYPE_APP ? "base application" : (curTikType == TICKET_TYPE_PATCH ? "update" : "DLC"))); + goto out; + } + + if (rights_info.missing_tik) + { + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: the selected %s uses titlekey crypto, but no ticket for it is available! This is probably a pre-install.", (curTikType == TICKET_TYPE_APP ? "base application" : (curTikType == TICKET_TYPE_PATCH ? "update" : "DLC"))); + goto out; + } + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Rights ID: \"%s\".", rights_info.rights_id_str); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Ticket type: %s (0x%02X).", (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_COMMON ? "common" : (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_PERSONALIZED ? "personalized" : "unknown")), rights_info.tik_data.titlekey_type); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Encrypted title key: \"%s\".", encTitleKeyStr); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Decrypted title key: \"%s\".", decTitleKeyStr); + breaks += 2; + + uiRefreshDisplay(); + + // Only mess with the ticket data if removeConsoleData is true and if we're dealing with a personalized ticket (checked in removeConsoleDataFromTicket()) + if (removeConsoleData) removeConsoleDataFromTicket(&rights_info); + + outFile = fopen(dumpPath, "wb"); + if (!outFile) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: failed to open output file \"%s\"!", dumpPath); + goto out; + } + + size_t wr = fwrite(&(rights_info.tik_data), 1, sizeof(rsa2048_sha256_ticket), outFile); + if (wr != sizeof(rsa2048_sha256_ticket)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "dumpTicketFromTitle: failed to write %u bytes long ticket data to \"%s\"! Wrote %lu bytes.", sizeof(rsa2048_sha256_ticket), dumpPath, wr); + removeFile = true; + goto out; + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully finished!"); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Ticket saved to \"%s\".", dumpPath); + + success = true; + +out: + breaks += 2; + + if (outFile) fclose(outFile); + + if (!success && removeFile) unlink(dumpPath); + + serviceClose(&(ncmStorage.s)); + + if (titleContentRecords) free(titleContentRecords); + + if (dumpName) free(dumpName); + + return success; +} diff --git a/source/dumper.h b/source/dumper.h index c9fed5b..2facf36 100644 --- a/source/dumper.h +++ b/source/dumper.h @@ -39,6 +39,8 @@ typedef struct { FsStorageId storageId; // Source storage from which the data is dumped bool removeConsoleData; // Original value for the "Remove console specific data" option. Overrides the selected setting in the current session bool tiklessDump; // Original value for the "Generate ticket-less dump" option. Overrides the selected setting in the current session. Ignored if removeConsoleData == false + bool npdmAcidRsaPatch; // Original value for the "Change NPDM RSA key/sig in Program NCA" option. Overrides the selected setting in the current session + bool preInstall; // Indicates if we're dealing with a preinstalled title - e.g. if the user already accepted the missing ticket prompt u8 partNumber; // Next part number u32 nspFileCount; // PFS0 file count u32 ncaCount; // NCA count @@ -69,5 +71,6 @@ bool dumpRomFsSectionData(u32 titleIndex, selectedRomFsType curRomFsType, bool d bool dumpFileFromRomFsSection(u32 titleIndex, u32 file_offset, selectedRomFsType curRomFsType, bool doSplitting); bool dumpCurrentDirFromRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType, bool doSplitting); bool dumpGameCardCertificate(); +bool dumpTicketFromTitle(u32 titleIndex, selectedTicketType curTikType, ticketOptions *tikDumpCfg); #endif diff --git a/source/fatfs/ffconf.h b/source/fatfs/ffconf.h index 911069a..f84b28e 100644 --- a/source/fatfs/ffconf.h +++ b/source/fatfs/ffconf.h @@ -33,7 +33,7 @@ / 2: Enable with LF-CRLF conversion. */ -#define FF_USE_FIND 2 +#define FF_USE_FIND 0 /* This option switches filtered directory read functions, f_findfirst() and / f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */ @@ -68,7 +68,7 @@ / Locale and Namespace Configurations /---------------------------------------------------------------------------*/ -#define FF_CODE_PAGE 932 +#define FF_CODE_PAGE 850 /* This option specifies the OEM code page to be used on the target system. / Incorrect code page setting can cause a file open failure. / @@ -97,7 +97,7 @@ */ -#define FF_USE_LFN 1 +#define FF_USE_LFN 3 #define FF_MAX_LFN 255 /* The FF_USE_LFN switches the support for LFN (long file name). / @@ -137,7 +137,7 @@ / on character encoding. When LFN is not enabled, these options have no effect. */ -#define FF_STRF_ENCODE 3 +#define FF_STRF_ENCODE 0 /* When FF_LFN_UNICODE >= 1 with LFN enabled, string I/O functions, f_gets(), / f_putc(), f_puts and f_printf() convert the character encoding in it. / This option selects assumption of character encoding ON THE FILE to be @@ -150,7 +150,7 @@ */ -#define FF_FS_RPATH 1 +#define FF_FS_RPATH 0 /* This option configures support for relative path. / / 0: Disable relative path and remove related functions. @@ -167,8 +167,8 @@ /* Number of volumes (logical drives) to be used. (1-10) */ -#define FF_STR_VOLUME_ID 0 -#define FF_VOLUME_STRS "RAM","NAND","CF","SD","SD2","USB","USB2","USB3" +#define FF_STR_VOLUME_ID 1 +#define FF_VOLUME_STRS "sys" /* FF_STR_VOLUME_ID switches support for volume ID in arbitrary strings. / When FF_STR_VOLUME_ID is set to 1 or 2, arbitrary strings can be used as drive / number in the path name. FF_VOLUME_STRS defines the volume ID strings for each @@ -239,7 +239,7 @@ #define FF_FS_NORTC 0 #define FF_NORTC_MON 1 #define FF_NORTC_MDAY 1 -#define FF_NORTC_YEAR 2018 +#define FF_NORTC_YEAR 2019 /* The option FF_FS_NORTC switches timestamp functiton. If the system does not have / any RTC function or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable / the timestamp function. Every object modified by FatFs will have a fixed timestamp diff --git a/source/fatfs/ffsystem.c b/source/fatfs/ffsystem.c new file mode 100644 index 0000000..110f4b8 --- /dev/null +++ b/source/fatfs/ffsystem.c @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------*/ +/* Sample Code of OS Dependent Functions for FatFs */ +/* (C) ChaN, 2018 */ +/* (C) CTCaer, 2018 */ +/*------------------------------------------------------------------------*/ + +#include +#include "ff.h" + +#if FF_USE_LFN == 3 /* Dynamic memory allocation */ + +/*------------------------------------------------------------------------*/ +/* Allocate a memory block */ +/*------------------------------------------------------------------------*/ + +void* ff_memalloc ( /* Returns pointer to the allocated memory block (null if not enough core) */ + UINT msize /* Number of bytes to allocate */ +) +{ + return malloc(msize); /* Allocate a new memory block with POSIX API */ +} + + +/*------------------------------------------------------------------------*/ +/* Free a memory block */ +/*------------------------------------------------------------------------*/ + +void ff_memfree ( + void* mblock /* Pointer to the memory block to free (nothing to do if null) */ +) +{ + free(mblock); /* Free the memory block with POSIX API */ +} + +#endif diff --git a/source/keys.c b/source/keys.c index 0f1324c..852a459 100644 --- a/source/keys.c +++ b/source/keys.c @@ -9,46 +9,24 @@ #include "ui.h" #include "es.h" #include "set_ext.h" +#include "save.h" /* Extern variables */ extern int breaks; extern int font_height; +extern u8 *dumpBuf; + extern char strbuf[NAME_BUF_LEN]; /* Statically allocated variables */ nca_keyset_t nca_keyset; -FsStorage fatFsStorage; +static u8 eticket_data[ETICKET_DEVKEY_DATA_SIZE]; +static bool setcal_eticket_retrieved = false; -static const char *cert_CA00000003_path = "romfs:/certificate/CA00000003"; -static const char *cert_XS00000020_path = "romfs:/certificate/XS00000020"; -static const char *cert_XS00000021_path = "romfs:/certificate/XS00000021"; - -static u8 cert_CA00000003_data[ETICKET_CA_CERT_SIZE]; -static u8 cert_XS00000020_data[ETICKET_XS_CERT_SIZE]; -static u8 cert_XS00000021_data[ETICKET_XS_CERT_SIZE]; - -static const u8 cert_CA00000003_hash[0x20] = { - 0x62, 0x69, 0x0E, 0xC0, 0x4C, 0x62, 0x9D, 0x08, 0x38, 0xBB, 0xDF, 0x65, 0xC5, 0xA6, 0xB0, 0x9A, - 0x54, 0x94, 0x2C, 0x87, 0x0E, 0x01, 0x55, 0x73, 0xCF, 0x7D, 0x58, 0xF2, 0x59, 0xFE, 0x36, 0xFA -}; - -static const u8 cert_XS00000020_hash[0x20] = { - 0x55, 0x23, 0x17, 0xD4, 0x4B, 0xAF, 0x4C, 0xF5, 0x31, 0x8E, 0xF5, 0xC6, 0x4E, 0x0F, 0x75, 0xD9, - 0x75, 0xD4, 0x03, 0xFD, 0x7B, 0x93, 0x7B, 0xAB, 0x46, 0x7D, 0x37, 0x94, 0x62, 0x39, 0x33, 0xE9 -}; - -static const u8 cert_XS00000021_hash[0x20] = { - 0xDE, 0xFF, 0x96, 0x01, 0x42, 0x1E, 0x00, 0xC1, 0x52, 0x60, 0x5C, 0x9F, 0x42, 0xCD, 0x91, 0xD7, - 0x90, 0x01, 0xC5, 0x7F, 0xC3, 0x27, 0x58, 0x4B, 0xD9, 0x6F, 0x71, 0x78, 0xC9, 0x44, 0xD0, 0xAD -}; - -static const char *keysFilePath = "sdmc:/switch/prod.keys"; - -/* Variables related to the FS process */ static keyLocation FSRodata = { FS_TID, SEG_RODATA, @@ -132,19 +110,22 @@ bool retrieveProcessMemory(keyLocation *location) // If not a kernel process, get PID from pm:dmnt u64 pid; - if (R_FAILED(result = pmdmntGetTitlePid(&pid, location->titleID))) + result = pmdmntGetTitlePid(&pid, location->titleID); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: pmdmntGetTitlePid failed! (0x%08X)", result); return false; } - if (R_FAILED(result = svcDebugActiveProcess(&debug_handle, pid))) + result = svcDebugActiveProcess(&debug_handle, pid); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: svcDebugActiveProcess failed! (0x%08X)", result); return false; } - if (R_FAILED(result = svcGetDebugEvent((u8*)&d, debug_handle))) + result = svcGetDebugEvent((u8*)&d, debug_handle); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: svcGetDebugEvent failed! (0x%08X)", result); return false; @@ -154,7 +135,8 @@ bool retrieveProcessMemory(keyLocation *location) u64 pids[300]; u32 num_processes; - if (R_FAILED(result = svcGetProcessList(&num_processes, pids, 300))) + result = svcGetProcessList(&num_processes, pids, 300); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: svcGetProcessList failed! (0x%08X)", result); return false; @@ -191,7 +173,8 @@ bool retrieveProcessMemory(keyLocation *location) // Locate "real" .text segment as Atmosphere emuMMC has two for(;;) { - if (R_FAILED(result = svcQueryDebugProcessMemory(&mem_info, &page_info, debug_handle, addr))) + result = svcQueryDebugProcessMemory(&mem_info, &page_info, debug_handle, addr); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: svcQueryDebugProcessMemory failed! (0x%08X)", result); success = false; @@ -214,7 +197,8 @@ bool retrieveProcessMemory(keyLocation *location) for(segment = 1; segment < BIT(3);) { - if (R_FAILED(result = svcQueryDebugProcessMemory(&mem_info, &page_info, debug_handle, addr))) + result = svcQueryDebugProcessMemory(&mem_info, &page_info, debug_handle, addr); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: svcQueryDebugProcessMemory failed! (0x%08X)", result); success = false; @@ -238,7 +222,8 @@ bool retrieveProcessMemory(keyLocation *location) memset(location->data + location->dataSize, 0, mem_info.size); - if (R_FAILED(result = svcReadDebugProcessMemory(location->data + location->dataSize, debug_handle, mem_info.addr, mem_info.size))) + result = svcReadDebugProcessMemory(location->data + location->dataSize, debug_handle, mem_info.addr, mem_info.size); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: svcReadDebugProcessMemory failed! (0x%08X)", result); success = false; @@ -337,13 +322,15 @@ bool loadMemoryKeys() nca_keyset.memory_key_cnt++; // Derive NCA header key - if (R_FAILED(result = splCryptoInitialize())) + result = splCryptoInitialize(); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize the spl:crypto service! (0x%08X)", result); return false; } - if (R_FAILED(result = splCryptoGenerateAesKek(nca_keyset.header_kek_source, 0, 0, nca_keyset.header_kek))) + result = splCryptoGenerateAesKek(nca_keyset.header_kek_source, 0, 0, nca_keyset.header_kek); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splCryptoGenerateAesKek(header_kek_source) failed! (0x%08X)", result); splCryptoExit(); @@ -352,14 +339,16 @@ bool loadMemoryKeys() nca_keyset.memory_key_cnt++; - if (R_FAILED(result = splCryptoGenerateAesKey(nca_keyset.header_kek, nca_keyset.header_key_source + 0x00, nca_keyset.header_key + 0x00))) + result = splCryptoGenerateAesKey(nca_keyset.header_kek, nca_keyset.header_key_source + 0x00, nca_keyset.header_key + 0x00); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splCryptoGenerateAesKey(header_key_source + 0x00) failed! (0x%08X)", result); splCryptoExit(); return false; } - if (R_FAILED(result = splCryptoGenerateAesKey(nca_keyset.header_kek, nca_keyset.header_key_source + 0x10, nca_keyset.header_key + 0x10))) + result = splCryptoGenerateAesKey(nca_keyset.header_kek, nca_keyset.header_key_source + 0x10, nca_keyset.header_key + 0x10); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splCryptoGenerateAesKey(header_key_source + 0x10) failed! (0x%08X)", result); splCryptoExit(); @@ -397,13 +386,15 @@ bool decryptNcaKeyArea(nca_header_t *dec_nca_header, u8 *out) u8 *kek_source = (dec_nca_header->kaek_ind == 0 ? nca_keyset.key_area_key_application_source : (dec_nca_header->kaek_ind == 1 ? nca_keyset.key_area_key_ocean_source : nca_keyset.key_area_key_system_source)); - if (R_FAILED(result = splCryptoInitialize())) + result = splCryptoInitialize(); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize the spl:crypto service! (0x%08X)", result); return false; } - if (R_FAILED(result = splCryptoGenerateAesKek(kek_source, crypto_type, 0, tmp_kek))) + result = splCryptoGenerateAesKek(kek_source, crypto_type, 0, tmp_kek); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splCryptoGenerateAesKek(kek_source) failed! (0x%08X)", result); splCryptoExit(); @@ -415,7 +406,8 @@ bool decryptNcaKeyArea(nca_header_t *dec_nca_header, u8 *out) for(i = 0; i < NCA_KEY_AREA_KEY_CNT; i++) { - if (R_FAILED(result = splCryptoGenerateAesKey(tmp_kek, dec_nca_header->nca_keys[i], decrypted_nca_keys[i]))) + result = splCryptoGenerateAesKey(tmp_kek, dec_nca_header->nca_keys[i], decrypted_nca_keys[i]); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splCryptoGenerateAesKey(nca_kaek_%02u) failed! (0x%08X)", i, result); success = false; @@ -625,6 +617,11 @@ int readKeysFromFile(FILE *f) { if (!parse_hex_key(nca_keyset.eticket_rsa_kek, value, sizeof(nca_keyset.eticket_rsa_kek))) return 0; nca_keyset.ext_key_cnt++; + } else + if (strcasecmp(key, "save_mac_key") == 0) + { + if (!parse_hex_key(nca_keyset.save_mac_key, value, sizeof(nca_keyset.save_mac_key))) return 0; + nca_keyset.ext_key_cnt++; } else { memset(test_name, 0, sizeof(test_name)); @@ -679,10 +676,10 @@ bool loadExternalKeys() if (nca_keyset.ext_key_cnt > 0) return true; // Open keys file - FILE *keysFile = fopen(keysFilePath, "rb"); + FILE *keysFile = fopen(KEYS_FILE_PATH, "rb"); if (!keysFile) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to open \"%s\" to retrieve \"eticket_rsa_kek\", titlekeks and KAEKs!", keysFilePath); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to open \"%s\" to retrieve \"eticket_rsa_kek\", titlekeks and KAEKs!", KEYS_FILE_PATH); return false; } @@ -692,7 +689,7 @@ bool loadExternalKeys() if (ret < 1) { - if (ret == -1) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse necessary keys from \"%s\"! (keys file empty?)", keysFilePath); + if (ret == -1) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse necessary keys from \"%s\"! (keys file empty?)", KEYS_FILE_PATH); return false; } @@ -717,13 +714,15 @@ bool testKeyPair(const void *E, const void *D, const void *N) X[0xFE] = 0xBA; X[0xFF] = 0xBE; - if (R_FAILED(result = splUserExpMod(X, N, D, 0x100, Y))) + result = splUserExpMod(X, N, D, 0x100, Y); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splUserExpMod failed! (testKeyPair #1) (0x%08X)", result); return false; } - if (R_FAILED(result = splUserExpMod(Y, N, E, 4, Z))) + result = splUserExpMod(Y, N, E, 4, Z); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splUserExpMod failed! (testKeyPair #2) (0x%08X)", result); return false; @@ -770,15 +769,17 @@ void mgf1(const u8 *data, size_t data_length, u8 *mask, size_t mask_length) free(data_counter); } -bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_enc_key, u8 *out_dec_key) +int retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_enc_key, u8 *out_dec_key) { + int ret = -1; + if (!dec_nca_header || dec_nca_header->kaek_ind > 2 || (!out_tik && !out_dec_key && !out_enc_key)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid parameters to retrieve NCA ticket and/or titlekey."); - return false; + return ret; } - u32 i; + u32 i, j; bool has_rights_id = false; for(i = 0; i < 0x10; i++) @@ -793,7 +794,7 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e if (!has_rights_id) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: NCA doesn't use titlekey crypto."); - return false; + return ret; } u8 crypto_type = (dec_nca_header->crypto_type2 > dec_nca_header->crypto_type ? dec_nca_header->crypto_type2 : dec_nca_header->crypto_type); @@ -802,7 +803,7 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e if (crypto_type >= 0x20) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NCA keyblob index."); - return false; + return ret; } Result result; @@ -812,51 +813,58 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e bool foundRightsId = false; u8 rightsIdType = 0; // 1 = Common, 2 = Personalized - u8 eticket_data[ETICKET_DEVKEY_DATA_SIZE]; - memset(eticket_data, 0, ETICKET_DEVKEY_DATA_SIZE); - Aes128CtrContext eticket_aes_ctx; unsigned char ctr[0x10]; u8 *D = NULL, *N = NULL, *E = NULL; - FATFS fs; FRESULT fr; FIL eTicketSave; - u64 offset = 0; - u8 eTicketEntry[ETICKET_ENTRY_SIZE], titleKeyBlock[0x100]; - u32 read_bytes = 0; + save_ctx_t *save_ctx = NULL; + allocation_table_storage_ctx_t fat_storage; + save_fs_list_entry_t entry; + const char ticket_bin_path[SAVE_FS_LIST_MAX_NAME_LENGTH] = "/ticket.bin"; + + u32 buf_size = (ETICKET_ENTRY_SIZE * 0x10); + u32 br = buf_size; + u64 total_br = 0; + + char tmp[NAME_BUF_LEN / 2] = {'\0'}; + bool foundEticket = false, proceed = true; u8 titlekey[0x10]; Aes128Context titlekey_aes_ctx; - if (R_FAILED(result = esInitialize())) + result = esInitialize(); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize the ES service! (0x%08X)", result); - return false; + return ret; } - if (R_FAILED(result = esCountCommonTicket(&common_count))) + result = esCountCommonTicket(&common_count); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: esCountCommonTicket failed! (0x%08X)", result); esExit(); - return false; + return ret; } - if (R_FAILED(result = esCountPersonalizedTicket(&personalized_count))) + result = esCountPersonalizedTicket(&personalized_count); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: esCountPersonalizedTicket failed! (0x%08X)", result); esExit(); - return false; + return ret; } if (!common_count && !personalized_count) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: no tickets available!"); esExit(); - return false; + return ret; } if (common_count) @@ -866,15 +874,16 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to allocate memory for common tickets' rights IDs!"); esExit(); - return false; + return ret; } - if (R_FAILED(result = esListCommonTicket(&ids_written, common_rights_ids, common_count * sizeof(FsRightsId)))) + result = esListCommonTicket(&ids_written, common_rights_ids, common_count * sizeof(FsRightsId)); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: esListCommonTicket failed! (0x%08X)", result); free(common_rights_ids); esExit(); - return false; + return ret; } for(i = 0; i < common_count; i++) @@ -897,15 +906,16 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to allocate memory for personalized tickets' rights IDs!"); esExit(); - return false; + return ret; } - if (R_FAILED(result = esListPersonalizedTicket(&ids_written, personalized_rights_ids, personalized_count * sizeof(FsRightsId)))) + result = esListPersonalizedTicket(&ids_written, personalized_rights_ids, personalized_count * sizeof(FsRightsId)); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: esListPersonalizedTicket failed! (0x%08X)", result); free(personalized_rights_ids); esExit(); - return false; + return ret; } for(i = 0; i < personalized_count; i++) @@ -926,150 +936,195 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e if (!foundRightsId || (rightsIdType != 1 && rightsIdType != 2)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: NCA rights ID unavailable in this console!"); - return false; - } - - // Get extended eTicket RSA key from PRODINFO - if (R_FAILED(result = setcalInitialize())) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize the set:cal service! (0x%08X)", result); - return false; - } - - result = setcalGetEticketDeviceKey(eticket_data); - - setcalExit(); - - if (R_FAILED(result)) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: setcalGetEticketDeviceKey failed! (0x%08X)", result); - return false; + ret = -2; + return ret; } // Load external keys - if (!loadExternalKeys()) return false; + if (!loadExternalKeys()) return ret; - // Decrypt eTicket RSA key - memcpy(ctr, eticket_data + ETICKET_DEVKEY_CTR_OFFSET, 0x10); - aes128CtrContextCreate(&eticket_aes_ctx, nca_keyset.eticket_rsa_kek, ctr); - aes128CtrCrypt(&eticket_aes_ctx, eticket_data + ETICKET_DEVKEY_RSA_OFFSET, eticket_data + ETICKET_DEVKEY_RSA_OFFSET, ETICKET_DEVKEY_RSA_SIZE); - - // Public exponent must use RSA-2048 SHA-1 signature method - // The value is stored use big endian byte order - if (bswap_32(*((u32*)(&(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x200])))) != SIGTYPE_RSA2048_SHA1) + if (!setcal_eticket_retrieved) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid public RSA exponent for eTicket data! Wrong keys?"); - return false; - } - - D = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET]); - N = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x100]); - E = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x200]); - - if (!testKeyPair(E, D, N)) return false; - - if (R_FAILED(result = fsOpenBisStorage(&fatFsStorage, FsBisStorageId_System))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open BIS System partition! (0x%08X)", result); - return false; + // Get extended eTicket RSA key from PRODINFO + memset(eticket_data, 0, ETICKET_DEVKEY_DATA_SIZE); + + result = setcalInitialize(); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize the set:cal service! (0x%08X)", result); + return ret; + } + + result = setcalGetEticketDeviceKey(eticket_data); + + setcalExit(); + + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: setcalGetEticketDeviceKey failed! (0x%08X)", result); + return ret; + } + + // Decrypt eTicket RSA key + memcpy(ctr, eticket_data + ETICKET_DEVKEY_CTR_OFFSET, 0x10); + aes128CtrContextCreate(&eticket_aes_ctx, nca_keyset.eticket_rsa_kek, ctr); + aes128CtrCrypt(&eticket_aes_ctx, eticket_data + ETICKET_DEVKEY_RSA_OFFSET, eticket_data + ETICKET_DEVKEY_RSA_OFFSET, ETICKET_DEVKEY_RSA_SIZE); + + // Public exponent must use RSA-2048 SHA-1 signature method + // The value is stored use big endian byte order + if (bswap_32(*((u32*)(&(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x200])))) != SIGTYPE_RSA2048_SHA1) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid public RSA exponent for eTicket data! Wrong keys?\nTry running Lockpick_RCM to generate the keys file from scratch."); + return ret; + } + + D = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET]); + N = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x100]); + E = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x200]); + + if (!testKeyPair(E, D, N)) return ret; + + setcal_eticket_retrieved = true; + } else { + D = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET]); + N = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x100]); + E = &(eticket_data[ETICKET_DEVKEY_RSA_OFFSET + 0x200]); } // FatFs is used to mount the BIS System partition and read the ES savedata files to avoid 0xE02 (file already in use) errors - fr = f_mount(&fs, "sys", 1); - if (fr != FR_OK) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to mount BIS System partition! (%u)", fr); - fsStorageClose(&fatFsStorage); - return false; - } - - fr = f_chdir("/save"); - if (fr != FR_OK) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to change BIS System partition directory! (%u)", fr); - f_unmount("sys"); - fsStorageClose(&fatFsStorage); - return false; - } - - fr = f_open(&eTicketSave, (rightsIdType == 1 ? COMMON_ETICKET_FILENAME : PERSONALIZED_ETICKET_FILENAME), FA_READ | FA_OPEN_EXISTING); - if (fr != FR_OK) + fr = f_open(&eTicketSave, (rightsIdType == 1 ? BIS_COMMON_TIK_SAVE_NAME : BIS_PERSONALIZED_TIK_SAVE_NAME), FA_READ | FA_OPEN_EXISTING); + if (fr) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open ES %s eTicket save! (%u)", (rightsIdType == 1 ? "common" : "personalized"), fr); - f_unmount("sys"); - fsStorageClose(&fatFsStorage); - return false; + return ret; } - while(true) + save_ctx = calloc(1, sizeof(save_ctx_t)); + if (!save_ctx) { - fr = f_read(&eTicketSave, eTicketEntry, ETICKET_ENTRY_SIZE, &read_bytes); - if (fr || read_bytes != ETICKET_ENTRY_SIZE) break; - - // Only read eTicket entries with RSA-2048 SHA-256 signature method - if (*((u32*)eTicketEntry) == SIGTYPE_RSA2048_SHA256) + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to allocate memory for ticket savefile context!"); + f_close(&eTicketSave); + return ret; + } + + save_ctx->file = &eTicketSave; + save_ctx->tool_ctx.action = 0; + memcpy(save_ctx->save_mac_key, nca_keyset.save_mac_key, 0x10); + + if (!save_process(save_ctx)) + { + strcat(strbuf, "\nError: failed to process ticket savefile!"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf); + free(save_ctx); + f_close(&eTicketSave); + return ret; + } + + if (!save_hierarchical_file_table_get_file_entry_by_path(&save_ctx->save_filesystem_core.file_table, ticket_bin_path, &entry)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nError: failed to get file entry for \"%s\" in ticket savefile!", ticket_bin_path); + strcat(strbuf, tmp); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf); + save_free_contexts(save_ctx); + free(save_ctx); + f_close(&eTicketSave); + return ret; + } + + if (!save_open_fat_storage(&save_ctx->save_filesystem_core, &fat_storage, entry.value.save_file_info.start_block)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nError: failed to open FAT storage at block 0x%X for \"%s\" in ticket savefile!", entry.value.save_file_info.start_block, ticket_bin_path); + strcat(strbuf, tmp); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf); + save_free_contexts(save_ctx); + free(save_ctx); + f_close(&eTicketSave); + return ret; + } + + while(br == buf_size && total_br < entry.value.save_file_info.length) + { + br = save_allocation_table_storage_read(&fat_storage, dumpBuf, total_br, buf_size); + if (br != buf_size) { - // Check if our current eTicket entry matches our rights ID - if (!memcmp(eTicketEntry + ETICKET_RIGHTSID_OFFSET, dec_nca_header->rights_id, 0x10)) - { - foundEticket = true; - - if (rightsIdType == 1) - { - // Common - memcpy(titlekey, eTicketEntry + ETICKET_TITLEKEY_OFFSET, 0x10); - } else { - // Personalized - u8 M[0x100], salt[0x20], db[0xDF]; - - memcpy(titleKeyBlock, eTicketEntry + ETICKET_TITLEKEY_OFFSET, 0x100); - - if (R_FAILED(result = splUserExpMod(titleKeyBlock, N, D, 0x100, M))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splUserExpMod failed! (titleKeyBlock) (0x%08X)", result); - proceed = false; - break; - } - - // Decrypt the titlekey - mgf1(M + 0x21, 0xDF, salt, 0x20); - for(i = 0; i < 0x20; i++) salt[i] ^= M[i + 1]; - - mgf1(salt, 0x20, db, 0xDF); - for(i = 0; i < 0xDF; i++) db[i] ^= M[i + 0x21]; - - // Verify if it starts with a null string hash - if (memcmp(db, null_hash, 0x20) != 0) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: titlekey decryption failed! Wrong keys?"); - proceed = false; - break; - } - - memcpy(titlekey, db + 0xCF, 0x10); - } - - break; - } + snprintf(tmp, MAX_ELEMENTS(tmp), "\nError: failed to read %u bytes chunk at offset 0x%lX from \"%s\" in ticket savefile!", buf_size, total_br, ticket_bin_path); + strcat(strbuf, tmp); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf); + proceed = false; + break; } - offset += ETICKET_ENTRY_SIZE; + if (dumpBuf[0] == 0) break; + + total_br += br; + + for(i = 0; i < buf_size; i += ETICKET_ENTRY_SIZE) + { + // Only read eTicket entries with RSA-2048 SHA-256 signature method + // Also check if our current eTicket entry matches our rights ID + if (*((u32*)(dumpBuf + i)) != SIGTYPE_RSA2048_SHA256 || memcmp(dumpBuf + i + ETICKET_RIGHTSID_OFFSET, dec_nca_header->rights_id, 0x10) != 0) continue; + + foundEticket = true; + + if (rightsIdType == 1) + { + // Common + memcpy(titlekey, dumpBuf + i + ETICKET_TITLEKEY_OFFSET, 0x10); + } else { + // Personalized + u8 M[0x100], salt[0x20], db[0xDF]; + + u8 *titleKeyBlock = (dumpBuf + i + ETICKET_TITLEKEY_OFFSET); + + result = splUserExpMod(titleKeyBlock, N, D, 0x100, M); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: splUserExpMod failed! (titleKeyBlock) (0x%08X)", result); + proceed = false; + break; + } + + // Decrypt the titlekey + mgf1(M + 0x21, 0xDF, salt, 0x20); + for(j = 0; j < 0x20; j++) salt[j] ^= M[j + 1]; + + mgf1(salt, 0x20, db, 0xDF); + for(j = 0; j < 0xDF; j++) db[j] ^= M[j + 0x21]; + + // Verify if it starts with a null string hash + if (memcmp(db, null_hash, 0x20) != 0) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: titlekey decryption failed! Wrong keys?\nTry running Lockpick_RCM to generate the keys file from scratch."); + proceed = false; + break; + } + + memcpy(titlekey, db + 0xCF, 0x10); + } + + break; + } + + if (foundEticket) break; } + save_free_contexts(save_ctx); + free(save_ctx); f_close(&eTicketSave); - f_unmount("sys"); - fsStorageClose(&fatFsStorage); - if (!proceed) return false; + if (!proceed) return ret; if (!foundEticket) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to find a matching eTicket entry for NCA rights ID!"); - return false; + ret = -2; + return ret; } + ret = 0; + // Copy ticket data to output pointer - if (out_tik != NULL) memcpy(out_tik, eTicketEntry, ETICKET_TIK_FILE_SIZE); + if (out_tik != NULL) memcpy(out_tik, dumpBuf + i, ETICKET_TIK_FILE_SIZE); // Copy encrypted titlekey to output pointer // It is used in personalized -> common ticket conversion @@ -1083,7 +1138,7 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e aes128DecryptBlock(&titlekey_aes_ctx, out_dec_key, titlekey); } - return true; + return ret; } bool generateEncryptedNcaKeyAreaWithTitlekey(nca_header_t *dec_nca_header, u8 *decrypted_nca_keys) @@ -1112,75 +1167,3 @@ bool generateEncryptedNcaKeyAreaWithTitlekey(nca_header_t *dec_nca_header, u8 *d return true; } - -bool readCertsFromApplicationRomFs() -{ - FILE *certFile; - u64 certSize; - - u8 i; - size_t read_bytes; - u8 tmp_hash[0x20]; - - for(i = 0; i < 3; i++) - { - const char *path = (i == 0 ? cert_CA00000003_path : (i == 1 ? cert_XS00000020_path : cert_XS00000021_path)); - u8 *data_ptr = (i == 0 ? cert_CA00000003_data : (i == 1 ? cert_XS00000020_data : cert_XS00000021_data)); - const u8 *hash_ptr = (i == 0 ? cert_CA00000003_hash : (i == 1 ? cert_XS00000020_hash : cert_XS00000021_hash)); - u64 expected_size = (i == 0 ? ETICKET_CA_CERT_SIZE : ETICKET_XS_CERT_SIZE); - - certFile = NULL; - certSize = 0; - - certFile = fopen(path, "rb"); - if (!certFile) - { - snprintf(strbuf, MAX_ELEMENTS(strbuf), "readCertsFromApplicationRomFs: failed to open file \"%s\".\n", path); - return false; - } - - fseek(certFile, 0, SEEK_END); - certSize = ftell(certFile); - rewind(certFile); - - if (certSize != expected_size) - { - snprintf(strbuf, MAX_ELEMENTS(strbuf), "readCertsFromApplicationRomFs: invalid file size for \"%s\".\n", path); - return false; - } - - read_bytes = fread(data_ptr, 1, expected_size, certFile); - - fclose(certFile); - - if (read_bytes != expected_size) - { - snprintf(strbuf, MAX_ELEMENTS(strbuf), "readCertsFromApplicationRomFs: error reading file \"%s\".\n", path); - return false; - } - - sha256CalculateHash(tmp_hash, data_ptr, expected_size); - - if (memcmp(tmp_hash, hash_ptr, 0x20) != 0) - { - snprintf(strbuf, MAX_ELEMENTS(strbuf), "readCertsFromApplicationRomFs: invalid hash for file \"%s\".\n", path); - return false; - } - } - - return true; -} - -bool retrieveCertData(u8 *out_cert, bool personalized) -{ - if (!out_cert) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid parameters to retrieve %s ticket certificate chain.", (!personalized ? "common" : "personalized")); - return false; - } - - memcpy(out_cert, cert_CA00000003_data, ETICKET_CA_CERT_SIZE); - memcpy(out_cert + ETICKET_CA_CERT_SIZE, (!personalized ? cert_XS00000020_data : cert_XS00000021_data), ETICKET_XS_CERT_SIZE); - - return true; -} diff --git a/source/keys.h b/source/keys.h index 1ca1b84..7bf1afe 100644 --- a/source/keys.h +++ b/source/keys.h @@ -20,9 +20,6 @@ #define SIGTYPE_RSA2048_SHA1 (u32)0x10001 #define SIGTYPE_RSA2048_SHA256 (u32)0x10004 -#define COMMON_ETICKET_FILENAME "80000000000000e1" -#define PERSONALIZED_ETICKET_FILENAME "80000000000000e2" - typedef struct { u64 titleID; u8 mask; @@ -42,30 +39,31 @@ typedef struct { u32 total_key_cnt; /* Total key counter. */ // Needed to decrypt the NCA header using AES-128-XTS - u8 header_kek_source[0x10]; /* Seed for header kek. */ - u8 header_key_source[0x20]; /* Seed for NCA header key. */ - u8 header_kek[0x10]; /* NCA header kek. */ - u8 header_key[0x20]; /* NCA header key. */ + u8 header_kek_source[0x10]; /* Seed for header kek. Retrieved from the .rodata section in the FS sysmodule. */ + u8 header_key_source[0x20]; /* Seed for NCA header key. Retrieved from the .data section in the FS sysmodule. */ + u8 header_kek[0x10]; /* NCA header kek. Generated from header_kek_source. */ + u8 header_key[0x20]; /* NCA header key. Generated from header_kek and header_key_source. */ - // Needed to derive the KAEK needed to decrypt the NCA key area - u8 key_area_key_application_source[0x10]; /* Seed for kaek 0. */ - u8 key_area_key_ocean_source[0x10]; /* Seed for kaek 1. */ - u8 key_area_key_system_source[0x10]; /* Seed for kaek 2. */ + // Needed to derive the KAEK used to decrypt the NCA key area + u8 key_area_key_application_source[0x10]; /* Seed for kaek 0. Retrieved from the .rodata section in the FS sysmodule. */ + u8 key_area_key_ocean_source[0x10]; /* Seed for kaek 1. Retrieved from the .rodata section in the FS sysmodule. */ + u8 key_area_key_system_source[0x10]; /* Seed for kaek 2. Retrieved from the .rodata section in the FS sysmodule. */ - // Needed to decrypt the title key block from an eTicket + // Needed to decrypt the title key block from an eTicket. Retrieved from the Lockpick_RCM keys file. u8 eticket_rsa_kek[0x10]; /* eTicket RSA kek. */ u8 titlekeks[0x20][0x10]; /* Title key encryption keys. */ - // Needed to reencrypt the NCA key area for tik-less NSP dumps + // Needed to reencrypt the NCA key area for tik-less NSP dumps. Retrieved from the Lockpick_RCM keys file. u8 key_area_keys[0x20][3][0x10]; /* Key area encryption keys. */ + + // Needed to decrypt saves from system and application titles + u8 save_mac_key[0x10]; } PACKED nca_keyset_t; bool loadMemoryKeys(); bool decryptNcaKeyArea(nca_header_t *dec_nca_header, u8 *out); bool loadExternalKeys(); -bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_enc_key, u8 *out_dec_key); +int retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_enc_key, u8 *out_dec_key); bool generateEncryptedNcaKeyAreaWithTitlekey(nca_header_t *dec_nca_header, u8 *decrypted_nca_keys); -bool readCertsFromApplicationRomFs(); -bool retrieveCertData(u8 *out_cert, bool personalized); #endif diff --git a/source/main.c b/source/main.c index 37efe48..8a7f2b4 100644 --- a/source/main.c +++ b/source/main.c @@ -12,6 +12,8 @@ /* Extern variables */ +extern bool keysFileAvailable; + extern FsDeviceOperator fsOperatorInstance; extern FsEventNotifier fsGameCardEventNotifier; @@ -44,319 +46,356 @@ int main(int argc, char *argv[]) /* Initialize UI */ if (!uiInit()) return -1; + /* Zero out NCA keyset */ + memset(&nca_keyset, 0, sizeof(nca_keyset_t)); + + /* Init ExeFS context */ + initExeFsContext(); + + /* Init RomFS context */ + initRomFsContext(); + + /* Init BKTR context */ + initBktrContext(); + + /* Make sure output directories exist */ + createOutputDirectories(); + + /* Load settings from configuration file */ + loadConfig(); + + /* Check if the Lockpick_RCM keys file is available */ + keysFileAvailable = checkIfFileExists(KEYS_FILE_PATH); + /* Enable CPU boost mode */ appletSetCpuBoostMode(ApmCpuBoostMode_Type1); - int ret = 0; Result result; + Thread thread; + + int ret = 0; + bool exitMainLoop = false; + bool initNcm = false, initNs = false, initCsrng = false, initSpl = false, initPmdmnt = false; + bool openFsDevOp = false, openGcEvtNotifier = false, loadGcKernEvt = false, startGcThread = false; /* Initialize the ncm service */ result = ncmInitialize(); - if (R_SUCCEEDED(result)) + if (R_FAILED(result)) { - /* Initialize the ns service */ - result = nsInitialize(); - if (R_SUCCEEDED(result)) + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to initialize the ncm service! (0x%08X)", result); + ret = -2; + goto out; + } + + initNcm = true; + + /* Initialize the ns service */ + result = nsInitialize(); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to initialize the ns service! (0x%08X)", result); + ret = -3; + goto out; + } + + initNs = true; + + /* Initialize the csrng service */ + result = csrngInitialize(); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to initialize the csrng service! (0x%08X)", result); + ret = -4; + goto out; + } + + initCsrng = true; + + /* Initialize the spl service */ + result = splInitialize(); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to initialize the spl service! (0x%08X)", result); + ret = -5; + goto out; + } + + initSpl = true; + + /* Initialize the pm:dmnt service */ + result = pmdmntInitialize(); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to initialize the pm:dmnt service! (0x%08X)", result); + ret = -6; + goto out; + } + + initPmdmnt = true; + + /* Open device operator */ + result = fsOpenDeviceOperator(&fsOperatorInstance); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to open device operator! (0x%08X)", result); + ret = -7; + goto out; + } + + openFsDevOp = true; + + /* Open gamecard detection event notifier */ + result = fsOpenGameCardDetectionEventNotifier(&fsGameCardEventNotifier); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to open gamecard detection event notifier! (0x%08X)", result); + ret = -8; + goto out; + } + + openGcEvtNotifier = true; + + /* Retrieve gamecard detection event handle */ + result = fsEventNotifierGetEventHandle(&fsGameCardEventNotifier, &fsGameCardEventHandle); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to retrieve gamecard detection event handle! (0x%08X)", result); + ret = -9; + goto out; + } + + /* Retrieve initial gamecard status */ + gameCardInserted = isGameCardInserted(); + + /* Load gamecard detection kernel event */ + eventLoadRemote(&fsGameCardKernelEvent, fsGameCardEventHandle, false); + + loadGcKernEvt = true; + + /* Create usermode exit event */ + ueventCreate(&exitEvent, false); + + /* Create gamecard detection thread */ + result = threadCreate(&thread, fsGameCardDetectionThreadFunc, NULL, 0x10000, 0x2C, -2); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to create gamecard detection thread! (0x%08X)", result); + ret = -10; + goto out; + } + + /* Start gamecard detection thread */ + result = threadStart(&thread); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Failed to start gamecard detection thread! (0x%08X)", result); + ret = -11; + goto out; + } + + startGcThread = true; + + /* Mount BIS System partition from the eMMC */ + if (!mountSysEmmcPartition()) + { + ret = -12; + goto out; + } + + /* Main application loop */ + while(appletMainLoop()) + { + UIResult result = uiProcess(); + switch(result) { - /* Initialize the csrng service */ - result = csrngInitialize(); - if (R_SUCCEEDED(result)) - { - /* Initialize the spl service */ - result = splInitialize(); - if (R_SUCCEEDED(result)) - { - /* Initialize the pm:dmnt service */ - result = pmdmntInitialize(); - if (R_SUCCEEDED(result)) - { - /* Open device operator */ - result = fsOpenDeviceOperator(&fsOperatorInstance); - if (R_SUCCEEDED(result)) - { - /* Open gamecard detection event notifier */ - result = fsOpenGameCardDetectionEventNotifier(&fsGameCardEventNotifier); - if (R_SUCCEEDED(result)) - { - /* Retrieve gamecard detection event handle */ - result = fsEventNotifierGetEventHandle(&fsGameCardEventNotifier, &fsGameCardEventHandle); - if (R_SUCCEEDED(result)) - { - /* Retrieve initial gamecard status */ - gameCardInserted = isGameCardInserted(); - - /* Load gamecard detection kernel event */ - eventLoadRemote(&fsGameCardKernelEvent, fsGameCardEventHandle, false); - - /* Create usermode exit event */ - ueventCreate(&exitEvent, false); - - /* Create gamecard detection thread */ - Thread thread; - result = threadCreate(&thread, fsGameCardDetectionThreadFunc, NULL, 0x10000, 0x2C, -2); - if (R_SUCCEEDED(result)) - { - /* Start gamecard detection thread */ - result = threadStart(&thread); - if (R_SUCCEEDED(result)) - { - /* Zero out NCA keyset */ - memset(&nca_keyset, 0, sizeof(nca_keyset_t)); - - /* Init ExeFS context */ - initExeFsContext(); - - /* Init RomFS context */ - initRomFsContext(); - - /* Init BKTR context */ - initBktrContext(); - - /* Make sure output directories exist */ - createOutputDirectories(); - - /* Load settings from configuration file */ - loadConfig(); - - /* Main application loop */ - bool exitLoop = false; - while(appletMainLoop()) - { - UIResult result = uiProcess(); - switch(result) - { - case resultShowMainMenu: - uiSetState(stateMainMenu); - break; - case resultShowGameCardMenu: - uiSetState(stateGameCardMenu); - break; - case resultShowXciDumpMenu: - uiSetState(stateXciDumpMenu); - break; - case resultDumpXci: - uiSetState(stateDumpXci); - break; - case resultShowNspDumpMenu: - uiSetState(stateNspDumpMenu); - break; - case resultShowNspAppDumpMenu: - uiSetState(stateNspAppDumpMenu); - break; - case resultShowNspPatchDumpMenu: - uiSetState(stateNspPatchDumpMenu); - break; - case resultShowNspAddOnDumpMenu: - uiSetState(stateNspAddOnDumpMenu); - break; - case resultDumpNsp: - uiSetState(stateDumpNsp); - break; - case resultShowHfs0Menu: - uiSetState(stateHfs0Menu); - break; - case resultShowRawHfs0PartitionDumpMenu: - uiSetState(stateRawHfs0PartitionDumpMenu); - break; - case resultDumpRawHfs0Partition: - uiSetState(stateDumpRawHfs0Partition); - break; - case resultShowHfs0PartitionDataDumpMenu: - uiSetState(stateHfs0PartitionDataDumpMenu); - break; - case resultDumpHfs0PartitionData: - uiSetState(stateDumpHfs0PartitionData); - break; - case resultShowHfs0BrowserMenu: - uiSetState(stateHfs0BrowserMenu); - break; - case resultHfs0BrowserGetList: - uiSetState(stateHfs0BrowserGetList); - break; - case resultShowHfs0Browser: - uiSetState(stateHfs0Browser); - break; - case resultHfs0BrowserCopyFile: - uiSetState(stateHfs0BrowserCopyFile); - break; - case resultShowExeFsMenu: - uiSetState(stateExeFsMenu); - break; - case resultShowExeFsSectionDataDumpMenu: - uiSetState(stateExeFsSectionDataDumpMenu); - break; - case resultDumpExeFsSectionData: - uiSetState(stateDumpExeFsSectionData); - break; - case resultShowExeFsSectionBrowserMenu: - uiSetState(stateExeFsSectionBrowserMenu); - break; - case resultExeFsSectionBrowserGetList: - uiSetState(stateExeFsSectionBrowserGetList); - break; - case resultShowExeFsSectionBrowser: - uiSetState(stateExeFsSectionBrowser); - break; - case resultExeFsSectionBrowserCopyFile: - uiSetState(stateExeFsSectionBrowserCopyFile); - break; - case resultShowRomFsMenu: - uiSetState(stateRomFsMenu); - break; - case resultShowRomFsSectionDataDumpMenu: - uiSetState(stateRomFsSectionDataDumpMenu); - break; - case resultDumpRomFsSectionData: - uiSetState(stateDumpRomFsSectionData); - break; - case resultShowRomFsSectionBrowserMenu: - uiSetState(stateRomFsSectionBrowserMenu); - break; - case resultRomFsSectionBrowserGetEntries: - uiSetState(stateRomFsSectionBrowserGetEntries); - break; - case resultShowRomFsSectionBrowser: - uiSetState(stateRomFsSectionBrowser); - break; - case resultRomFsSectionBrowserChangeDir: - uiSetState(stateRomFsSectionBrowserChangeDir); - break; - case resultRomFsSectionBrowserCopyFile: - uiSetState(stateRomFsSectionBrowserCopyFile); - break; - case resultRomFsSectionBrowserCopyDir: - uiSetState(stateRomFsSectionBrowserCopyDir); - break; - case resultDumpGameCardCertificate: - uiSetState(stateDumpGameCardCertificate); - break; - case resultShowSdCardEmmcMenu: - uiSetState(stateSdCardEmmcMenu); - break; - case resultShowSdCardEmmcTitleMenu: - uiSetState(stateSdCardEmmcTitleMenu); - break; - case resultShowSdCardEmmcOrphanPatchAddOnMenu: - uiSetState(stateSdCardEmmcOrphanPatchAddOnMenu); - break; - case resultShowSdCardEmmcBatchModeMenu: - uiSetState(stateSdCardEmmcBatchModeMenu); - break; - case resultSdCardEmmcBatchDump: - uiSetState(stateSdCardEmmcBatchDump); - break; - case resultShowUpdateMenu: - uiSetState(stateUpdateMenu); - break; - case resultUpdateNSWDBXml: - uiSetState(stateUpdateNSWDBXml); - break; - case resultUpdateApplication: - uiSetState(stateUpdateApplication); - break; - case resultExit: - exitLoop = true; - break; - default: - break; - } - - if (exitLoop) break; - } - - /* Signal the exit event to terminate the gamecard detection thread */ - ueventSignal(&exitEvent); - - /* Wait for the gamecard detection thread to exit */ - threadWaitForExit(&thread); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to start gamecard detection thread! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -11; - } - - /* Close gamecard detection thread */ - threadClose(&thread); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to create gamecard detection thread! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -10; - } - - /* Close gamecard detection kernel event */ - eventClose(&fsGameCardKernelEvent); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to retrieve gamecard detection event handle! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -9; - } - - /* Close gamecard detection event notifier */ - fsEventNotifierClose(&fsGameCardEventNotifier); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to open gamecard detection event notifier! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -8; - } - - /* Close device operator */ - fsDeviceOperatorClose(&fsOperatorInstance); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to open device operator! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -7; - } - - /* Denitialize the pm:dmnt service */ - pmdmntExit(); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to initialize the pm:dmnt service! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -6; - } - - /* Denitialize the spl service */ - splExit(); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to initialize the spl service! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -5; - } - - /* Denitialize the csrng service */ - csrngExit(); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to initialize the csrng service! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -4; - } - - /* Denitialize the ns service */ - nsExit(); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to initialize the ns service! (0x%08X)", result); - uiRefreshDisplay(); - delay(5); - ret = -3; + case resultShowMainMenu: + uiSetState(stateMainMenu); + break; + case resultShowGameCardMenu: + uiSetState(stateGameCardMenu); + break; + case resultShowXciDumpMenu: + uiSetState(stateXciDumpMenu); + break; + case resultDumpXci: + uiSetState(stateDumpXci); + break; + case resultShowNspDumpMenu: + uiSetState(stateNspDumpMenu); + break; + case resultShowNspAppDumpMenu: + uiSetState(stateNspAppDumpMenu); + break; + case resultShowNspPatchDumpMenu: + uiSetState(stateNspPatchDumpMenu); + break; + case resultShowNspAddOnDumpMenu: + uiSetState(stateNspAddOnDumpMenu); + break; + case resultDumpNsp: + uiSetState(stateDumpNsp); + break; + case resultShowHfs0Menu: + uiSetState(stateHfs0Menu); + break; + case resultShowRawHfs0PartitionDumpMenu: + uiSetState(stateRawHfs0PartitionDumpMenu); + break; + case resultDumpRawHfs0Partition: + uiSetState(stateDumpRawHfs0Partition); + break; + case resultShowHfs0PartitionDataDumpMenu: + uiSetState(stateHfs0PartitionDataDumpMenu); + break; + case resultDumpHfs0PartitionData: + uiSetState(stateDumpHfs0PartitionData); + break; + case resultShowHfs0BrowserMenu: + uiSetState(stateHfs0BrowserMenu); + break; + case resultHfs0BrowserGetList: + uiSetState(stateHfs0BrowserGetList); + break; + case resultShowHfs0Browser: + uiSetState(stateHfs0Browser); + break; + case resultHfs0BrowserCopyFile: + uiSetState(stateHfs0BrowserCopyFile); + break; + case resultShowExeFsMenu: + uiSetState(stateExeFsMenu); + break; + case resultShowExeFsSectionDataDumpMenu: + uiSetState(stateExeFsSectionDataDumpMenu); + break; + case resultDumpExeFsSectionData: + uiSetState(stateDumpExeFsSectionData); + break; + case resultShowExeFsSectionBrowserMenu: + uiSetState(stateExeFsSectionBrowserMenu); + break; + case resultExeFsSectionBrowserGetList: + uiSetState(stateExeFsSectionBrowserGetList); + break; + case resultShowExeFsSectionBrowser: + uiSetState(stateExeFsSectionBrowser); + break; + case resultExeFsSectionBrowserCopyFile: + uiSetState(stateExeFsSectionBrowserCopyFile); + break; + case resultShowRomFsMenu: + uiSetState(stateRomFsMenu); + break; + case resultShowRomFsSectionDataDumpMenu: + uiSetState(stateRomFsSectionDataDumpMenu); + break; + case resultDumpRomFsSectionData: + uiSetState(stateDumpRomFsSectionData); + break; + case resultShowRomFsSectionBrowserMenu: + uiSetState(stateRomFsSectionBrowserMenu); + break; + case resultRomFsSectionBrowserGetEntries: + uiSetState(stateRomFsSectionBrowserGetEntries); + break; + case resultShowRomFsSectionBrowser: + uiSetState(stateRomFsSectionBrowser); + break; + case resultRomFsSectionBrowserChangeDir: + uiSetState(stateRomFsSectionBrowserChangeDir); + break; + case resultRomFsSectionBrowserCopyFile: + uiSetState(stateRomFsSectionBrowserCopyFile); + break; + case resultRomFsSectionBrowserCopyDir: + uiSetState(stateRomFsSectionBrowserCopyDir); + break; + case resultDumpGameCardCertificate: + uiSetState(stateDumpGameCardCertificate); + break; + case resultShowSdCardEmmcMenu: + uiSetState(stateSdCardEmmcMenu); + break; + case resultShowSdCardEmmcTitleMenu: + uiSetState(stateSdCardEmmcTitleMenu); + break; + case resultShowSdCardEmmcOrphanPatchAddOnMenu: + uiSetState(stateSdCardEmmcOrphanPatchAddOnMenu); + break; + case resultShowSdCardEmmcBatchModeMenu: + uiSetState(stateSdCardEmmcBatchModeMenu); + break; + case resultSdCardEmmcBatchDump: + uiSetState(stateSdCardEmmcBatchDump); + break; + case resultShowTicketMenu: + uiSetState(stateTicketMenu); + break; + case resultDumpTicket: + uiSetState(stateDumpTicket); + break; + case resultShowUpdateMenu: + uiSetState(stateUpdateMenu); + break; + case resultUpdateNSWDBXml: + uiSetState(stateUpdateNSWDBXml); + break; + case resultUpdateApplication: + uiSetState(stateUpdateApplication); + break; + case resultExit: + exitMainLoop = true; + break; + default: + break; } - /* Denitialize the ncm service */ - ncmExit(); - } else { - uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_RGB, "Failed to initialize the ncm service! (0x%08X)", result); + if (exitMainLoop) break; + } + + /* Signal the exit event to terminate the gamecard detection thread */ + ueventSignal(&exitEvent); + + /* Wait for the gamecard detection thread to exit */ + threadWaitForExit(&thread); + +out: + if (ret < 0) + { uiRefreshDisplay(); delay(5); - ret = -2; } + /* Unmount BIS System partition from the eMMC */ + unmountSysEmmcPartition(); + + /* Close gamecard detection thread */ + if (startGcThread) threadClose(&thread); + + /* Close gamecard detection kernel event */ + if (loadGcKernEvt) eventClose(&fsGameCardKernelEvent); + + /* Close gamecard detection event notifier */ + if (openGcEvtNotifier) fsEventNotifierClose(&fsGameCardEventNotifier); + + /* Close device operator */ + if (openFsDevOp) fsDeviceOperatorClose(&fsOperatorInstance); + + /* Denitialize the pm:dmnt service */ + if (initPmdmnt) pmdmntExit(); + + /* Denitialize the spl service */ + if (initSpl) splExit(); + + /* Denitialize the csrng service */ + if (initCsrng) csrngExit(); + + /* Denitialize the ns service */ + if (initNs) nsExit(); + + /* Denitialize the ncm service */ + if (initNcm) ncmExit(); + /* Disable CPU boost mode */ appletSetCpuBoostMode(ApmCpuBoostMode_Disabled); diff --git a/source/nca.c b/source/nca.c index 6e8cf7c..ac7a85a 100644 --- a/source/nca.c +++ b/source/nca.c @@ -150,13 +150,14 @@ void generateCnmtXml(cnmt_xml_program_info *xml_program_info, cnmt_xml_content_i " %lu\n" \ " %s\n" \ " %u\n" \ - " 0\n" \ - " \n", \ + " %u\n" \ + " \n", getContentType(xml_content_info[i].type), \ xml_content_info[i].nca_id_str, \ xml_content_info[i].size, \ xml_content_info[i].hash_str, \ - xml_content_info[i].keyblob); \ + xml_content_info[i].keyblob, \ + (xml_content_info[i].type != NcmContentType_CNMT ? xml_content_info[i].id_offset : 0)); strcat(out, tmp); } @@ -300,8 +301,8 @@ bool processNcaCtrSectionBlock(NcmContentStorage *ncmStorage, const NcmNcaId *nc Result result; unsigned char ctr[0x10]; - char nca_id[33] = {'\0'}; - convertDataToHexString(ncaId->c, 16, nca_id, 33); + char nca_id[SHA256_HASH_SIZE + 1] = {'\0'}; + convertDataToHexString(ncaId->c, SHA256_HASH_SIZE / 2, nca_id, SHA256_HASH_SIZE + 1); u64 block_start_offset = (offset - (offset % 0x10)); u64 block_end_offset = (u64)round_up(offset + bufSize, 0x10); @@ -310,7 +311,8 @@ bool processNcaCtrSectionBlock(NcmContentStorage *ncmStorage, const NcmNcaId *nc u64 block_size_used = (block_size > NCA_CTR_BUFFER_SIZE ? NCA_CTR_BUFFER_SIZE : block_size); u64 output_block_size = (block_size > NCA_CTR_BUFFER_SIZE ? (NCA_CTR_BUFFER_SIZE - (offset - block_start_offset)) : bufSize); - if (R_FAILED(result = ncmContentStorageReadContentIdFile(ncmStorage, ncaId, block_start_offset, ncaCtrBuf, block_size_used))) + result = ncmContentStorageReadContentIdFile(ncmStorage, ncaId, block_start_offset, ncaCtrBuf, block_size_used); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to read encrypted %lu bytes block at offset 0x%016lX from NCA \"%s\"! (0x%08X)", block_size_used, block_start_offset, nca_id, result); return false; @@ -509,7 +511,8 @@ bool bktrSectionPhysicalRead(void *outBuf, size_t bufSize) { u64 block_size_used = (block_size > NCA_CTR_BUFFER_SIZE ? NCA_CTR_BUFFER_SIZE : block_size); - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&(bktrContext.ncmStorage), &(bktrContext.ncaId), block_start_offset, ncaCtrBuf, block_size_used))) + result = ncmContentStorageReadContentIdFile(&(bktrContext.ncmStorage), &(bktrContext.ncaId), block_start_offset, ncaCtrBuf, block_size_used); + if (R_FAILED(result)) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "BKTR: failed to read encrypted %lu bytes block at offset 0x%016lX! (0x%08X)", block_size_used, block_start_offset, result); return false; @@ -655,6 +658,8 @@ bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title if (!loadNcaKeyset()) return false; + int ret; + u32 i; size_t crypt_res; Aes128XtsContext hdr_aes_ctx; @@ -702,7 +707,7 @@ bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title } } } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NCA magic word! Wrong header key? (0x%08X)", bswap_32(out->magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NCA magic word! Wrong header key? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(out->magic)); return false; } @@ -732,16 +737,28 @@ bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title if (retrieveTitleKeyData) { - if (!retrieveNcaTikTitleKey(out, (u8*)(&(rights_info->tik_data)), rights_info->enc_titlekey, rights_info->dec_titlekey)) return false; + ret = retrieveNcaTikTitleKey(out, (u8*)(&(rights_info->tik_data)), rights_info->enc_titlekey, rights_info->dec_titlekey); - memset(decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); - memcpy(decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), rights_info->dec_titlekey, 0x10); - - rights_info->retrieved_tik = true; + if (ret >= 0) + { + memset(decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); + memcpy(decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), rights_info->dec_titlekey, 0x10); + + rights_info->retrieved_tik = true; + } else { + if (ret == -2) + { + // We are probably dealing with a pre-installed title + // Let's enable our missing ticket flag - we'll use it to display a prompt asking the user if they want to proceed anyway + rights_info->missing_tik = true; + } else { + return false; + } + } } } else { // Copy what we already have - if (retrieveTitleKeyData) + if (retrieveTitleKeyData && rights_info->retrieved_tik) { memset(decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); memcpy(decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), rights_info->dec_titlekey, 0x10); @@ -753,7 +770,7 @@ bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title { u8 tmp_dec_titlekey[0x10]; - if (!retrieveNcaTikTitleKey(out, NULL, NULL, tmp_dec_titlekey)) return false; + if (retrieveNcaTikTitleKey(out, NULL, NULL, tmp_dec_titlekey) < 0) return false; memset(decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); memcpy(decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), tmp_dec_titlekey, 0x10); @@ -854,13 +871,13 @@ bool processProgramNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca if (bswap_32(nca_pfs0_header.magic) != PFS0_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid magic word for Program NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)", bswap_32(nca_pfs0_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid magic word for Program NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(nca_pfs0_header.magic)); return false; } if (!nca_pfs0_header.file_cnt || !nca_pfs0_header.str_table_size) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: Program NCA section #0 PFS0 partition is empty! Wrong KAEK?"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: Program NCA section #0 PFS0 partition is empty! Wrong KAEK?\nTry running Lockpick_RCM to generate the keys file from scratch."); return false; } @@ -1085,7 +1102,7 @@ bool processProgramNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca return true; } -bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpType, u8 *ncaBuf, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *output, title_rights_ctx *rights_info, bool replaceKeyArea) +bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpType, u8 *ncaBuf, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, u32 cnmtNcaIndex, nca_cnmt_mod_data *output, title_rights_ctx *rights_info, bool replaceKeyArea) { if (!ncaBuf || !xml_program_info || !xml_content_info || !output || !rights_info) { @@ -1095,7 +1112,7 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy nca_header_t dec_header; - u32 i; + u32 i, j, k = 0; u64 section_offset; u64 section_size; @@ -1125,7 +1142,7 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy // Decrypt the NCA header // Don't retrieve the ticket and/or titlekey if we're dealing with a Patch with titlekey crypto bundled with the inserted gamecard - if (!decryptNcaHeader(ncaBuf, xml_content_info->size, &dec_header, rights_info, xml_content_info->decrypted_nca_keys, (curStorageId != FsStorageId_GameCard))) return false; + if (!decryptNcaHeader(ncaBuf, xml_content_info[cnmtNcaIndex].size, &dec_header, rights_info, xml_content_info[cnmtNcaIndex].decrypted_nca_keys, (curStorageId != FsStorageId_GameCard))) return false; if (dec_header.fs_headers[0].partition_type != NCA_FS_HEADER_PARTITION_PFS0 || dec_header.fs_headers[0].fs_type != NCA_FS_HEADER_FSTYPE_PFS0) { @@ -1172,7 +1189,7 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy if (has_rights_id && replaceKeyArea) { // Generate new encrypted NCA key area using titlekey - if (!generateEncryptedNcaKeyAreaWithTitlekey(&dec_header, xml_content_info->decrypted_nca_keys)) return false; + if (!generateEncryptedNcaKeyAreaWithTitlekey(&dec_header, xml_content_info[cnmtNcaIndex].decrypted_nca_keys)) return false; // Remove rights ID from NCA memset(dec_header.rights_id, 0, 0x10); @@ -1200,7 +1217,7 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy } u8 ctr_key[NCA_KEY_AREA_KEY_SIZE]; - memcpy(ctr_key, xml_content_info->decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), NCA_KEY_AREA_KEY_SIZE); + memcpy(ctr_key, xml_content_info[cnmtNcaIndex].decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), NCA_KEY_AREA_KEY_SIZE); aes128CtrContextCreate(&aes_ctx, ctr_key, ctr); section_data = malloc(section_size); @@ -1217,14 +1234,14 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy if (bswap_32(nca_pfs0_header.magic) != PFS0_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid magic word for CNMT NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)", bswap_32(nca_pfs0_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid magic word for CNMT NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(nca_pfs0_header.magic)); free(section_data); return false; } if (!nca_pfs0_header.file_cnt || !nca_pfs0_header.str_table_size) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: CNMT NCA section #0 PFS0 partition is empty! Wrong KAEK?"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: CNMT NCA section #0 PFS0 partition is empty! Wrong KAEK?\nTry running Lockpick_RCM to generate the keys file from scratch."); free(section_data); return false; } @@ -1270,14 +1287,48 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy // Fill information for our CNMT XML digest_offset = (title_cnmt_offset + title_cnmt_size - (u64)SHA256_HASH_SIZE); memcpy(xml_program_info->digest, section_data + digest_offset, SHA256_HASH_SIZE); - convertDataToHexString(xml_program_info->digest, 32, xml_program_info->digest_str, 65); - xml_content_info->keyblob = (dec_header.crypto_type2 > dec_header.crypto_type ? dec_header.crypto_type2 : dec_header.crypto_type); - xml_program_info->min_keyblob = (rights_info->has_rights_id ? rights_info->rights_id[15] : xml_content_info->keyblob); + convertDataToHexString(xml_program_info->digest, SHA256_HASH_SIZE, xml_program_info->digest_str, (SHA256_HASH_SIZE * 2) + 1); + xml_content_info[cnmtNcaIndex].keyblob = (dec_header.crypto_type2 > dec_header.crypto_type ? dec_header.crypto_type2 : dec_header.crypto_type); + xml_program_info->required_dl_sysver = title_cnmt_header.required_dl_sysver; + xml_program_info->min_keyblob = (rights_info->has_rights_id ? rights_info->rights_id[15] : xml_content_info[cnmtNcaIndex].keyblob); xml_program_info->min_sysver = title_cnmt_extended_header.min_sysver; xml_program_info->patch_tid = title_cnmt_extended_header.patch_tid; - // Empty CNMT content records - memset(section_data + title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.table_offset, 0, title_cnmt_size - sizeof(cnmt_header) - (u64)title_cnmt_header.table_offset - (u64)SHA256_HASH_SIZE); + // Retrieve the ID offset and content record offset for each of our NCAs (except the CNMT NCA) + // Also wipe each of the content records we're gonna replace (excluding Delta Fragments) + for(i = 0; i < (xml_program_info->nca_cnt - 1); i++) // Discard CNMT NCA + { + for(j = 0; j < title_cnmt_header.content_cnt; j++) + { + cnmt_content_record cnt_record; + memcpy(&cnt_record, section_data + title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.extended_header_size + (j * sizeof(cnmt_content_record)), sizeof(cnmt_content_record)); + + if (cnt_record.type == NCA_CONTENT_TYPE_DELTA) continue; + + if (!memcmp(xml_content_info[i].nca_id, cnt_record.nca_id, SHA256_HASH_SIZE / 2) || xml_content_info[i].type == cnt_record.type) + { + // Save ID offset and content record offset + xml_content_info[i].id_offset = cnt_record.id_offset; + xml_content_info[i].cnt_record_offset = (j * sizeof(cnmt_content_record)); + + // Empty CNMT content record + memset(section_data + title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.extended_header_size + (j * sizeof(cnmt_content_record)), 0, sizeof(cnmt_content_record)); + + // Increase counter + k++; + + break; + } + } + } + + // Verify counter + if (k != (xml_program_info->nca_cnt - 1)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid content record entries in the CNMT NCA!"); + free(section_data); + return false; + } // Replace input buffer data in-place memcpy(ncaBuf, &dec_header, NCA_FULL_HEADER_LENGTH); @@ -1293,6 +1344,8 @@ bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpTy output->section_offset = section_offset; output->section_size = section_size; output->hash_table_offset = (section_offset + dec_header.fs_headers[0].pfs0_superblock.hash_table_offset); + output->hash_block_size = dec_header.fs_headers[0].pfs0_superblock.block_size; + output->hash_block_cnt = (dec_header.fs_headers[0].pfs0_superblock.hash_table_size / SHA256_HASH_SIZE); output->pfs0_offset = nca_pfs0_offset; output->pfs0_size = dec_header.fs_headers[0].pfs0_superblock.pfs0_size; output->title_cnmt_offset = title_cnmt_offset; @@ -1310,52 +1363,47 @@ bool patchCnmtNca(u8 *ncaBuf, u64 ncaBufSize, cnmt_xml_program_info *xml_program } u32 i; - u32 nca_cnt; + + u32 nca_cnt = (xml_program_info->nca_cnt - 1); // Discard CNMT NCA cnmt_header title_cnmt_header; - cnmt_content_record *title_cnmt_content_records = NULL; + cnmt_content_record title_cnmt_content_record; u64 title_cnmt_content_records_offset; - u8 pfs0_block_hash[SHA256_HASH_SIZE]; - nca_header_t dec_header; Aes128CtrContext aes_ctx; - // Update number of content records - nca_cnt = (xml_program_info->nca_cnt - 1); // Discard CNMT NCA + // Copy CNMT header memcpy(&title_cnmt_header, ncaBuf + cnmt_mod->title_cnmt_offset, sizeof(cnmt_header)); - title_cnmt_header.content_records_cnt = (u16)nca_cnt; - memcpy(ncaBuf + cnmt_mod->title_cnmt_offset, &title_cnmt_header, sizeof(cnmt_header)); - // Allocate memory for our content records - title_cnmt_content_records = calloc(nca_cnt, sizeof(cnmt_content_record)); - if (!title_cnmt_content_records) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for CNMT NCA content records!"); - return false; - } - - title_cnmt_content_records_offset = (cnmt_mod->title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.table_offset); - memcpy(title_cnmt_content_records, ncaBuf + title_cnmt_content_records_offset, (u64)nca_cnt * sizeof(cnmt_content_record)); + // Calculate the start offset for the content records + title_cnmt_content_records_offset = (cnmt_mod->title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.extended_header_size); // Write content records for(i = 0; i < nca_cnt; i++) { - memcpy(title_cnmt_content_records[i].hash, xml_content_info[i].hash, 32); - memcpy(title_cnmt_content_records[i].nca_id, xml_content_info[i].nca_id, 16); - convertU64ToNcaSize(xml_content_info[i].size, title_cnmt_content_records[i].size); - title_cnmt_content_records[i].type = xml_content_info[i].type; + memset(&title_cnmt_content_record, 0, sizeof(cnmt_content_record)); - u64 cur_content_record = (title_cnmt_content_records_offset + ((u64)i * sizeof(cnmt_content_record))); - memcpy(ncaBuf + cur_content_record, &(title_cnmt_content_records[i]), sizeof(cnmt_content_record)); + memcpy(title_cnmt_content_record.hash, xml_content_info[i].hash, SHA256_HASH_SIZE); + memcpy(title_cnmt_content_record.nca_id, xml_content_info[i].nca_id, SHA256_HASH_SIZE / 2); + convertU64ToNcaSize(xml_content_info[i].size, title_cnmt_content_record.size); + title_cnmt_content_record.type = xml_content_info[i].type; + title_cnmt_content_record.id_offset = xml_content_info[i].id_offset; + + memcpy(ncaBuf + title_cnmt_content_records_offset + xml_content_info[i].cnt_record_offset, &title_cnmt_content_record, sizeof(cnmt_content_record)); } - free(title_cnmt_content_records); - - // Calculate block hash - sha256CalculateHash(pfs0_block_hash, ncaBuf + cnmt_mod->pfs0_offset, cnmt_mod->pfs0_size); - memcpy(ncaBuf + cnmt_mod->hash_table_offset, pfs0_block_hash, SHA256_HASH_SIZE); + // Recalculate block hashes + for(i = 0; i < cnmt_mod->hash_block_cnt; i++) + { + u64 blk_offset = ((u64)i * cnmt_mod->hash_block_size); + + u64 rest_size = (cnmt_mod->pfs0_size - blk_offset); + u64 blk_size = (rest_size > cnmt_mod->hash_block_size ? cnmt_mod->hash_block_size : rest_size); + + sha256CalculateHash(ncaBuf + cnmt_mod->hash_table_offset + (i * SHA256_HASH_SIZE), ncaBuf + cnmt_mod->pfs0_offset + blk_offset, blk_size); + } // Copy header to struct memcpy(&dec_header, ncaBuf, sizeof(nca_header_t)); @@ -1393,9 +1441,9 @@ bool patchCnmtNca(u8 *ncaBuf, u64 ncaBufSize, cnmt_xml_program_info *xml_program // Calculate CNMT NCA SHA-256 checksum and fill information for our CNMT XML sha256CalculateHash(xml_content_info[xml_program_info->nca_cnt - 1].hash, ncaBuf, ncaBufSize); - convertDataToHexString(xml_content_info[xml_program_info->nca_cnt - 1].hash, 32, xml_content_info[xml_program_info->nca_cnt - 1].hash_str, 65); - memcpy(xml_content_info[xml_program_info->nca_cnt - 1].nca_id, xml_content_info[xml_program_info->nca_cnt - 1].hash, 16); - convertDataToHexString(xml_content_info[xml_program_info->nca_cnt - 1].nca_id, SHA256_HASH_SIZE / 2, xml_content_info[xml_program_info->nca_cnt - 1].nca_id_str, 33); + convertDataToHexString(xml_content_info[xml_program_info->nca_cnt - 1].hash, SHA256_HASH_SIZE, xml_content_info[xml_program_info->nca_cnt - 1].hash_str, (SHA256_HASH_SIZE * 2) + 1); + memcpy(xml_content_info[xml_program_info->nca_cnt - 1].nca_id, xml_content_info[xml_program_info->nca_cnt - 1].hash, SHA256_HASH_SIZE / 2); + convertDataToHexString(xml_content_info[xml_program_info->nca_cnt - 1].nca_id, SHA256_HASH_SIZE / 2, xml_content_info[xml_program_info->nca_cnt - 1].nca_id_str, SHA256_HASH_SIZE + 1); return true; } @@ -1517,7 +1565,7 @@ bool readExeFsEntryFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, if (!found_exefs) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: NCA doesn't hold an ExeFS section!"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: NCA doesn't hold an ExeFS section! Wrong keys?\nTry running Lockpick_RCM to generate the keys file from scratch."); return false; } @@ -1616,7 +1664,7 @@ bool readRomFsEntryFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, if (bswap_32(dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.magic) != IVFC_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid IVFC magic word for NCA RomFS section! Wrong KAEK? (0x%08X)", bswap_32(dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid IVFC magic word for NCA RomFS section! Wrong KAEK? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.magic)); return false; } @@ -1784,7 +1832,7 @@ bool readBktrEntryFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, if (bswap_32(bktrContext.superblock.ivfc_header.magic) != IVFC_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid IVFC magic word for NCA BKTR section! Wrong KAEK? (0x%08X)", bswap_32(bktrContext.superblock.ivfc_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid IVFC magic word for NCA BKTR section! Wrong KAEK? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(bktrContext.superblock.ivfc_header.magic)); return false; } @@ -1796,7 +1844,7 @@ bool readBktrEntryFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, if (bswap_32(bktrContext.superblock.relocation_header.magic) != BKTR_MAGIC || bswap_32(bktrContext.superblock.subsection_header.magic) != BKTR_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid BKTR magic word for NCA BKTR relocation/subsection header! (0x%02X | 0x%02X)", bswap_32(bktrContext.superblock.relocation_header.magic), bswap_32(bktrContext.superblock.subsection_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid BKTR magic word for NCA BKTR relocation/subsection header! Wrong KAEK? (0x%02X | 0x%02X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(bktrContext.superblock.relocation_header.magic), bswap_32(bktrContext.superblock.subsection_header.magic)); return false; } @@ -2076,13 +2124,13 @@ bool generateProgramInfoXml(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId if (bswap_32(nca_pfs0_header.magic) != PFS0_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid magic word for Program NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)", bswap_32(nca_pfs0_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid magic word for Program NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(nca_pfs0_header.magic)); return false; } if (!nca_pfs0_header.file_cnt || !nca_pfs0_header.str_table_size) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: Program NCA section #0 PFS0 partition is empty! Wrong KAEK?"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: Program NCA section #0 PFS0 partition is empty! Wrong KAEK?\nTry running Lockpick_RCM to generate the keys file from scratch."); return false; } @@ -2160,7 +2208,7 @@ bool generateProgramInfoXml(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId if (bswap_32(npdm_header.magic) != META_MAGIC) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NPDM META magic word! (0x%08X)", bswap_32(npdm_header.magic)); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid NPDM META magic word! Wrong KAEK? (0x%08X)\nTry running Lockpick_RCM to generate the keys file from scratch.", bswap_32(npdm_header.magic)); goto out; } @@ -2526,6 +2574,9 @@ char *getNacpRatingAgeOrganizationName(u8 val) case 11: out = "OFLC"; break; + case 12: + out = "IARCGeneric"; + break; default: out = "Unknown"; break; @@ -2717,6 +2768,29 @@ char *getNacpRuntimeAddOnContentInstall(u8 val) return out; } +char *getNacpRuntimeParameterDelivery(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Always"; + break; + case 1: + out = "AlwaysIfUserStateMatched"; + break; + case 2: + out = "OnRestart"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + char *getNacpPlayLogQueryCapability(u8 val) { char *out = NULL; @@ -3147,9 +3221,18 @@ bool retrieveNacpDataFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaI sprintf(tmp, " %s\n", getNacpRuntimeAddOnContentInstall(controlNacp.RuntimeAddOnContentInstall)); strcat(nacpXml, tmp); - sprintf(tmp, " 0x%016lx\n", controlNacp.PlayLogQueryableApplicationId[0]); + sprintf(tmp, " %s\n", getNacpRuntimeParameterDelivery(controlNacp.RuntimeParameterDelivery)); strcat(nacpXml, tmp); + for(i = 0; i < 16; i++) + { + if (controlNacp.PlayLogQueryableApplicationId[i] != 0) + { + sprintf(tmp, " 0x%016lx\n", controlNacp.PlayLogQueryableApplicationId[i]); + strcat(nacpXml, tmp); + } + } + sprintf(tmp, " %s\n", getNacpPlayLogQueryCapability(controlNacp.PlayLogQueryCapability)); strcat(nacpXml, tmp); diff --git a/source/nca.h b/source/nca.h index 2c77fef..0d1fb43 100644 --- a/source/nca.h +++ b/source/nca.h @@ -228,10 +228,13 @@ typedef struct { u32 version; u8 type; u8 unk1; - u16 table_offset; - u16 content_records_cnt; - u16 meta_records_cnt; - u8 unk2[12]; + u16 extended_header_size; + u16 content_cnt; + u16 content_meta_cnt; + u8 attr; + u8 unk2[0x03]; + u32 required_dl_sysver; + u8 unk3[0x04]; } PACKED cnmt_header; typedef struct { @@ -244,7 +247,7 @@ typedef struct { u8 nca_id[0x10]; u8 size[6]; u8 type; - u8 unk; + u8 id_offset; } PACKED cnmt_content_record; typedef struct { @@ -253,8 +256,8 @@ typedef struct { u32 version; u32 required_dl_sysver; u32 nca_cnt; - u8 digest[32]; - char digest_str[65]; + u8 digest[SHA256_HASH_SIZE]; + char digest_str[(SHA256_HASH_SIZE * 2) + 1]; u8 min_keyblob; u32 min_sysver; u64 patch_tid; @@ -262,12 +265,14 @@ typedef struct { typedef struct { u8 type; - u8 nca_id[16]; - char nca_id_str[33]; + u8 nca_id[SHA256_HASH_SIZE / 2]; + char nca_id_str[SHA256_HASH_SIZE + 1]; u64 size; - u8 hash[32]; - char hash_str[65]; + u8 hash[SHA256_HASH_SIZE]; + char hash_str[(SHA256_HASH_SIZE * 2) + 1]; u8 keyblob; + u8 id_offset; + u64 cnt_record_offset; // Relative to the start of the content record structs in the CNMT u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE]; u8 encrypted_header_mod[NCA_FULL_HEADER_LENGTH]; } PACKED cnmt_xml_content_info; @@ -286,6 +291,8 @@ typedef struct { u64 section_offset; // Relative to NCA start u64 section_size; u64 hash_table_offset; // Relative to NCA start + u64 hash_block_size; + u32 hash_block_cnt; u64 pfs0_offset; // Relative to NCA start u64 pfs0_size; u64 title_cnmt_offset; // Relative to NCA start @@ -321,6 +328,7 @@ typedef struct { u8 cert_data[ETICKET_CERT_FILE_SIZE]; rsa2048_sha256_ticket tik_data; bool retrieved_tik; + bool missing_tik; } PACKED title_rights_ctx; typedef struct { @@ -463,7 +471,8 @@ typedef struct { u8 LogoType; u8 LogoHandling; u8 RuntimeAddOnContentInstall; - u8 Reserved_0x30F3[0x3]; + u8 RuntimeParameterDelivery; + u8 Reserved_0x30F4[0x2]; u8 CrashReport; u8 Hdcp; u64 SeedForPseudoDeviceId; @@ -515,7 +524,7 @@ bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title bool processProgramNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, cnmt_xml_content_info *xml_content_info, nca_program_mod_data *output); -bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpType, u8 *ncaBuf, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *output, title_rights_ctx *rights_info, bool replaceKeyArea); +bool retrieveCnmtNcaData(FsStorageId curStorageId, nspDumpType selectedNspDumpType, u8 *ncaBuf, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, u32 cnmtNcaIndex, nca_cnmt_mod_data *output, title_rights_ctx *rights_info, bool replaceKeyArea); bool patchCnmtNca(u8 *ncaBuf, u64 ncaBufSize, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *cnmt_mod); diff --git a/source/save.c b/source/save.c new file mode 100644 index 0000000..eeb277f --- /dev/null +++ b/source/save.c @@ -0,0 +1,1931 @@ +#include +#include +#include +#include + +#include "save.h" +#include "util.h" +#include "keys.h" + +#define REMAP_ENTRY_LENGTH 0x20 + +/* Extern variables */ + +extern nca_keyset_t nca_keyset; + +extern char strbuf[NAME_BUF_LEN]; + +/* Statically allocated variables */ + +static bool loadedCerts = false; + +static const char *cert_CA00000003_path = "/certificate/CA00000003"; +static const char *cert_XS00000020_path = "/certificate/XS00000020"; +static const char *cert_XS00000021_path = "/certificate/XS00000021"; +static const char *cert_XS00000024_path = "/certificate/XS00000024"; // 9.0.0+ + +static const u8 cert_CA00000003_hash[0x20] = { + 0x62, 0x69, 0x0E, 0xC0, 0x4C, 0x62, 0x9D, 0x08, 0x38, 0xBB, 0xDF, 0x65, 0xC5, 0xA6, 0xB0, 0x9A, + 0x54, 0x94, 0x2C, 0x87, 0x0E, 0x01, 0x55, 0x73, 0xCF, 0x7D, 0x58, 0xF2, 0x59, 0xFE, 0x36, 0xFA +}; + +static const u8 cert_XS00000020_hash[0x20] = { + 0x55, 0x23, 0x17, 0xD4, 0x4B, 0xAF, 0x4C, 0xF5, 0x31, 0x8E, 0xF5, 0xC6, 0x4E, 0x0F, 0x75, 0xD9, + 0x75, 0xD4, 0x03, 0xFD, 0x7B, 0x93, 0x7B, 0xAB, 0x46, 0x7D, 0x37, 0x94, 0x62, 0x39, 0x33, 0xE9 +}; + +static const u8 cert_XS00000021_hash[0x20] = { + 0xDE, 0xFF, 0x96, 0x01, 0x42, 0x1E, 0x00, 0xC1, 0x52, 0x60, 0x5C, 0x9F, 0x42, 0xCD, 0x91, 0xD7, + 0x90, 0x01, 0xC5, 0x7F, 0xC3, 0x27, 0x58, 0x4B, 0xD9, 0x6F, 0x71, 0x78, 0xC9, 0x44, 0xD0, 0xAD +}; + +static const u8 cert_XS00000024_hash[0x20] = { + 0xB7, 0x32, 0x8D, 0x69, 0xB9, 0xA7, 0xE4, 0x1C, 0x9C, 0xF9, 0xD8, 0x6B, 0x79, 0x81, 0x07, 0x9B, + 0x20, 0x02, 0x12, 0xF6, 0xB9, 0x41, 0x33, 0x2C, 0x61, 0x57, 0x1E, 0x3A, 0x62, 0x53, 0x7C, 0x89 +}; + +static u8 cert_root_data[ETICKET_CA_CERT_SIZE]; +static u8 cert_common_data[ETICKET_XS_CERT_SIZE]; +static u8 cert_personalized_data[ETICKET_XS_CERT_SIZE]; + +static const u8 personalized_cert_count = 2; + +static inline void save_bitmap_set_bit(void *buffer, size_t bit_offset) +{ + *((u8*)buffer + (bit_offset >> 3)) |= 1 << (bit_offset & 7); +} + +static inline void save_bitmap_clear_bit(void *buffer, size_t bit_offset) +{ + *((u8*)buffer + (bit_offset >> 3)) &= ~(u8)(1 << (bit_offset & 7)); +} + +static inline u8 save_bitmap_check_bit(const void *buffer, size_t bit_offset) +{ + return (*((u8*)buffer + (bit_offset >> 3)) & (1 << (bit_offset & 7))); +} + +bool save_duplex_storage_init(duplex_storage_ctx_t *ctx, duplex_fs_layer_info_t *layer, void *bitmap, u64 bitmap_size) +{ + if (!ctx || !layer || !layer->data_a || !layer->data_b || !layer->info.block_size_power || !bitmap || !bitmap_size) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_duplex_storage_init: invalid parameters to initialize duplex storage!"); + return false; + } + + ctx->data_a = layer->data_a; + ctx->data_b = layer->data_b; + ctx->bitmap_storage = (u8*)bitmap; + ctx->block_size = (1 << layer->info.block_size_power); + ctx->bitmap.data = ctx->bitmap_storage; + + ctx->bitmap.bitmap = calloc(1, bitmap_size >> 3); + if (!ctx->bitmap.bitmap) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_duplex_storage_init: failed to allocate memory for duplex bitmap!"); + return false; + } + + u32 bits_remaining = bitmap_size; + u32 bitmap_pos = 0; + u32 *buffer_pos = (u32*)bitmap; + + while(bits_remaining) + { + u32 bits_to_read = (bits_remaining < 32 ? bits_remaining : 32); + u32 val = *buffer_pos; + + for(u32 i = 0; i < bits_to_read; i++) + { + if (val & 0x80000000) + { + save_bitmap_set_bit(ctx->bitmap.bitmap, bitmap_pos); + } else { + save_bitmap_clear_bit(ctx->bitmap.bitmap, bitmap_pos); + } + + bitmap_pos++; + bits_remaining--; + val <<= 1; + } + + buffer_pos++; + } + + return true; +} + +u32 save_duplex_storage_read(duplex_storage_ctx_t *ctx, void *buffer, u64 offset, size_t count) +{ + if (!ctx || !ctx->block_size || !ctx->bitmap.bitmap || !buffer || !count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_duplex_storage_read: invalid parameters to read duplex storage data!"); + return 0; + } + + u64 in_pos = offset; + u32 out_pos = 0; + u32 remaining = count; + + while(remaining) + { + u32 block_num = (u32)(in_pos / ctx->block_size); + u32 block_pos = (u32)(in_pos % ctx->block_size); + u32 bytes_to_read = ((ctx->block_size - block_pos) < remaining ? (ctx->block_size - block_pos) : remaining); + + u8 *data = (save_bitmap_check_bit(ctx->bitmap.bitmap, block_num) ? ctx->data_b : ctx->data_a); + memcpy((u8*)buffer + out_pos, data + in_pos, bytes_to_read); + + out_pos += bytes_to_read; + in_pos += bytes_to_read; + remaining -= bytes_to_read; + } + + return out_pos; +} + +remap_segment_ctx_t *save_remap_init_segments(remap_header_t *header, remap_entry_ctx_t *map_entries, u32 num_map_entries) +{ + if (!header || !header->map_segment_count || !map_entries || !num_map_entries) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_init_segments: invalid parameters to initialize remap segments!"); + return NULL; + } + + remap_segment_ctx_t *segments = calloc(sizeof(remap_segment_ctx_t), header->map_segment_count); + if (!segments) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_init_segments: failed to allocate initial memory for remap segments!"); + return NULL; + } + + unsigned int i, entry_idx = 0; + bool success = false; + + for(i = 0; i < header->map_segment_count; i++) + { + remap_segment_ctx_t *seg = &segments[i]; + + seg->entries = calloc(1, sizeof(remap_entry_ctx_t)); + if (!seg->entries) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_init_segments: failed to allocate memory for remap segment entry #%u!", entry_idx); + goto out; + } + + memcpy(seg->entries, &map_entries[entry_idx], sizeof(remap_entry_ctx_t)); + seg->offset = map_entries[entry_idx].virtual_offset; + map_entries[entry_idx].segment = seg; + seg->entry_count = 1; + entry_idx++; + + while(entry_idx < num_map_entries && map_entries[entry_idx - 1].virtual_offset_end == map_entries[entry_idx].virtual_offset) + { + map_entries[entry_idx].segment = seg; + map_entries[entry_idx - 1].next = &map_entries[entry_idx]; + + seg->entries = calloc(1, sizeof(remap_entry_ctx_t)); + if (!seg->entries) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_init_segments: failed to allocate memory for remap segment entry #%u!", entry_idx); + goto out; + } + + memcpy(seg->entries, &map_entries[entry_idx], sizeof(remap_entry_ctx_t)); + seg->entry_count++; + entry_idx++; + } + + seg->length = (seg->entries[seg->entry_count - 1].virtual_offset_end - seg->entries[0].virtual_offset); + } + + success = true; + +out: + if (!success) + { + entry_idx = 0; + + for(unsigned int j = 0; j <= i; j++) + { + if (!map_entries[entry_idx].segment) break; + + if (map_entries[entry_idx].segment->entries) + { + free(map_entries[entry_idx].segment->entries); + map_entries[entry_idx].segment->entries = NULL; + } + + map_entries[entry_idx].segment = NULL; + entry_idx++; + + while(entry_idx < num_map_entries && map_entries[entry_idx - 1].virtual_offset_end == map_entries[entry_idx].virtual_offset) + { + map_entries[entry_idx - 1].next = NULL; + + if (!map_entries[entry_idx].segment) break; + + if (map_entries[entry_idx].segment->entries) + { + free(map_entries[entry_idx].segment->entries); + map_entries[entry_idx].segment->entries = NULL; + } + + map_entries[entry_idx].segment = NULL; + entry_idx++; + } + } + + free(segments); + segments = NULL; + } + + return segments; +} + +remap_entry_ctx_t *save_remap_get_map_entry(remap_storage_ctx_t *ctx, u64 offset) +{ + if (!ctx || !ctx->header || !ctx->segments) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_get_map_entry: invalid parameters to retrieve map entry!"); + return NULL; + } + + u32 segment_idx = (u32)(offset >> (64 - ctx->header->segment_bits)); + + if (segment_idx < ctx->header->map_segment_count) + { + for(unsigned int i = 0; i < ctx->segments[segment_idx].entry_count; i++) + { + if (ctx->segments[segment_idx].entries[i].virtual_offset_end > offset) return (&ctx->segments[segment_idx].entries[i]); + } + } + + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_get_map_entry: unable to find map entry for offset 0x%lX!", offset); + return NULL; +} + +u32 save_remap_read(remap_storage_ctx_t *ctx, void *buffer, u64 offset, size_t count) +{ + if (!ctx || (ctx->type == STORAGE_BYTES && !ctx->file) || (ctx->type == STORAGE_DUPLEX && !ctx->duplex) || (ctx->type != STORAGE_BYTES && ctx->type != STORAGE_DUPLEX) || !buffer || !count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_read: invalid parameters to read remap data!"); + return 0; + } + + remap_entry_ctx_t *entry = save_remap_get_map_entry(ctx, offset); + if (!entry) + { + strcat(strbuf, "\nsave_remap_read: failed to retrieve map entry!"); + return 0; + } + + u64 in_pos = offset; + u32 out_pos = 0; + u32 remaining = count; + + UINT br = 0; + FRESULT fr; + + while(remaining) + { + u64 entry_pos = (in_pos - entry->virtual_offset); + u32 bytes_to_read = ((entry->virtual_offset_end - in_pos) < remaining ? (u32)(entry->virtual_offset_end - in_pos) : remaining); + + switch (ctx->type) + { + case STORAGE_BYTES: + fr = f_lseek(ctx->file, ctx->base_storage_offset + entry->physical_offset + entry_pos); + if (fr || f_tell(ctx->file) != (ctx->base_storage_offset + entry->physical_offset + entry_pos)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_read: failed to seek to offset 0x%lX in savefile! (%u)", ctx->base_storage_offset + entry->physical_offset + entry_pos, fr); + return out_pos; + } + + fr = f_read(ctx->file, (u8*)buffer + out_pos, bytes_to_read, &br); + if (fr || br != bytes_to_read) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_remap_read: failed to read %u bytes chunk from offset 0x%lX in savefile! (%u)", bytes_to_read, ctx->base_storage_offset + entry->physical_offset + entry_pos, fr); + return (out_pos + br); + } + + break; + case STORAGE_DUPLEX: + br = save_duplex_storage_read(ctx->duplex, (u8*)buffer + out_pos, ctx->base_storage_offset + entry->physical_offset + entry_pos, bytes_to_read); + if (br != bytes_to_read) + { + strcat(strbuf, "\nsave_remap_read: failed to read remap data from duplex storage!"); + return (out_pos + br); + } + break; + default: + break; + } + + out_pos += bytes_to_read; + in_pos += bytes_to_read; + remaining -= bytes_to_read; + + if (in_pos >= entry->virtual_offset_end) entry = entry->next; + } + + return out_pos; +} + +u32 save_journal_storage_read(journal_storage_ctx_t *ctx, remap_storage_ctx_t *remap, void *buffer, u64 offset, size_t count) +{ + if (!ctx || !ctx->block_size || !remap || !buffer || !count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_journal_storage_read: invalid parameters to read journal storage data!"); + return 0; + } + + u64 in_pos = offset; + u32 out_pos = 0; + u32 remaining = count; + u32 br; + + while(remaining) + { + u32 block_num = (u32)(in_pos / ctx->block_size); + u32 block_pos = (u32)(in_pos % ctx->block_size); + u64 physical_offset = (ctx->map.entries[block_num].physical_index * ctx->block_size + block_pos); + u32 bytes_to_read = ((ctx->block_size - block_pos) < remaining ? (ctx->block_size - block_pos) : remaining); + + br = save_remap_read(remap, (u8*)buffer + out_pos, ctx->journal_data_offset + physical_offset, bytes_to_read); + if (br != bytes_to_read) + { + strcat(strbuf, "\nsave_journal_storage_read: invalid parameters to read journal storage data!"); + return (out_pos + br); + } + + out_pos += bytes_to_read; + in_pos += bytes_to_read; + remaining -= bytes_to_read; + } + + return out_pos; +} + +bool save_ivfc_storage_init(hierarchical_integrity_verification_storage_ctx_t *ctx, u64 master_hash_offset, ivfc_save_hdr_t *ivfc) +{ + if (!ctx || !ctx->levels || !ivfc || !ivfc->num_levels) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_init: invalid parameters to initialize IVFC storage!"); + return false; + } + + bool success = false; + + ivfc_level_save_ctx_t *levels = ctx->levels; + levels[0].type = STORAGE_BYTES; + levels[0].hash_offset = master_hash_offset; + + for(unsigned int i = 1; i < 4; i++) + { + ivfc_level_hdr_t *level = &ivfc->level_headers[i - 1]; + levels[i].type = STORAGE_REMAP; + levels[i].data_offset = level->logical_offset; + levels[i].data_size = level->hash_data_size; + } + + if (ivfc->num_levels == 5) + { + ivfc_level_hdr_t *data_level = &ivfc->level_headers[ivfc->num_levels - 2]; + levels[ivfc->num_levels - 1].type = STORAGE_JOURNAL; + levels[ivfc->num_levels - 1].data_offset = data_level->logical_offset; + levels[ivfc->num_levels - 1].data_size = data_level->hash_data_size; + } + + struct salt_source_t { + char string[50]; + u32 length; + }; + + static struct salt_source_t salt_sources[6] = { + {"HierarchicalIntegrityVerificationStorage::Master", 48}, + {"HierarchicalIntegrityVerificationStorage::L1", 44}, + {"HierarchicalIntegrityVerificationStorage::L2", 44}, + {"HierarchicalIntegrityVerificationStorage::L3", 44}, + {"HierarchicalIntegrityVerificationStorage::L4", 44}, + {"HierarchicalIntegrityVerificationStorage::L5", 44} + }; + + integrity_verification_info_ctx_t init_info[ivfc->num_levels]; + + init_info[0].data = &levels[0]; + init_info[0].block_size = 0; + + for(unsigned int i = 1; i < ivfc->num_levels; i++) + { + init_info[i].data = &levels[i]; + init_info[i].block_size = (1 << ivfc->level_headers[i - 1].block_size); + hmacSha256CalculateMac(init_info[i].salt, salt_sources[i - 1].string, salt_sources[i - 1].length, ivfc->salt_source, 0x20); + } + + ctx->integrity_storages[0].next_level = NULL; + + ctx->level_validities = calloc(sizeof(validity_t*), (ivfc->num_levels - 1)); + if (!ctx->level_validities) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_init: failed to allocate memory for level validities!"); + goto out; + } + + for(unsigned int i = 1; i < ivfc->num_levels; i++) + { + integrity_verification_storage_ctx_t *level_data = &ctx->integrity_storages[i - 1]; + level_data->hash_storage = &levels[i - 1]; + level_data->base_storage = &levels[i]; + level_data->sector_size = init_info[i].block_size; + level_data->_length = init_info[i].data->data_size; + level_data->sector_count = ((level_data->_length + level_data->sector_size - 1) / level_data->sector_size); + memcpy(level_data->salt, init_info[i].salt, 0x20); + + level_data->block_validities = calloc(sizeof(validity_t), level_data->sector_count); + if (!level_data->block_validities) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_init: failed to allocate memory for block validities in IVFC level #%u!", i); + goto out; + } + + ctx->level_validities[i - 1] = level_data->block_validities; + if (i > 1) level_data->next_level = &ctx->integrity_storages[i - 2]; + } + + ctx->data_level = &levels[ivfc->num_levels - 1]; + ctx->_length = ctx->integrity_storages[ivfc->num_levels - 2]._length; + + success = true; + +out: + if (!success && ctx->level_validities) + { + free(ctx->level_validities); + ctx->level_validities = NULL; + + for(unsigned int i = 1; i < ivfc->num_levels; i++) + { + integrity_verification_storage_ctx_t *level_data = &ctx->integrity_storages[i - 1]; + + if (level_data->block_validities) + { + free(level_data->block_validities); + level_data->block_validities = NULL; + ctx->level_validities[i - 1] = NULL; + } else { + break; + } + } + } + + return success; +} + +size_t save_ivfc_level_fread(ivfc_level_save_ctx_t *ctx, void *buffer, u64 offset, size_t count) +{ + if (!ctx || (ctx->type == STORAGE_BYTES && !ctx->save_ctx->file) || (ctx->type != STORAGE_BYTES && ctx->type != STORAGE_REMAP && ctx->type != STORAGE_JOURNAL) || !buffer || !count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_level_fread: invalid parameters to read IVFC level data!"); + return 0; + } + + UINT br = 0; + FRESULT fr; + + switch (ctx->type) + { + case STORAGE_BYTES: + fr = f_lseek(ctx->save_ctx->file, ctx->hash_offset + offset); + if (fr || f_tell(ctx->save_ctx->file) != (ctx->hash_offset + offset)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_level_fread: failed to seek to offset 0x%lX in savefile! (%u)", ctx->hash_offset + offset, fr); + return (size_t)br; + } + + fr = f_read(ctx->save_ctx->file, buffer, count, &br); + if (fr || br != count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_level_fread: failed to read IVFC level data from offset 0x%lX in savefile! (%u)", ctx->hash_offset + offset, fr); + return (size_t)br; + } + + break; + case STORAGE_REMAP: + br = save_remap_read(&ctx->save_ctx->meta_remap_storage, buffer, ctx->data_offset + offset, count); + if (br != count) + { + strcat(strbuf, "\nsave_ivfc_level_fread: failed to read IVFC level data from remap storage!"); + return (size_t)br; + } + + break; + case STORAGE_JOURNAL: + br = save_journal_storage_read(&ctx->save_ctx->journal_storage, &ctx->save_ctx->data_remap_storage, buffer, ctx->data_offset + offset, count); + if (br != count) + { + strcat(strbuf, "\nsave_ivfc_level_fread: failed to read IVFC level data from journal storage!"); + return (size_t)br; + } + + break; + default: + return 0; + } + + return count; +} + +bool save_ivfc_storage_read(integrity_verification_storage_ctx_t *ctx, void *buffer, u64 offset, size_t count, u32 verify) +{ + if (!ctx || !ctx->sector_size || (!ctx->next_level && !ctx->hash_storage && !ctx->base_storage) || !buffer || !count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_read: invalid parameters to read IVFC storage data!"); + return false; + } + + if (count > ctx->sector_size) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_read: IVFC read exceeds sector size!"); + return false; + } + + u64 block_index = (offset / ctx->sector_size); + + if (ctx->block_validities[block_index] == VALIDITY_INVALID && verify) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_read: hash error from previous check found at offset 0x%08X, count 0x%lX!", (u32)offset, count); + return false; + } + + u8 hash_buffer[0x20] = {0}; + u8 zeroes[0x20] = {0}; + u64 hash_pos = (block_index * 0x20); + + if (ctx->next_level) + { + if (!save_ivfc_storage_read(ctx->next_level, hash_buffer, hash_pos, 0x20, verify)) + { + strcat(strbuf, "\nsave_ivfc_storage_read: failed to read hash from next IVFC level!"); + return false; + } + } else { + if (save_ivfc_level_fread(ctx->hash_storage, hash_buffer, hash_pos, 0x20) != 0x20) + { + strcat(strbuf, "\nsave_ivfc_storage_read: failed to read hash from hash storage!"); + return false; + } + } + + if (!memcmp(hash_buffer, zeroes, 0x20)) + { + memset(buffer, 0, count); + ctx->block_validities[block_index] = VALIDITY_VALID; + return true; + } + + if (save_ivfc_level_fread(ctx->base_storage, buffer, offset, count) != count) + { + strcat(strbuf, "\nsave_ivfc_storage_read: failed to read IVFC level from base storage!"); + return false; + } + + if (!(verify && ctx->block_validities[block_index] == VALIDITY_UNCHECKED)) return true; + + u8 hash[0x20] = {0}; + + u8 *data_buffer = calloc(1, ctx->sector_size + 0x20); + if (!data_buffer) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_read: failed to allocate memory for data buffer!"); + return false; + } + + memcpy(data_buffer, ctx->salt, 0x20); + memcpy(data_buffer + 0x20, buffer, count); + + sha256CalculateHash(hash, data_buffer, ctx->sector_size + 0x20); + hash[0x1F] |= 0x80; + + free(data_buffer); + + ctx->block_validities[block_index] = (!memcmp(hash_buffer, hash, 0x20) ? VALIDITY_VALID : VALIDITY_INVALID); + + if (ctx->block_validities[block_index] == VALIDITY_INVALID && verify) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_storage_read: hash error from current check found at offset 0x%08X, count 0x%lX!", (u32)offset, count); + return false; + } + + return true; +} + +u32 save_allocation_table_read_entry_with_length(allocation_table_ctx_t *ctx, allocation_table_entry_t *entry) +{ + if (!ctx || !ctx->base_storage || !entry) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_read_entry_with_length: invalid parameters to read entry!"); + return 0; + } + + u32 length = 1; + u32 entry_index = allocation_table_block_to_entry_index(entry->next); + + allocation_table_entry_t *entries = (allocation_table_entry_t*)((u8*)(ctx->base_storage) + (entry_index * SAVE_FAT_ENTRY_SIZE)); + + if ((entries[0].next & 0x80000000) == 0) + { + if ((entries[0].prev & 0x80000000) && entries[0].prev != 0x80000000) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_read_entry_with_length: invalid range entry in allocation table!"); + return 0; + } + } else { + length = (entries[1].next - entry_index + 1); + } + + if (allocation_table_is_list_end(&entries[0])) + { + entry->next = 0xFFFFFFFF; + } else { + entry->next = allocation_table_entry_index_to_block(allocation_table_get_next(&entries[0])); + } + + if (allocation_table_is_list_start(&entries[0])) + { + entry->prev = 0xFFFFFFFF; + } else { + entry->prev = allocation_table_entry_index_to_block(allocation_table_get_prev(&entries[0])); + } + + return length; +} + +u32 save_allocation_table_get_list_length(allocation_table_ctx_t *ctx, u32 block_index) +{ + if (!ctx || !ctx->header->allocation_table_block_count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_get_list_length: invalid parameters to calculate FAT list length!"); + return 0; + } + + allocation_table_entry_t entry; + entry.next = block_index; + u32 total_length = 0; + u32 table_size = ctx->header->allocation_table_block_count; + u32 nodes_iterated = 0; + + while(entry.next != 0xFFFFFFFF) + { + u32 entry_length = save_allocation_table_read_entry_with_length(ctx, &entry); + if (!entry_length) + { + strcat(strbuf, "\nsave_allocation_table_get_list_length: failed to retrieve FAT entry length!"); + return 0; + } + + total_length += entry_length; + nodes_iterated++; + + if (nodes_iterated > table_size) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_get_list_length: cycle detected in allocation table!"); + return 0; + } + } + + return total_length; +} + +bool save_allocation_table_iterator_begin(allocation_table_iterator_ctx_t *ctx, allocation_table_ctx_t *table, u32 initial_block) +{ + if (!ctx || !table) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_iterator_begin: invalid parameters to initialize FAT interator!"); + return false; + } + + ctx->fat = table; + ctx->physical_block = initial_block; + ctx->virtual_block = 0; + + allocation_table_entry_t entry; + entry.next = initial_block; + + ctx->current_segment_size = save_allocation_table_read_entry_with_length(ctx->fat, &entry); + if (!ctx->current_segment_size) + { + strcat(strbuf, "\nsave_allocation_table_iterator_begin: failed to retrieve FAT entry length!"); + return false; + } + + ctx->next_block = entry.next; + ctx->prev_block = entry.prev; + + if (ctx->prev_block != 0xFFFFFFFF) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_iterator_begin: attempted to start FAT iteration from invalid block 0x%08X!", initial_block); + return false; + } + + return true; +} + +bool save_allocation_table_iterator_move_next(allocation_table_iterator_ctx_t *ctx) +{ + if (!ctx || ctx->next_block == 0xFFFFFFFF) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_iterator_move_next: invalid parameters to move iterator to the next block."); + return false; + } + + ctx->virtual_block += ctx->current_segment_size; + ctx->physical_block = ctx->next_block; + + allocation_table_entry_t entry; + entry.next = ctx->next_block; + + ctx->current_segment_size = save_allocation_table_read_entry_with_length(ctx->fat, &entry); + if (!ctx->current_segment_size) + { + strcat(strbuf, "\nsave_allocation_table_iterator_move_next: failed to retrieve current segment size!"); + return false; + } + + ctx->next_block = entry.next; + ctx->prev_block = entry.prev; + + return true; +} + +bool save_allocation_table_iterator_move_prev(allocation_table_iterator_ctx_t *ctx) +{ + if (!ctx || ctx->prev_block == 0xFFFFFFFF) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_iterator_move_prev: invalid parameters to move iterator to the previous block."); + return false; + } + + ctx->physical_block = ctx->prev_block; + + allocation_table_entry_t entry; + entry.next = ctx->prev_block; + + ctx->current_segment_size = save_allocation_table_read_entry_with_length(ctx->fat, &entry); + if (!ctx->current_segment_size) + { + strcat(strbuf, "\nsave_allocation_table_iterator_move_prev: failed to retrieve current segment size!"); + return false; + } + + ctx->next_block = entry.next; + ctx->prev_block = entry.prev; + + ctx->virtual_block -= ctx->current_segment_size; + + return true; +} + +bool save_allocation_table_iterator_seek(allocation_table_iterator_ctx_t *ctx, u32 block) +{ + while(true) + { + if (block < ctx->virtual_block) + { + if (!save_allocation_table_iterator_move_prev(ctx)) return false; + } else + if (block >= ctx->virtual_block + ctx->current_segment_size) + { + if (!save_allocation_table_iterator_move_next(ctx)) return false; + } else { + return true; + } + } +} + +u32 save_allocation_table_storage_read(allocation_table_storage_ctx_t *ctx, void *buffer, u64 offset, size_t count) +{ + if (!ctx || !ctx->fat || !ctx->block_size || !buffer || !count) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_allocation_table_storage_read: invalid parameters to read data from FAT storage!"); + return 0; + } + + allocation_table_iterator_ctx_t iterator; + if (!save_allocation_table_iterator_begin(&iterator, ctx->fat, ctx->initial_block)) + { + strcat(strbuf, "\nsave_allocation_table_storage_read: failed to initialize FAT interator!"); + return 0; + } + + u64 in_pos = offset; + u32 out_pos = 0; + u32 remaining = count; + + char tmp[NAME_BUF_LEN / 2] = {'\0'}; + + while(remaining) + { + u32 block_num = (u32)(in_pos / ctx->block_size); + if (!save_allocation_table_iterator_seek(&iterator, block_num)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_allocation_table_storage_read: failed to seek to block #%u within offset 0x%lX!", block_num, offset); + strcat(strbuf, tmp); + return out_pos; + } + + u32 segment_pos = (u32)(in_pos - ((u64)iterator.virtual_block * ctx->block_size)); + u64 physical_offset = ((iterator.physical_block * ctx->block_size) + segment_pos); + + u32 remaining_in_segment = ((iterator.current_segment_size * ctx->block_size) - segment_pos); + u32 bytes_to_read = (remaining < remaining_in_segment ? remaining : remaining_in_segment); + + u32 sector_size = ctx->base_storage->integrity_storages[3].sector_size; + u32 chunk_remaining = bytes_to_read; + + for(unsigned int i = 0; i < bytes_to_read; i += sector_size) + { + u32 bytes_to_request = (chunk_remaining < sector_size ? chunk_remaining : sector_size); + + if (!save_ivfc_storage_read(&ctx->base_storage->integrity_storages[3], (u8*)buffer + out_pos + i, physical_offset + i, bytes_to_request, ctx->base_storage->data_level->save_ctx->tool_ctx.action & ACTION_VERIFY)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_allocation_table_storage_read: failed to read %u bytes chunk from IVFC storage at physical offset 0x%lX!", bytes_to_request, physical_offset + i); + strcat(strbuf, tmp); + return (out_pos + bytes_to_read - chunk_remaining); + } + + chunk_remaining -= bytes_to_request; + } + + out_pos += bytes_to_read; + in_pos += bytes_to_read; + remaining -= bytes_to_read; + } + + return out_pos; +} + +u32 save_fs_list_get_capacity(save_filesystem_list_ctx_t *ctx) +{ + if (!ctx) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_list_get_capacity: invalid parameters to retrieve FS capacity!"); + return 0; + } + + if (!ctx->capacity) + { + if (save_allocation_table_storage_read(&ctx->storage, &ctx->capacity, 4, 4) != 4) + { + strcat(strbuf, "\nsave_fs_list_get_capacity: failed to read FS capacity from FAT storage!"); + return 0; + } + } + + return ctx->capacity; +} + +u32 save_fs_list_read_entry(save_filesystem_list_ctx_t *ctx, u32 index, save_fs_list_entry_t *entry) +{ + if (!ctx || !entry) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_list_read_entry: invalid parameters to read FS entry!"); + return 0; + } + + u32 ret = save_allocation_table_storage_read(&ctx->storage, entry, index * SAVE_FS_LIST_ENTRY_SIZE, SAVE_FS_LIST_ENTRY_SIZE); + if (ret != SAVE_FS_LIST_ENTRY_SIZE) + { + strcat(strbuf, "\nsave_fs_list_read_entry: failed to read FS entry from FAT storage!"); + return 0; + } + + return ret; +} + +bool save_fs_list_get_value(save_filesystem_list_ctx_t *ctx, u32 index, save_fs_list_entry_t *value) +{ + if (!ctx || !value) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_list_get_value: invalid parameters to retrieve value for index!"); + return false; + } + + u32 capacity = save_fs_list_get_capacity(ctx); + if (!capacity) + { + strcat(strbuf, "\nsave_fs_list_get_value: failed to retrieve FS capacity!"); + return false; + } + + if (index >= capacity) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_list_get_value: provided index exceeds FS capacity!"); + return false; + } + + if (!save_fs_list_read_entry(ctx, index, value)) + { + strcat(strbuf, "\nsave_fs_list_get_value: failed to read FS entry!"); + return false; + } + + return true; +} + +u32 save_fs_get_index_from_key(save_filesystem_list_ctx_t *ctx, save_entry_key_t *key, u32 *prev_index) +{ + char tmp[NAME_BUF_LEN / 2] = {'\0'}; + + u32 prev; + if (!prev_index) prev_index = &prev; + + if (!ctx || !key) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_get_index_from_key: invalid parameters to retrieve FS index from key!"); + goto out; + } + + u32 capacity = save_fs_list_get_capacity(ctx); + if (!capacity) + { + strcat(strbuf, "\nsave_fs_get_index_from_key: failed to retrieve FS capacity!"); + goto out; + } + + save_fs_list_entry_t entry; + if (!save_fs_list_read_entry(ctx, ctx->used_list_head_index, &entry)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_fs_get_index_from_key: failed to read FS entry for initial index %u!", ctx->used_list_head_index); + strcat(strbuf, tmp); + goto out; + } + + *prev_index = ctx->used_list_head_index; + u32 index = entry.next; + + while(index) + { + if (index > capacity) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_get_index_from_key: save entry index %d out of range!", index); + break; + } + + if (!save_fs_list_read_entry(ctx, index, &entry)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_fs_get_index_from_key: failed to read FS entry for index %u!", index); + strcat(strbuf, tmp); + break; + } + + if (entry.parent == key->parent && !strcmp(entry.name, key->name)) return index; + + *prev_index = index; + index = entry.next; + } + + if (!index) snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_fs_get_index_from_key: unable to find FS index from key!"); + +out: + *prev_index = 0xFFFFFFFF; + return 0xFFFFFFFF; +} + +bool save_hierarchical_file_table_find_path_recursive(hierarchical_save_file_table_ctx_t *ctx, save_entry_key_t *key, const char *path) +{ + if (!ctx || !key || !path || !strlen(path)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_hierarchical_file_table_find_path_recursive: invalid parameters to find FS path!"); + return false; + } + + key->parent = 0; + char *pos = strchr(path, '/'); + + while(pos) + { + memset(key->name, 0, SAVE_FS_LIST_MAX_NAME_LENGTH); + + char *tmp = strchr(pos, '/'); + if (!tmp) + { + memcpy(key->name, pos, strlen(pos)); + break; + } + + memcpy(key->name, pos, tmp - pos); + + key->parent = save_fs_get_index_from_key(&ctx->directory_table, key, NULL); + if (key->parent == 0xFFFFFFFF) return false; + + pos = (tmp + 1); + } + + return true; +} + +bool save_hierarchical_file_table_get_file_entry_by_path(hierarchical_save_file_table_ctx_t *ctx, const char *path, save_fs_list_entry_t *entry) +{ + if (!ctx || !path || !strlen(path) || !entry) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_hierarchical_file_table_get_file_entry_by_path: invalid parameters to retrieve file entry!"); + return false; + } + + char tmp[NAME_BUF_LEN / 2] = {'\0'}; + + save_entry_key_t key; + if (!save_hierarchical_file_table_find_path_recursive(ctx, &key, path)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_hierarchical_file_table_get_file_entry_by_path: unable to locate file \"%s\".", path); + strcat(strbuf, tmp); + return false; + } + + u32 index = save_fs_get_index_from_key(&ctx->file_table, &key, NULL); + if (index == 0xFFFFFFFF) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_hierarchical_file_table_get_file_entry_by_path: unable to get table index for file \"%s\".", path); + strcat(strbuf, tmp); + return false; + } + + if (!save_fs_list_get_value(&ctx->file_table, index, entry)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nsave_hierarchical_file_table_get_file_entry_by_path: unable to get file entry for \"%s\" from index.", path); + strcat(strbuf, tmp); + return false; + } + + return true; +} + +bool save_open_fat_storage(save_filesystem_ctx_t *ctx, allocation_table_storage_ctx_t *storage_ctx, u32 block_index) +{ + if (!ctx || !ctx->base_storage || !storage_ctx) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_open_fat_storage: invalid parameters to open savefile FAT storage!"); + return false; + } + + storage_ctx->base_storage = ctx->base_storage; + storage_ctx->fat = &ctx->allocation_table; + storage_ctx->block_size = (u32)ctx->header->block_size; + storage_ctx->initial_block = block_index; + + if (block_index == 0xFFFFFFFF) + { + storage_ctx->_length = 0; + } else { + u32 fat_list_length = save_allocation_table_get_list_length(storage_ctx->fat, block_index); + if (!fat_list_length) + { + strcat(strbuf, "\nsave_open_fat_storage: failed to retrieve FAT list length!"); + return false; + } + + storage_ctx->_length = (fat_list_length * storage_ctx->block_size); + } + + return true; +} + +bool save_filesystem_init(save_filesystem_ctx_t *ctx, void *fat, save_fs_header_t *save_fs_header, fat_header_t *fat_header) +{ + if (!ctx || !fat || !save_fs_header || !fat_header) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_filesystem_init: invalid parameters to initialize savefile FS!"); + return false; + } + + ctx->allocation_table.base_storage = fat; + ctx->allocation_table.header = fat_header; + ctx->allocation_table.free_list_entry_index = 0; + ctx->header = save_fs_header; + + if (!save_open_fat_storage(ctx, &ctx->file_table.directory_table.storage, fat_header->directory_table_block)) + { + strcat(strbuf, "\nsave_filesystem_init: failed to open FAT directory storage!"); + return false; + } + + if (!save_open_fat_storage(ctx, &ctx->file_table.file_table.storage, fat_header->file_table_block)) + { + strcat(strbuf, "\nsave_filesystem_init: failed to open FAT file storage!"); + return false; + } + + ctx->file_table.file_table.free_list_head_index = 0; + ctx->file_table.file_table.used_list_head_index = 1; + ctx->file_table.directory_table.free_list_head_index = 0; + ctx->file_table.directory_table.used_list_head_index = 1; + + return true; +} + +validity_t save_ivfc_validate(hierarchical_integrity_verification_storage_ctx_t *ctx, ivfc_save_hdr_t *ivfc) +{ + if (!ctx || !ivfc || !ivfc->num_levels) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_filesystem_verify: invalid parameters to verify savefile FS!"); + return VALIDITY_INVALID; + } + + validity_t result = VALIDITY_VALID; + + for(unsigned int i = 0; i < (ivfc->num_levels - 1) && result != VALIDITY_INVALID; i++) + { + integrity_verification_storage_ctx_t *storage = &ctx->integrity_storages[i]; + + u64 block_size = storage->sector_size; + u32 block_count = (u32)((storage->_length + block_size - 1) / block_size); + + u8 *buffer = calloc(1, block_size); + if (!buffer) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_filesystem_verify: failed to allocate memory for input buffer!"); + result = VALIDITY_INVALID; + break; + } + + for(unsigned int j = 0; j < block_count; j++) + { + if (ctx->level_validities[ivfc->num_levels - 2][j] == VALIDITY_UNCHECKED) + { + u32 to_read = ((storage->_length - (block_size * j)) < block_size ? (storage->_length - (block_size * j)) : block_size); + if (!save_ivfc_storage_read(storage, buffer, block_size * j, to_read, 1)) + { + strcat(strbuf, "\nsave_ivfc_validate: failed to read IVFC storage data!"); + result = VALIDITY_INVALID; + break; + } + } + + if (ctx->level_validities[ivfc->num_levels - 2][j] == VALIDITY_INVALID) + { + result = VALIDITY_INVALID; + break; + } + } + + free(buffer); + + if (result == VALIDITY_INVALID) break; + } + + return result; +} + +bool save_ivfc_set_level_validities(hierarchical_integrity_verification_storage_ctx_t *ctx, ivfc_save_hdr_t *ivfc) +{ + if (!ctx || !ivfc || !ivfc->num_levels) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_set_level_validities: invalid parameters to set IVFC level validities!"); + return false; + } + + bool success = true; + + for(unsigned int i = 0; i < (ivfc->num_levels - 1); i++) + { + validity_t level_validity = VALIDITY_VALID; + + for(unsigned int j = 0; j < ctx->integrity_storages[i].sector_count; j++) + { + if (ctx->level_validities[i][j] == VALIDITY_INVALID) + { + level_validity = VALIDITY_INVALID; + break; + } + + if (ctx->level_validities[i][j] == VALIDITY_UNCHECKED && level_validity != VALIDITY_INVALID) level_validity = VALIDITY_UNCHECKED; + } + + ctx->levels[i].hash_validity = level_validity; + + if (success && level_validity == VALIDITY_INVALID) success = false; + } + + if (!success) snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_ivfc_set_level_validities: invalid IVFC level!"); + + return success; +} + +validity_t save_filesystem_verify(save_ctx_t *ctx) +{ + if (!ctx) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_filesystem_verify: invalid parameters to verify savefile FS!"); + return VALIDITY_INVALID; + } + + validity_t journal_validity = save_ivfc_validate(&ctx->core_data_ivfc_storage, &ctx->header.data_ivfc_header); + if (journal_validity == VALIDITY_INVALID) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_filesystem_verify: invalid core IVFC storage!"); + return journal_validity; + } + + if (!save_ivfc_set_level_validities(&ctx->core_data_ivfc_storage, &ctx->header.data_ivfc_header)) + { + strcat(strbuf, "\nsave_filesystem_verify: invalid IVFC level in core IVFC storage!"); + journal_validity = VALIDITY_INVALID; + return journal_validity; + } + + if (!ctx->fat_ivfc_storage.levels[0].save_ctx) return journal_validity; + + validity_t fat_validity = save_ivfc_validate(&ctx->fat_ivfc_storage, &ctx->header.fat_ivfc_header); + if (fat_validity == VALIDITY_INVALID) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_filesystem_verify: invalid FAT IVFC storage!"); + return fat_validity; + } + + if (!save_ivfc_set_level_validities(&ctx->fat_ivfc_storage, &ctx->header.fat_ivfc_header)) + { + strcat(strbuf, "\nsave_filesystem_verify: invalid IVFC level in FAT IVFC storage!"); + fat_validity = VALIDITY_INVALID; + return fat_validity; + } + + if (journal_validity != VALIDITY_VALID) return journal_validity; + if (fat_validity != VALIDITY_VALID) return fat_validity; + + return journal_validity; +} + +bool save_process(save_ctx_t *ctx) +{ + strbuf[0] = '\0'; + + if (!ctx || !ctx->file) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: invalid parameters to process savefile!"); + return false; + } + + UINT br = 0; + FRESULT fr; + bool success = false; + + /* Try to parse Header A. */ + f_rewind(ctx->file); + + fr = f_read(ctx->file, &ctx->header, sizeof(ctx->header), &br); + if (fr || br != sizeof(ctx->header)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to read savefile header A! (%u)", fr); + return success; + } + + if (!save_process_header(ctx)) return success; + + if (ctx->header_hash_validity == VALIDITY_INVALID) + { + /* Try to parse Header B. */ + fr = f_lseek(ctx->file, 0x4000); + if (fr || f_tell(ctx->file) != 0x4000) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to seek to offset 0x4000 in savefile! (%u)", fr); + return success; + } + + fr = f_read(ctx->file, &ctx->header, sizeof(ctx->header), &br); + if (fr || br != sizeof(ctx->header)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to read savefile header B! (%u)", fr); + return success; + } + + if (!save_process_header(ctx)) return success; + + if (ctx->header_hash_validity == VALIDITY_INVALID) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: savefile header is invalid!"); + return success; + } + } + + unsigned char cmac[0x10]; + memset(cmac, 0, 0x10); + cmacAes128CalculateMac(cmac, ctx->save_mac_key, &ctx->header.layout, sizeof(ctx->header.layout)); + + ctx->header_cmac_validity = (!memcmp(cmac, &ctx->header.cmac, 0x10) ? VALIDITY_VALID : VALIDITY_INVALID); + + /* Initialize remap storages. */ + ctx->data_remap_storage.type = STORAGE_BYTES; + ctx->data_remap_storage.base_storage_offset = ctx->header.layout.file_map_data_offset; + ctx->data_remap_storage.header = &ctx->header.main_remap_header; + ctx->data_remap_storage.file = ctx->file; + + ctx->data_remap_storage.map_entries = calloc(sizeof(remap_entry_ctx_t), ctx->data_remap_storage.header->map_entry_count); + if (!ctx->data_remap_storage.map_entries) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for data remap storage entries!"); + return success; + } + + fr = f_lseek(ctx->file, ctx->header.layout.file_map_entry_offset); + if (fr || f_tell(ctx->file) != ctx->header.layout.file_map_entry_offset) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to seek to file map entry offset 0x%lX in savefile! (%u)", ctx->header.layout.file_map_entry_offset, fr); + return success; + } + + for(unsigned int i = 0; i < ctx->data_remap_storage.header->map_entry_count; i++) + { + fr = f_read(ctx->file, &ctx->data_remap_storage.map_entries[i], 0x20, &br); + if (fr || br != 0x20) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to read data remap storage entry #%u! (%u)", i, fr); + goto out; + } + + ctx->data_remap_storage.map_entries[i].physical_offset_end = (ctx->data_remap_storage.map_entries[i].physical_offset + ctx->data_remap_storage.map_entries[i].size); + ctx->data_remap_storage.map_entries[i].virtual_offset_end = (ctx->data_remap_storage.map_entries[i].virtual_offset + ctx->data_remap_storage.map_entries[i].size); + } + + /* Initialize data remap storage. */ + ctx->data_remap_storage.segments = save_remap_init_segments(ctx->data_remap_storage.header, ctx->data_remap_storage.map_entries, ctx->data_remap_storage.header->map_entry_count); + if (!ctx->data_remap_storage.segments) + { + strcat(strbuf, "\nsave_process: failed to retrieve data remap storage segments!"); + goto out; + } + + /* Initialize duplex storage. */ + ctx->duplex_layers[0].data_a = ((u8*)&ctx->header + ctx->header.layout.duplex_master_offset_a); + ctx->duplex_layers[0].data_b = ((u8*)&ctx->header + ctx->header.layout.duplex_master_offset_b); + memcpy(&ctx->duplex_layers[0].info, &ctx->header.duplex_header.layers[0], sizeof(duplex_info_t)); + + ctx->duplex_layers[1].data_a = calloc(1, ctx->header.layout.duplex_l1_size); + if (!ctx->duplex_layers[1].data_a) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for data_a block in duplex layer #1!"); + goto out; + } + + if (save_remap_read(&ctx->data_remap_storage, ctx->duplex_layers[1].data_a, ctx->header.layout.duplex_l1_offset_a, ctx->header.layout.duplex_l1_size) != ctx->header.layout.duplex_l1_size) + { + strcat(strbuf, "\nsave_process: failed to read data_a block from duplex layer #1 in data remap storage!"); + goto out; + } + + ctx->duplex_layers[1].data_b = calloc(1, ctx->header.layout.duplex_l1_size); + if (!ctx->duplex_layers[1].data_b) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for data_b block in duplex layer #1!"); + goto out; + } + + if (save_remap_read(&ctx->data_remap_storage, ctx->duplex_layers[1].data_b, ctx->header.layout.duplex_l1_offset_b, ctx->header.layout.duplex_l1_size) != ctx->header.layout.duplex_l1_size) + { + strcat(strbuf, "\nsave_process: failed to read data_b block from duplex layer #1 in data remap storage!"); + goto out; + } + + memcpy(&ctx->duplex_layers[1].info, &ctx->header.duplex_header.layers[1], sizeof(duplex_info_t)); + + ctx->duplex_layers[2].data_a = calloc(1, ctx->header.layout.duplex_data_size); + if (!ctx->duplex_layers[2].data_a) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for data_a block in duplex layer #2!"); + goto out; + } + + if (save_remap_read(&ctx->data_remap_storage, ctx->duplex_layers[2].data_a, ctx->header.layout.duplex_data_offset_a, ctx->header.layout.duplex_data_size) != ctx->header.layout.duplex_data_size) + { + strcat(strbuf, "\nsave_process: failed to read data_a block from duplex layer #2 in data remap storage!"); + goto out; + } + + ctx->duplex_layers[2].data_b = calloc(1, ctx->header.layout.duplex_data_size); + if (!ctx->duplex_layers[2].data_b) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for data_b block in duplex layer #2!"); + goto out; + } + + if (save_remap_read(&ctx->data_remap_storage, ctx->duplex_layers[2].data_b, ctx->header.layout.duplex_data_offset_b, ctx->header.layout.duplex_data_size) != ctx->header.layout.duplex_data_size) + { + strcat(strbuf, "\nsave_process: failed to read data_b block from duplex layer #2 in data remap storage!"); + goto out; + } + + memcpy(&ctx->duplex_layers[2].info, &ctx->header.duplex_header.layers[2], sizeof(duplex_info_t)); + + /* Initialize hierarchical duplex storage. */ + u8 *bitmap = (ctx->header.layout.duplex_index == 1 ? ctx->duplex_layers[0].data_b : ctx->duplex_layers[0].data_a); + + if (!save_duplex_storage_init(&ctx->duplex_storage.layers[0], &ctx->duplex_layers[1], bitmap, ctx->header.layout.duplex_master_size)) + { + strcat(strbuf, "\nsave_process: failed to initialize duplex storage layer #0!"); + goto out; + } + + ctx->duplex_storage.layers[0]._length = ctx->header.layout.duplex_l1_size; + + bitmap = calloc(1, ctx->duplex_storage.layers[0]._length); + if (!bitmap) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for duplex storage layer #0 bitmap!"); + goto out; + } + + if (save_duplex_storage_read(&ctx->duplex_storage.layers[0], bitmap, 0, ctx->duplex_storage.layers[0]._length) != ctx->duplex_storage.layers[0]._length) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to read duplex storage layer #0 bitmap!"); + free(bitmap); + goto out; + } + + if (!save_duplex_storage_init(&ctx->duplex_storage.layers[1], &ctx->duplex_layers[2], bitmap, ctx->duplex_storage.layers[0]._length)) + { + strcat(strbuf, "\nsave_process: failed to initialize duplex storage layer #1!"); + goto out; + } + + ctx->duplex_storage.layers[1]._length = ctx->header.layout.duplex_data_size; + + ctx->duplex_storage.data_layer = ctx->duplex_storage.layers[1]; + + /* Initialize meta remap storage. */ + ctx->meta_remap_storage.type = STORAGE_DUPLEX; + ctx->meta_remap_storage.duplex = &ctx->duplex_storage.data_layer; + ctx->meta_remap_storage.header = &ctx->header.meta_remap_header; + ctx->meta_remap_storage.file = ctx->file; + + ctx->meta_remap_storage.map_entries = calloc(sizeof(remap_entry_ctx_t), ctx->meta_remap_storage.header->map_entry_count); + if (!ctx->meta_remap_storage.map_entries) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for meta remap storage entries!"); + goto out; + } + + fr = f_lseek(ctx->file, ctx->header.layout.meta_map_entry_offset); + if (fr || f_tell(ctx->file) != ctx->header.layout.meta_map_entry_offset) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to seek to meta map entry offset 0x%lX in savefile! (%u)", ctx->header.layout.meta_map_entry_offset, fr); + goto out; + } + + for(unsigned int i = 0; i < ctx->meta_remap_storage.header->map_entry_count; i++) + { + fr = f_read(ctx->file, &ctx->meta_remap_storage.map_entries[i], 0x20, &br); + if (fr || br != 0x20) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to read meta remap storage entry #%u! (%u)", i, fr); + goto out; + } + + ctx->meta_remap_storage.map_entries[i].physical_offset_end = (ctx->meta_remap_storage.map_entries[i].physical_offset + ctx->meta_remap_storage.map_entries[i].size); + ctx->meta_remap_storage.map_entries[i].virtual_offset_end = (ctx->meta_remap_storage.map_entries[i].virtual_offset + ctx->meta_remap_storage.map_entries[i].size); + } + + ctx->meta_remap_storage.segments = save_remap_init_segments(ctx->meta_remap_storage.header, ctx->meta_remap_storage.map_entries, ctx->meta_remap_storage.header->map_entry_count); + if (!ctx->meta_remap_storage.segments) + { + strcat(strbuf, "\nsave_process: failed to retrieve meta remap storage segments!"); + goto out; + } + + /* Initialize journal map. */ + ctx->journal_map_info.map_storage = calloc(1, ctx->header.layout.journal_map_table_size); + if (!ctx->journal_map_info.map_storage) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for journal map info!"); + goto out; + } + + if (save_remap_read(&ctx->meta_remap_storage, ctx->journal_map_info.map_storage, ctx->header.layout.journal_map_table_offset, ctx->header.layout.journal_map_table_size) != ctx->header.layout.journal_map_table_size) + { + strcat(strbuf, "\nsave_process: failed to read map storage from journal map info in meta remap storage!"); + goto out; + } + + /* Initialize journal storage. */ + ctx->journal_storage.header = &ctx->header.journal_header; + ctx->journal_storage.journal_data_offset = ctx->header.layout.journal_data_offset; + ctx->journal_storage._length = (ctx->journal_storage.header->total_size - ctx->journal_storage.header->journal_size); + ctx->journal_storage.file = ctx->file; + ctx->journal_storage.map.header = &ctx->header.map_header; + ctx->journal_storage.map.map_storage = ctx->journal_map_info.map_storage; + + ctx->journal_storage.map.entries = calloc(sizeof(journal_map_entry_t), ctx->journal_storage.map.header->main_data_block_count); + if (!ctx->journal_storage.map.entries) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for journal map storage entries!"); + goto out; + } + + u32 *pos = (u32*)ctx->journal_storage.map.map_storage; + + for(unsigned int i = 0; i < ctx->journal_storage.map.header->main_data_block_count; i++) + { + ctx->journal_storage.map.entries[i].virtual_index = i; + ctx->journal_storage.map.entries[i].physical_index = (*pos & 0x7FFFFFFF); + pos += 2; + } + + ctx->journal_storage.block_size = ctx->journal_storage.header->block_size; + ctx->journal_storage._length = (ctx->journal_storage.header->total_size - ctx->journal_storage.header->journal_size); + + /* Initialize core IVFC storage. */ + for(unsigned int i = 0; i < 5; i++) ctx->core_data_ivfc_storage.levels[i].save_ctx = ctx; + + if (!save_ivfc_storage_init(&ctx->core_data_ivfc_storage, ctx->header.layout.ivfc_master_hash_offset_a, &ctx->header.data_ivfc_header)) + { + strcat(strbuf, "\nsave_process: failed to initialize core IVFC storage!"); + goto out; + } + + /* Initialize FAT storage. */ + if (ctx->header.layout.version < 0x50000) + { + ctx->fat_storage = calloc(1, ctx->header.layout.fat_size); + if (!ctx->fat_storage) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for FAT storage!"); + goto out; + } + + if (save_remap_read(&ctx->meta_remap_storage, ctx->fat_storage, ctx->header.layout.fat_offset, ctx->header.layout.fat_size) != ctx->header.layout.fat_size) + { + strcat(strbuf, "\nsave_process: failed to read FAT storage from meta remap storage!"); + goto out; + } + } else { + for(unsigned int i = 0; i < 5; i++) ctx->fat_ivfc_storage.levels[i].save_ctx = ctx; + + if (!save_ivfc_storage_init(&ctx->fat_ivfc_storage, ctx->header.layout.fat_ivfc_master_hash_a, &ctx->header.fat_ivfc_header)) + { + strcat(strbuf, "\nsave_process: failed to initialize FAT storage (IVFC)!"); + goto out; + } + + ctx->fat_storage = calloc(1, ctx->fat_ivfc_storage._length); + if (!ctx->fat_storage) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process: failed to allocate memory for FAT storage (IVFC)!"); + goto out; + } + + if (save_remap_read(&ctx->meta_remap_storage, ctx->fat_storage, ctx->header.fat_ivfc_header.level_headers[ctx->header.fat_ivfc_header.num_levels - 2].logical_offset, ctx->fat_ivfc_storage._length) != ctx->fat_ivfc_storage._length) + { + strcat(strbuf, "\nsave_process: failed to read FAT storage from meta remap storage (IVFC)!"); + goto out; + } + } + + if (ctx->tool_ctx.action & ACTION_VERIFY) + { + if (save_filesystem_verify(ctx) == VALIDITY_INVALID) + { + strcat(strbuf, "\nsave_process: savefile FS verification failed!"); + goto out; + } + } + + /* Initialize core save filesystem. */ + ctx->save_filesystem_core.base_storage = &ctx->core_data_ivfc_storage; + if (!save_filesystem_init(&ctx->save_filesystem_core, ctx->fat_storage, &ctx->header.save_header, &ctx->header.fat_header)) + { + strcat(strbuf, "\nsave_process: failed to initialize savefile FS!"); + goto out; + } + + success = true; + +out: + if (!success) save_free_contexts(ctx); + + return success; +} + +bool save_process_header(save_ctx_t *ctx) +{ + if (!ctx) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process_header: invalid parameters to process savefile header!"); + return false; + } + + if (ctx->header.layout.magic != MAGIC_DISF || ctx->header.duplex_header.magic != MAGIC_DPFS || \ + ctx->header.data_ivfc_header.magic != MAGIC_IVFC || ctx->header.journal_header.magic != MAGIC_JNGL || \ + ctx->header.save_header.magic != MAGIC_SAVE || ctx->header.main_remap_header.magic != MAGIC_RMAP || \ + ctx->header.meta_remap_header.magic != MAGIC_RMAP) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "save_process_header: save header is corrupt!"); + return false; + } + + ctx->data_ivfc_master = ((u8*)&ctx->header + ctx->header.layout.ivfc_master_hash_offset_a); + ctx->fat_ivfc_master = ((u8*)&ctx->header + ctx->header.layout.fat_ivfc_master_hash_a); + + u8 hash[0x20]; + sha256CalculateHash(hash, &ctx->header.duplex_header, 0x3D00); + + ctx->header_hash_validity = (memcmp(hash, ctx->header.layout.hash, 0x20) == 0 ? VALIDITY_VALID : VALIDITY_INVALID); + + ctx->header.data_ivfc_header.num_levels = 5; + + if (ctx->header.layout.version >= 0x50000) ctx->header.fat_ivfc_header.num_levels = 4; + + return true; +} + +void save_free_contexts(save_ctx_t *ctx) +{ + if (!ctx) return; + + if (ctx->data_remap_storage.segments) + { + if (ctx->data_remap_storage.header) + { + for(unsigned int i = 0; i < ctx->data_remap_storage.header->map_segment_count; i++) + { + for(unsigned int j = 0; j < ctx->data_remap_storage.segments[i].entry_count; j++) + { + if (&(ctx->data_remap_storage.segments[i].entries[j])) free(&(ctx->data_remap_storage.segments[i].entries[j])); + } + } + } + + free(ctx->data_remap_storage.segments); + ctx->data_remap_storage.segments = NULL; + } + + if (ctx->data_remap_storage.map_entries) + { + free(ctx->data_remap_storage.map_entries); + ctx->data_remap_storage.map_entries = NULL; + } + + if (ctx->meta_remap_storage.segments) + { + if (ctx->meta_remap_storage.header) + { + for(unsigned int i = 0; i < ctx->meta_remap_storage.header->map_segment_count; i++) + { + for(unsigned int j = 0; j < ctx->meta_remap_storage.segments[i].entry_count; j++) + { + if (&(ctx->meta_remap_storage.segments[i].entries[j])) free(&(ctx->meta_remap_storage.segments[i].entries[j])); + } + } + } + + free(ctx->meta_remap_storage.segments); + ctx->meta_remap_storage.segments = NULL; + } + + if (ctx->meta_remap_storage.map_entries) + { + free(ctx->meta_remap_storage.map_entries); + ctx->meta_remap_storage.map_entries = NULL; + } + + if (ctx->duplex_storage.layers[0].bitmap.bitmap) + { + free(ctx->duplex_storage.layers[0].bitmap.bitmap); + ctx->duplex_storage.layers[0].bitmap.bitmap = NULL; + } + + if (ctx->duplex_storage.layers[1].bitmap.bitmap) + { + free(ctx->duplex_storage.layers[1].bitmap.bitmap); + ctx->duplex_storage.layers[1].bitmap.bitmap = NULL; + } + + if (ctx->duplex_storage.layers[1].bitmap_storage) + { + free(ctx->duplex_storage.layers[1].bitmap_storage); + ctx->duplex_storage.layers[1].bitmap_storage = NULL; + } + + for(unsigned int i = 1; i < 3; i++) + { + if (ctx->duplex_layers[i].data_a) + { + free(ctx->duplex_layers[i].data_a); + ctx->duplex_layers[i].data_a = NULL; + } + + if (ctx->duplex_layers[i].data_b) + { + free(ctx->duplex_layers[i].data_b); + ctx->duplex_layers[i].data_b = NULL; + } + } + + if (ctx->journal_map_info.map_storage) + { + free(ctx->journal_map_info.map_storage); + ctx->journal_map_info.map_storage = NULL; + } + + if (ctx->journal_storage.map.entries) + { + free(ctx->journal_storage.map.entries); + ctx->journal_storage.map.entries = NULL; + } + + for(unsigned int i = 0; i < ctx->header.data_ivfc_header.num_levels - 1; i++) + { + if (ctx->core_data_ivfc_storage.integrity_storages[i].block_validities) + { + free(ctx->core_data_ivfc_storage.integrity_storages[i].block_validities); + ctx->core_data_ivfc_storage.integrity_storages[i].block_validities = NULL; + } + } + + if (ctx->core_data_ivfc_storage.level_validities) + { + free(ctx->core_data_ivfc_storage.level_validities); + ctx->core_data_ivfc_storage.level_validities = NULL; + } + + if (ctx->header.layout.version >= 0x50000) + { + for(unsigned int i = 0; i < ctx->header.fat_ivfc_header.num_levels - 1; i++) + { + if (ctx->fat_ivfc_storage.integrity_storages[i].block_validities) + { + free(ctx->fat_ivfc_storage.integrity_storages[i].block_validities); + ctx->fat_ivfc_storage.integrity_storages[i].block_validities = NULL; + } + } + } + + if (ctx->fat_ivfc_storage.level_validities) + { + free(ctx->fat_ivfc_storage.level_validities); + ctx->fat_ivfc_storage.level_validities = NULL; + } + + if (ctx->fat_storage) + { + free(ctx->fat_storage); + ctx->fat_storage = NULL; + } +} + +bool readCertsFromSystemSave() +{ + if (loadedCerts) return true; + + FRESULT fr; + FIL certSave; + + save_ctx_t *save_ctx = NULL; + allocation_table_storage_ctx_t fat_storage; + save_fs_list_entry_t entry; + + u64 internalCertSize; + + u8 i, j; + UINT br = 0; + u8 tmp_hash[0x20]; + + char tmp[NAME_BUF_LEN / 2] = {'\0'}; + + bool success = false, openSave = false, initSaveCtx = false; + + fr = f_open(&certSave, BIS_CERT_SAVE_NAME, FA_READ | FA_OPEN_EXISTING); + if (fr != FR_OK) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Error: failed to open \"%s\" savefile from BIS System partition! (%u)", BIS_CERT_SAVE_NAME, fr); + goto out; + } + + openSave = true; + + save_ctx = calloc(1, sizeof(save_ctx_t)); + if (!save_ctx) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Error: failed to allocate memory for savefile context!"); + goto out; + } + + save_ctx->file = &certSave; + save_ctx->tool_ctx.action = 0; + memcpy(save_ctx->save_mac_key, nca_keyset.save_mac_key, 0x10); + + initSaveCtx = save_process(save_ctx); + if (!initSaveCtx) + { + strcat(strbuf, "\nError: failed to process system savefile!"); + goto out; + } + + // 0 = Root, 1 = Common, 2 = Personalized + for(i = 0; i < 3; i++) + { + char cert_path[SAVE_FS_LIST_MAX_NAME_LENGTH * 2] = {'\0'}; + u64 cert_expected_size = (i == 0 ? ETICKET_CA_CERT_SIZE : ETICKET_XS_CERT_SIZE); + u8 *cert_data_ptr = (i == 0 ? cert_root_data : (i == 1 ? cert_common_data : cert_personalized_data)); + u8 cert_expected_hash[SHA256_HASH_SIZE]; + + memset(&entry, 0, sizeof(save_fs_list_entry_t)); + memset(&fat_storage, 0, sizeof(allocation_table_storage_ctx_t)); + + bool getFileEntry = false; + + if (i < 2) + { + snprintf(cert_path, MAX_ELEMENTS(cert_path), (i == 0 ? cert_CA00000003_path : cert_XS00000020_path)); + memcpy(cert_expected_hash, (i == 0 ? cert_CA00000003_hash : cert_XS00000020_hash), SHA256_HASH_SIZE); + + getFileEntry = save_hierarchical_file_table_get_file_entry_by_path(&save_ctx->save_filesystem_core.file_table, cert_path, &entry); + } else { + // Try loading multiple personalized certificates until we hit the right one + for(j = 0; j < personalized_cert_count; j++) + { + switch(j) + { + case 0: // < 9.0.0 + snprintf(cert_path, MAX_ELEMENTS(cert_path), cert_XS00000021_path); + memcpy(cert_expected_hash, cert_XS00000021_hash, SHA256_HASH_SIZE); + break; + case 1: // >= 9.0.0 + snprintf(cert_path, MAX_ELEMENTS(cert_path), cert_XS00000024_path); + memcpy(cert_expected_hash, cert_XS00000024_hash, SHA256_HASH_SIZE); + break; + default: + break; + } + + strbuf[0] = '\0'; + + getFileEntry = save_hierarchical_file_table_get_file_entry_by_path(&save_ctx->save_filesystem_core.file_table, cert_path, &entry); + if (getFileEntry) break; + } + } + + if (!getFileEntry) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nError: failed to get file entry for \"%s\" in system savefile!", cert_path); + strcat(strbuf, tmp); + goto out; + } + + if (!save_open_fat_storage(&save_ctx->save_filesystem_core, &fat_storage, entry.value.save_file_info.start_block)) + { + snprintf(tmp, MAX_ELEMENTS(tmp), "\nError: failed to open FAT storage at block 0x%X for \"%s\" in system savefile!", entry.value.save_file_info.start_block, cert_path); + strcat(strbuf, tmp); + goto out; + } + + internalCertSize = entry.value.save_file_info.length; + if (internalCertSize != cert_expected_size) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Error: invalid size for \"%s\" in system savefile! (%lu != %lu)", cert_path, internalCertSize, cert_expected_size); + goto out; + } + + br = save_allocation_table_storage_read(&fat_storage, cert_data_ptr, 0, cert_expected_size); + if (br != (UINT)cert_expected_size) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Error: failed to read \"%s\" from system savefile!", cert_path); + goto out; + } + + sha256CalculateHash(tmp_hash, cert_data_ptr, cert_expected_size); + + if (memcmp(tmp_hash, cert_expected_hash, 0x20) != 0) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Error: invalid hash for \"%s\" in system savefile!", cert_path); + goto out; + } + } + + success = loadedCerts = true; + +out: + if (save_ctx) + { + if (initSaveCtx) save_free_contexts(save_ctx); + free(save_ctx); + } + + if (openSave) f_close(&certSave); + + return success; +} + +bool retrieveCertData(u8 *out_cert, bool personalized) +{ + if (!out_cert) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Error: invalid parameters to retrieve %s ticket certificate chain.", (!personalized ? "common" : "personalized")); + return false; + } + + if (!readCertsFromSystemSave()) return false; + + memcpy(out_cert, cert_root_data, ETICKET_CA_CERT_SIZE); + memcpy(out_cert + ETICKET_CA_CERT_SIZE, (!personalized ? cert_common_data : cert_personalized_data), ETICKET_XS_CERT_SIZE); + + return true; +} diff --git a/source/save.h b/source/save.h new file mode 100644 index 0000000..9c191c4 --- /dev/null +++ b/source/save.h @@ -0,0 +1,475 @@ +#pragma once + +#ifndef _SAVE_H +#define _SAVE_H + +#include +#include +#include + +#include "fatfs/ff.h" +#include "nca.h" + +#define SAVE_HEADER_SIZE 0x4000 +#define SAVE_FAT_ENTRY_SIZE 8 +#define SAVE_FS_LIST_MAX_NAME_LENGTH 0x40 +#define SAVE_FS_LIST_ENTRY_SIZE 0x60 + +#define MAGIC_DISF 0x46534944 +#define MAGIC_DPFS 0x53465044 +#define MAGIC_JNGL 0x4C474E4A +#define MAGIC_SAVE 0x45564153 +#define MAGIC_RMAP 0x50414D52 +#define MAGIC_IVFC 0x43465649 + +#define ACTION_VERIFY (1<<2) + +typedef enum { + VALIDITY_UNCHECKED = 0, + VALIDITY_INVALID, + VALIDITY_VALID +} validity_t; + +typedef struct save_ctx_t save_ctx_t; + +typedef struct { + u32 magic; /* DISF */ + u32 version; + u8 hash[0x20]; + u64 file_map_entry_offset; + u64 file_map_entry_size; + u64 meta_map_entry_offset; + u64 meta_map_entry_size; + u64 file_map_data_offset; + u64 file_map_data_size; + u64 duplex_l1_offset_a; + u64 duplex_l1_offset_b; + u64 duplex_l1_size; + u64 duplex_data_offset_a; + u64 duplex_data_offset_b; + u64 duplex_data_size; + u64 journal_data_offset; + u64 journal_data_size_a; + u64 journal_data_size_b; + u64 journal_size; + u64 duplex_master_offset_a; + u64 duplex_master_offset_b; + u64 duplex_master_size; + u64 ivfc_master_hash_offset_a; + u64 ivfc_master_hash_offset_b; + u64 ivfc_master_hash_size; + u64 journal_map_table_offset; + u64 journal_map_table_size; + u64 journal_physical_bitmap_offset; + u64 journal_physical_bitmap_size; + u64 journal_virtual_bitmap_offset; + u64 journal_virtual_bitmap_size; + u64 journal_free_bitmap_offset; + u64 journal_free_bitmap_size; + u64 ivfc_l1_offset; + u64 ivfc_l1_size; + u64 ivfc_l2_offset; + u64 ivfc_l2_size; + u64 ivfc_l3_offset; + u64 ivfc_l3_size; + u64 fat_offset; + u64 fat_size; + u64 duplex_index; + u64 fat_ivfc_master_hash_a; + u64 fat_ivfc_master_hash_b; + u64 fat_ivfc_l1_offset; + u64 fat_ivfc_l1_size; + u64 fat_ivfc_l2_offset; + u64 fat_ivfc_l2_size; + u8 _0x190[0x70]; +} fs_layout_t; + +typedef struct { + u64 offset; + u64 length; + u32 block_size_power; +} PACKED duplex_info_t; + +typedef struct { + u32 magic; /* DPFS */ + u32 version; + duplex_info_t layers[3]; +} duplex_header_t; + +typedef struct { + u32 version; + u32 main_data_block_count; + u32 journal_block_count; + u32 _0x0C; +} journal_map_header_t; + +typedef struct { + u32 magic; /* JNGL */ + u32 version; + u64 total_size; + u64 journal_size; + u64 block_size; +} journal_header_t; + +typedef struct { + u32 magic; /* SAVE */ + u32 version; + u64 block_count; + u64 block_size; +} save_fs_header_t; + +typedef struct { + u64 block_size; + u64 allocation_table_offset; + u32 allocation_table_block_count; + u32 _0x14; + u64 data_offset; + u32 data_block_count; + u32 _0x24; + u32 directory_table_block; + u32 file_table_block; +} fat_header_t; + +typedef struct { + u32 magic; /* RMAP */ + u32 version; + u32 map_entry_count; + u32 map_segment_count; + u32 segment_bits; + u8 _0x14[0x2C]; +} remap_header_t; + +typedef struct remap_segment_ctx_t remap_segment_ctx_t; +typedef struct remap_entry_ctx_t remap_entry_ctx_t; + +struct remap_entry_ctx_t { + u64 virtual_offset; + u64 physical_offset; + u64 size; + u32 alignment; + u32 _0x1C; + u64 virtual_offset_end; + u64 physical_offset_end; + remap_segment_ctx_t *segment; + remap_entry_ctx_t *next; +} PACKED; + +struct remap_segment_ctx_t{ + u64 offset; + u64 length; + remap_entry_ctx_t *entries; + u64 entry_count; +}; + +typedef struct { + u8 *data; + u8 *bitmap; +} duplex_bitmap_t; + +typedef struct { + u32 block_size; + u8 *bitmap_storage; + u8 *data_a; + u8 *data_b; + duplex_bitmap_t bitmap; + u64 _length; +} duplex_storage_ctx_t; + +enum base_storage_type { + STORAGE_BYTES = 0, + STORAGE_DUPLEX = 1, + STORAGE_REMAP = 2, + STORAGE_JOURNAL = 3 +}; + +typedef struct { + remap_header_t *header; + remap_entry_ctx_t *map_entries; + remap_segment_ctx_t *segments; + enum base_storage_type type; + u64 base_storage_offset; + duplex_storage_ctx_t *duplex; + FIL *file; +} remap_storage_ctx_t; + +typedef struct { + u64 title_id; + u8 user_id[0x10]; + u64 save_id; + u8 save_data_type; + u8 _0x21[0x1F]; + u64 save_owner_id; + u64 timestamp; + u64 _0x50; + u64 data_size; + u64 journal_size; + u64 commit_id; +} extra_data_t; + +typedef struct { + u32 magic; + u32 id; + u32 master_hash_size; + u32 num_levels; + ivfc_level_hdr_t level_headers[IVFC_MAX_LEVEL]; + u8 salt_source[0x20]; +} ivfc_save_hdr_t; + +typedef struct { + u8 cmac[0x10]; + u8 _0x10[0xF0]; + fs_layout_t layout; + duplex_header_t duplex_header; + ivfc_save_hdr_t data_ivfc_header; + u32 _0x404; + journal_header_t journal_header; + journal_map_header_t map_header; + u8 _0x438[0x1D0]; + save_fs_header_t save_header; + fat_header_t fat_header; + remap_header_t main_remap_header, meta_remap_header; + u64 _0x6D0; + extra_data_t extra_data; + u8 _0x748[0x390]; + ivfc_save_hdr_t fat_ivfc_header; + u8 _0xB98[0x3468]; +} PACKED save_header_t; + +typedef struct { + duplex_storage_ctx_t layers[2]; + duplex_storage_ctx_t data_layer; + u64 _length; +} hierarchical_duplex_storage_ctx_t; + +typedef struct { + u8 *data_a; + u8 *data_b; + duplex_info_t info; +} duplex_fs_layer_info_t; + +typedef struct { + u8 *map_storage; + u8 *physical_block_bitmap; + u8 *virtual_block_bitmap; + u8 *free_block_bitmap; +} journal_map_params_t; + +typedef struct { + u32 physical_index; + u32 virtual_index; +} journal_map_entry_t; + +typedef struct { + journal_map_header_t *header; + journal_map_entry_t *entries; + u8 *map_storage; +} journal_map_ctx_t; + +typedef struct { + journal_map_ctx_t map; + journal_header_t *header; + u32 block_size; + u64 journal_data_offset; + u64 _length; + FIL *file; +} journal_storage_ctx_t; + +typedef struct { + u64 data_offset; + u64 data_size; + u64 hash_offset; + u32 hash_block_size; + validity_t hash_validity; + enum base_storage_type type; + save_ctx_t *save_ctx; +} ivfc_level_save_ctx_t; + +typedef struct { + ivfc_level_save_ctx_t *data; + u32 block_size; + u8 salt[0x20]; +} integrity_verification_info_ctx_t; + +typedef struct integrity_verification_storage_ctx_t integrity_verification_storage_ctx_t; + +struct integrity_verification_storage_ctx_t { + ivfc_level_save_ctx_t *hash_storage; + ivfc_level_save_ctx_t *base_storage; + validity_t *block_validities; + u8 salt[0x20]; + u32 sector_size; + u32 sector_count; + u64 _length; + integrity_verification_storage_ctx_t *next_level; +}; + +typedef struct { + ivfc_level_save_ctx_t levels[5]; + ivfc_level_save_ctx_t *data_level; + validity_t **level_validities; + u64 _length; + integrity_verification_storage_ctx_t integrity_storages[4]; +} hierarchical_integrity_verification_storage_ctx_t; + +typedef struct { + u32 prev; + u32 next; +} allocation_table_entry_t; + +typedef struct { + u32 free_list_entry_index; + void *base_storage; + fat_header_t *header; +} allocation_table_ctx_t; + +typedef struct { + hierarchical_integrity_verification_storage_ctx_t *base_storage; + u32 block_size; + u32 initial_block; + allocation_table_ctx_t *fat; + u64 _length; +} allocation_table_storage_ctx_t; + +typedef struct { + allocation_table_ctx_t *fat; + u32 virtual_block; + u32 physical_block; + u32 current_segment_size; + u32 next_block; + u32 prev_block; +} allocation_table_iterator_ctx_t; + +typedef struct { + char name[SAVE_FS_LIST_MAX_NAME_LENGTH]; + u32 parent; +} save_entry_key_t; + +typedef struct { + u32 start_block; + u64 length; + u32 _0xC[2]; +} PACKED save_file_info_t; + +typedef struct { + u32 next_directory; + u32 next_file; + u32 _0x8[3]; +} PACKED save_find_position_t; + +typedef struct { + u32 next_sibling; + union { /* Save table entry type. Size = 0x14. */ + save_file_info_t save_file_info; + save_find_position_t save_find_position; + }; +} PACKED save_table_entry_t; + +typedef struct { + u32 parent; + char name[SAVE_FS_LIST_MAX_NAME_LENGTH]; + save_table_entry_t value; + u32 next; +} PACKED save_fs_list_entry_t; + +typedef struct { + u32 free_list_head_index; + u32 used_list_head_index; + allocation_table_storage_ctx_t storage; + u32 capacity; +} save_filesystem_list_ctx_t; + +typedef struct { + save_filesystem_list_ctx_t file_table; + save_filesystem_list_ctx_t directory_table; +} hierarchical_save_file_table_ctx_t; + +typedef struct { + hierarchical_integrity_verification_storage_ctx_t *base_storage; + allocation_table_ctx_t allocation_table; + save_fs_header_t *header; + hierarchical_save_file_table_ctx_t file_table; +} save_filesystem_ctx_t; + +struct save_ctx_t { + save_header_t header; + FIL *file; + struct { + FIL *file; + u32 action; + } tool_ctx; + validity_t header_cmac_validity; + validity_t header_hash_validity; + u8 *data_ivfc_master; + u8 *fat_ivfc_master; + remap_storage_ctx_t data_remap_storage; + remap_storage_ctx_t meta_remap_storage; + duplex_fs_layer_info_t duplex_layers[3]; + hierarchical_duplex_storage_ctx_t duplex_storage; + journal_storage_ctx_t journal_storage; + journal_map_params_t journal_map_info; + hierarchical_integrity_verification_storage_ctx_t core_data_ivfc_storage; + hierarchical_integrity_verification_storage_ctx_t fat_ivfc_storage; + u8 *fat_storage; + save_filesystem_ctx_t save_filesystem_core; + u8 save_mac_key[0x10]; +}; + +static inline u32 allocation_table_entry_index_to_block(u32 entry_index) +{ + return (entry_index - 1); +} + +static inline u32 allocation_table_block_to_entry_index(u32 block_index) +{ + return (block_index + 1); +} + +static inline int allocation_table_is_list_end(allocation_table_entry_t *entry) +{ + return ((entry->next & 0x7FFFFFFF) == 0); +} + +static inline int allocation_table_is_list_start(allocation_table_entry_t *entry) +{ + return (entry->prev == 0x80000000); +} + +static inline int allocation_table_get_next(allocation_table_entry_t *entry) +{ + return (entry->next & 0x7FFFFFFF); +} + +static inline int allocation_table_get_prev(allocation_table_entry_t *entry) +{ + return (entry->prev & 0x7FFFFFFF); +} + +static inline allocation_table_entry_t *save_allocation_table_read_entry(allocation_table_ctx_t *ctx, u32 entry_index) +{ + return ((allocation_table_entry_t*)((u8*)ctx->base_storage + (entry_index * SAVE_FAT_ENTRY_SIZE))); +} + +static inline u32 save_allocation_table_get_free_list_entry_index(allocation_table_ctx_t *ctx) +{ + return allocation_table_get_next(save_allocation_table_read_entry(ctx, ctx->free_list_entry_index)); +} + +static inline u32 save_allocation_table_get_free_list_block_index(allocation_table_ctx_t *ctx) +{ + return allocation_table_entry_index_to_block(save_allocation_table_get_free_list_entry_index(ctx)); +} + +bool save_process(save_ctx_t *ctx); +bool save_process_header(save_ctx_t *ctx); +void save_free_contexts(save_ctx_t *ctx); + +bool save_open_fat_storage(save_filesystem_ctx_t *ctx, allocation_table_storage_ctx_t *storage_ctx, u32 block_index); +u32 save_allocation_table_storage_read(allocation_table_storage_ctx_t *ctx, void *buffer, u64 offset, size_t count); +bool save_fs_list_get_value(save_filesystem_list_ctx_t *ctx, u32 index, save_fs_list_entry_t *value); +u32 save_fs_get_index_from_key(save_filesystem_list_ctx_t *ctx, save_entry_key_t *key, u32 *prev_index); +bool save_hierarchical_file_table_find_path_recursive(hierarchical_save_file_table_ctx_t *ctx, save_entry_key_t *key, const char *path); +bool save_hierarchical_file_table_get_file_entry_by_path(hierarchical_save_file_table_ctx_t *ctx, const char *path, save_fs_list_entry_t *entry); + +bool retrieveCertData(u8 *out_cert, bool personalized); + +#endif diff --git a/source/ui.c b/source/ui.c index ef9f173..1279546 100644 --- a/source/ui.c +++ b/source/ui.c @@ -23,6 +23,8 @@ extern dumpOptions dumpCfg; +extern bool keysFileAvailable; + extern AppletType programAppletType; extern bool runningSxOs; @@ -126,6 +128,10 @@ static nspDumpType selectedNspDumpType; static bool exeFsUpdateFlag = false; static selectedRomFsType curRomFsType = ROMFS_TYPE_APP; +static selectedTicketType curTikType = TICKET_TYPE_APP; + +static bool updatePerformed = false; + bool highlight = false; static char statusMessage[2048] = {'\0'}; @@ -162,7 +168,7 @@ u8 *disabledHighlightIconBuf = NULL; static const char *appHeadline = "NXDumpTool v" APP_VERSION ".\nOriginal codebase by MCMrARM.\nUpdated and maintained by DarkMatterCore.\n\n"; static const char *appControlsCommon = "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Select | [ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_PLUS " ] Exit"; -static const char *appControlsGameCardMultiApp = "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Select | [ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_L " / " NINTENDO_FONT_R " / " NINTENDO_FONT_ZL " / " NINTENDO_FONT_ZR " ] Change displayed base application info | [ " NINTENDO_FONT_PLUS " ] Exit"; +static const char *appControlsGameCardMultiApp = "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Select | [ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_L " / " NINTENDO_FONT_R " / " NINTENDO_FONT_ZL " / " NINTENDO_FONT_ZR " ] Show info from another base application | [ " NINTENDO_FONT_PLUS " ] Exit"; static const char *appControlsNoContent = "[ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_PLUS " ] Exit"; static const char *appControlsSdCardEmmcFull = "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Select | [ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_X " ] Batch mode | [ " NINTENDO_FONT_Y " ] Dump installed content with missing base application | [ " NINTENDO_FONT_PLUS " ] Exit"; static const char *appControlsSdCardEmmcNoApp = "[ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_X " ] Batch mode | [ " NINTENDO_FONT_Y " ] Dump installed content with missing base application | [ " NINTENDO_FONT_PLUS " ] Exit"; @@ -173,8 +179,8 @@ static const char *gameCardMenuItems[] = { "Cartridge Image (XCI) dump", "Ninten static const char *xciDumpMenuItems[] = { "Start XCI dump process", "Split output dump (FAT32 support): ", "Create directory with archive bit set: ", "Keep certificate: ", "Trim output dump: ", "CRC32 checksum calculation + dump verification: " }; static const char *nspDumpGameCardMenuItems[] = { "Dump base application NSP", "Dump bundled update NSP", "Dump bundled DLC NSP" }; static const char *nspDumpSdCardEmmcMenuItems[] = { "Dump base application NSP", "Dump installed update NSP", "Dump installed DLC NSP" }; -static const char *nspAppDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Remove console specific data: ", "Generate ticket-less dump: ", "Base application to dump: " }; -static const char *nspPatchDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Remove console specific data: ", "Generate ticket-less dump: ", "Update to dump: " }; +static const char *nspAppDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Remove console specific data: ", "Generate ticket-less dump: ", "Change NPDM RSA key/sig in Program NCA: ", "Base application to dump: " }; +static const char *nspPatchDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Remove console specific data: ", "Generate ticket-less dump: ", "Change NPDM RSA key/sig in Program NCA: ", "Update to dump: " }; static const char *nspAddOnDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Remove console specific data: ", "Generate ticket-less dump: ", "DLC to dump: " }; static const char *hfs0MenuItems[] = { "Raw HFS0 partition dump", "HFS0 partition data dump", "Browse HFS0 partitions" }; static const char *hfs0PartitionDumpType1MenuItems[] = { "Dump HFS0 partition 0 (Update)", "Dump HFS0 partition 1 (Normal)", "Dump HFS0 partition 2 (Secure)" }; @@ -187,8 +193,9 @@ static const char *exeFsSectionBrowserMenuItems[] = { "Browse ExeFS section", "B static const char *romFsMenuItems[] = { "RomFS section data dump", "Browse RomFS section", "Use update/DLC: " }; static const char *romFsSectionDumpMenuItems[] = { "Start RomFS data dump process", "Base application to dump: ", "Use update/DLC: " }; static const char *romFsSectionBrowserMenuItems[] = { "Browse RomFS section", "Base application to browse: ", "Use update/DLC: " }; -static const char *sdCardEmmcMenuItems[] = { "Nintendo Submission Package (NSP) dump", "ExeFS options", "RomFS options" }; -static const char *batchModeMenuItems[] = { "Start batch dump process", "Dump base applications: ", "Dump updates: ", "Dump DLCs: ", "Split output dumps (FAT32 support): ", "Remove console specific data: ", "Generate ticket-less dumps: ", "Skip already dumped titles: ", "Source storage: ", "Remember dumped titles: " }; +static const char *sdCardEmmcMenuItems[] = { "Nintendo Submission Package (NSP) dump", "ExeFS options", "RomFS options", "Ticket options" }; +static const char *batchModeMenuItems[] = { "Start batch dump process", "Dump base applications: ", "Dump updates: ", "Dump DLCs: ", "Split output dumps (FAT32 support): ", "Remove console specific data: ", "Generate ticket-less dumps: ", "Change NPDM RSA key/sig in Program NCA: ", "Skip already dumped titles: ", "Remember dumped titles: ", "Source storage: " }; +static const char *ticketMenuItems[] = { "Start ticket dump", "Remove console specific data: ", "Use ticket from title: " }; static const char *updateMenuItems[] = { "Update NSWDB.COM XML database", "Update application" }; void uiFill(int x, int y, int width, int height, u8 r, u8 g, u8 b) @@ -612,7 +619,7 @@ void uiRefreshDisplay() void uiStatusMsg(const char *fmt, ...) { - statusMessageFadeout = 1000; + statusMessageFadeout = 5000; va_list args; va_start(args, fmt); @@ -865,12 +872,6 @@ int uiInit() romfs_init = true; - if (!readCertsFromApplicationRomFs()) - { - error_screen(strbuf); - goto out; - } - if (!uiLoadJpgFromFile(dirNormalIconPath, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, &dirNormalIconBuf)) { strcat(strbuf, "Failed to load directory icon (normal).\n"); @@ -931,29 +932,6 @@ int uiInit() romfsExit(); romfs_init = false; - /* Reinitialize FS stuff */ - /* Fixes a problem where the file descriptor for the application NRO isn't properly closed */ - /* We'll need to have write access to the NRO if the user runs the update procedure */ - if (!envIsNso()) - { - fsdevUnmountAll(); - fsExit(); - - rc = fsInitialize(); - if (R_FAILED(rc)) - { - error_screen("fsInitialize() failed (0x%08X).\n", rc); - goto out; - } - - rc = fsdevMountSdmc(); - if (R_FAILED(rc)) - { - error_screen("fsdevMountSdmc() failed (0x%08X).\n", rc); - goto out; - } - } - /* Create framebuffer */ framebufferCreate(&fb, nwindowGetDefault(), FB_WIDTH, FB_HEIGHT, PIXEL_FORMAT_RGBA_8888, 2); framebufferMakeLinear(&fb); @@ -1096,9 +1074,6 @@ void uiSetState(UIState state) } else { cursor = 0; scroll = 0; - - // Avoid placing the cursor on the parent directory entry ("..") right after entering the RomFS browser - if (state == stateRomFsSectionBrowser && strlen(curRomFsPath) <= 1) cursor = 1; } titleSelectorStr[0] = '\0'; @@ -1138,7 +1113,7 @@ UIResult uiProcess() uiPrintHeadline(); loadTitleInfo(); - if (uiState == stateMainMenu || uiState == stateGameCardMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateHfs0Menu || uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu || uiState == stateHfs0Browser || uiState == stateExeFsMenu || uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu || uiState == stateExeFsSectionBrowser || uiState == stateRomFsMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu || uiState == stateRomFsSectionBrowser || uiState == stateSdCardEmmcMenu || uiState == stateSdCardEmmcTitleMenu || uiState == stateSdCardEmmcOrphanPatchAddOnMenu || uiState == stateSdCardEmmcBatchModeMenu || uiState == stateUpdateMenu) + if (uiState == stateMainMenu || uiState == stateGameCardMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateHfs0Menu || uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu || uiState == stateHfs0Browser || uiState == stateExeFsMenu || uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu || uiState == stateExeFsSectionBrowser || uiState == stateRomFsMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu || uiState == stateRomFsSectionBrowser || uiState == stateSdCardEmmcMenu || uiState == stateSdCardEmmcTitleMenu || uiState == stateSdCardEmmcOrphanPatchAddOnMenu || uiState == stateSdCardEmmcBatchModeMenu || uiState == stateTicketMenu || uiState == stateUpdateMenu) { switch(menuType) { @@ -1165,6 +1140,9 @@ UIResult uiProcess() if (uiState == stateSdCardEmmcMenu && ((titlePatchCount && checkOrphanPatchOrAddOn(false)) || (titleAddOnCount && checkOrphanPatchOrAddOn(true)))) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, appControlsSdCardEmmcFull); + breaks += 2; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Hint: installed updates/DLCs for gamecard titles can be found in the orphan title list (press the " NINTENDO_FONT_Y " button)."); } else if (uiState == stateRomFsSectionBrowser && strlen(curRomFsPath) > 1) { @@ -1236,10 +1214,13 @@ UIResult uiProcess() uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Gamecard is not inserted!"); } + breaks += 2; + if (forcedXciDump) { - breaks += 2; uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Press " NINTENDO_FONT_Y " to dump the cartridge image to \"gamecard.xci\"."); + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Are you using \"nogc\" spoofing in your CFW? If so, please consider this option disables all gamecard I/O."); } uiUpdateStatusMsg(); @@ -1298,7 +1279,7 @@ UIResult uiProcess() uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No base applications available in the SD card / eMMC storage!"); breaks++; - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Use the Y button to dump installed content with missing base applications!"); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Use the " NINTENDO_FONT_Y " button to dump installed content with missing base applications!"); } else { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No titles available in the SD card / eMMC storage!"); } @@ -1332,7 +1313,7 @@ UIResult uiProcess() } } - if (uiState == stateMainMenu || uiState == stateGameCardMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateHfs0Menu || uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu || uiState == stateHfs0Browser || uiState == stateExeFsMenu || uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu || uiState == stateExeFsSectionBrowser || uiState == stateRomFsMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu || uiState == stateRomFsSectionBrowser || uiState == stateSdCardEmmcMenu || uiState == stateSdCardEmmcTitleMenu || uiState == stateSdCardEmmcOrphanPatchAddOnMenu || uiState == stateSdCardEmmcBatchModeMenu || uiState == stateUpdateMenu) + if (uiState == stateMainMenu || uiState == stateGameCardMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateHfs0Menu || uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu || uiState == stateHfs0Browser || uiState == stateExeFsMenu || uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu || uiState == stateExeFsSectionBrowser || uiState == stateRomFsMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu || uiState == stateRomFsSectionBrowser || uiState == stateSdCardEmmcMenu || uiState == stateSdCardEmmcTitleMenu || uiState == stateSdCardEmmcOrphanPatchAddOnMenu || uiState == stateSdCardEmmcBatchModeMenu || uiState == stateTicketMenu || uiState == stateUpdateMenu) { if ((menuType == MENUTYPE_GAMECARD && uiState != stateHfs0Browser && uiState != stateExeFsSectionBrowser && uiState != stateRomFsSectionBrowser) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && uiState != stateSdCardEmmcMenu && uiState != stateSdCardEmmcBatchModeMenu && uiState != stateExeFsSectionBrowser && uiState != stateRomFsSectionBrowser)) { @@ -1636,12 +1617,18 @@ UIResult uiProcess() breaks += 2; } else - if (menuType == MENUTYPE_SDCARD_EMMC && orphanMode && (uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateSdCardEmmcTitleMenu || uiState == stateRomFsMenu)) + if (menuType == MENUTYPE_SDCARD_EMMC && orphanMode && (uiState == stateSdCardEmmcTitleMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateRomFsMenu || uiState == stateTicketMenu)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Title ID: %016lX", (uiState == stateNspPatchDumpMenu ? titlePatchTitleID[selectedPatchIndex] : titleAddOnTitleID[selectedAddOnIndex])); + if (strlen(orphanEntries[orphanListCursor].name)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Parent base application: %s", orphanEntries[orphanListCursor].name); + breaks++; + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Title ID: %016lX", (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH ? titlePatchTitleID[selectedPatchIndex] : titleAddOnTitleID[selectedAddOnIndex])); breaks++; - convertTitleVersionToDecimal((uiState == stateNspPatchDumpMenu ? titlePatchVersion[selectedPatchIndex] : titleAddOnVersion[selectedAddOnIndex]), versionStr, MAX_ELEMENTS(versionStr)); + convertTitleVersionToDecimal((orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH ? titlePatchVersion[selectedPatchIndex] : titleAddOnVersion[selectedAddOnIndex]), versionStr, MAX_ELEMENTS(versionStr)); uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Version: %s", versionStr); breaks++; @@ -1653,11 +1640,11 @@ UIResult uiProcess() snprintf(dumpedContentInfoStr, MAX_ELEMENTS(dumpedContentInfoStr), "Title already dumped: "); - if (uiState == stateNspPatchDumpMenu) + if (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH) { dumpName = generateNSPDumpName(DUMP_PATCH_NSP, selectedPatchIndex); } else - if (uiState == stateNspAddOnDumpMenu || uiState == stateRomFsMenu) + if (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_ADDON) { dumpName = generateNSPDumpName(DUMP_ADDON_NSP, selectedAddOnIndex); } @@ -1851,6 +1838,13 @@ UIResult uiProcess() menu = (const char**)filenames; menuItemsCount = filenamesCount; + // Skip the parent directory entry ("..") in the RomFS browser if we're currently at the root directory + if (menu && menuItemsCount && strlen(curRomFsPath) <= 1) + { + menu++; + menuItemsCount--; + } + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, romFsMenuItems[1]); breaks++; @@ -1876,9 +1870,9 @@ UIResult uiProcess() uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "Path: romfs:%s", curRomFsPath); breaks += 2; - if (cursor > 0) + if (strlen(curRomFsPath) <= 1 || (strlen(curRomFsPath) > 1 && cursor > 0)) { - snprintf(strbuf, MAX_ELEMENTS(strbuf), "Entry count: %d | Current entry: %d", menuItemsCount - 1, cursor); + snprintf(strbuf, MAX_ELEMENTS(strbuf), "Entry count: %d | Current entry: %d", menuItemsCount - 1, (strlen(curRomFsPath) <= 1 ? (cursor + 1) : cursor)); } else { snprintf(strbuf, MAX_ELEMENTS(strbuf), "Entry count: %d", menuItemsCount - 1); } @@ -1915,9 +1909,6 @@ UIResult uiProcess() menuItemsCount = filenamesCount; uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "Dump installed content with missing base application"); - breaks += 2; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Hint: installed updates/DLCs for gamecard titles can be found in this section."); if (menuItemsCount) { @@ -1932,6 +1923,13 @@ UIResult uiProcess() uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "Batch mode"); + break; + case stateTicketMenu: + menu = ticketMenuItems; + menuItemsCount = MAX_ELEMENTS(ticketMenuItems); + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, sdCardEmmcMenuItems[3]); + break; case stateUpdateMenu: menu = updateMenuItems; @@ -1948,7 +1946,7 @@ UIResult uiProcess() { breaks++; - if (scroll > 0 && (uiState != stateRomFsSectionBrowser || (uiState == stateRomFsSectionBrowser && (strlen(curRomFsPath) > 1 || cursor > 1)))) + if (scroll > 0) { u32 arrowWidth = uiGetStrWidth(upwardsArrow); @@ -1971,20 +1969,12 @@ UIResult uiProcess() continue; } - // Avoid printing the "Dump bundled update NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled updates, or if we're dealing with a SD/eMMC title without installed updates - // Also avoid printing the "Dump bundled DLC NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled DLCs, or if we're dealing with a SD/eMMC title without installed DLCs - if (uiState == stateNspDumpMenu) + // Avoid printing the "Dump bundled update NSP" / "Dump installed update NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled updates, or if we're dealing with a SD/eMMC title without installed updates + // Also avoid printing the "Dump bundled DLC NSP" / "Dump installed DLC NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled DLCs, or if we're dealing with a SD/eMMC title without installed DLCs + if (uiState == stateNspDumpMenu && ((i == 1 && (!titlePatchCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)))) || (i == 2 && (!titleAddOnCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true)))))) { - if (i == 1) - { - if (!titlePatchCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false))) continue; - } else - if (i == 2) - { - if (!titleAddOnCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) continue; - - if (!titlePatchCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false))) j--; - } + j--; + continue; } // Avoid printing the "Remove console specific data" option in the NSP dump menus if we're dealing with a gamecard title @@ -2017,7 +2007,7 @@ UIResult uiProcess() } // Avoid printing the "Source storage" option in the batch mode menu if we only have titles available in a single source storage device - if (uiState == stateSdCardEmmcBatchModeMenu && i == 8 && ((!sdCardTitleAppCount && !sdCardTitlePatchCount && !sdCardTitleAddOnCount) || (!nandUserTitleAppCount && !nandUserTitlePatchCount && !nandUserTitleAddOnCount))) + if (uiState == stateSdCardEmmcBatchModeMenu && i == 10 && ((!sdCardTitleAppCount && !sdCardTitlePatchCount && !sdCardTitleAddOnCount) || (!nandUserTitleAppCount && !nandUserTitlePatchCount && !nandUserTitleAddOnCount))) { j--; continue; @@ -2053,15 +2043,16 @@ UIResult uiProcess() continue; } - // Avoid printing the parent directory entry ("..") in the RomFS browser if we're currently at the root directory - if (uiState == stateRomFsSectionBrowser && i == 0 && strlen(curRomFsPath) <= 1) + // Avoid printing the "ExeFS options" element in the SD card / eMMC title menu if we're dealing with an orphan DLC + // Also avoid printing the "RomFS options" element in the SD card / eMMC title menu if we're dealing with an orphan Patch + if (uiState == stateSdCardEmmcTitleMenu && orphanMode && ((orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_ADDON && i == 1) || (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH && (i == 1 || i == 2)))) { j--; continue; } - // Avoid printing the "ExeFS options" element in the SD card / eMMC title menu if we're dealing with an orphan DLC - if (uiState == stateSdCardEmmcTitleMenu && i == 1 && orphanMode && selectedAddOnIndex < titleAddOnCount) + // Avoid printing the "Use ticket from title" element in the Ticket menu if we're dealing with an orphan title + if (uiState == stateTicketMenu && orphanMode && i == 2) { j--; continue; @@ -2150,7 +2141,28 @@ UIResult uiProcess() case 4: // Generate ticket-less dump uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.nspDumpCfg.tiklessDump, !dumpCfg.nspDumpCfg.tiklessDump, (dumpCfg.nspDumpCfg.tiklessDump ? 0 : 255), (dumpCfg.nspDumpCfg.tiklessDump ? 255 : 0), 0, (dumpCfg.nspDumpCfg.tiklessDump ? "Yes" : "No")); break; - case 5: // Bundled application/update/DLC to dump + case 5: // Change NPDM RSA key/sig in Program NCA || DLC to dump + if (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu) + { + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.nspDumpCfg.npdmAcidRsaPatch, !dumpCfg.nspDumpCfg.npdmAcidRsaPatch, (dumpCfg.nspDumpCfg.npdmAcidRsaPatch ? 0 : 255), (dumpCfg.nspDumpCfg.npdmAcidRsaPatch ? 255 : 0), 0, (dumpCfg.nspDumpCfg.npdmAcidRsaPatch ? "Yes" : "No")); + } else + if (uiState == stateNspAddOnDumpMenu) + { + if (!strlen(titleSelectorStr)) + { + // Find a matching application to print its name and Title ID + // Otherwise, just print the Title ID + retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, (menuType == MENUTYPE_GAMECARD), NULL, titleSelectorStr, MAX_ELEMENTS(titleSelectorStr)); + uiTruncateOptionStr(titleSelectorStr, xpos, ypos, OPTIONS_X_END_POS_NSP); + } + + leftArrowCondition = ((menuType == MENUTYPE_GAMECARD && titleAddOnCount > 0 && selectedAddOnIndex > 0) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); + rightArrowCondition = ((menuType == MENUTYPE_GAMECARD && titleAddOnCount > 0 && selectedAddOnIndex < (titleAddOnCount - 1)) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); + + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, FONT_COLOR_RGB, titleSelectorStr); + } + break; + case 6: // Application/update to dump if (uiState == stateNspAppDumpMenu) { if (!strlen(titleSelectorStr)) @@ -2176,19 +2188,6 @@ UIResult uiProcess() leftArrowCondition = ((menuType == MENUTYPE_GAMECARD && titlePatchCount > 0 && selectedPatchIndex > 0) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false) != selectedPatchIndex)); rightArrowCondition = ((menuType == MENUTYPE_GAMECARD && titlePatchCount > 0 && selectedPatchIndex < (titlePatchCount - 1)) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false) != selectedPatchIndex)); - } else - if (uiState == stateNspAddOnDumpMenu) - { - if (!strlen(titleSelectorStr)) - { - // Find a matching application to print its name and Title ID - // Otherwise, just print the Title ID - retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, (menuType == MENUTYPE_GAMECARD), NULL, titleSelectorStr, MAX_ELEMENTS(titleSelectorStr)); - uiTruncateOptionStr(titleSelectorStr, xpos, ypos, OPTIONS_X_END_POS_NSP); - } - - leftArrowCondition = ((menuType == MENUTYPE_GAMECARD && titleAddOnCount > 0 && selectedAddOnIndex > 0) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); - rightArrowCondition = ((menuType == MENUTYPE_GAMECARD && titleAddOnCount > 0 && selectedAddOnIndex < (titleAddOnCount - 1)) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); } uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, FONT_COLOR_RGB, titleSelectorStr); @@ -2236,18 +2235,21 @@ UIResult uiProcess() case 6: // Generate ticket-less dumps uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.batchDumpCfg.tiklessDump, !dumpCfg.batchDumpCfg.tiklessDump, (dumpCfg.batchDumpCfg.tiklessDump ? 0 : 255), (dumpCfg.batchDumpCfg.tiklessDump ? 255 : 0), 0, (dumpCfg.batchDumpCfg.tiklessDump ? "Yes" : "No")); break; - case 7: // Skip dumped titles + case 7: // Change NPDM RSA key/sig in Program NCA + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.batchDumpCfg.npdmAcidRsaPatch, !dumpCfg.batchDumpCfg.npdmAcidRsaPatch, (dumpCfg.batchDumpCfg.npdmAcidRsaPatch ? 0 : 255), (dumpCfg.batchDumpCfg.npdmAcidRsaPatch ? 255 : 0), 0, (dumpCfg.batchDumpCfg.npdmAcidRsaPatch ? "Yes" : "No")); + break; + case 8: // Skip dumped titles uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.batchDumpCfg.skipDumpedTitles, !dumpCfg.batchDumpCfg.skipDumpedTitles, (dumpCfg.batchDumpCfg.skipDumpedTitles ? 0 : 255), (dumpCfg.batchDumpCfg.skipDumpedTitles ? 255 : 0), 0, (dumpCfg.batchDumpCfg.skipDumpedTitles ? "Yes" : "No")); break; - case 8: // Source storage + case 9: // Remember dumped titles + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.batchDumpCfg.rememberDumpedTitles, !dumpCfg.batchDumpCfg.rememberDumpedTitles, (dumpCfg.batchDumpCfg.rememberDumpedTitles ? 0 : 255), (dumpCfg.batchDumpCfg.rememberDumpedTitles ? 255 : 0), 0, (dumpCfg.batchDumpCfg.rememberDumpedTitles ? "Yes" : "No")); + break; + case 10: // Source storage leftArrowCondition = (dumpCfg.batchDumpCfg.batchModeSrc != BATCH_SOURCE_ALL); rightArrowCondition = (dumpCfg.batchDumpCfg.batchModeSrc != BATCH_SOURCE_EMMC); uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, FONT_COLOR_RGB, (dumpCfg.batchDumpCfg.batchModeSrc == BATCH_SOURCE_ALL ? "All (SD card + eMMC)" : (dumpCfg.batchDumpCfg.batchModeSrc == BATCH_SOURCE_SDCARD ? "SD card" : "eMMC"))); - break; - case 9: // Remember dumped titles - uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.batchDumpCfg.rememberDumpedTitles, !dumpCfg.batchDumpCfg.rememberDumpedTitles, (dumpCfg.batchDumpCfg.rememberDumpedTitles ? 0 : 255), (dumpCfg.batchDumpCfg.rememberDumpedTitles ? 255 : 0), 0, (dumpCfg.batchDumpCfg.rememberDumpedTitles ? "Yes" : "No")); break; default: break; @@ -2417,7 +2419,7 @@ UIResult uiProcess() uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, FONT_COLOR_RGB, titleSelectorStr); break; - case 2: // Use update + case 2: // Use update/DLC if (curRomFsType != ROMFS_TYPE_APP) { if (!strlen(exeFsAndRomFsSelectorStr)) @@ -2462,6 +2464,60 @@ UIResult uiProcess() } } + // Print settings values for the Ticket menu + if (uiState == stateTicketMenu && i > 0) + { + switch(i) + { + case 1: // Remove console specific data + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCfg.tikDumpCfg.removeConsoleData, !dumpCfg.tikDumpCfg.removeConsoleData, (dumpCfg.tikDumpCfg.removeConsoleData ? 0 : 255), (dumpCfg.tikDumpCfg.removeConsoleData ? 255 : 0), 0, (dumpCfg.tikDumpCfg.removeConsoleData ? "Yes" : "No")); + break; + case 2: // Use ticket from title + if (curTikType != TICKET_TYPE_APP) + { + if (!strlen(titleSelectorStr)) + { + // Print update/DLC TID + switch(curTikType) + { + case TICKET_TYPE_PATCH: + retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, false, NULL, titleSelectorStr, MAX_ELEMENTS(titleSelectorStr)); + strcat(titleSelectorStr, " (UPD)"); + break; + case TICKET_TYPE_ADDON: + retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, false, NULL, titleSelectorStr, MAX_ELEMENTS(titleSelectorStr)); + strcat(titleSelectorStr, " (DLC)"); + break; + default: + break; + } + + uiTruncateOptionStr(titleSelectorStr, xpos, ypos, OPTIONS_X_END_POS_NSP); + } + + leftArrowCondition = true; + rightArrowCondition = ((curTikType == TICKET_TYPE_PATCH && (retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false) != selectedPatchIndex || checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) || (curTikType == TICKET_TYPE_ADDON && retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); + } else { + if (!strlen(titleSelectorStr)) + { + // Print application TID + convertTitleVersionToDecimal(titleAppVersion[selectedAppInfoIndex], versionStr, MAX_ELEMENTS(versionStr)); + snprintf(titleSelectorStr, MAX_ELEMENTS(titleSelectorStr), "%016lX v%s (BASE)", titleAppTitleID[selectedAppInfoIndex], versionStr); + uiTruncateOptionStr(titleSelectorStr, xpos, ypos, OPTIONS_X_END_POS_NSP); + } + + leftArrowCondition = false; + rightArrowCondition = (checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false) || checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true)); + } + + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, FONT_COLOR_RGB, titleSelectorStr); + + break; + default: + break; + } + } + if (i == cursor) highlight = false; } @@ -2474,6 +2530,42 @@ UIResult uiProcess() uiDrawString((FB_WIDTH / 2) - (arrowWidth / 2), ypos, FONT_COLOR_RGB, downwardsArrow); } + // Print warning about missing Lockpick_RCM keys file + if (uiState == stateMainMenu && !keysFileAvailable) + { + j++; + if ((scroll + maxElements) < menuItemsCount) j++; + + ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_ERROR_RGB, "Warning: missing keys file at \"%s\".", KEYS_FILE_PATH); + j++; + + ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_ERROR_RGB, "This file is needed to deal with the encryption schemes used by Nintendo Switch content files."); + j++; + + ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_ERROR_RGB, "SD/eMMC operations will be entirely disabled, along with NSP/ExeFS/RomFS related operations."); + j++; + + ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_ERROR_RGB, "Please run Lockpick_RCM to generate this file. More info at: https://github.com/shchmue/Lockpick_RCM"); + } + + // Print information about the "Change NPDM RSA key/sig in Program NCA" option + if (((uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu) && cursor == 5) || (uiState == stateSdCardEmmcBatchModeMenu && cursor == 7)) + { + j++; + if ((scroll + maxElements) < menuItemsCount) j++; + + ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_RGB, "Replaces the public RSA key in the NPDM ACID section and the NPDM RSA signature in the Program NCA (only if it needs other modifications)."); + j++; + + ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_RGB, "Disabling this will make the output NSP require ACID patches to work under any CFW, but will also make the Program NCA verifiable by PC tools."); + } + // Print hint about dumping RomFS content from DLCs if ((uiState == stateRomFsMenu && ((menuType == MENUTYPE_GAMECARD && titleAppCount <= 1 && checkIfBaseApplicationHasPatchOrAddOn(0, true)) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true)))) || ((uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu) && (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && checkIfBaseApplicationHasPatchOrAddOn(selectedAppIndex, true)))) { @@ -2482,7 +2574,7 @@ UIResult uiProcess() ypos = ((breaks * LINE_HEIGHT) + (j * (font_height + 12))); - uiDrawString(STRING_X_POS, ypos, FONT_COLOR_RGB, "Hint: choosing a DLC will only access RomFS data from it, unlike updates."); + uiDrawString(STRING_X_POS, ypos, FONT_COLOR_RGB, "Hint: choosing a DLC will only access RomFS data from it, unlike updates (which share their RomFS data with its base application)."); } } else { if (uiState == stateSdCardEmmcOrphanPatchAddOnMenu) @@ -2643,7 +2735,7 @@ UIResult uiProcess() res = resultShowNspDumpMenu; } } else { - res = (uiState == stateNspAddOnDumpMenu ? resultShowSdCardEmmcTitleMenu : resultShowSdCardEmmcOrphanPatchAddOnMenu); + res = resultShowSdCardEmmcTitleMenu; } } } @@ -2665,7 +2757,36 @@ UIResult uiProcess() case 4: // Generate ticket-less dump dumpCfg.nspDumpCfg.tiklessDump = false; break; - case 5: // Bundled application/update/DLC to dump + case 5: // Change NPDM RSA key/sig in Program NCA || DLC to dump + if (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu) + { + dumpCfg.nspDumpCfg.npdmAcidRsaPatch = false; + } else + if (uiState == stateNspAddOnDumpMenu) + { + if (menuType == MENUTYPE_GAMECARD) + { + if (selectedAddOnIndex > 0) + { + selectedAddOnIndex--; + titleSelectorStr[0] = '\0'; + } + } else + if (menuType == MENUTYPE_SDCARD_EMMC) + { + if (!orphanMode) + { + u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true); + if (newIndex != selectedAddOnIndex) + { + selectedAddOnIndex = newIndex; + titleSelectorStr[0] = '\0'; + } + } + } + } + break; + case 6: // Application/update to dump if (uiState == stateNspAppDumpMenu) { if (menuType == MENUTYPE_GAMECARD) @@ -2699,29 +2820,6 @@ UIResult uiProcess() } } } - } else - if (uiState == stateNspAddOnDumpMenu) - { - if (menuType == MENUTYPE_GAMECARD) - { - if (selectedAddOnIndex > 0) - { - selectedAddOnIndex--; - titleSelectorStr[0] = '\0'; - } - } else - if (menuType == MENUTYPE_SDCARD_EMMC) - { - if (!orphanMode) - { - u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true); - if (newIndex != selectedAddOnIndex) - { - selectedAddOnIndex = newIndex; - titleSelectorStr[0] = '\0'; - } - } - } } break; default: @@ -2749,43 +2847,11 @@ UIResult uiProcess() case 4: // Generate ticket-less dump dumpCfg.nspDumpCfg.tiklessDump = true; break; - case 5: // Bundled application/update/DLC to dump - if (uiState == stateNspAppDumpMenu) - { - if (menuType == MENUTYPE_GAMECARD) - { - if (titleAppCount > 1 && (selectedAppIndex + 1) < titleAppCount) - { - selectedAppIndex++; - titleSelectorStr[0] = '\0'; - } - } - } else - if (uiState == stateNspPatchDumpMenu) - { - if (menuType == MENUTYPE_GAMECARD) - { - if (titlePatchCount > 1 && (selectedPatchIndex + 1) < titlePatchCount) - { - selectedPatchIndex++; - titleSelectorStr[0] = '\0'; - } - } else - if (menuType == MENUTYPE_SDCARD_EMMC) - { - if (!orphanMode) - { - u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false); - if (newIndex != selectedPatchIndex) - { - selectedPatchIndex = newIndex; - titleSelectorStr[0] = '\0'; - } - } - } - } else - if (uiState == stateNspAddOnDumpMenu) + case 5: // Change NPDM RSA key/sig in Program NCA || DLC to dump + if (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu) { + dumpCfg.nspDumpCfg.npdmAcidRsaPatch = true; + } else { if (menuType == MENUTYPE_GAMECARD) { if (titleAddOnCount > 1 && (selectedAddOnIndex + 1) < titleAddOnCount) @@ -2808,6 +2874,40 @@ UIResult uiProcess() } } break; + case 6: // Application/update to dump + if (uiState == stateNspAppDumpMenu) + { + if (menuType == MENUTYPE_GAMECARD) + { + if (titleAppCount > 1 && (selectedAppIndex + 1) < titleAppCount) + { + selectedAppIndex++; + titleSelectorStr[0] = '\0'; + } + } + } else { + if (menuType == MENUTYPE_GAMECARD) + { + if (titlePatchCount > 1 && (selectedPatchIndex + 1) < titlePatchCount) + { + selectedPatchIndex++; + titleSelectorStr[0] = '\0'; + } + } else + if (menuType == MENUTYPE_SDCARD_EMMC) + { + if (!orphanMode) + { + u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false); + if (newIndex != selectedPatchIndex) + { + selectedPatchIndex = newIndex; + titleSelectorStr[0] = '\0'; + } + } + } + } + break; default: break; } @@ -2861,10 +2961,16 @@ UIResult uiProcess() case 6: // Generate ticket-less dumps dumpCfg.batchDumpCfg.tiklessDump = false; break; - case 7: // Skip already dumped titles + case 7: // Change NPDM RSA key/sig in Program NCA + dumpCfg.batchDumpCfg.npdmAcidRsaPatch = false; + break; + case 8: // Skip already dumped titles dumpCfg.batchDumpCfg.skipDumpedTitles = false; break; - case 8: // Source storage + case 9: // Remember dumped titles + dumpCfg.batchDumpCfg.rememberDumpedTitles = false; + break; + case 10: // Source storage if (dumpCfg.batchDumpCfg.batchModeSrc != BATCH_SOURCE_ALL) { dumpCfg.batchDumpCfg.batchModeSrc--; @@ -2889,9 +2995,6 @@ UIResult uiProcess() } } break; - case 9: // Remember dumped titles - dumpCfg.batchDumpCfg.rememberDumpedTitles = false; - break; default: break; } @@ -2923,10 +3026,16 @@ UIResult uiProcess() case 6: // Generate ticket-less dumps dumpCfg.batchDumpCfg.tiklessDump = true; break; - case 7: // Skip already dumped titles + case 7: // Change NPDM RSA key/sig in Program NCA + dumpCfg.batchDumpCfg.npdmAcidRsaPatch = true; + break; + case 8: // Skip already dumped titles dumpCfg.batchDumpCfg.skipDumpedTitles = true; break; - case 8: // Source storage + case 9: // Remember dumped titles + dumpCfg.batchDumpCfg.rememberDumpedTitles = true; + break; + case 10: // Source storage if (dumpCfg.batchDumpCfg.batchModeSrc != BATCH_SOURCE_EMMC) { dumpCfg.batchDumpCfg.batchModeSrc++; @@ -2951,9 +3060,6 @@ UIResult uiProcess() } } break; - case 9: // Remember dumped titles - dumpCfg.batchDumpCfg.rememberDumpedTitles = true; - break; default: break; } @@ -3081,6 +3187,104 @@ UIResult uiProcess() scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); } } else + if (uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu) + { + // Select + if ((keysDown & KEY_A) && cursor == 0) res = (uiState == stateExeFsSectionDataDumpMenu ? resultDumpExeFsSectionData : resultExeFsSectionBrowserGetList); + + // Back + if (keysDown & KEY_B) res = resultShowExeFsMenu; + + // Change option to false + if (keysDown & KEY_LEFT) + { + switch(cursor) + { + case 1: // Bundled application to dump/browse + if (menuType == MENUTYPE_GAMECARD) + { + if (selectedAppIndex > 0) + { + selectedAppIndex--; + titleSelectorStr[0] = '\0'; + exeFsUpdateFlag = false; + } + } + break; + case 2: // Use update + if (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && checkIfBaseApplicationHasPatchOrAddOn(selectedAppIndex, false)) + { + if (exeFsUpdateFlag) + { + u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppIndex, false); + if (newIndex != selectedPatchIndex) + { + selectedPatchIndex = newIndex; + exeFsAndRomFsSelectorStr[0] = '\0'; + } else { + exeFsUpdateFlag = false; + } + } + } + break; + default: + break; + } + } + + // Change option to true + if (keysDown & KEY_RIGHT) + { + switch(cursor) + { + case 1: // Bundled application to dump/browse + if (menuType == MENUTYPE_GAMECARD) + { + if (titleAppCount > 1 && (selectedAppIndex + 1) < titleAppCount) + { + selectedAppIndex++; + titleSelectorStr[0] = '\0'; + exeFsUpdateFlag = false; + } + } + break; + case 2: // Use update + if (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && checkIfBaseApplicationHasPatchOrAddOn(selectedAppIndex, false)) + { + if (exeFsUpdateFlag) + { + u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppIndex, false); + if (newIndex != selectedPatchIndex) + { + selectedPatchIndex = newIndex; + exeFsAndRomFsSelectorStr[0] = '\0'; + } + } else { + exeFsUpdateFlag = true; + selectedPatchIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppIndex, false); + exeFsAndRomFsSelectorStr[0] = '\0'; + } + } + break; + default: + break; + } + } + + // Go up + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) + { + scrollAmount = -1; + scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); + } + + // Go down + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) + { + scrollAmount = 1; + scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); + } + } else if (uiState == stateRomFsMenu) { // Select @@ -3231,104 +3435,6 @@ UIResult uiProcess() scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); } } else - if (uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu) - { - // Select - if ((keysDown & KEY_A) && cursor == 0) res = (uiState == stateExeFsSectionDataDumpMenu ? resultDumpExeFsSectionData : resultExeFsSectionBrowserGetList); - - // Back - if (keysDown & KEY_B) res = resultShowExeFsMenu; - - // Change option to false - if (keysDown & KEY_LEFT) - { - switch(cursor) - { - case 1: // Bundled application to dump/browse - if (menuType == MENUTYPE_GAMECARD) - { - if (selectedAppIndex > 0) - { - selectedAppIndex--; - titleSelectorStr[0] = '\0'; - exeFsUpdateFlag = false; - } - } - break; - case 2: // Use update - if (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && checkIfBaseApplicationHasPatchOrAddOn(selectedAppIndex, false)) - { - if (exeFsUpdateFlag) - { - u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppIndex, false); - if (newIndex != selectedPatchIndex) - { - selectedPatchIndex = newIndex; - exeFsAndRomFsSelectorStr[0] = '\0'; - } else { - exeFsUpdateFlag = false; - } - } - } - break; - default: - break; - } - } - - // Change option to true - if (keysDown & KEY_RIGHT) - { - switch(cursor) - { - case 1: // Bundled application to dump/browse - if (menuType == MENUTYPE_GAMECARD) - { - if (titleAppCount > 1 && (selectedAppIndex + 1) < titleAppCount) - { - selectedAppIndex++; - titleSelectorStr[0] = '\0'; - exeFsUpdateFlag = false; - } - } - break; - case 2: // Use update - if (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && checkIfBaseApplicationHasPatchOrAddOn(selectedAppIndex, false)) - { - if (exeFsUpdateFlag) - { - u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppIndex, false); - if (newIndex != selectedPatchIndex) - { - selectedPatchIndex = newIndex; - exeFsAndRomFsSelectorStr[0] = '\0'; - } - } else { - exeFsUpdateFlag = true; - selectedPatchIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppIndex, false); - exeFsAndRomFsSelectorStr[0] = '\0'; - } - } - break; - default: - break; - } - } - - // Go up - if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) - { - scrollAmount = -1; - scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); - } - - // Go down - if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) - { - scrollAmount = 1; - scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); - } - } else if (uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu) { // Select @@ -3465,6 +3571,136 @@ UIResult uiProcess() scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); } + // Go down + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) + { + scrollAmount = 1; + scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); + } + } else + if (uiState == stateTicketMenu) + { + // Select + if ((keysDown & KEY_A) && cursor == 0) res = resultDumpTicket; + + // Back + if (keysDown & KEY_B) res = resultShowSdCardEmmcTitleMenu; + + // Go left + if (keysDown & KEY_LEFT) + { + switch(cursor) + { + case 1: // Remove console specific data + dumpCfg.tikDumpCfg.removeConsoleData = false; + saveConfig(); + break; + case 2: // Use ticket from title + if (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && (checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false) || checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) + { + if (curTikType != TICKET_TYPE_APP) + { + u32 curIndex = (curTikType == TICKET_TYPE_PATCH ? selectedPatchIndex : selectedAddOnIndex); + u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(curIndex, selectedAppInfoIndex, (curTikType == TICKET_TYPE_ADDON)); + + if (newIndex != curIndex) + { + if (curTikType == TICKET_TYPE_PATCH) + { + selectedPatchIndex = newIndex; + } else { + selectedAddOnIndex = newIndex; + } + + titleSelectorStr[0] = '\0'; + } else { + if (curTikType == TICKET_TYPE_ADDON) + { + if (checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) + { + curTikType = TICKET_TYPE_PATCH; + selectedPatchIndex = retrieveLastPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, false); + titleSelectorStr[0] = '\0'; + } else { + curTikType = TICKET_TYPE_APP; + } + } else { + curTikType = TICKET_TYPE_APP; + titleSelectorStr[0] = '\0'; + } + } + } + } + break; + default: + break; + } + } + + // Go right + if (keysDown & KEY_RIGHT) + { + switch(cursor) + { + case 1: // Remove console specific data + dumpCfg.tikDumpCfg.removeConsoleData = true; + saveConfig(); + break; + case 2: // Use update/DLC + if (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && (checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false) || checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) + { + if (curTikType != TICKET_TYPE_APP) + { + u32 curIndex = (curTikType == TICKET_TYPE_PATCH ? selectedPatchIndex : selectedAddOnIndex); + u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(curIndex, selectedAppInfoIndex, (curTikType == TICKET_TYPE_ADDON)); + + if (newIndex != curIndex) + { + if (curTikType == TICKET_TYPE_PATCH) + { + selectedPatchIndex = newIndex; + } else { + selectedAddOnIndex = newIndex; + } + + titleSelectorStr[0] = '\0'; + } else { + if (curTikType == TICKET_TYPE_PATCH) + { + if (checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true)) + { + curTikType = TICKET_TYPE_ADDON; + selectedAddOnIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, true); + titleSelectorStr[0] = '\0'; + } + } + } + } else { + if (checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) + { + curTikType = TICKET_TYPE_PATCH; + selectedPatchIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, false); + } else { + curTikType = TICKET_TYPE_ADDON; + selectedAddOnIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, true); + } + + titleSelectorStr[0] = '\0'; + } + } + break; + default: + break; + } + } + + // Go up + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) + { + scrollAmount = -1; + scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); + } + // Go down if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) { @@ -3486,8 +3722,13 @@ UIResult uiProcess() menuType = MENUTYPE_GAMECARD; break; case 1: - res = resultShowSdCardEmmcMenu; - menuType = MENUTYPE_SDCARD_EMMC; + if (keysFileAvailable) + { + res = resultShowSdCardEmmcMenu; + menuType = MENUTYPE_SDCARD_EMMC; + } else { + uiStatusMsg("Keys file unavailable at \"%s\". Option disabled.", KEYS_FILE_PATH); + } break; case 2: res = resultShowUpdateMenu; @@ -3504,39 +3745,52 @@ UIResult uiProcess() res = resultShowXciDumpMenu; break; case 1: - if (!titlePatchCount && !titleAddOnCount) + if (keysFileAvailable) { - res = resultShowNspAppDumpMenu; - - // Reset option to its default value - selectedAppIndex = 0; + if (!titlePatchCount && !titleAddOnCount) + { + res = resultShowNspAppDumpMenu; + + // Reset option to its default value + selectedAppIndex = 0; + } else { + res = resultShowNspDumpMenu; + } } else { - res = resultShowNspDumpMenu; + uiStatusMsg("Keys file unavailable at \"%s\". Option disabled.", KEYS_FILE_PATH); } break; case 2: res = resultShowHfs0Menu; break; case 3: - loadTitlesFromSdCardAndEmmc(META_DB_PATCH); - - res = resultShowExeFsMenu; - - // Reset options to their default values - exeFsUpdateFlag = false; - selectedPatchIndex = 0; - + if (keysFileAvailable) + { + loadTitlesFromSdCardAndEmmc(META_DB_PATCH); + + res = resultShowExeFsMenu; + + // Reset options to their default values + exeFsUpdateFlag = false; + selectedPatchIndex = 0; + } else { + uiStatusMsg("Keys file unavailable at \"%s\". Option disabled.", KEYS_FILE_PATH); + } break; case 4: - loadTitlesFromSdCardAndEmmc(META_DB_PATCH); - loadTitlesFromSdCardAndEmmc(META_DB_ADDON); - - res = resultShowRomFsMenu; - - // Reset options to their default values - selectedPatchIndex = selectedAddOnIndex = 0; - curRomFsType = ROMFS_TYPE_APP; - + if (keysFileAvailable) + { + loadTitlesFromSdCardAndEmmc(META_DB_PATCH); + loadTitlesFromSdCardAndEmmc(META_DB_ADDON); + + res = resultShowRomFsMenu; + + // Reset options to their default values + selectedPatchIndex = selectedAddOnIndex = 0; + curRomFsType = ROMFS_TYPE_APP; + } else { + uiStatusMsg("Keys file unavailable at \"%s\". Option disabled.", KEYS_FILE_PATH); + } break; case 5: res = resultDumpGameCardCertificate; @@ -3559,25 +3813,12 @@ UIResult uiProcess() if (menuType == MENUTYPE_SDCARD_EMMC) selectedAppIndex = selectedAppInfoIndex; break; case 1: - if (menuType == MENUTYPE_GAMECARD) - { - res = (titlePatchCount > 0 ? resultShowNspPatchDumpMenu : resultShowNspAddOnDumpMenu); - } else - if (menuType == MENUTYPE_SDCARD_EMMC) - { - if (titlePatchCount > 0 && checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) - { - res = resultShowNspPatchDumpMenu; - selectedPatchIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, false); - } else { - res = resultShowNspAddOnDumpMenu; - selectedAddOnIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, true); - } - } + res = resultShowNspPatchDumpMenu; + if (menuType == MENUTYPE_SDCARD_EMMC) selectedPatchIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, false); break; case 2: res = resultShowNspAddOnDumpMenu; - selectedAddOnIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, true); + if (menuType == MENUTYPE_SDCARD_EMMC) selectedAddOnIndex = retrieveFirstPatchOrAddOnIndexFromBaseApplication(selectedAppInfoIndex, true); break; default: break; @@ -3639,6 +3880,7 @@ UIResult uiProcess() { // Save selected file index selectedFileIndex = (u32)cursor; + if (strlen(curRomFsPath) <= 1) selectedFileIndex++; // Adjust index if we're at the root directory res = (romFsBrowserEntries[cursor].type == ROMFS_ENTRY_DIR ? resultRomFsSectionBrowserChangeDir : resultRomFsSectionBrowserCopyFile); } } else @@ -3663,7 +3905,7 @@ UIResult uiProcess() res = resultShowNspDumpMenu; } } else { - res = resultShowNspAddOnDumpMenu; + res = (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH ? resultShowNspPatchDumpMenu : resultShowNspAddOnDumpMenu); } break; case 1: @@ -3688,6 +3930,19 @@ UIResult uiProcess() curRomFsType = ROMFS_TYPE_ADDON; } + break; + case 3: + res = resultShowTicketMenu; + + if (!orphanMode) + { + // Reset options to their default values + selectedPatchIndex = selectedAddOnIndex = 0; + curTikType = TICKET_TYPE_APP; + } else { + curTikType = (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH ? TICKET_TYPE_PATCH : TICKET_TYPE_ADDON); + } + break; default: break; @@ -3700,13 +3955,13 @@ UIResult uiProcess() if (orphanEntries[cursor].type == ORPHAN_ENTRY_TYPE_PATCH) { selectedPatchIndex = orphanEntries[cursor].index; - res = resultShowNspPatchDumpMenu; } else if (orphanEntries[cursor].type == ORPHAN_ENTRY_TYPE_ADDON) { selectedAddOnIndex = orphanEntries[cursor].index; - res = resultShowSdCardEmmcTitleMenu; } + + res = resultShowSdCardEmmcTitleMenu; } } else if (uiState == stateUpdateMenu) @@ -3717,7 +3972,12 @@ UIResult uiProcess() res = resultUpdateNSWDBXml; break; case 1: - res = resultUpdateApplication; + if (!updatePerformed) + { + res = resultUpdateApplication; + } else { + uiStatusMsg("Update already performed. Please restart the application."); + } break; default: break; @@ -3785,24 +4045,16 @@ UIResult uiProcess() { res = (!orphanMode ? resultShowSdCardEmmcMenu : resultShowSdCardEmmcOrphanPatchAddOnMenu); } else - if (menuType == MENUTYPE_SDCARD_EMMC && (uiState == stateRomFsMenu || (!orphanMode && (uiState == stateNspDumpMenu || uiState == stateExeFsMenu)))) + if (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && uiState == stateNspDumpMenu) { res = resultShowSdCardEmmcTitleMenu; } else if (uiState == stateSdCardEmmcOrphanPatchAddOnMenu) { - if (orphanEntries != NULL) - { - free(orphanEntries); - orphanEntries = NULL; - } + freeOrphanPatchOrAddOnList(); res = resultShowSdCardEmmcMenu; orphanMode = false; - } else - if (menuType == MENUTYPE_SDCARD_EMMC && orphanMode && (uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu)) - { - res = resultShowSdCardEmmcOrphanPatchAddOnMenu; } } @@ -3874,11 +4126,11 @@ UIResult uiProcess() } else { for(i = 0; i < scrollAmount; i++) { - if (cursor < (menuItemsCount - 1)) - { - cursor++; - if ((cursor - scroll) >= maxElements) scroll++; - } + if (cursor >= (menuItemsCount - 1)) break; + + cursor++; + + if ((cursor - scroll) >= maxElements) scroll++; } } } else @@ -3892,11 +4144,11 @@ UIResult uiProcess() } else { for(i = 0; i < -scrollAmount; i++) { - if (cursor > 0) - { - cursor--; - if ((cursor - scroll) < 0) scroll--; - } + if (cursor <= 0) break; + + cursor--; + + if ((cursor - scroll) < 0) scroll--; } } } @@ -3914,32 +4166,42 @@ UIResult uiProcess() } } - // Avoid placing the cursor on the "Dump bundled update NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled updates, or if we're dealing with a SD/eMMC title without installed updates - // Also avoid placing the cursor on the "Dump bundled DLC NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled DLCs, or if we're dealing with a SD/eMMC title without installed DLCs - if (uiState == stateNspDumpMenu) + // Avoid placing the cursor on the "Dump bundled update NSP" / "Dump installed update NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled updates, or if we're dealing with a SD/eMMC title without installed updates + // Also avoid placing the cursor on the "Dump bundled DLC NSP" / "Dump installed DLC NSP" option in the NSP dump menu if we're dealing with a gamecard and it doesn't include any bundled DLCs, or if we're dealing with a SD/eMMC title without installed DLCs + if (uiState == stateNspDumpMenu && ((cursor == 1 && (!titlePatchCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)))) || (cursor == 2 && (!titleAddOnCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true)))))) { - if (menuType == MENUTYPE_GAMECARD) + if (cursor == 1) { - if ((titlePatchCount && !titleAddOnCount) || (!titlePatchCount && titleAddOnCount)) - { - if (cursor >= 2) cursor = ((scrollWithKeysDown && scrollAmount > 0) ? 0 : 1); - } else - if (!titlePatchCount && !titleAddOnCount) + if ((!titleAddOnCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true)))) { // Just in case cursor = 0; + } else { + if (scrollAmount > 0) + { + cursor = 2; + } else + if (scrollAmount < 0) + { + cursor = 0; + } } } else - if (menuType == MENUTYPE_SDCARD_EMMC) + if (cursor == 2) { - if ((titlePatchCount && checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false) && (!titleAddOnCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) || ((!titlePatchCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) && titleAddOnCount && checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) - { - if (cursor >= 2) cursor = ((scrollWithKeysDown && scrollAmount > 0) ? 0 : 1); - } else - if ((!titlePatchCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) && (!titleAddOnCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) + if (!titlePatchCount || (menuType == MENUTYPE_SDCARD_EMMC && !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false))) { // Just in case cursor = 0; + } else { + if (scrollAmount > 0) + { + cursor = (scrollWithKeysDown ? 0 : 1); + } else + if (scrollAmount < 0) + { + cursor = 1; + } } } } @@ -3998,11 +4260,11 @@ UIResult uiProcess() } // Avoid placing the cursor on the "Source storage" option in the batch mode menu if we only have titles available in a single source storage device - if (uiState == stateSdCardEmmcBatchModeMenu && cursor == 8 && ((!sdCardTitleAppCount && !sdCardTitlePatchCount && !sdCardTitleAddOnCount) || (!nandUserTitleAppCount && !nandUserTitlePatchCount && !nandUserTitleAddOnCount))) + if (uiState == stateSdCardEmmcBatchModeMenu && cursor == 10 && ((!sdCardTitleAppCount && !sdCardTitlePatchCount && !sdCardTitleAddOnCount) || (!nandUserTitleAppCount && !nandUserTitlePatchCount && !nandUserTitleAddOnCount))) { if (scrollAmount > 0) { - cursor++; + cursor = (scrollWithKeysDown ? 0 : 9); } else if (scrollAmount < 0) { @@ -4064,24 +4326,26 @@ UIResult uiProcess() } } - // Avoid placing the cursor on the parent directory entry ("..") in the RomFS browser if we're currently at the root directory - if (uiState == stateRomFsSectionBrowser && cursor == 0 && strlen(curRomFsPath) <= 1) - { - cursor = ((scrollWithKeysDown && scrollAmount < 0) ? (menuItemsCount - 1) : 1); - - if (cursor == (menuItemsCount - 1)) - { - scroll = (menuItemsCount - maxElements); - if (scroll < 0) scroll = 0; - } - } - // Avoid placing the cursor on the "ExeFS options" element in the SD card / eMMC title menu if we're dealing with an orphan DLC - if (uiState == stateSdCardEmmcTitleMenu && cursor == 1 && orphanMode && selectedAddOnIndex < titleAddOnCount) + // Also avoid placing the cursor on the "RomFS options" element in the SD card / eMMC title menu if we're dealing with an orphan Patch + if (uiState == stateSdCardEmmcTitleMenu && orphanMode && ((orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_ADDON && cursor == 1) || (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_PATCH && (cursor == 1 || cursor == 2)))) { if (scrollAmount > 0) { - cursor = 2; + cursor = (orphanEntries[orphanListCursor].type == ORPHAN_ENTRY_TYPE_ADDON ? 2 : 3); + } else + if (scrollAmount < 0) + { + cursor = 0; + } + } + + // Avoid placing the cursor on the "Use ticket from title" element in the Ticket menu if we're dealing with an orphan title + if (uiState == stateTicketMenu && orphanMode && cursor == 2) + { + if (scrollAmount > 0) + { + cursor = (scrollWithKeysDown ? 0 : 1); } else if (scrollAmount < 0) { @@ -4155,17 +4419,19 @@ UIResult uiProcess() } } - if (selectedNspDumpType == DUMP_APP_NSP) - { - convertTitleVersionToDecimal(titleAppVersion[selectedAppIndex], versionStr, MAX_ELEMENTS(versionStr)); - snprintf(strbuf, MAX_ELEMENTS(strbuf), "%s%s v%s", menu[5], titleName[selectedAppIndex], versionStr); - } else - if (selectedNspDumpType == DUMP_PATCH_NSP) - { - retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, true, menu[5], strbuf, MAX_ELEMENTS(strbuf)); - } else - if (selectedNspDumpType == DUMP_ADDON_NSP) + if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_PATCH_NSP) { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[5], (dumpCfg.nspDumpCfg.npdmAcidRsaPatch ? "Yes" : "No")); + breaks++; + + if (selectedNspDumpType == DUMP_APP_NSP) + { + convertTitleVersionToDecimal(titleAppVersion[selectedAppIndex], versionStr, MAX_ELEMENTS(versionStr)); + snprintf(strbuf, MAX_ELEMENTS(strbuf), "%s%s v%s", menu[6], titleName[selectedAppIndex], versionStr); + } else { + retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, true, menu[6], strbuf, MAX_ELEMENTS(strbuf)); + } + } else { retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, true, menu[5], strbuf, MAX_ELEMENTS(strbuf)); } @@ -4221,18 +4487,22 @@ UIResult uiProcess() breaks++; } - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[7], (dumpCfg.batchDumpCfg.skipDumpedTitles ? "Yes" : "No")); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[7], (dumpCfg.batchDumpCfg.npdmAcidRsaPatch ? "Yes" : "No")); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[8], (dumpCfg.batchDumpCfg.skipDumpedTitles ? "Yes" : "No")); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[9], (dumpCfg.batchDumpCfg.rememberDumpedTitles ? "Yes" : "No")); breaks++; if ((sdCardTitleAppCount || sdCardTitlePatchCount || sdCardTitleAddOnCount) && (nandUserTitleAppCount || nandUserTitlePatchCount || nandUserTitleAddOnCount)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[8], (dumpCfg.batchDumpCfg.batchModeSrc == BATCH_SOURCE_ALL ? "All (SD card + eMMC)" : (dumpCfg.batchDumpCfg.batchModeSrc == BATCH_SOURCE_SDCARD ? "SD card" : "eMMC"))); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[10], (dumpCfg.batchDumpCfg.batchModeSrc == BATCH_SOURCE_ALL ? "All (SD card + eMMC)" : (dumpCfg.batchDumpCfg.batchModeSrc == BATCH_SOURCE_SDCARD ? "SD card" : "eMMC"))); breaks++; } - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[9], (dumpCfg.batchDumpCfg.rememberDumpedTitles ? "Yes" : "No")); - - breaks += 2; + breaks++; uiRefreshDisplay(); dumpNintendoSubmissionPackageBatch(&(dumpCfg.batchDumpCfg)); @@ -4638,6 +4908,46 @@ UIResult uiProcess() uiUpdateFreeSpace(); res = resultShowGameCardMenu; } else + if (uiState == stateDumpTicket) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "Dump ticket"); + breaks++; + + menu = ticketMenuItems; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, "%s%s", menu[1], (dumpCfg.tikDumpCfg.removeConsoleData ? "Yes" : "No")); + breaks++; + + switch(curTikType) + { + case TICKET_TYPE_APP: + convertTitleVersionToDecimal(titleAppVersion[selectedAppInfoIndex], versionStr, MAX_ELEMENTS(versionStr)); + snprintf(strbuf, MAX_ELEMENTS(strbuf), "%s%s | %016lX v%s (BASE)", menu[2], titleName[selectedAppInfoIndex], titleAppTitleID[selectedAppInfoIndex], versionStr); + break; + case TICKET_TYPE_PATCH: + retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, true, menu[2], strbuf, MAX_ELEMENTS(strbuf)); + strcat(strbuf, " (UPD)"); + break; + case TICKET_TYPE_ADDON: + retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, true, menu[2], strbuf, MAX_ELEMENTS(strbuf)); + strcat(strbuf, " (DLC)"); + break; + default: + break; + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, strbuf); + breaks += 2; + + u32 titleIndex = (curTikType == TICKET_TYPE_APP ? selectedAppInfoIndex : (curTikType == TICKET_TYPE_PATCH ? selectedPatchIndex : selectedAddOnIndex)); + + dumpTicketFromTitle(titleIndex, curTikType, &(dumpCfg.tikDumpCfg)); + + waitForButtonPress(); + + uiUpdateFreeSpace(); + res = resultShowTicketMenu; + } else if (uiState == stateUpdateNSWDBXml) { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, updateMenuItems[0]); @@ -4655,7 +4965,7 @@ UIResult uiProcess() uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_TITLE_RGB, updateMenuItems[1]); breaks += 2; - updateApplication(); + updatePerformed = updateApplication(); waitForButtonPress(); diff --git a/source/ui.h b/source/ui.h index 852f57c..519aca4 100644 --- a/source/ui.h +++ b/source/ui.h @@ -105,6 +105,8 @@ typedef enum { resultShowSdCardEmmcOrphanPatchAddOnMenu, resultShowSdCardEmmcBatchModeMenu, resultSdCardEmmcBatchDump, + resultShowTicketMenu, + resultDumpTicket, resultShowUpdateMenu, resultUpdateNSWDBXml, resultUpdateApplication, @@ -152,6 +154,8 @@ typedef enum { stateSdCardEmmcOrphanPatchAddOnMenu, stateSdCardEmmcBatchModeMenu, stateSdCardEmmcBatchDump, + stateTicketMenu, + stateDumpTicket, stateUpdateMenu, stateUpdateNSWDBXml, stateUpdateApplication diff --git a/source/util.c b/source/util.c index cdee781..07f9c2d 100644 --- a/source/util.c +++ b/source/util.c @@ -20,6 +20,7 @@ #include "fs_ext.h" #include "ui.h" #include "util.h" +#include "fatfs/ff.h" /* Extern variables */ @@ -53,6 +54,8 @@ const char *userAgent = "nxdumptool/" APP_VERSION " (Nintendo Switch)"; dumpOptions dumpCfg; +bool keysFileAvailable = false; + static char *result_buf = NULL; static size_t result_sz = 0; static size_t result_written = 0; @@ -111,7 +114,7 @@ u32 nandUserTitleAppCount = 0; u32 nandUserTitlePatchCount = 0; u32 nandUserTitleAddOnCount = 0; -static bool sdCardAndEmmcTitleInfoLoaded = false; +static bool gamecardInfoLoaded = false, sdCardAndEmmcTitleInfoLoaded = false; u32 gameCardSdCardEmmcPatchCount = 0, gameCardSdCardEmmcAddOnCount = 0; @@ -133,11 +136,15 @@ u8 *dumpBuf = NULL; u8 *ncaCtrBuf = NULL; orphan_patch_addon_entry *orphanEntries = NULL; +u32 orphanEntriesCnt = 0; char strbuf[NAME_BUF_LEN] = {'\0'}; char appLaunchPath[NAME_BUF_LEN] = {'\0'}; +FsStorage fatFsStorage; +static bool openBis = false, mountBisFatFs = false; + void loadConfig() { // Set default configuration values @@ -147,8 +154,10 @@ void loadConfig() dumpCfg.xciDumpCfg.calcCrc = true; dumpCfg.nspDumpCfg.isFat32 = true; + dumpCfg.nspDumpCfg.npdmAcidRsaPatch = true; dumpCfg.batchDumpCfg.isFat32 = true; + dumpCfg.batchDumpCfg.npdmAcidRsaPatch = true; dumpCfg.batchDumpCfg.skipDumpedTitles = true; dumpCfg.batchDumpCfg.batchModeSrc = BATCH_SOURCE_ALL; @@ -230,6 +239,37 @@ void fsGameCardDetectionThreadFunc(void *arg) waitMulti(&idx, 0, waiterForEvent(&fsGameCardKernelEvent), waiterForUEvent(&exitEvent)); } +bool mountSysEmmcPartition() +{ + FATFS fs; + + Result result = fsOpenBisStorage(&fatFsStorage, FsBisStorageId_System); + if (R_FAILED(result)) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Error: failed to open BIS System partition! (0x%08X)", result); + return false; + } + + openBis = true; + + FRESULT fr = f_mount(&fs, BIS_MOUNT_NAME, 1); + if (fr != FR_OK) + { + uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "Error: failed to mount BIS System partition! (%u)", fr); + return false; + } + + mountBisFatFs = true; + + return true; +} + +void unmountSysEmmcPartition() +{ + if (mountBisFatFs) f_unmount(BIS_MOUNT_NAME); + if (openBis) fsStorageClose(&fatFsStorage); +} + bool isServiceRunning(const char *serviceName) { if (!serviceName || !strlen(serviceName)) return false; @@ -572,11 +612,7 @@ void freeGlobalData() romFsBrowserEntries = NULL; } - if (orphanEntries != NULL) - { - free(orphanEntries); - orphanEntries = NULL; - } + freeOrphanPatchOrAddOnList(); } bool listTitlesByType(NcmContentMetaDatabase *ncmDb, u8 filter) @@ -595,164 +631,170 @@ bool listTitlesByType(NcmContentMetaDatabase *ncmDb, u8 filter) NcmApplicationContentMetaKey *titleListTmp = NULL; size_t titleListSize = sizeof(NcmApplicationContentMetaKey); - u32 written = 0, total = 0; + u32 i, written = 0, total = 0; u64 *titleIDs = NULL, *tmpTIDs = NULL; u32 *versions = NULL, *tmpVersions = NULL; titleList = calloc(1, titleListSize); - if (titleList) + if (!titleList) { - if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(ncmDb, filter, titleList, titleListSize, &written, &total))) + uiStatusMsg("listTitlesByType: unable to allocate memory for the ApplicationContentMetaKey struct (0x%02X filter).", filter); + goto out; + } + + result = ncmContentMetaDatabaseListApplication(ncmDb, filter, titleList, titleListSize, &written, &total); + if (R_FAILED(result)) + { + uiStatusMsg("listTitlesByType: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter); + goto out; + } + + if (!written || !total) + { + // There are no titles that match the provided filter in the opened storage device + success = true; + goto out; + } + + if (total > written) + { + titleListSize *= total; + titleListTmp = realloc(titleList, titleListSize); + if (titleListTmp) { - if (written && total) + titleList = titleListTmp; + memset(titleList, 0, titleListSize); + + result = ncmContentMetaDatabaseListApplication(ncmDb, filter, titleList, titleListSize, &written, &total); + if (R_SUCCEEDED(result)) { - if (total > written) + if (written != total) { - titleListSize *= total; - titleListTmp = realloc(titleList, titleListSize); - if (titleListTmp) - { - titleList = titleListTmp; - memset(titleList, 0, titleListSize); - - if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(ncmDb, filter, titleList, titleListSize, &written, &total))) - { - if (written != total) - { - uiStatusMsg("listTitlesByType: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u) (0x%02X filter).", written, total, filter); - proceed = false; - } - } else { - uiStatusMsg("listTitlesByType: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter); - proceed = false; - } - } else { - uiStatusMsg("listTitlesByType: error reallocating output buffer for ncmContentMetaDatabaseListApplication (%u %s) (0x%02X filter).", total, (total == 1 ? "entry" : "entries"), filter); - proceed = false; - } - } - - if (proceed) - { - titleIDs = calloc(total, sizeof(u64)); - versions = calloc(total, sizeof(u32)); - - if (titleIDs != NULL && versions != NULL) - { - u32 i; - for(i = 0; i < total; i++) - { - titleIDs[i] = titleList[i].metaRecord.titleId; - versions[i] = titleList[i].metaRecord.version; - } - - if (filter == META_DB_REGULAR_APPLICATION) - { - // If ptr == NULL, realloc will essentially act as a malloc - tmpTIDs = realloc(titleAppTitleID, (titleAppCount + total) * sizeof(u64)); - tmpVersions = realloc(titleAppVersion, (titleAppCount + total) * sizeof(u32)); - - if (tmpTIDs != NULL && tmpVersions != NULL) - { - titleAppTitleID = tmpTIDs; - memcpy(titleAppTitleID + titleAppCount, titleIDs, total * sizeof(u64)); - - titleAppVersion = tmpVersions; - memcpy(titleAppVersion + titleAppCount, versions, total * sizeof(u32)); - - titleAppCount += total; - - success = true; - } else { - if (tmpTIDs != NULL) titleAppTitleID = tmpTIDs; - - if (tmpVersions != NULL) titleAppVersion = tmpVersions; - - memError = true; - } - } else - if (filter == META_DB_PATCH) - { - // If ptr == NULL, realloc will essentially act as a malloc - tmpTIDs = realloc(titlePatchTitleID, (titlePatchCount + total) * sizeof(u64)); - tmpVersions = realloc(titlePatchVersion, (titlePatchCount + total) * sizeof(u32)); - - if (tmpTIDs != NULL && tmpVersions != NULL) - { - titlePatchTitleID = tmpTIDs; - memcpy(titlePatchTitleID + titlePatchCount, titleIDs, total * sizeof(u64)); - - titlePatchVersion = tmpVersions; - memcpy(titlePatchVersion + titlePatchCount, versions, total * sizeof(u32)); - - titlePatchCount += total; - - success = true; - } else { - if (tmpTIDs != NULL) titlePatchTitleID = tmpTIDs; - - if (tmpVersions != NULL) titlePatchVersion = tmpVersions; - - memError = true; - } - } else - if (filter == META_DB_ADDON) - { - // If ptr == NULL, realloc will essentially act as a malloc - tmpTIDs = realloc(titleAddOnTitleID, (titleAddOnCount + total) * sizeof(u64)); - tmpVersions = realloc(titleAddOnVersion, (titleAddOnCount + total) * sizeof(u32)); - - if (tmpTIDs != NULL && tmpVersions != NULL) - { - titleAddOnTitleID = tmpTIDs; - memcpy(titleAddOnTitleID + titleAddOnCount, titleIDs, total * sizeof(u64)); - - titleAddOnVersion = tmpVersions; - memcpy(titleAddOnVersion + titleAddOnCount, versions, total * sizeof(u32)); - - titleAddOnCount += total; - - success = true; - } else { - if (tmpTIDs != NULL) titleAddOnTitleID = tmpTIDs; - - if (tmpVersions != NULL) titleAddOnVersion = tmpVersions; - - memError = true; - } - } - } else { - memError = true; - } - - if (titleIDs != NULL) free(titleIDs); - - if (versions != NULL) free(versions); - - if (memError) uiStatusMsg("listTitlesByType: failed to allocate memory for TID/version buffer! (0x%02X filter).", filter); + uiStatusMsg("listTitlesByType: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u) (0x%02X filter).", written, total, filter); + proceed = false; } } else { - // There are no titles that match the provided filter in the opened storage device - success = true; + uiStatusMsg("listTitlesByType: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter); + proceed = false; } } else { - uiStatusMsg("listTitlesByType: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter); + uiStatusMsg("listTitlesByType: error reallocating output buffer for ncmContentMetaDatabaseListApplication (%u %s) (0x%02X filter).", total, (total == 1 ? "entry" : "entries"), filter); + proceed = false; } - - free(titleList); - } else { - uiStatusMsg("listTitlesByType: unable to allocate memory for the ApplicationContentMetaKey struct (0x%02X filter).", filter); } + if (!proceed) goto out; + + titleIDs = calloc(total, sizeof(u64)); + versions = calloc(total, sizeof(u32)); + + if (!titleIDs || !versions) + { + memError = true; + goto out; + } + + for(i = 0; i < total; i++) + { + titleIDs[i] = titleList[i].metaRecord.titleId; + versions[i] = titleList[i].metaRecord.version; + } + + free(titleList); + titleList = NULL; + + if (filter == META_DB_REGULAR_APPLICATION) + { + // If ptr == NULL, realloc will essentially act as a malloc + tmpTIDs = realloc(titleAppTitleID, (titleAppCount + total) * sizeof(u64)); + tmpVersions = realloc(titleAppVersion, (titleAppCount + total) * sizeof(u32)); + + if (tmpTIDs && tmpVersions) + { + titleAppTitleID = tmpTIDs; + tmpTIDs = NULL; + memcpy(titleAppTitleID + titleAppCount, titleIDs, total * sizeof(u64)); + + titleAppVersion = tmpVersions; + tmpVersions = NULL; + memcpy(titleAppVersion + titleAppCount, versions, total * sizeof(u32)); + + titleAppCount += total; + + success = true; + } else { + if (tmpTIDs) titleAppTitleID = tmpTIDs; + if (tmpVersions) titleAppVersion = tmpVersions; + memError = true; + } + } else + if (filter == META_DB_PATCH) + { + // If ptr == NULL, realloc will essentially act as a malloc + tmpTIDs = realloc(titlePatchTitleID, (titlePatchCount + total) * sizeof(u64)); + tmpVersions = realloc(titlePatchVersion, (titlePatchCount + total) * sizeof(u32)); + + if (tmpTIDs && tmpVersions) + { + titlePatchTitleID = tmpTIDs; + tmpTIDs = NULL; + memcpy(titlePatchTitleID + titlePatchCount, titleIDs, total * sizeof(u64)); + + titlePatchVersion = tmpVersions; + tmpVersions = NULL; + memcpy(titlePatchVersion + titlePatchCount, versions, total * sizeof(u32)); + + titlePatchCount += total; + + success = true; + } else { + if (tmpTIDs) titlePatchTitleID = tmpTIDs; + if (tmpVersions) titlePatchVersion = tmpVersions; + memError = true; + } + } else + if (filter == META_DB_ADDON) + { + // If ptr == NULL, realloc will essentially act as a malloc + tmpTIDs = realloc(titleAddOnTitleID, (titleAddOnCount + total) * sizeof(u64)); + tmpVersions = realloc(titleAddOnVersion, (titleAddOnCount + total) * sizeof(u32)); + + if (tmpTIDs && tmpVersions) + { + titleAddOnTitleID = tmpTIDs; + tmpTIDs = NULL; + memcpy(titleAddOnTitleID + titleAddOnCount, titleIDs, total * sizeof(u64)); + + titleAddOnVersion = tmpVersions; + tmpVersions = NULL; + memcpy(titleAddOnVersion + titleAddOnCount, versions, total * sizeof(u32)); + + titleAddOnCount += total; + + success = true; + } else { + if (tmpTIDs) titleAddOnTitleID = tmpTIDs; + if (tmpVersions) titleAddOnVersion = tmpVersions; + memError = true; + } + } + +out: + if (memError) uiStatusMsg("listTitlesByType: failed to allocate memory for TID/version buffer! (0x%02X filter).", filter); + + if (titleIDs) free(titleIDs); + if (versions) free(versions); + if (titleList) free(titleList); + return success; } -bool getTitleIDAndVersionList(FsStorageId curStorageId) +bool getTitleIDAndVersionList(FsStorageId curStorageId, bool loadBaseApps, bool loadPatches, bool loadAddOns) { - if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser) + if ((curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser) || (!loadBaseApps && !loadPatches && !loadAddOns)) { - uiStatusMsg("getTitleIDAndVersionList: invalid storage ID!"); + uiStatusMsg("getTitleIDAndVersionList: invalid parameters to retrieve Title ID + version list!"); return false; } @@ -768,7 +810,21 @@ bool getTitleIDAndVersionList(FsStorageId curStorageId) FsStorageId *tmpStorages = NULL; u32 curAppCount = titleAppCount, curPatchCount = titlePatchCount, curAddOnCount = titleAddOnCount; - if (R_SUCCEEDED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb))) + result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb); + if (R_FAILED(result)) + { + if (curStorageId == FsStorageId_SdCard && result == 0x21005) + { + // If the SD card is mounted, but is isn't currently used by HOS because of some weird reason, just filter this particular error and continue + // This can occur when using the "Nintendo" directory from a different console, or when the "sdmc:/Nintendo/Contents/private" file is corrupted + return true; + } else { + uiStatusMsg("getTitleIDAndVersionList: ncmOpenContentMetaDatabase failed for storage ID %u! (0x%08X)", result, curStorageId); + return false; + } + } + + if (loadBaseApps) { listApp = listTitlesByType(&ncmDb, META_DB_REGULAR_APPLICATION); if (listApp && titleAppCount > curAppCount) @@ -786,7 +842,10 @@ bool getTitleIDAndVersionList(FsStorageId curStorageId) listApp = false; } } - + } + + if (loadPatches) + { listPatch = listTitlesByType(&ncmDb, META_DB_PATCH); if (listPatch && titlePatchCount > curPatchCount) { @@ -803,7 +862,10 @@ bool getTitleIDAndVersionList(FsStorageId curStorageId) listPatch = false; } } - + } + + if (loadAddOns) + { listAddOn = listTitlesByType(&ncmDb, META_DB_ADDON); if (listAddOn && titleAddOnCount > curAddOnCount) { @@ -820,21 +882,12 @@ bool getTitleIDAndVersionList(FsStorageId curStorageId) listAddOn = false; } } - - success = (listApp || listPatch || listAddOn); - - serviceClose(&(ncmDb.s)); - } else { - if (curStorageId == FsStorageId_SdCard && result == 0x21005) - { - // If the SD card is mounted, but is isn't currently used by HOS because of some weird reason, just filter this particular error and continue - // This can occur when using the "Nintendo" directory from a different console, or when the "sdmc:/Nintendo/Contents/private" file is corrupted - success = true; - } else { - uiStatusMsg("getTitleIDAndVersionList: ncmOpenContentMetaDatabase failed! (0x%08X)", result); - } } + success = (listApp || listPatch || listAddOn); + + serviceClose(&(ncmDb.s)); + return success; } @@ -844,164 +897,47 @@ bool loadTitlesFromSdCardAndEmmc(u8 titleType) if ((titleType == META_DB_PATCH && gameCardSdCardEmmcPatchCount) || (titleType == META_DB_ADDON && gameCardSdCardEmmcAddOnCount)) return true; - u32 i, j; - - Result result; - NcmContentMetaDatabase ncmDb; - - NcmApplicationContentMetaKey *titleList; - NcmApplicationContentMetaKey *titleListTmp; - size_t titleListSize = sizeof(NcmApplicationContentMetaKey); - - u32 written, total; - - u64 *titleIDs, *tmpTIDs; - u32 *versions, *tmpVersions; - FsStorageId *tmpStorages; - - bool proceed; + u8 i; + u32 curPatchCount = titlePatchCount; + u32 curAddOnCount = titleAddOnCount; for(i = 0; i < 2; i++) { FsStorageId curStorageId = (i == 0 ? FsStorageId_SdCard : FsStorageId_NandUser); - /* Check if the SD card is really mounted */ - if (curStorageId == FsStorageId_SdCard && fsdevGetDefaultFileSystem() == NULL) continue; + if (!getTitleIDAndVersionList(curStorageId, false, (titleType == META_DB_PATCH), (titleType == META_DB_ADDON))) continue; - memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase)); - - titleList = titleListTmp = NULL; - - written = total = 0; - - titleIDs = tmpTIDs = NULL; - versions = tmpVersions = NULL; - tmpStorages = NULL; - - proceed = true; - - if (R_SUCCEEDED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb))) + if (titleType == META_DB_PATCH) { - titleList = calloc(1, titleListSize); - if (titleList) + if (titlePatchCount > curPatchCount) { - if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(&ncmDb, titleType, titleList, titleListSize, &written, &total)) && written && total) - { - if (total > written) - { - titleListSize *= total; - titleListTmp = realloc(titleList, titleListSize); - if (titleListTmp) - { - titleList = titleListTmp; - memset(titleList, 0, titleListSize); - - if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(&ncmDb, titleType, titleList, titleListSize, &written, &total))) - { - if (written != total) proceed = false; - } else { - proceed = false; - } - } else { - proceed = false; - } - } - - if (proceed) - { - titleIDs = calloc(total, sizeof(u64)); - versions = calloc(total, sizeof(u32)); - - if (titleIDs != NULL && versions != NULL) - { - for(j = 0; j < total; j++) - { - titleIDs[j] = titleList[j].metaRecord.titleId; - versions[j] = titleList[j].metaRecord.version; - } - - if (titleType == META_DB_PATCH) - { - // If ptr == NULL, realloc will essentially act as a malloc - tmpTIDs = realloc(titlePatchTitleID, (titlePatchCount + total) * sizeof(u64)); - tmpVersions = realloc(titlePatchVersion, (titlePatchCount + total) * sizeof(u32)); - tmpStorages = realloc(titlePatchStorageId, (titlePatchCount + total) * sizeof(FsStorageId)); - - if (tmpTIDs != NULL && tmpVersions != NULL && tmpStorages != NULL) - { - titlePatchTitleID = tmpTIDs; - memcpy(titlePatchTitleID + titlePatchCount, titleIDs, total * sizeof(u64)); - - titlePatchVersion = tmpVersions; - memcpy(titlePatchVersion + titlePatchCount, versions, total * sizeof(u32)); - - titlePatchStorageId = tmpStorages; - for(j = titlePatchCount; j < (titlePatchCount + total); j++) titlePatchStorageId[j] = curStorageId; - - titlePatchCount += total; - - gameCardSdCardEmmcPatchCount += total; - - if (curStorageId == FsStorageId_SdCard) - { - sdCardTitlePatchCount = total; - } else { - nandUserTitlePatchCount = total; - } - } else { - if (tmpTIDs != NULL) titlePatchTitleID = tmpTIDs; - - if (tmpVersions != NULL) titlePatchVersion = tmpVersions; - - if (tmpStorages != NULL) titlePatchStorageId = tmpStorages; - } - } else { - // If ptr == NULL, realloc will essentially act as a malloc - tmpTIDs = realloc(titleAddOnTitleID, (titleAddOnCount + total) * sizeof(u64)); - tmpVersions = realloc(titleAddOnVersion, (titleAddOnCount + total) * sizeof(u32)); - tmpStorages = realloc(titleAddOnStorageId, (titleAddOnCount + total) * sizeof(FsStorageId)); - - if (tmpTIDs != NULL && tmpVersions != NULL && tmpStorages != NULL) - { - titleAddOnTitleID = tmpTIDs; - memcpy(titleAddOnTitleID + titleAddOnCount, titleIDs, total * sizeof(u64)); - - titleAddOnVersion = tmpVersions; - memcpy(titleAddOnVersion + titleAddOnCount, versions, total * sizeof(u32)); - - titleAddOnStorageId = tmpStorages; - for(j = titleAddOnCount; j < (titleAddOnCount + total); j++) titleAddOnStorageId[j] = curStorageId; - - titleAddOnCount += total; - - gameCardSdCardEmmcAddOnCount += total; - - if (curStorageId == FsStorageId_SdCard) - { - sdCardTitleAddOnCount = total; - } else { - nandUserTitleAddOnCount = total; - } - } else { - if (tmpTIDs != NULL) titleAddOnTitleID = tmpTIDs; - - if (tmpVersions != NULL) titleAddOnVersion = tmpVersions; - - if (tmpStorages != NULL) titleAddOnStorageId = tmpStorages; - } - } - } - - if (titleIDs != NULL) free(titleIDs); - - if (versions != NULL) free(versions); - } - } + u32 newPatchCount = (titlePatchCount - curPatchCount); - free(titleList); + gameCardSdCardEmmcPatchCount = newPatchCount; + + if (curStorageId == FsStorageId_SdCard) + { + sdCardTitlePatchCount = newPatchCount; + } else { + nandUserTitlePatchCount = newPatchCount; + } + } + } else + if (titleType == META_DB_ADDON) + { + if (titleAddOnCount > curAddOnCount) + { + u32 newAddOnCount = (titleAddOnCount - curAddOnCount); + + gameCardSdCardEmmcAddOnCount = newAddOnCount; + + if (curStorageId == FsStorageId_SdCard) + { + sdCardTitleAddOnCount = newAddOnCount; + } else { + nandUserTitleAddOnCount = newAddOnCount; + } } - - serviceClose(&(ncmDb.s)); } } @@ -1035,9 +971,7 @@ void freeTitlesFromSdCardAndEmmc(u8 titleType) titlePatchStorageId = tmpStorages; } else { if (tmpTIDs != NULL) titlePatchTitleID = tmpTIDs; - if (tmpVersions != NULL) titlePatchVersion = tmpVersions; - if (tmpStorages != NULL) titlePatchStorageId = tmpStorages; } } else { @@ -1054,9 +988,7 @@ void freeTitlesFromSdCardAndEmmc(u8 titleType) titlePatchCount -= gameCardSdCardEmmcPatchCount; gameCardSdCardEmmcPatchCount = 0; - sdCardTitlePatchCount = 0; - nandUserTitlePatchCount = 0; } else { if ((titleAddOnCount - gameCardSdCardEmmcAddOnCount) > 0) @@ -1074,9 +1006,7 @@ void freeTitlesFromSdCardAndEmmc(u8 titleType) titleAddOnStorageId = tmpStorages; } else { if (tmpTIDs != NULL) titleAddOnTitleID = tmpTIDs; - if (tmpVersions != NULL) titleAddOnVersion = tmpVersions; - if (tmpStorages != NULL) titleAddOnStorageId = tmpStorages; } } else { @@ -1093,9 +1023,7 @@ void freeTitlesFromSdCardAndEmmc(u8 titleType) titleAddOnCount -= gameCardSdCardEmmcAddOnCount; gameCardSdCardEmmcAddOnCount = 0; - sdCardTitleAddOnCount = 0; - nandUserTitleAddOnCount = 0; } } @@ -1112,7 +1040,8 @@ void convertTitleVersionToDecimal(u32 version, char *versionBuf, size_t versionB bool getTitleControlNacp(u64 titleID, char *nameBuf, int nameBufSize, char *authorBuf, int authorBufSize, u8 **iconBuf) { - if (!titleID || !nameBuf || !nameBufSize || !authorBuf || !authorBufSize || !iconBuf) + // At least the name must be retrieved + if (!titleID || !nameBuf || !nameBufSize || (authorBuf != NULL && !authorBufSize)) { uiStatusMsg("getTitleControlNacp: invalid parameters to retrieve Control.nacp."); return false; @@ -1127,23 +1056,28 @@ bool getTitleControlNacp(u64 titleID, char *nameBuf, int nameBufSize, char *auth buf = calloc(1, sizeof(NsApplicationControlData)); if (buf) { - if (R_SUCCEEDED(result = nsGetApplicationControlData(1, titleID, buf, sizeof(NsApplicationControlData), &outsize))) + result = nsGetApplicationControlData(1, titleID, buf, sizeof(NsApplicationControlData), &outsize); + if (R_SUCCEEDED(result)) { if (outsize >= sizeof(buf->nacp)) { - if (R_SUCCEEDED(result = nacpGetLanguageEntry(&buf->nacp, &langentry))) + result = nacpGetLanguageEntry(&buf->nacp, &langentry); + if (R_SUCCEEDED(result)) { strncpy(nameBuf, langentry->name, nameBufSize); - strncpy(authorBuf, langentry->author, authorBufSize); + if (authorBuf != NULL && authorBufSize) strncpy(authorBuf, langentry->author, authorBufSize); getNameAndAuthor = true; } else { uiStatusMsg("getTitleControlNacp: GetLanguageEntry failed! (0x%08X)", result); } - getIcon = uiLoadJpgFromMem(buf->icon, sizeof(buf->icon), NACP_ICON_SQUARE_DIMENSION, NACP_ICON_SQUARE_DIMENSION, NACP_ICON_DOWNSCALED, NACP_ICON_DOWNSCALED, iconBuf); - if (!getIcon) uiStatusMsg(strbuf); + if (iconBuf != NULL) + { + getIcon = uiLoadJpgFromMem(buf->icon, sizeof(buf->icon), NACP_ICON_SQUARE_DIMENSION, NACP_ICON_SQUARE_DIMENSION, NACP_ICON_DOWNSCALED, NACP_ICON_DOWNSCALED, iconBuf); + if (!getIcon) uiStatusMsg(strbuf); + } - success = (getNameAndAuthor && getIcon); + success = (iconBuf != NULL ? (getNameAndAuthor && getIcon) : getNameAndAuthor); } else { uiStatusMsg("getTitleControlNacp: Control.nacp buffer size (%u bytes) is too small! Expected: %u bytes", outsize, sizeof(buf->nacp)); } @@ -1179,6 +1113,7 @@ void createOutputDirectories() mkdir(ROMFS_DUMP_PATH, 0744); mkdir(CERT_DUMP_PATH, 0744); mkdir(BATCH_OVERRIDES_PATH, 0744); + mkdir(TICKET_PATH, 0744); } void strtrim(char *str) @@ -1212,19 +1147,22 @@ bool getRootHfs0Header() workaroundPartitionZeroAccess(); - if (R_FAILED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_FAILED(result)) { uiStatusMsg("getRootHfs0Header: GetGameCardHandle failed! (0x%08X)", result); return false; } - if (R_FAILED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, 0))) + result = fsOpenGameCardStorage(&gameCardStorage, &handle, 0); + if (R_FAILED(result)) { uiStatusMsg("getRootHfs0Header: OpenGameCardStorage failed! (0x%08X)", result); return false; } - if (R_FAILED(result = fsStorageRead(&gameCardStorage, 0, gamecard_header, GAMECARD_HEADER_SIZE))) + result = fsStorageRead(&gameCardStorage, 0, gamecard_header, GAMECARD_HEADER_SIZE); + if (R_FAILED(result)) { uiStatusMsg("getRootHfs0Header: StorageRead failed to read %u-byte chunk from offset 0x%016lX! (0x%08X)", GAMECARD_HEADER_SIZE, 0, result); fsStorageClose(&gameCardStorage); @@ -1287,7 +1225,8 @@ bool getRootHfs0Header() return false; } - if (R_FAILED(result = fsStorageRead(&gameCardStorage, hfs0_offset, hfs0_header, hfs0_size))) + result = fsStorageRead(&gameCardStorage, hfs0_offset, hfs0_header, hfs0_size); + if (R_FAILED(result)) { uiStatusMsg("getRootHfs0Header: StorageRead failed to read %u-byte chunk from offset 0x%016lX! (0x%08X)", hfs0_size, hfs0_offset, result); @@ -1344,14 +1283,16 @@ void getGameCardUpdateInfo() gameCardUpdateTitleID = 0; gameCardUpdateVersion = 0; - if (R_FAILED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_FAILED(result)) { uiStatusMsg("getGameCardUpdateInfo: GetGameCardHandle failed! (0x%08X)", result); return; } // Get bundled FW version update - if (R_SUCCEEDED(result = fsDeviceOperatorUpdatePartitionInfo(&fsOperatorInstance, &handle, &gameCardUpdateVersion, &gameCardUpdateTitleID))) + result = fsDeviceOperatorUpdatePartitionInfo(&fsOperatorInstance, &handle, &gameCardUpdateVersion, &gameCardUpdateTitleID); + if (R_SUCCEEDED(result)) { if (gameCardUpdateTitleID == GAMECARD_UPDATE_TITLEID) { @@ -1374,7 +1315,7 @@ void loadTitleInfo() if (menuType == MENUTYPE_MAIN) { freeGlobalData(); - sdCardAndEmmcTitleInfoLoaded = false; + gamecardInfoLoaded = sdCardAndEmmcTitleInfoLoaded = false; return; } @@ -1384,12 +1325,15 @@ void loadTitleInfo() { if (gameCardInserted) { - if (hfs0_header != NULL) return; + if (hfs0_header != NULL || gamecardInfoLoaded) return; /* Don't access the gamecard immediately to avoid conflicts with the fsp-srv, ncm and ns services */ uiPleaseWait(GAMECARD_WAIT_TIME); - if (!getRootHfs0Header()) + proceed = getRootHfs0Header(); + gamecardInfoLoaded = true; + + if (!proceed) { uiPrintHeadline(); return; @@ -1399,7 +1343,7 @@ void loadTitleInfo() freeTitleInfo(); - proceed = getTitleIDAndVersionList(FsStorageId_GameCard); + proceed = getTitleIDAndVersionList(FsStorageId_GameCard, true, true, true); } else { freeGlobalData(); return; @@ -1409,17 +1353,17 @@ void loadTitleInfo() { if (titleAppCount || titlePatchCount || titleAddOnCount || sdCardAndEmmcTitleInfoLoaded) return; - uiPleaseWait(1); + uiPleaseWait(0); freeTitleInfo(); - if (getTitleIDAndVersionList(FsStorageId_SdCard)) + if (getTitleIDAndVersionList(FsStorageId_SdCard, true, true, true)) { sdCardTitleAppCount = titleAppCount; sdCardTitlePatchCount = titlePatchCount; sdCardTitleAddOnCount = titleAddOnCount; - if (getTitleIDAndVersionList(FsStorageId_NandUser)) + if (getTitleIDAndVersionList(FsStorageId_NandUser, true, true, true)) { nandUserTitleAppCount = (titleAppCount - sdCardTitleAppCount); nandUserTitlePatchCount = (titlePatchCount - sdCardTitlePatchCount); @@ -1563,104 +1507,117 @@ bool getPartitionHfs0Header(u32 partition) u32 magic = 0; bool success = false; - if (getHfs0EntryDetails(hfs0_header, hfs0_offset, hfs0_size, hfs0_partition_cnt, partition, true, 0, &partitionHfs0HeaderOffset, &partitionSize)) + if (!getHfs0EntryDetails(hfs0_header, hfs0_offset, hfs0_size, hfs0_partition_cnt, partition, true, 0, &partitionHfs0HeaderOffset, &partitionSize)) { - workaroundPartitionZeroAccess(); - - if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) - { - /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "GetGameCardHandle succeeded: 0x%08X", handle.value); - breaks++;*/ - - // Same ugly hack from dumpRawHfs0Partition() - if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)))) - { - /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "OpenGameCardStorage succeeded: 0x%08X", handle); - breaks++;*/ - - // First read MEDIA_UNIT_SIZE bytes - if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, buf, MEDIA_UNIT_SIZE))) - { - // Check the HFS0 magic word - memcpy(&magic, buf, sizeof(u32)); - magic = bswap_32(magic); - if (magic == HFS0_MAGIC) - { - // Calculate the size for the partition HFS0 header - memcpy(&partitionHfs0FileCount, buf + HFS0_FILE_COUNT_ADDR, sizeof(u32)); - memcpy(&partitionHfs0StrTableSize, buf + HFS0_STR_TABLE_SIZE_ADDR, sizeof(u32)); - partitionHfs0HeaderSize = (HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * partitionHfs0FileCount) + partitionHfs0StrTableSize); - - /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u HFS0 header offset (relative to IStorage instance): 0x%016lX", partition, partitionHfs0HeaderOffset); - breaks++; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u HFS0 header size: %lu bytes", partition, partitionHfs0HeaderSize); - breaks++; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u file count: %u", partition, partitionHfs0FileCount); - breaks++; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u string table size: %u bytes", partition, partitionHfs0StrTableSize); - breaks++; - - uiRefreshDisplay();*/ - - // Round up the partition HFS0 header size to a MEDIA_UNIT_SIZE bytes boundary - partitionHfs0HeaderSize = round_up(partitionHfs0HeaderSize, MEDIA_UNIT_SIZE); - - partitionHfs0Header = malloc(partitionHfs0HeaderSize); - if (partitionHfs0Header) - { - // Check if we were dealing with the correct header size all along - if (partitionHfs0HeaderSize == MEDIA_UNIT_SIZE) - { - // Just copy what we already have - memcpy(partitionHfs0Header, buf, MEDIA_UNIT_SIZE); - success = true; - } else { - // Read the whole HFS0 header - if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, partitionHfs0Header, partitionHfs0HeaderSize))) - { - success = true; - } else { - free(partitionHfs0Header); - partitionHfs0Header = NULL; - partitionHfs0HeaderOffset = 0; - partitionHfs0HeaderSize = 0; - partitionHfs0FileCount = 0; - partitionHfs0StrTableSize = 0; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset); - } - } - - //if (success) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u HFS0 header successfully retrieved!", partition); - } else { - partitionHfs0HeaderOffset = 0; - partitionHfs0HeaderSize = 0; - partitionHfs0FileCount = 0; - partitionHfs0StrTableSize = 0; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to allocate memory for the HFS0 header from partition #%u!", partition); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Magic word mismatch! 0x%08X != 0x%08X", magic, HFS0_MAGIC); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset); - } - - fsStorageClose(&gameCardStorage); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "OpenGameCardStorage failed! (0x%08X)", result); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "GetGameCardHandle failed! (0x%08X)", result); - } - } else { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to get partition details from the root HFS0 header!"); + return success; } + workaroundPartitionZeroAccess(); + + result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "GetGameCardHandle failed! (0x%08X)", result); + return success; + } + + /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "GetGameCardHandle succeeded: 0x%08X", handle.value); + breaks++;*/ + + // Same ugly hack from dumpRawHfs0Partition() + result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "OpenGameCardStorage failed! (0x%08X)", result); + return success; + } + + /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "OpenGameCardStorage succeeded: 0x%08X", handle); + breaks++;*/ + + // First read MEDIA_UNIT_SIZE bytes + result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, buf, MEDIA_UNIT_SIZE); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset); + goto out; + } + + // Check the HFS0 magic word + memcpy(&magic, buf, sizeof(u32)); + magic = bswap_32(magic); + if (magic != HFS0_MAGIC) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Magic word mismatch! 0x%08X != 0x%08X", magic, HFS0_MAGIC); + goto out; + } + + // Calculate the size for the partition HFS0 header + memcpy(&partitionHfs0FileCount, buf + HFS0_FILE_COUNT_ADDR, sizeof(u32)); + memcpy(&partitionHfs0StrTableSize, buf + HFS0_STR_TABLE_SIZE_ADDR, sizeof(u32)); + partitionHfs0HeaderSize = (HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * partitionHfs0FileCount) + partitionHfs0StrTableSize); + + /*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u HFS0 header offset (relative to IStorage instance): 0x%016lX", partition, partitionHfs0HeaderOffset); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u HFS0 header size: %lu bytes", partition, partitionHfs0HeaderSize); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u file count: %u", partition, partitionHfs0FileCount); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u string table size: %u bytes", partition, partitionHfs0StrTableSize); + breaks++; + + uiRefreshDisplay();*/ + + // Round up the partition HFS0 header size to a MEDIA_UNIT_SIZE bytes boundary + partitionHfs0HeaderSize = round_up(partitionHfs0HeaderSize, MEDIA_UNIT_SIZE); + + partitionHfs0Header = malloc(partitionHfs0HeaderSize); + if (!partitionHfs0Header) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to allocate memory for the HFS0 header from partition #%u!", partition); + goto out; + } + + // Check if we were dealing with the correct header size all along + if (partitionHfs0HeaderSize == MEDIA_UNIT_SIZE) + { + // Just copy what we already have + memcpy(partitionHfs0Header, buf, MEDIA_UNIT_SIZE); + success = true; + } else { + // Read the whole HFS0 header + result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, partitionHfs0Header, partitionHfs0HeaderSize); + if (R_SUCCEEDED(result)) + { + success = true; + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset); + } + } + +out: + if (success) + { + //uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Partition #%u HFS0 header successfully retrieved!", partition); + } else { + if (partitionHfs0Header) + { + free(partitionHfs0Header); + partitionHfs0Header = NULL; + } + + partitionHfs0HeaderOffset = 0; + partitionHfs0HeaderSize = 0; + partitionHfs0FileCount = 0; + partitionHfs0StrTableSize = 0; + } + + fsStorageClose(&gameCardStorage); + return success; } @@ -1725,7 +1682,7 @@ bool getHfs0FileList(u32 partition) } // Used to retrieve tik/cert files from the HFS0 Secure partition -bool getPartitionHfs0FileByName(FsStorage *gameCardStorage, const char *filename, u8 *outBuf, u64 outBufSize) +bool getFileFromHfs0PartitionByName(FsStorage *gameCardStorage, const char *filename, u8 *outBuf, u64 outBufSize) { if (!partitionHfs0Header || !partitionHfs0FileCount || !partitionHfs0HeaderSize || !gameCardStorage || !filename || !outBuf || !outBufSize) { @@ -1743,28 +1700,28 @@ bool getPartitionHfs0FileByName(FsStorage *gameCardStorage, const char *filename { memcpy(&tmp_hfs0_entry, partitionHfs0Header + (u64)HFS0_ENTRY_TABLE_ADDR + ((u64)i * sizeof(hfs0_entry_table)), sizeof(hfs0_entry_table)); - if (!strncasecmp((char*)partitionHfs0Header + (u64)HFS0_ENTRY_TABLE_ADDR + ((u64)partitionHfs0FileCount * sizeof(hfs0_entry_table)) + (u64)tmp_hfs0_entry.filename_offset, filename, strlen(filename))) + if (strncasecmp((char*)partitionHfs0Header + (u64)HFS0_ENTRY_TABLE_ADDR + ((u64)partitionHfs0FileCount * sizeof(hfs0_entry_table)) + (u64)tmp_hfs0_entry.filename_offset, filename, strlen(filename)) != 0) continue; + + found = true; + + if (outBufSize > tmp_hfs0_entry.file_size) { - found = true; - - if (outBufSize > tmp_hfs0_entry.file_size) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: file \"%s\" is smaller than expected! (0x%016lX < 0x%016lX)", filename, tmp_hfs0_entry.file_size, outBufSize); - proceed = false; - break; - } - - if (R_FAILED(result = fsStorageRead(gameCardStorage, partitionHfs0HeaderSize + tmp_hfs0_entry.file_offset, outBuf, outBufSize))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to read file \"%s\" from the HFS0 partition!", filename); - proceed = false; - break; - } - - success = true; - + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: file \"%s\" is smaller than expected! (0x%016lX < 0x%016lX)", filename, tmp_hfs0_entry.file_size, outBufSize); + proceed = false; break; } + + result = fsStorageRead(gameCardStorage, partitionHfs0HeaderSize + tmp_hfs0_entry.file_offset, outBuf, outBufSize); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to read file \"%s\" from the HFS0 partition!", filename); + proceed = false; + break; + } + + success = true; + + break; } if (proceed && !found) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to find file \"%s\" in the HFS0 partition!", filename); @@ -1875,36 +1832,213 @@ bool calculateRomFsExtractedDirSize(u32 dir_offset, bool usePatch, u64 *out) return true; } -bool readNcaExeFsSection(u32 titleIndex, bool usePatch) +bool retrieveNcaContentRecords(FsStorageId curStorageId, u8 filter, u32 titleCount, u32 titleIndex, NcmContentRecord **outContentRecords, u32 *outContentRecordsCnt) { Result result; - u32 i = 0; - u32 written = 0; - u32 total = 0; - u32 titleCount = 0; - u32 ncmTitleIndex = 0; - u32 titleNcaCount = 0; - u32 partition = 0; - - FsStorageId curStorageId; NcmContentMetaDatabase ncmDb; memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase)); NcmContentMetaRecordsHeader contentRecordsHeader; memset(&contentRecordsHeader, 0, sizeof(NcmContentMetaRecordsHeader)); - u64 contentRecordsHeaderReadSize = 0; + NcmApplicationContentMetaKey *titleList = NULL; + size_t titleListSize = (sizeof(NcmApplicationContentMetaKey) * titleCount); + + NcmContentRecord *titleContentRecords = NULL; + u32 titleContentRecordsCnt = 0; + + u32 written = 0, total = 0; + + bool success = false; + + if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: invalid title storage ID!"); + goto out; + } + + if (filter != META_DB_REGULAR_APPLICATION && filter != META_DB_PATCH && filter != META_DB_ADDON) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: invalid title filter!"); + goto out; + } + + if (!titleCount) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: invalid title type count!"); + goto out; + } + + if (titleIndex >= titleCount) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: invalid title index!"); + goto out; + } + + if (!outContentRecords || !outContentRecordsCnt) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: invalid output parameters!"); + goto out; + } + + // If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002 + if (curStorageId == FsStorageId_GameCard && !partitionHfs0Header) + { + u32 partition = (hfs0_partition_cnt - 1); // Select the secure partition + + workaroundPartitionZeroAccess(); + + if (!getPartitionHfs0Header(partition)) + { + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: failed to read the Secure HFS0 partition header!"); + goto out; + } + + if (!partitionHfs0FileCount) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: the Secure HFS0 partition is empty!"); + goto out; + } + } + + titleList = calloc(1, titleListSize); + if (!titleList) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: unable to allocate memory for the ApplicationContentMetaKey struct!"); + goto out; + } + + result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: ncmOpenContentMetaDatabase failed! (0x%08X)", result); + goto out; + } + + result = ncmContentMetaDatabaseListApplication(&ncmDb, filter, titleList, titleListSize, &written, &total); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); + goto out; + } + + if (!written || !total) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!"); + goto out; + } + + if (written != total) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: title count mismatch in ncmContentMetaDatabaseListApplication! (%u != %u)", written, total); + goto out; + } + + if (titleIndex >= total) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: provided title index exceeds title count from ncmContentMetaDatabaseListApplication!"); + goto out; + } + + result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[titleIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: ncmContentMetaDatabaseGet failed! (0x%08X)", result); + goto out; + } + + titleContentRecordsCnt = (u32)(contentRecordsHeader.numContentRecords); + + titleContentRecords = calloc(titleContentRecordsCnt, sizeof(NcmContentRecord)); + if (!titleContentRecords) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: unable to allocate memory for the ContentRecord struct!"); + goto out; + } + + written = 0; + + result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[titleIndex].metaRecord), 0, titleContentRecords, titleContentRecordsCnt * sizeof(NcmContentRecord), &written); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); + goto out; + } + + if (written != titleContentRecordsCnt) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "retrieveNcaContentRecords: title content record count mismatch in ncmContentMetaDatabaseListContentInfo! (%u != %u)", written, titleContentRecordsCnt); + goto out; + } + + success = true; + + // Update output parameters + *outContentRecords = titleContentRecords; + *outContentRecordsCnt = titleContentRecordsCnt; + +out: + if (!success && titleContentRecords) free(titleContentRecords); + + serviceClose(&(ncmDb.s)); + + if (titleList) free(titleList); + + if (curStorageId == FsStorageId_GameCard) + { + if (partitionHfs0Header) + { + free(partitionHfs0Header); + partitionHfs0Header = NULL; + partitionHfs0HeaderOffset = 0; + partitionHfs0HeaderSize = 0; + partitionHfs0FileCount = 0; + partitionHfs0StrTableSize = 0; + } + } + + return success; +} + +void removeConsoleDataFromTicket(title_rights_ctx *rights_info) +{ + if (!rights_info || !rights_info->has_rights_id || !rights_info->retrieved_tik || rights_info->missing_tik || rights_info->tik_data.titlekey_type != ETICKET_TITLEKEY_PERSONALIZED) return; + + memset(rights_info->tik_data.signature, 0xFF, 0x100); + + memset(rights_info->tik_data.sig_issuer, 0, 0x40); + sprintf(rights_info->tik_data.sig_issuer, "Root-CA00000003-XS00000020"); + + memset(rights_info->tik_data.titlekey_block, 0, 0x100); + memcpy(rights_info->tik_data.titlekey_block, rights_info->enc_titlekey, 0x10); + + rights_info->tik_data.titlekey_type = ETICKET_TITLEKEY_COMMON; + rights_info->tik_data.ticket_id = 0; + rights_info->tik_data.device_id = 0; + rights_info->tik_data.account_id = 0; +} + +bool readNcaExeFsSection(u32 titleIndex, bool usePatch) +{ + u32 i = 0; + Result result; + + FsStorageId curStorageId; + u8 filter; + u32 titleCount = 0, ncmTitleIndex = 0; + + NcmContentRecord *titleContentRecords = NULL; + u32 titleContentRecordsCnt = 0; + + NcmNcaId ncaId; + char ncaIdStr[SHA256_HASH_SIZE + 1] = {'\0'}; + NcmContentStorage ncmStorage; memset(&ncmStorage, 0, sizeof(NcmContentStorage)); - NcmApplicationContentMetaKey *titleList = NULL; - NcmContentRecord *titleContentRecords = NULL; - size_t titleListSize = sizeof(NcmApplicationContentMetaKey); - - NcmNcaId ncaId; - char ncaIdStr[33] = {'\0'}; u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0}; nca_header_t dec_nca_header; @@ -1914,19 +2048,19 @@ bool readNcaExeFsSection(u32 titleIndex, bool usePatch) if ((!usePatch && !titleAppStorageId) || (usePatch && !titlePatchStorageId)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: title storage ID unavailable!"); - breaks += 2; - return false; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaExeFsSection: title storage ID unavailable!"); + goto out; + } + + if ((!usePatch && titleIndex >= titleAppCount) || (usePatch && titleIndex >= titlePatchCount)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaExeFsSection: invalid title index!"); + goto out; } curStorageId = (!usePatch ? titleAppStorageId[titleIndex] : titlePatchStorageId[titleIndex]); - if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title storage ID!"); - breaks += 2; - return false; - } + filter = (!usePatch ? META_DB_REGULAR_APPLICATION : META_DB_PATCH); switch(curStorageId) { @@ -1951,7 +2085,7 @@ bool readNcaExeFsSection(u32 titleIndex, bool usePatch) if (menuType == MENUTYPE_SDCARD_EMMC) { - ncmTitleIndex = (titleIndex - (!usePatch ? sdCardTitleAppCount : sdCardTitlePatchCount)); // Substract SD card patch count + ncmTitleIndex = (titleIndex - (!usePatch ? sdCardTitleAppCount : sdCardTitlePatchCount)); // Substract SD card title count } else { // Patches loaded using loadTitlesFromSdCardAndEmmc() ncmTitleIndex = (titleIndex - ((titlePatchCount - gameCardSdCardEmmcPatchCount) + sdCardTitlePatchCount)); // Substract gamecard + SD card patch count @@ -1962,112 +2096,18 @@ bool readNcaExeFsSection(u32 titleIndex, bool usePatch) break; } - if (!titleCount) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title type count!"); - breaks += 2; - return false; - } - - if (ncmTitleIndex > (titleCount - 1)) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title index!"); - breaks += 2; - return false; - } - - titleListSize *= titleCount; - - // If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002 - if (curStorageId == FsStorageId_GameCard && !partitionHfs0Header) - { - partition = (hfs0_partition_cnt - 1); // Select the secure partition - - workaroundPartitionZeroAccess(); - - if (!getPartitionHfs0Header(partition)) - { - breaks += 2; - return false; - } - - if (!partitionHfs0FileCount) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "The Secure HFS0 partition is empty!"); - goto out; - } - } - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Looking for the Program NCA (%s)...", (!usePatch ? "base application" : "update")); uiRefreshDisplay(); breaks++; - titleList = calloc(1, titleListSize); - if (!titleList) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the ApplicationContentMetaKey struct!"); - goto out; - } + if (!retrieveNcaContentRecords(curStorageId, filter, titleCount, ncmTitleIndex, &titleContentRecords, &titleContentRecordsCnt)) goto out; - if (R_FAILED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentMetaDatabase failed! (0x%08X)", result); - goto out; - } - - u8 filter = (!usePatch ? META_DB_REGULAR_APPLICATION : META_DB_PATCH); - - if (R_FAILED(result = ncmContentMetaDatabaseListApplication(&ncmDb, filter, titleList, titleListSize, &written, &total))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); - goto out; - } - - if (!written || !total) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!"); - goto out; - } - - if (written != total) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total); - goto out; - } - - if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseGet failed! (0x%08X)", result); - goto out; - } - - titleNcaCount = (u32)(contentRecordsHeader.numContentRecords); - - titleContentRecords = calloc(titleNcaCount, sizeof(NcmContentRecord)); - if (!titleContentRecords) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the ContentRecord struct!"); - goto out; - } - - if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), 0, titleContentRecords, titleNcaCount * sizeof(NcmContentRecord), &written))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); - goto out; - } - - if (R_FAILED(result = ncmOpenContentStorage(curStorageId, &ncmStorage))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentStorage failed! (0x%08X)", result); - goto out; - } - - for(i = 0; i < titleNcaCount; i++) + for(i = 0; i < titleContentRecordsCnt; i++) { if (titleContentRecords[i].type == NcmContentType_Program) { memcpy(&ncaId, &(titleContentRecords[i].ncaId), sizeof(NcmNcaId)); - convertDataToHexString(titleContentRecords[i].ncaId.c, SHA256_HASH_SIZE / 2, ncaIdStr, 33); + convertDataToHexString(titleContentRecords[i].ncaId.c, SHA256_HASH_SIZE / 2, ncaIdStr, SHA256_HASH_SIZE + 1); foundProgram = true; break; } @@ -2087,9 +2127,17 @@ bool readNcaExeFsSection(u32 titleIndex, bool usePatch) uiRefreshDisplay(); breaks++;*/ - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) + result = ncmOpenContentStorage(curStorageId, &ncmStorage); + if (R_FAILED(result)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to read header from Program NCA! (0x%08X)", result); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaExeFsSection: ncmOpenContentStorage failed! (0x%08X)", result); + goto out; + } + + result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaExeFsSection: failed to read header from Program NCA! (0x%08X)", result); goto out; } @@ -2120,60 +2168,35 @@ bool readNcaExeFsSection(u32 titleIndex, bool usePatch) success = readExeFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys); out: - if (titleContentRecords) free(titleContentRecords); - - if (!success) serviceClose(&(ncmStorage.s)); - - serviceClose(&(ncmDb.s)); - - if (titleList) free(titleList); - - if (curStorageId == FsStorageId_GameCard) + if (!success) { - if (partitionHfs0Header) - { - free(partitionHfs0Header); - partitionHfs0Header = NULL; - partitionHfs0HeaderOffset = 0; - partitionHfs0HeaderSize = 0; - partitionHfs0FileCount = 0; - partitionHfs0StrTableSize = 0; - } + breaks += 2; + serviceClose(&(ncmStorage.s)); } + if (titleContentRecords) free(titleContentRecords); + return success; } bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType) { - Result result; u32 i = 0; - u32 written = 0; - u32 total = 0; - u32 titleCount = 0; - u32 ncmTitleIndex = 0; - u32 titleNcaCount = 0; - u32 partition = 0; + Result result; FsStorageId curStorageId; + u8 filter; + u32 titleCount = 0, ncmTitleIndex = 0; - NcmContentMetaDatabase ncmDb; - memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase)); + NcmContentRecord *titleContentRecords = NULL; + u32 titleContentRecordsCnt = 0; - NcmContentMetaRecordsHeader contentRecordsHeader; - memset(&contentRecordsHeader, 0, sizeof(NcmContentMetaRecordsHeader)); - - u64 contentRecordsHeaderReadSize = 0; + NcmNcaId ncaId; + char ncaIdStr[SHA256_HASH_SIZE + 1] = {'\0'}; NcmContentStorage ncmStorage; memset(&ncmStorage, 0, sizeof(NcmContentStorage)); - NcmApplicationContentMetaKey *titleList = NULL; - NcmContentRecord *titleContentRecords = NULL; - size_t titleListSize = sizeof(NcmApplicationContentMetaKey); - - NcmNcaId ncaId; - char ncaIdStr[33] = {'\0'}; u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0}; nca_header_t dec_nca_header; @@ -2181,21 +2204,27 @@ bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType) bool success = false, foundNca = false; + if (curRomFsType != ROMFS_TYPE_APP && curRomFsType != ROMFS_TYPE_PATCH && curRomFsType != ROMFS_TYPE_ADDON) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaRomFsSection: invalid RomFS title type!"); + goto out; + } + if ((curRomFsType == ROMFS_TYPE_APP && !titleAppStorageId) || (curRomFsType == ROMFS_TYPE_PATCH && !titlePatchStorageId) || (curRomFsType == ROMFS_TYPE_ADDON && !titlePatchStorageId)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: title storage ID unavailable!"); - breaks += 2; - return false; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaRomFsSection: title storage ID unavailable!"); + goto out; + } + + if ((curRomFsType == ROMFS_TYPE_APP && titleIndex >= titleAppCount) || (curRomFsType == ROMFS_TYPE_PATCH && titleIndex >= titlePatchCount) || (curRomFsType == ROMFS_TYPE_ADDON && titleIndex >= titleAddOnCount)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaRomFsSection: invalid title index!"); + goto out; } curStorageId = (curRomFsType == ROMFS_TYPE_APP ? titleAppStorageId[titleIndex] : (curRomFsType == ROMFS_TYPE_PATCH ? titlePatchStorageId[titleIndex] : titleAddOnStorageId[titleIndex])); - if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title storage ID!"); - breaks += 2; - return false; - } + filter = (curRomFsType == ROMFS_TYPE_APP ? META_DB_REGULAR_APPLICATION : (curRomFsType == ROMFS_TYPE_PATCH ? META_DB_PATCH : META_DB_ADDON)); switch(curStorageId) { @@ -2256,112 +2285,18 @@ bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType) break; } - if (!titleCount) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title type count!"); - breaks += 2; - return false; - } - - if (ncmTitleIndex > (titleCount - 1)) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid title index!"); - breaks += 2; - return false; - } - - titleListSize *= titleCount; - - // If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002 - if (curStorageId == FsStorageId_GameCard && !partitionHfs0Header) - { - partition = (hfs0_partition_cnt - 1); // Select the secure partition - - workaroundPartitionZeroAccess(); - - if (!getPartitionHfs0Header(partition)) - { - breaks += 2; - return false; - } - - if (!partitionHfs0FileCount) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "The Secure HFS0 partition is empty!"); - goto out; - } - } - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Looking for the %s NCA (%s)...", (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"), (curRomFsType == ROMFS_TYPE_APP ? "base application" : (curRomFsType == ROMFS_TYPE_PATCH ? "update" : "DLC"))); uiRefreshDisplay(); breaks++; - titleList = calloc(1, titleListSize); - if (!titleList) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the ApplicationContentMetaKey struct!"); - goto out; - } + if (!retrieveNcaContentRecords(curStorageId, filter, titleCount, ncmTitleIndex, &titleContentRecords, &titleContentRecordsCnt)) goto out; - if (R_FAILED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentMetaDatabase failed! (0x%08X)", result); - goto out; - } - - u8 filter = (curRomFsType == ROMFS_TYPE_APP ? META_DB_REGULAR_APPLICATION : (curRomFsType == ROMFS_TYPE_PATCH ? META_DB_PATCH : META_DB_ADDON)); - - if (R_FAILED(result = ncmContentMetaDatabaseListApplication(&ncmDb, filter, titleList, titleListSize, &written, &total))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); - goto out; - } - - if (!written || !total) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!"); - goto out; - } - - if (written != total) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total); - goto out; - } - - if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseGet failed! (0x%08X)", result); - goto out; - } - - titleNcaCount = (u32)(contentRecordsHeader.numContentRecords); - - titleContentRecords = calloc(titleNcaCount, sizeof(NcmContentRecord)); - if (!titleContentRecords) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to allocate memory for the ContentRecord struct!"); - goto out; - } - - if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), 0, titleContentRecords, titleNcaCount * sizeof(NcmContentRecord), &written))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); - goto out; - } - - if (R_FAILED(result = ncmOpenContentStorage(curStorageId, &ncmStorage))) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: ncmOpenContentStorage failed! (0x%08X)", result); - goto out; - } - - for(i = 0; i < titleNcaCount; i++) + for(i = 0; i < titleContentRecordsCnt; i++) { if (((curRomFsType == ROMFS_TYPE_APP || curRomFsType == ROMFS_TYPE_PATCH) && titleContentRecords[i].type == NcmContentType_Program) || (curRomFsType == ROMFS_TYPE_ADDON && titleContentRecords[i].type == NcmContentType_Data)) { memcpy(&ncaId, &(titleContentRecords[i].ncaId), sizeof(NcmNcaId)); - convertDataToHexString(titleContentRecords[i].ncaId.c, SHA256_HASH_SIZE / 2, ncaIdStr, 33); + convertDataToHexString(titleContentRecords[i].ncaId.c, SHA256_HASH_SIZE / 2, ncaIdStr, SHA256_HASH_SIZE + 1); foundNca = true; break; } @@ -2381,9 +2316,17 @@ bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType) uiRefreshDisplay(); breaks++;*/ - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) + result = ncmOpenContentStorage(curStorageId, &ncmStorage); + if (R_FAILED(result)) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Failed to read header from %s NCA! (0x%08X)", (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"), result); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaRomFsSection: ncmOpenContentStorage failed! (0x%08X)", result); + goto out; + } + + result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "readNcaRomFsSection: failed to read header from %s NCA! (0x%08X)", (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"), result); goto out; } @@ -2413,7 +2356,7 @@ bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType) if (curRomFsType != ROMFS_TYPE_PATCH) { // Read directory and file tables from the RomFS section - if (!readRomFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys)) goto out; + success = readRomFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys); } else { // Look for the base application title index u32 appIndex; @@ -2438,32 +2381,19 @@ bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType) // Read BKTR entry data in the Program NCA from the update if (!readBktrEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys)) goto out; + + success = true; } - success = true; - out: - if (titleContentRecords) free(titleContentRecords); - - if (!success) serviceClose(&(ncmStorage.s)); - - serviceClose(&(ncmDb.s)); - - if (titleList) free(titleList); - - if (curStorageId == FsStorageId_GameCard) + if (!success) { - if (partitionHfs0Header) - { - free(partitionHfs0Header); - partitionHfs0Header = NULL; - partitionHfs0HeaderOffset = 0; - partitionHfs0HeaderSize = 0; - partitionHfs0FileCount = 0; - partitionHfs0StrTableSize = 0; - } + breaks += 2; + serviceClose(&(ncmStorage.s)); } + if (titleContentRecords) free(titleContentRecords); + return success; } @@ -2746,7 +2676,8 @@ int getSdCardFreeSpace(u64 *out) return rc; } - if (R_SUCCEEDED(result = fsFsGetFreeSpace(sdfs, "/", &size))) + result = fsFsGetFreeSpace(sdfs, "/", &size); + if (R_SUCCEEDED(result)) { *out = size; rc = 1; @@ -2873,6 +2804,9 @@ char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex) u32 app; bool foundApp = false; + u32 i; + bool nameless = true; + size_t strsize = NAME_BUF_LEN; char *fullname = calloc(strsize, sizeof(char)); if (!fullname) return NULL; @@ -2896,7 +2830,20 @@ char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex) { snprintf(fullname, strsize, "%s v%u (%016lX) (UPD)", fixedTitleName[app], titlePatchVersion[titleIndex], titlePatchTitleID[titleIndex]); } else { - snprintf(fullname, strsize, "%016lX v%u (UPD)", titlePatchTitleID[titleIndex], titlePatchVersion[titleIndex]); + if (orphanEntries != NULL && orphanEntriesCnt) + { + for(i = 0; i < orphanEntriesCnt; i++) + { + if (orphanEntries[i].type == ORPHAN_ENTRY_TYPE_PATCH && orphanEntries[i].index == titleIndex && strlen(orphanEntries[i].fixedName)) + { + snprintf(fullname, strsize, "%s v%u (%016lX) (UPD)", orphanEntries[i].fixedName, titlePatchVersion[titleIndex], titlePatchTitleID[titleIndex]); + nameless = false; + break; + } + } + } + + if (nameless) snprintf(fullname, strsize, "%016lX v%u (UPD)", titlePatchTitleID[titleIndex], titlePatchVersion[titleIndex]); } } else if (selectedNspDumpType == DUMP_ADDON_NSP) @@ -2914,7 +2861,20 @@ char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex) { snprintf(fullname, strsize, "%s v%u (%016lX) (DLC)", fixedTitleName[app], titleAddOnVersion[titleIndex], titleAddOnTitleID[titleIndex]); } else { - snprintf(fullname, strsize, "%016lX v%u (DLC)", titleAddOnTitleID[titleIndex], titleAddOnVersion[titleIndex]); + if (orphanEntries != NULL && orphanEntriesCnt) + { + for(i = 0; i < orphanEntriesCnt; i++) + { + if (orphanEntries[i].type == ORPHAN_ENTRY_TYPE_ADDON && orphanEntries[i].index == titleIndex && strlen(orphanEntries[i].fixedName)) + { + snprintf(fullname, strsize, "%s v%u (%016lX) (DLC)", orphanEntries[i].fixedName, titleAddOnVersion[titleIndex], titleAddOnTitleID[titleIndex]); + nameless = false; + break; + } + } + } + + if (nameless) snprintf(fullname, strsize, "%016lX v%u (DLC)", titleAddOnTitleID[titleIndex], titleAddOnVersion[titleIndex]); } } else { free(fullname); @@ -2946,6 +2906,9 @@ void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, bo u32 app; bool foundApp = false; + u32 i; + bool nameless = true; + for(app = 0; app < titleAppCount; app++) { if ((!addOn && titleID == (titleAppTitleID[app] | APPLICATION_PATCH_BITMASK)) || (addOn && (titleID & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[app] & APPLICATION_ADDON_BITMASK))) @@ -2964,11 +2927,33 @@ void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, bo snprintf(outBuf, outBufSize, "%s | %016lX v%s", titleName[app], titleID, versionStr); } } else { - if (prefix) + if (orphanEntries != NULL && orphanEntriesCnt) { - snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, titleID, versionStr); - } else { - snprintf(outBuf, outBufSize, "%016lX v%s", titleID, versionStr); + for(i = 0; i < orphanEntriesCnt; i++) + { + if (strlen(orphanEntries[i].name) && ((!addOn && orphanEntries[i].type == ORPHAN_ENTRY_TYPE_PATCH && titleID == titlePatchTitleID[orphanEntries[i].index]) || (addOn && orphanEntries[i].type == ORPHAN_ENTRY_TYPE_ADDON && titleID == titleAddOnTitleID[orphanEntries[i].index]))) + { + if (prefix) + { + snprintf(outBuf, outBufSize, "%s%s | %016lX v%s", prefix, orphanEntries[i].name, titleID, versionStr); + } else { + snprintf(outBuf, outBufSize, "%s | %016lX v%s", orphanEntries[i].name, titleID, versionStr); + } + + nameless = false; + break; + } + } + } + + if (nameless) + { + if (prefix) + { + snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, titleID, versionStr); + } else { + snprintf(outBuf, outBufSize, "%016lX v%s", titleID, versionStr); + } } } } @@ -2999,30 +2984,108 @@ bool checkOrphanPatchOrAddOn(bool addOn) return false; } -void generateOrphanPatchOrAddOnList() +void freeOrphanPatchOrAddOnList() { - u32 i, j; - char *nextFilename = filenameBuffer; - filenamesCount = 0; - - bool foundMatch; - char versionStr[128] = {'\0'}; - u32 orphanEntryIndex = 0, patchCount = 0, addOnCount = 0; - orphan_patch_addon_entry *orphanPatchEntries = NULL, *orphanAddOnEntries = NULL; - if (orphanEntries != NULL) { free(orphanEntries); orphanEntries = NULL; } + orphanEntriesCnt = 0; +} + +void generateOrphanPatchOrAddOnList() +{ + Result result; + size_t nsAppRecordCnt = 0; + + bool foundMatch; + char versionStr[128] = {'\0'}; + u32 orphanPatchCount = 0, orphanAddOnCount = 0, orphanEntryIndex = 0; + + u32 i, j; + char *nextFilename = filenameBuffer; + filenamesCount = 0; + + freeOrphanPatchOrAddOnList(); + if ((!titlePatchCount || !titlePatchTitleID || !titlePatchVersion) && (!titleAddOnCount || !titleAddOnTitleID || !titleAddOnVersion)) return; + // Retrieve all cached Application IDs + NsApplicationRecord *appRecords = calloc(1024, sizeof(NsApplicationRecord)); + if (!appRecords) return; + + result = nsListApplicationRecord(appRecords, 1024, 0, &nsAppRecordCnt); + if (R_FAILED(result)) + { + free(appRecords); + return; + } + if (titlePatchCount) { - orphanPatchEntries = calloc(titlePatchCount, sizeof(orphan_patch_addon_entry)); - if (!orphanPatchEntries) return; - + // Retrieve orphan patch count + for(i = 0; i < titlePatchCount; i++) + { + foundMatch = false; + + if (titleAppCount && titleAppTitleID) + { + for(j = 0; j < titleAppCount; j++) + { + if (titlePatchTitleID[i] == (titleAppTitleID[j] | APPLICATION_PATCH_BITMASK)) + { + foundMatch = true; + break; + } + } + } + + if (!foundMatch) orphanPatchCount++; + } + } + + if (titleAddOnCount) + { + // Retrieve orphan add-on count + for(i = 0; i < titleAddOnCount; i++) + { + foundMatch = false; + + if (titleAppCount && titleAppTitleID) + { + for(j = 0; j < titleAppCount; j++) + { + if ((titleAddOnTitleID[i] & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[j] & APPLICATION_ADDON_BITMASK)) + { + foundMatch = true; + break; + } + } + } + + if (!foundMatch) orphanAddOnCount++; + } + } + + if (!orphanPatchCount && !orphanAddOnCount) + { + free(appRecords); + return; + } + + // Allocate memory for our orphan entries + orphanEntries = calloc(orphanPatchCount + orphanAddOnCount, sizeof(orphan_patch_addon_entry)); + if (!orphanEntries) + { + free(appRecords); + return; + } + + if (orphanPatchCount) + { + // Save orphan patch data for(i = 0; i < titlePatchCount; i++) { foundMatch = false; @@ -3041,30 +3104,42 @@ void generateOrphanPatchOrAddOnList() if (!foundMatch) { - convertTitleVersionToDecimal(titlePatchVersion[i], versionStr, MAX_ELEMENTS(versionStr)); - snprintf(strbuf, MAX_ELEMENTS(strbuf), "%016lX v%s (Update)", titlePatchTitleID[i], versionStr); + // Look for a matching Application ID in our NS records + for(j = 0; j < nsAppRecordCnt; j++) + { + if (titlePatchTitleID[i] == (appRecords[j].titleID | APPLICATION_PATCH_BITMASK)) + { + if (getTitleControlNacp(appRecords[j].titleID, orphanEntries[orphanEntryIndex].name, NACP_APPNAME_LEN, NULL, 0, NULL)) + { + snprintf(orphanEntries[orphanEntryIndex].fixedName, NACP_APPNAME_LEN, orphanEntries[orphanEntryIndex].name); + removeIllegalCharacters(orphanEntries[orphanEntryIndex].fixedName); + } + + break; + } + } + + if (strlen(orphanEntries[orphanEntryIndex].name)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "%s v%u (%016lX) (Update)", orphanEntries[orphanEntryIndex].name, titlePatchVersion[i], titlePatchTitleID[i]); + } else { + convertTitleVersionToDecimal(titlePatchVersion[i], versionStr, MAX_ELEMENTS(versionStr)); + snprintf(strbuf, MAX_ELEMENTS(strbuf), "%016lX v%s (Update)", titlePatchTitleID[i], versionStr); + } + addStringToFilenameBuffer(strbuf, &nextFilename); - orphanPatchEntries[orphanEntryIndex].index = i; - orphanPatchEntries[orphanEntryIndex].type = ORPHAN_ENTRY_TYPE_PATCH; + orphanEntries[orphanEntryIndex].index = i; + orphanEntries[orphanEntryIndex].type = ORPHAN_ENTRY_TYPE_PATCH; orphanEntryIndex++; - patchCount++; } } } - if (titleAddOnCount) + if (orphanAddOnCount) { - orphanAddOnEntries = calloc(titleAddOnCount, sizeof(orphan_patch_addon_entry)); - if (!orphanAddOnEntries) - { - if (orphanPatchEntries) free(orphanPatchEntries); - return; - } - - orphanEntryIndex = 0; - + // Save orphan add-on data for(i = 0; i < titleAddOnCount; i++) { foundMatch = false; @@ -3083,43 +3158,43 @@ void generateOrphanPatchOrAddOnList() if (!foundMatch) { - convertTitleVersionToDecimal(titleAddOnVersion[i], versionStr, MAX_ELEMENTS(versionStr)); - snprintf(strbuf, MAX_ELEMENTS(strbuf), "%016lX v%s (DLC)", titleAddOnTitleID[i], versionStr); + // Look for a matching Application ID in our NS records + for(j = 0; j < nsAppRecordCnt; j++) + { + if ((titleAddOnTitleID[i] & APPLICATION_ADDON_BITMASK) == (appRecords[j].titleID & APPLICATION_ADDON_BITMASK)) + { + if (getTitleControlNacp(appRecords[j].titleID, orphanEntries[orphanEntryIndex].name, NACP_APPNAME_LEN, NULL, 0, NULL)) + { + snprintf(orphanEntries[orphanEntryIndex].fixedName, NACP_APPNAME_LEN, orphanEntries[orphanEntryIndex].name); + removeIllegalCharacters(orphanEntries[orphanEntryIndex].fixedName); + } + + break; + } + } + + if (strlen(orphanEntries[orphanEntryIndex].name)) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "%s v%u (%016lX) (DLC)", orphanEntries[orphanEntryIndex].name, titleAddOnVersion[i], titleAddOnTitleID[i]); + } else { + convertTitleVersionToDecimal(titleAddOnVersion[i], versionStr, MAX_ELEMENTS(versionStr)); + snprintf(strbuf, MAX_ELEMENTS(strbuf), "%016lX v%s (DLC)", titleAddOnTitleID[i], versionStr); + } + addStringToFilenameBuffer(strbuf, &nextFilename); - orphanAddOnEntries[orphanEntryIndex].index = i; - orphanAddOnEntries[orphanEntryIndex].type = ORPHAN_ENTRY_TYPE_ADDON; + orphanEntries[orphanEntryIndex].index = i; + orphanEntries[orphanEntryIndex].type = ORPHAN_ENTRY_TYPE_ADDON; orphanEntryIndex++; - addOnCount++; } } } - filenamesCount = (patchCount + addOnCount); + orphanEntriesCnt = (orphanPatchCount + orphanAddOnCount); + filenamesCount = orphanEntriesCnt; - orphanEntries = calloc(filenamesCount, sizeof(orphan_patch_addon_entry)); - if (!orphanEntries) - { - filenamesCount = 0; - if (orphanPatchEntries) free(orphanPatchEntries); - if (orphanAddOnEntries) free(orphanAddOnEntries); - return; - } - - if (titlePatchCount) - { - if (patchCount) memcpy(orphanEntries, orphanPatchEntries, patchCount * sizeof(orphan_patch_addon_entry)); - - free(orphanPatchEntries); - } - - if (titleAddOnCount) - { - if (addOnCount) memcpy(orphanEntries + patchCount, orphanAddOnEntries, addOnCount * sizeof(orphan_patch_addon_entry)); - - free(orphanAddOnEntries); - } + free(appRecords); } bool checkIfBaseApplicationHasPatchOrAddOn(u32 appIndex, bool addOn) @@ -3665,58 +3740,60 @@ void gameCardDumpNSWDBCheck(u32 crc) { if (!titleAppCount || !titleAppTitleID || !hfs0_partition_cnt) return; + u32 i; xmlDocPtr doc = NULL; bool found = false; doc = xmlParseFile(nswReleasesXmlPath); - if (doc) + if (!doc) { - u32 i; - for(i = 0; i < titleAppCount; i++) - { - snprintf(strbuf, MAX_ELEMENTS(strbuf), "//%s/%s[.//%s='%016lX']", nswReleasesRootElement, nswReleasesChildren, nswReleasesChildrenTitleID, titleAppTitleID[i]); - xmlXPathObjectPtr nodeSet = getNodeSet(doc, (xmlChar*)strbuf); - if (nodeSet) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Found %d %s with Title ID \"%016lX\".", nodeSet->nodesetval->nodeNr, (nodeSet->nodesetval->nodeNr > 1 ? "releases" : "release"), titleAppTitleID[i]); - breaks++; - - uiRefreshDisplay(); - - u32 i; - for(i = 0; i < nodeSet->nodesetval->nodeNr; i++) - { - xmlNodePtr node = nodeSet->nodesetval->nodeTab[i]->xmlChildrenNode; - - found = parseNSWDBRelease(doc, node, titleAppTitleID[i], crc); - if (found) break; - } - - xmlXPathFreeObject(nodeSet); - - if (!found) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No checksum matches found in XML document for Title ID \"%016lX\"!", titleAppTitleID[i]); - if ((i + 1) < titleAppCount) breaks += 2; - } else { - breaks--; - break; - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No records with Title ID \"%016lX\" found within the XML document!", titleAppTitleID[i]); - if ((i + 1) < titleAppCount) breaks += 2; - } - } - - xmlFreeDoc(doc); - - if (!found) - { - breaks++; - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "This could either be a bad dump or an undumped cartridge."); - } - } else { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open and/or parse \"%s\"!", nswReleasesXmlPath); + return; + } + + for(i = 0; i < titleAppCount; i++) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), "//%s/%s[.//%s='%016lX']", nswReleasesRootElement, nswReleasesChildren, nswReleasesChildrenTitleID, titleAppTitleID[i]); + + xmlXPathObjectPtr nodeSet = getNodeSet(doc, (xmlChar*)strbuf); + if (nodeSet) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Found %d %s with Title ID \"%016lX\".", nodeSet->nodesetval->nodeNr, (nodeSet->nodesetval->nodeNr > 1 ? "releases" : "release"), titleAppTitleID[i]); + breaks++; + + uiRefreshDisplay(); + + u32 i; + for(i = 0; i < nodeSet->nodesetval->nodeNr; i++) + { + xmlNodePtr node = nodeSet->nodesetval->nodeTab[i]->xmlChildrenNode; + + found = parseNSWDBRelease(doc, node, titleAppTitleID[i], crc); + if (found) break; + } + + xmlXPathFreeObject(nodeSet); + + if (!found) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No checksum matches found in XML document for Title ID \"%016lX\"!", titleAppTitleID[i]); + if ((i + 1) < titleAppCount) breaks += 2; + } else { + breaks--; + break; + } + } else { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No records with Title ID \"%016lX\" found within the XML document!", titleAppTitleID[i]); + if ((i + 1) < titleAppCount) breaks += 2; + } + } + + xmlFreeDoc(doc); + + if (!found) + { + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "This could either be a bad dump or an undumped cartridge."); } } @@ -3733,7 +3810,7 @@ void networkDeinit() socketExit(); } -size_t writeCurlFile(char *buffer, size_t size, size_t number_of_items, void *input_stream) +static size_t writeCurlFile(char *buffer, size_t size, size_t number_of_items, void *input_stream) { size_t total_size = (size * number_of_items); if (fwrite(buffer, 1, total_size, input_stream) != total_size) return 0; @@ -3754,7 +3831,7 @@ static size_t writeCurlBuffer(char *buffer, size_t size, size_t number_of_items, bool need_realloc = false; - while (result_written + bsz > result_sz) + while((result_written + bsz) > result_sz) { result_sz <<= 1; need_realloc = true; @@ -3772,83 +3849,114 @@ static size_t writeCurlBuffer(char *buffer, size_t size, size_t number_of_items, return bsz; } -void updateNSWDBXml() +bool performCurlRequest(CURL *curl, const char *url, FILE *filePtr, bool forceHttps, bool verbose) { - Result result; - CURL *curl; + if (!curl || !url || !strlen(url)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid parameters to perform CURL request!"); + return false; + } + + curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L); + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl, CURLOPT_HEADER, 0L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L); + if (forceHttps) curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); + + if (filePtr) + { + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, filePtr); + } else { + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlBuffer); + } + CURLcode res; long http_code = 0; double size = 0.0; bool success = false; - if (R_SUCCEEDED(result = networkInit())) + res = curl_easy_perform(curl); + + result_sz = result_written = 0; + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &size); + + if (res == CURLE_OK && http_code >= 200 && http_code <= 299 && size > 0) { - curl = curl_easy_init(); - if (curl) - { - FILE *nswdbXml = fopen(nswReleasesXmlTmpPath, "wb"); - if (nswdbXml) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Downloading XML database from \"%s\", please wait...", nswReleasesXmlUrl); - breaks += 2; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem."); - breaks += 2; - } - - uiRefreshDisplay(); - - curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(curl, CURLOPT_URL, nswReleasesXmlUrl); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, nswdbXml); - curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl, CURLOPT_HEADER, 0L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L); - - res = curl_easy_perform(curl); - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &size); - - if (res == CURLE_OK && http_code >= 200 && http_code <= 299 && size > 0) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Successfully downloaded %.0lf bytes!", size); - success = true; - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to request XML database! HTTP status code: %ld", http_code); - } - - fclose(nswdbXml); - - if (success) - { - unlink(nswReleasesXmlPath); - rename(nswReleasesXmlTmpPath, nswReleasesXmlPath); - } else { - unlink(nswReleasesXmlTmpPath); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open \"%s\" in write mode!", nswReleasesXmlTmpPath); - } - - curl_easy_cleanup(curl); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize CURL context!"); - } - - networkDeinit(); + if (verbose) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Successfully downloaded %.0lf bytes!", size); + success = true; } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize socket! (%08X)", result); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: CURL request failed for \"%s\" endpoint!\nHTTP status code: %ld", url, http_code); } + return success; +} + +void updateNSWDBXml() +{ + Result result; + CURL *curl = NULL; + bool success = false; + FILE *nswdbXml = NULL; + + result = networkInit(); + if (R_FAILED(result)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize socket! (%08X)", result); + goto out; + } + + curl = curl_easy_init(); + if (!curl) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize CURL context!"); + goto out; + } + + nswdbXml = fopen(nswReleasesXmlTmpPath, "wb"); + if (!nswdbXml) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open \"%s\" in write mode!", nswReleasesXmlTmpPath); + goto out; + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Downloading XML database from \"%s\", please wait...", nswReleasesXmlUrl); + breaks++; + + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + { + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem."); + breaks += 2; + } + + uiRefreshDisplay(); + + success = performCurlRequest(curl, nswReleasesXmlUrl, nswdbXml, false, true); + +out: + if (nswdbXml) fclose(nswdbXml); + + if (success) + { + unlink(nswReleasesXmlPath); + rename(nswReleasesXmlTmpPath, nswReleasesXmlPath); + } else { + unlink(nswReleasesXmlTmpPath); + } + + if (curl) curl_easy_cleanup(curl); + + if (R_SUCCEEDED(result)) networkDeinit(); + breaks += 2; } @@ -3962,194 +4070,223 @@ int versionNumCmp(char *ver1, char *ver2) return res; } -void updateApplication() +static const char *retrieveJsonObjStrMember(struct json_object *jobj, char *memberName) +{ + if (!jobj || !memberName || !strlen(memberName)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid parameters to retrieve string member from JSON object!"); + return NULL; + } + + struct json_object *memberObj = NULL; + const char *memberObjStr = NULL; + + if (!json_object_object_get_ex(jobj, memberName, &memberObj)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to retrieve object \"%s\" from JSON response!", memberName); + return memberObjStr; + } + + if (json_object_get_type(memberObj) != json_type_string) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid type for object \"%s\" in JSON response! (expected \"string\")", memberName); + return memberObjStr; + } + + memberObjStr = json_object_get_string(memberObj); + if (!memberObjStr || !strlen(memberObjStr)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: object \"%s\" from JSON response is empty!", memberName); + memberObjStr = NULL; + } + + return memberObjStr; +} + +static struct json_object *retrieveJsonObjArrayElementWithIndex(struct json_object *jobj, char *memberName, size_t idx) +{ + if (!jobj || !memberName || !strlen(memberName)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid parameters to retrieve indexed element in array member from JSON object!"); + return NULL; + } + + struct json_object *memberObj = NULL, *memberObjArrayElement = NULL; + + if (!json_object_object_get_ex(jobj, memberName, &memberObj)) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to retrieve object \"%s\" from JSON response!", memberName); + return memberObjArrayElement; + } + + if (json_object_get_type(memberObj) != json_type_array) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: invalid type for object \"%s\" in JSON response! (expected \"array\")", memberName); + return memberObjArrayElement; + } + + memberObjArrayElement = json_object_array_get_idx(memberObj, idx); + if (!memberObjArrayElement) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse object at index %lu from \"%s\" array in JSON response!"); + + return memberObjArrayElement; +} + +bool updateApplication() { if (envIsNso()) { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to update application. It is not running as a NRO."); + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to update application. Not running as a NRO."); breaks += 2; - return; + return false; } Result result; - CURL *curl; - CURLcode res; - long http_code = 0; - double size = 0.0; - char downloadUrl[512] = {'\0'}, releaseTag[32] = {'\0'}; - bool success = false; - struct json_object *jobj, *name, *assets; + CURL *curl = NULL; FILE *nxDumpToolNro = NULL; - char nroPath[NAME_BUF_LEN] = {'\0'}; - if (R_SUCCEEDED(result = networkInit())) + char releaseTag[32] = {'\0'}; + bool success = false; + + struct json_object *jobj = NULL, *assets = NULL; + const char *nameObjStr = NULL, *dlUrlObjStr = NULL; + + char nroPath[NAME_BUF_LEN] = {'\0'}; + snprintf(nroPath, MAX_ELEMENTS(nroPath), "%s.tmp", (strlen(appLaunchPath) ? appLaunchPath : nxDumpToolPath)); + + result = networkInit(); + if (R_FAILED(result)) { - curl = curl_easy_init(); - if (curl) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Requesting latest release information from \"%s\"...", githubReleasesApiUrl); - breaks++; - - uiRefreshDisplay(); - - curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(curl, CURLOPT_URL, githubReleasesApiUrl); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlBuffer); - curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl, CURLOPT_HEADER, 0L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L); - curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); - - res = curl_easy_perform(curl); - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &size); - - if (res == CURLE_OK && http_code >= 200 && http_code <= 299 && size > 0) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Parsing response JSON data from \"%s\"...", githubReleasesApiUrl); - breaks++; - - uiRefreshDisplay(); - - jobj = json_tokener_parse(result_buf); - if (jobj != NULL) - { - if (json_object_object_get_ex(jobj, "name", &name) && json_object_get_type(name) == json_type_string) - { - snprintf(releaseTag, MAX_ELEMENTS(releaseTag), json_object_get_string(name)); - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Latest release: %s.", releaseTag); - breaks++; - - uiRefreshDisplay(); - - // Compare versions - if (releaseTag[0] == 'v' || releaseTag[0] == 'V' || releaseTag[0] == 'r' || releaseTag[0] == 'R') - { - u32 releaseTagLen = strlen(releaseTag); - memmove(releaseTag, releaseTag + 1, releaseTagLen - 1); - releaseTag[releaseTagLen - 1] = '\0'; - } - - if (versionNumCmp(releaseTag, APP_VERSION) > 0) - { - if (json_object_object_get_ex(jobj, "assets", &assets) && json_object_get_type(assets) == json_type_array) - { - assets = json_object_array_get_idx(assets, 0); - if (assets != NULL) - { - if (json_object_object_get_ex(assets, "browser_download_url", &assets) && json_object_get_type(assets) == json_type_string) - { - snprintf(downloadUrl, MAX_ELEMENTS(downloadUrl), json_object_get_string(assets)); - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Download URL: \"%s\".", downloadUrl); - breaks++; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Please wait..."); - breaks += 2; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) - { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem."); - breaks += 2; - } - - uiRefreshDisplay(); - - snprintf(nroPath, MAX_ELEMENTS(nroPath), "%s.tmp", (strlen(appLaunchPath) ? appLaunchPath : nxDumpToolPath)); - - nxDumpToolNro = fopen(nroPath, "wb"); - if (nxDumpToolNro) - { - curl_easy_reset(curl); - - curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(curl, CURLOPT_URL, downloadUrl); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, nxDumpToolNro); - curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl, CURLOPT_HEADER, 0L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L); - curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); - - res = curl_easy_perform(curl); - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &size); - - if (res == CURLE_OK && http_code >= 200 && http_code <= 299 && size > 0) - { - success = true; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Successfully downloaded %.0lf bytes!", size); - breaks++; - - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Please restart the application to reflect the changes."); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to request latest update binary! HTTP status code: %ld", http_code); - } - - fclose(nxDumpToolNro); - - if (success) - { - snprintf(strbuf, MAX_ELEMENTS(strbuf), nroPath); - nroPath[strlen(nroPath) - 4] = '\0'; - - unlink(nroPath); - rename(strbuf, nroPath); - } else { - unlink(nroPath); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open \"%s\" in write mode!", nroPath); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse download URL from JSON response!"); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse object at index 0 from \"assets\" array in JSON response!"); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse \"assets\" array from JSON response!"); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "You already have the latest version!"); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse version tag from JSON response!"); - } - - json_object_put(jobj); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse JSON response!"); - } - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to request latest release information! HTTP status code: %ld", http_code); - } - - if (result_buf) free(result_buf); - - curl_easy_cleanup(curl); - } else { - uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize CURL context!"); - } - - networkDeinit(); - } else { uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize socket! (%08X)", result); + goto out; } + curl = curl_easy_init(); + if (!curl) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to initialize CURL context!"); + goto out; + } + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Requesting latest release information from \"%s\"...", githubReleasesApiUrl); + breaks++; + + uiRefreshDisplay(); + + if (!performCurlRequest(curl, githubReleasesApiUrl, NULL, true, false)) goto out; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Parsing response JSON data...", githubReleasesApiUrl); + breaks++; + + uiRefreshDisplay(); + + jobj = json_tokener_parse(result_buf); + if (!jobj) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: unable to parse JSON response!"); + goto out; + } + + nameObjStr = retrieveJsonObjStrMember(jobj, "name"); + if (!nameObjStr) goto out; + + snprintf(releaseTag, MAX_ELEMENTS(releaseTag), nameObjStr); + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Latest release: %s.", releaseTag); + breaks++; + + uiRefreshDisplay(); + + // Remove the first character from the release name if it's v/V/r/R + if (releaseTag[0] == 'v' || releaseTag[0] == 'V' || releaseTag[0] == 'r' || releaseTag[0] == 'R') + { + u32 releaseTagLen = strlen(releaseTag); + memmove(releaseTag, releaseTag + 1, releaseTagLen - 1); + releaseTag[releaseTagLen - 1] = '\0'; + } + + // Compare versions + if (versionNumCmp(releaseTag, APP_VERSION) <= 0) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "You already have the latest version!"); + breaks += 2; + + // Ask the user if they want to perform a forced update + int cur_breaks = breaks; + + if (yesNoPrompt("Do you want to perform a forced update?")) + { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - (8 + STRING_Y_POS(breaks)), BG_COLOR_RGB); + uiRefreshDisplay(); + } else { + breaks -= 2; + goto out; + } + } + + assets = retrieveJsonObjArrayElementWithIndex(jobj, "assets", 0); + if (!assets) goto out; + + dlUrlObjStr = retrieveJsonObjStrMember(assets, "browser_download_url"); + if (!dlUrlObjStr) goto out; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Download URL: \"%s\".", dlUrlObjStr); + breaks++; + + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Please wait..."); breaks += 2; + + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem."); + breaks += 2; + } + + uiRefreshDisplay(); + + nxDumpToolNro = fopen(nroPath, "wb"); + if (!nxDumpToolNro) + { + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Error: failed to open \"%s\" in write mode!", nroPath); + goto out; + } + + curl_easy_reset(curl); + + success = performCurlRequest(curl, dlUrlObjStr, nxDumpToolNro, true, true); + if (!success) goto out; + + breaks++; + uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Please restart the application to reflect the changes."); + +out: + if (nxDumpToolNro) fclose(nxDumpToolNro); + + if (success) + { + snprintf(strbuf, MAX_ELEMENTS(strbuf), nroPath); + nroPath[strlen(nroPath) - 4] = '\0'; + + unlink(nroPath); + rename(strbuf, nroPath); + } else { + unlink(nroPath); + } + + if (jobj) json_object_put(jobj); + + if (result_buf) + { + free(result_buf); + result_buf = NULL; + } + + if (curl) curl_easy_cleanup(curl); + + if (R_SUCCEEDED(result)) networkDeinit(); + + breaks += 2; + + return success; } diff --git a/source/util.h b/source/util.h index eae7a42..a90a265 100644 --- a/source/util.h +++ b/source/util.h @@ -15,6 +15,9 @@ #define ROMFS_DUMP_PATH NXDUMPTOOL_BASE_PATH "RomFS/" #define CERT_DUMP_PATH NXDUMPTOOL_BASE_PATH "Certificate/" #define BATCH_OVERRIDES_PATH NSP_DUMP_PATH "BatchOverrides/" +#define TICKET_PATH NXDUMPTOOL_BASE_PATH "Ticket/" + +#define KEYS_FILE_PATH APP_BASE_PATH "prod.keys" #define KiB (1024.0) #define MiB (1024.0 * KiB) @@ -93,6 +96,11 @@ #define MAX_ELEMENTS(x) ((sizeof((x))) / (sizeof((x)[0]))) // Returns the max number of elements that can be stored in an array +#define BIS_MOUNT_NAME "sys:" +#define BIS_CERT_SAVE_NAME BIS_MOUNT_NAME "/save/80000000000000e0" +#define BIS_COMMON_TIK_SAVE_NAME BIS_MOUNT_NAME "/save/80000000000000e1" +#define BIS_PERSONALIZED_TIK_SAVE_NAME BIS_MOUNT_NAME "/save/80000000000000e2" + typedef struct { u64 file_offset; u64 file_size; @@ -128,11 +136,35 @@ typedef enum { ROMFS_TYPE_ADDON } selectedRomFsType; +typedef enum { + TICKET_TYPE_APP = 0, + TICKET_TYPE_PATCH, + TICKET_TYPE_ADDON +} selectedTicketType; + typedef struct { u32 index; u8 type; // 1 = Patch, 2 = AddOn + char name[NACP_APPNAME_LEN + 1]; + char fixedName[NACP_APPNAME_LEN + 1]; } PACKED orphan_patch_addon_entry; +typedef struct { + bool isFat32; + bool setXciArchiveBit; + bool keepCert; + bool trimDump; + bool calcCrc; +} PACKED xciOptions; + +typedef struct { + bool isFat32; + bool calcCrc; + bool removeConsoleData; + bool tiklessDump; + bool npdmAcidRsaPatch; +} PACKED nspOptions; + typedef enum { BATCH_SOURCE_ALL = 0, BATCH_SOURCE_SDCARD, @@ -140,21 +172,6 @@ typedef enum { BATCH_SOURCE_CNT } batchModeSourceStorage; -typedef struct { - bool isFat32; - bool keepCert; - bool trimDump; - bool calcCrc; - bool setXciArchiveBit; -} PACKED xciOptions; - -typedef struct { - bool isFat32; - bool calcCrc; - bool removeConsoleData; - bool tiklessDump; -} PACKED nspOptions; - typedef struct { bool dumpAppTitles; bool dumpPatchTitles; @@ -162,15 +179,21 @@ typedef struct { bool isFat32; bool removeConsoleData; bool tiklessDump; + bool npdmAcidRsaPatch; bool skipDumpedTitles; - batchModeSourceStorage batchModeSrc; bool rememberDumpedTitles; + batchModeSourceStorage batchModeSrc; } PACKED batchOptions; +typedef struct { + bool removeConsoleData; +} PACKED ticketOptions; + typedef struct { xciOptions xciDumpCfg; nspOptions nspDumpCfg; batchOptions batchDumpCfg; + ticketOptions tikDumpCfg; } PACKED dumpOptions; void loadConfig(); @@ -181,6 +204,10 @@ bool isGameCardInserted(); void fsGameCardDetectionThreadFunc(void *arg); +bool mountSysEmmcPartition(); + +void unmountSysEmmcPartition(); + bool isServiceRunning(const char *serviceName); bool checkSxOsServices(); @@ -223,7 +250,7 @@ bool getPartitionHfs0Header(u32 partition); bool getHfs0FileList(u32 partition); -bool getPartitionHfs0FileByName(FsStorage *gameCardStorage, const char *filename, u8 *outBuf, u64 outBufSize); +bool getFileFromHfs0PartitionByName(FsStorage *gameCardStorage, const char *filename, u8 *outBuf, u64 outBufSize); bool calculateExeFsExtractedDataSize(u64 *out); @@ -231,6 +258,10 @@ bool calculateRomFsFullExtractedSize(bool usePatch, u64 *out); bool calculateRomFsExtractedDirSize(u32 dir_offset, bool usePatch, u64 *out); +bool retrieveNcaContentRecords(FsStorageId curStorageId, u8 filter, u32 titleCount, u32 titleIndex, NcmContentRecord **outContentRecords, u32 *outContentRecordsCnt); + +void removeConsoleDataFromTicket(title_rights_ctx *rights_info); + bool readNcaExeFsSection(u32 titleIndex, bool usePatch); bool readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType); @@ -253,6 +284,8 @@ void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, bo bool checkOrphanPatchOrAddOn(bool addOn); +void freeOrphanPatchOrAddOnList(); + void generateOrphanPatchOrAddOnList(); bool checkIfBaseApplicationHasPatchOrAddOn(u32 appIndex, bool addOn); @@ -291,6 +324,6 @@ void gameCardDumpNSWDBCheck(u32 crc); void updateNSWDBXml(); -void updateApplication(); +bool updateApplication(); #endif