From ad401d559dea98df660b170333fd04b5355b2dfd Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Thu, 13 Aug 2020 02:01:23 -0400 Subject: [PATCH] New template: threaded gamecard dumper via USB. Key area and certificate can be optionally appended and/or preserved, respectively. Trimming is supported. Also added a bunch of ZLT packet related fixes to usb.c, as well as a function to properly cancel an ongoing file transfer. --- code_templates/gc_key_area.c | 111 --- code_templates/threaded_usb_bktr_dumper.c | 1 + code_templates/threaded_usb_gc_dumper.c | 704 +++++++++++++++++ source/gamecard.h | 2 + source/main.c | 917 ++++++++++++++-------- source/usb.c | 122 ++- source/usb.h | 4 + 7 files changed, 1391 insertions(+), 470 deletions(-) delete mode 100644 code_templates/gc_key_area.c create mode 100644 code_templates/threaded_usb_gc_dumper.c diff --git a/code_templates/gc_key_area.c b/code_templates/gc_key_area.c deleted file mode 100644 index bceece5..0000000 --- a/code_templates/gc_key_area.c +++ /dev/null @@ -1,111 +0,0 @@ -/* - * main.c - * - * Copyright (c) 2020, DarkMatterCore . - * - * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). - * - * nxdumptool is free software; you can redistribute it and/or modify it - * under the terms and conditions of the GNU General Public License, - * version 2, as published by the Free Software Foundation. - * - * nxdumptool is distributed in the hope it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "utils.h" -#include "gamecard.h" - -static void consolePrint(const char *text, ...) -{ - va_list v; - va_start(v, text); - vfprintf(stdout, text, v); - va_end(v); - consoleUpdate(NULL); -} - -int main(int argc, char *argv[]) -{ - (void)argc; - (void)argv; - - int ret = 0; - - GameCardKeyArea gc_key_area = {0}; - char path[FS_MAX_PATH] = {0}; - - LOGFILE("nxdumptool starting."); - - consoleInit(NULL); - - consolePrint("initializing...\n"); - - if (!utilsInitializeResources()) - { - ret = -1; - goto out; - } - - consolePrint("waiting for gamecard. press b to cancel.\n"); - - while(true) - { - hidScanInput(); - - if (utilsHidKeysAllDown() == KEY_B) - { - consolePrint("process cancelled\n"); - goto out2; - } - - u8 status = gamecardGetStatus(); - if (status == GameCardStatus_InsertedAndInfoLoaded) break; - if (status == GameCardStatus_InsertedAndInfoNotLoaded) consolePrint("gamecard inserted but info couldn't be loaded from it. check nogc patch setting.\n"); - } - - utilsChangeHomeButtonBlockStatus(true); - - consolePrint("gamecard detected.\n"); - - if (!gamecardGetKeyArea(&gc_key_area)) - { - consolePrint("failed to get gamecard key area\n"); - goto out2; - } - - consolePrint("get gamecard key area ok\n"); - - sprintf(path, "sdmc:/card_key_area_%016lX.bin", gc_key_area.initial_data.key_source.package_id); - - FILE *kafd = fopen(path, "wb"); - if (!kafd) - { - consolePrint("failed to open \"%s\" for writing\n", path); - goto out2; - } - - fwrite(&gc_key_area, 1, sizeof(GameCardKeyArea), kafd); - fclose(kafd); - utilsCommitSdCardFileSystemChanges(); - - consolePrint("successfully saved key area to \"%s\"\n", path); - - utilsChangeHomeButtonBlockStatus(false); - -out2: - consolePrint("press any button to exit\n"); - utilsWaitForButtonPress(KEY_NONE); - -out: - utilsCloseResources(); - - consoleExit(NULL); - - return ret; -} diff --git a/code_templates/threaded_usb_bktr_dumper.c b/code_templates/threaded_usb_bktr_dumper.c index fac9d99..e3b5470 100644 --- a/code_templates/threaded_usb_bktr_dumper.c +++ b/code_templates/threaded_usb_bktr_dumper.c @@ -457,6 +457,7 @@ int main(int argc, char *argv[]) if ((btn_cancel_end_tmr - btn_cancel_start_tmr) >= 3) { mutexLock(&g_fileMutex); + usbCancelFileTransfer(); shared_data.transfer_cancelled = true; mutexUnlock(&g_fileMutex); break; diff --git a/code_templates/threaded_usb_gc_dumper.c b/code_templates/threaded_usb_gc_dumper.c new file mode 100644 index 0000000..16d8ac3 --- /dev/null +++ b/code_templates/threaded_usb_gc_dumper.c @@ -0,0 +1,704 @@ +/* + * main.c + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "gamecard.h" +#include "usb.h" + +#define BLOCK_SIZE USB_TRANSFER_BUFFER_SIZE + +/* Type definitions. */ + +typedef void (*MenuElementOptionFunction)(u32 idx); + +typedef struct { + u32 selected; ///< Used to keep track of the selected option. + MenuElementOptionFunction options_func; ///< Pointer to a function to be called each time a new option is selected. Should be set to NULL if not used. + const char **options; ///< Pointer to multiple char pointers with strings representing options. Last element must be set to NULL. +} MenuElementOption; + +typedef bool (*MenuElementFunction)(void); + +typedef struct { + const char *str; ///< Pointer to a string to be printed for this menu element. + void *child_menu; ///< Pointer to a child Menu element. Must be set to NULL if task_func != NULL. + MenuElementFunction task_func; ///< Pointer to a function to be called by this element. Must be set to NULL if child_menu != NULL. + MenuElementOption *element_options; ///< Options for this menu element. Should be set to NULL if not used. +} MenuElement; + +typedef struct _Menu { + struct _Menu *parent; ///< Set to NULL in the root menu element. + u32 selected, scroll; ///< Used to keep track of the selected element and scroll values. + MenuElement **elements; ///< Element info from this menu. Last element must be set to NULL. +} Menu; + +typedef struct +{ + void *data; + size_t data_size; + size_t data_written; + size_t total_size; + bool read_error; + bool write_error; + bool transfer_cancelled; +} ThreadSharedData; + +/* Function prototypes. */ + +static void consolePrint(const char *text, ...); + +static u32 menuGetElementCount(const Menu *menu); + +static bool sendGameCardKeyAreaViaUsb(void); +static bool sendGameCardCertificateViaUsb(void); +static bool sendGameCardImageViaUsb(void); + +static void changeKeyAreaOption(u32 idx); +static void changeCertificateOption(u32 idx); +static void changeTrimOption(u32 idx); + +static int read_thread_func(void *arg); +static int write_thread_func(void *arg); + +/* Global variables. */ + +static bool g_appendKeyArea = false, g_keepCertificate = false, g_trimDump = false; + +static const char *g_xciOptions[] = { "no", "yes", NULL }; + +static MenuElement *g_xciMenuElements[] = { + &(MenuElement){ + .str = "start dump", + .child_menu = NULL, + .task_func = &sendGameCardImageViaUsb, + .element_options = NULL + }, + &(MenuElement){ + .str = "append key area", + .child_menu = NULL, + .task_func = NULL, + .element_options = &(MenuElementOption){ + .selected = 0, + .options_func = &changeKeyAreaOption, + .options = g_xciOptions + } + }, + &(MenuElement){ + .str = "keep certificate", + .child_menu = NULL, + .task_func = NULL, + .element_options = &(MenuElementOption){ + .selected = 0, + .options_func = &changeCertificateOption, + .options = g_xciOptions + } + }, + &(MenuElement){ + .str = "trim dump", + .child_menu = NULL, + .task_func = NULL, + .element_options = &(MenuElementOption){ + .selected = 0, + .options_func = &changeTrimOption, + .options = g_xciOptions + } + }, + NULL +}; + +static Menu g_xciMenu = { + .parent = NULL, + .selected = 0, + .scroll = 0, + .elements = g_xciMenuElements +}; + +static MenuElement *g_rootMenuElements[] = { + &(MenuElement){ + .str = "dump key area", + .child_menu = NULL, + .task_func = &sendGameCardKeyAreaViaUsb, + .element_options = NULL + }, + &(MenuElement){ + .str = "dump certificate", + .child_menu = NULL, + .task_func = &sendGameCardCertificateViaUsb, + .element_options = NULL + }, + &(MenuElement){ + .str = "dump xci", + .child_menu = &g_xciMenu, + .task_func = NULL, + .element_options = NULL + }, + NULL +}; + +static Menu g_rootMenu = { + .parent = NULL, + .selected = 0, + .scroll = 0, + .elements = g_rootMenuElements +}; + +static Mutex g_fileMutex = 0; +static CondVar g_readCondvar = 0, g_writeCondvar = 0; + +static char path[FS_MAX_PATH] = {0}; + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + int ret = 0; + + Menu *cur_menu = &g_rootMenu; + u32 element_count = menuGetElementCount(cur_menu), page_size = 30; + + LOGFILE(APP_TITLE " starting."); + + consoleInit(NULL); + + consolePrint("initializing...\n"); + + if (!utilsInitializeResources()) + { + ret = -1; + goto out; + } + + while(appletMainLoop()) + { + consoleClear(); + printf("\npress b to %s.\n\n", cur_menu->parent ? "go back" : "exit"); + + u32 limit = (cur_menu->scroll + page_size); + MenuElement *selected_element = cur_menu->elements[cur_menu->selected]; + MenuElementOption *selected_element_options = selected_element->element_options; + + for(u32 i = cur_menu->scroll; cur_menu->elements[i] && i < element_count; i++) + { + if (i >= limit) break; + + MenuElement *cur_element = cur_menu->elements[i]; + MenuElementOption *cur_options = cur_menu->elements[i]->element_options; + + printf("%s%s", i == cur_menu->selected ? " -> " : " ", cur_element->str); + + if (cur_options) + { + printf(": "); + if (cur_options->selected > 0) printf("< "); + printf("%s", cur_options->options[cur_options->selected]); + if (cur_options->options[cur_options->selected + 1]) printf(" >"); + } + + printf("\n"); + } + + printf("\n"); + consoleUpdate(NULL); + + u64 btn_down = 0, btn_held = 0; + while(!btn_down && !btn_held) + { + hidScanInput(); + btn_down = utilsHidKeysAllDown(); + btn_held = utilsHidKeysAllHeld(); + } + + if (btn_down & KEY_A) + { + Menu *child_menu = (Menu*)selected_element->child_menu; + + if (child_menu) + { + child_menu->parent = cur_menu; + cur_menu = child_menu; + element_count = menuGetElementCount(cur_menu); + } else + if (selected_element->task_func) + { + selected_element->task_func(); + } + } else + if ((btn_down & KEY_DDOWN) || (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN))) + { + cur_menu->selected++; + + if (!cur_menu->elements[cur_menu->selected]) + { + if (btn_down & KEY_DDOWN) + { + cur_menu->selected = 0; + cur_menu->scroll = 0; + } else { + cur_menu->selected--; + } + } else + if (cur_menu->selected >= limit && cur_menu->elements[cur_menu->selected + 1]) + { + cur_menu->scroll++; + } + } else + if ((btn_down & KEY_DUP) || (btn_held & (KEY_LSTICK_UP | KEY_RSTICK_UP))) + { + cur_menu->selected--; + + if (cur_menu->selected == UINT32_MAX) + { + if (btn_down & KEY_DUP) + { + cur_menu->selected = (element_count - 1); + cur_menu->scroll = (element_count > page_size ? (element_count - page_size) : 0); + } else { + cur_menu->selected = 0; + } + } else + if (cur_menu->selected < cur_menu->scroll && cur_menu->scroll > 0) + { + cur_menu->scroll--; + } + } else + if ((btn_down & (KEY_DRIGHT | KEY_LSTICK_RIGHT | KEY_RSTICK_RIGHT)) && selected_element_options) + { + selected_element_options->selected++; + if (!selected_element_options->options[selected_element_options->selected]) selected_element_options->selected--; + if (selected_element_options->options_func) selected_element_options->options_func(selected_element_options->selected); + } else + if ((btn_down & (KEY_DLEFT | KEY_LSTICK_LEFT | KEY_RSTICK_LEFT)) && selected_element_options) + { + selected_element_options->selected--; + if (selected_element_options->selected == UINT32_MAX) selected_element_options->selected = 0; + if (selected_element_options->options_func) selected_element_options->options_func(selected_element_options->selected); + } else + if (btn_down & KEY_B) + { + if (!cur_menu->parent) break; + + cur_menu = cur_menu->parent; + element_count = menuGetElementCount(cur_menu); + } + + if (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN | KEY_LSTICK_UP | KEY_RSTICK_UP)) svcSleepThread(50000000); // 50 ms + } + +out: + utilsCloseResources(); + + consoleExit(NULL); + + return ret; +} + +static void consolePrint(const char *text, ...) +{ + va_list v; + va_start(v, text); + vfprintf(stdout, text, v); + va_end(v); + consoleUpdate(NULL); +} + +static u32 menuGetElementCount(const Menu *menu) +{ + if (!menu || !menu->elements || !menu->elements[0]) return 0; + + u32 cnt; + for(cnt = 0; menu->elements[cnt]; cnt++); + return cnt; +} + +static void waitForGameCardAndUsb(void) +{ + consoleClear(); + consolePrint("waiting for gamecard and usb session...\n"); + + while(true) + { + if (gamecardGetStatus() == GameCardStatus_InsertedAndInfoLoaded && usbIsReady()) break; + } +} + +static bool sendFileData(const char *path, void *data, size_t data_size) +{ + if (!path || !strlen(path) || !data || !data_size) + { + consolePrint("invalid parameters to send file data!\n"); + return false; + } + + if (!usbSendFileProperties(data_size, path)) + { + consolePrint("failed to send file properties for \"%s\"!\n", path); + return false; + } + + if (!usbSendFileData(data, data_size)) + { + consolePrint("failed to send file data for \"%s\"!\n", path); + return false; + } + + return true; +} + +static bool dumpGameCardKeyArea(GameCardKeyArea *out) +{ + if (!out) + { + consolePrint("invalid parameters to dump key area!\n"); + return false; + } + + if (!gamecardGetKeyArea(out)) + { + consolePrint("failed to get gamecard key area\n"); + return false; + } + + consolePrint("get gamecard key area ok\n"); + return true; +} + +static bool sendGameCardKeyAreaViaUsb(void) +{ + waitForGameCardAndUsb(); + + utilsChangeHomeButtonBlockStatus(false); + + GameCardKeyArea gc_key_area = {0}; + bool success = false; + + if (!dumpGameCardKeyArea(&gc_key_area)) goto end; + + sprintf(path, "card_key_area_%016lX.bin", gc_key_area.initial_data.key_source.package_id); + if (!sendFileData(path, &gc_key_area, sizeof(GameCardKeyArea))) goto end; + + consolePrint("successfully sent key area as \"%s\"\n", path); + success = true; + +end: + utilsChangeHomeButtonBlockStatus(false); + + consolePrint("press any button to continue"); + utilsWaitForButtonPress(KEY_NONE); + + return success; +} + +static bool sendGameCardCertificateViaUsb(void) +{ + waitForGameCardAndUsb(); + + utilsChangeHomeButtonBlockStatus(true); + + FsGameCardCertificate gc_cert = {0}; + char device_id_str[0x21] = {0}; + bool success = false; + + if (!gamecardGetCertificate(&gc_cert)) + { + consolePrint("failed to get gamecard certificate\n"); + goto end; + } + + consolePrint("get gamecard certificate ok\n"); + + utilsGenerateHexStringFromData(device_id_str, 0x21, gc_cert.device_id, 0x10); + sprintf(path, "card_certificate_%s.bin", device_id_str); + if (!sendFileData(path, &gc_cert, sizeof(FsGameCardCertificate))) goto end; + + consolePrint("successfully sent certificate as \"%s\"\n", path); + success = true; + +end: + utilsChangeHomeButtonBlockStatus(false); + + consolePrint("press any button to continue"); + utilsWaitForButtonPress(KEY_NONE); + + return success; +} + +static bool sendGameCardImageViaUsb(void) +{ + waitForGameCardAndUsb(); + + utilsChangeHomeButtonBlockStatus(true); + + u64 gc_size = 0; + GameCardKeyArea gc_key_area = {0}; + + ThreadSharedData shared_data = {0}; + thrd_t read_thread, write_thread; + + bool success = false; + + consolePrint("gamecard image dump\nappend key area: %s | keep certificate: %s | trim dump: %s\n\n", g_appendKeyArea ? "yes" : "no", g_keepCertificate ? "yes" : "no", g_trimDump ? "yes" : "no"); + + shared_data.data = usbAllocatePageAlignedBuffer(BLOCK_SIZE); + if (!shared_data.data) + { + consolePrint("failed to allocate memory for the dump procedure!\n"); + goto end; + } + + if ((!g_trimDump && !gamecardGetTotalSize(&gc_size)) || (g_trimDump && !gamecardGetTrimmedSize(&gc_size)) || !gc_size) + { + consolePrint("failed to get gamecard size!\n"); + goto end; + } + + shared_data.total_size = gc_size; + + consolePrint("gamecard size: 0x%lX\n", gc_size); + + if (g_appendKeyArea) + { + gc_size += sizeof(GameCardKeyArea); + if (!dumpGameCardKeyArea(&gc_key_area)) goto end; + consolePrint("gamecard size (with key area): 0x%lX\n", gc_size); + } + + sprintf(path, "gamecard (%s) (%s) (%s).xci", g_appendKeyArea ? "keyarea" : "keyarealess", g_keepCertificate ? "cert" : "certless", g_trimDump ? "trimmed" : "untrimmed"); + if (!usbSendFileProperties(gc_size, path)) + { + consolePrint("failed to send file properties for \"%s\"!\n", path); + goto end; + } + + if (g_appendKeyArea && !usbSendFileData(&gc_key_area, sizeof(GameCardKeyArea))) + { + consolePrint("failed to send gamecard key area data!\n"); + goto end; + } + + consolePrint("creating threads\n"); + thrd_create(&read_thread, read_thread_func, &shared_data); + thrd_create(&write_thread, write_thread_func, &shared_data); + + u8 prev_time = 0; + u64 prev_size = 0; + u8 percent = 0; + + time_t start = 0, btn_cancel_start_tmr = 0, btn_cancel_end_tmr = 0; + bool btn_cancel_cur_state = false, btn_cancel_prev_state = false; + + consolePrint("hold b to cancel\n\n"); + + start = time(NULL); + + while(shared_data.data_written < shared_data.total_size) + { + if (shared_data.read_error || shared_data.write_error) break; + + time_t now = time(NULL); + struct tm *ts = localtime(&now); + size_t size = shared_data.data_written; + + hidScanInput(); + btn_cancel_cur_state = (utilsHidKeysAllHeld() & KEY_B); + + if (btn_cancel_cur_state && btn_cancel_cur_state != btn_cancel_prev_state) + { + btn_cancel_start_tmr = now; + } else + if (btn_cancel_cur_state && btn_cancel_cur_state == btn_cancel_prev_state) + { + btn_cancel_end_tmr = now; + if ((btn_cancel_end_tmr - btn_cancel_start_tmr) >= 3) + { + mutexLock(&g_fileMutex); + usbCancelFileTransfer(); + shared_data.transfer_cancelled = true; + mutexUnlock(&g_fileMutex); + break; + } + } else { + btn_cancel_start_tmr = btn_cancel_end_tmr = 0; + } + + btn_cancel_prev_state = btn_cancel_cur_state; + + if (prev_time == ts->tm_sec || prev_size == size) continue; + + percent = (u8)((size * 100) / shared_data.total_size); + + prev_time = ts->tm_sec; + prev_size = size; + + printf("%lu / %lu (%u%%) | Time elapsed: %lu\n", size, shared_data.total_size, percent, (now - start)); + consoleUpdate(NULL); + } + + start = (time(NULL) - start); + + consolePrint("\nwaiting for threads to join\n"); + thrd_join(read_thread, NULL); + consolePrint("read_thread done: %lu\n", time(NULL)); + thrd_join(write_thread, NULL); + consolePrint("write_thread done: %lu\n", time(NULL)); + + if (shared_data.read_error || shared_data.write_error) + { + consolePrint("usb transfer error\n"); + goto end; + } + + if (shared_data.transfer_cancelled) + { + consolePrint("process cancelled\n"); + goto end; + } + + consolePrint("process completed in %lu seconds\n", start); + success = true; + +end: + if (shared_data.data) free(shared_data.data); + + utilsChangeHomeButtonBlockStatus(false); + + consolePrint("press any button to continue"); + utilsWaitForButtonPress(KEY_NONE); + + return success; +} + +static void changeKeyAreaOption(u32 idx) +{ + g_appendKeyArea = (idx > 0); +} + +static void changeCertificateOption(u32 idx) +{ + g_keepCertificate = (idx > 0); +} + +static void changeTrimOption(u32 idx) +{ + g_trimDump = (idx > 0); +} + +static int read_thread_func(void *arg) +{ + ThreadSharedData *shared_data = (ThreadSharedData*)arg; + if (!shared_data || !shared_data->data || !shared_data->total_size) + { + shared_data->read_error = true; + return -1; + } + + u8 *buf = malloc(BLOCK_SIZE); + if (!buf) + { + shared_data->read_error = true; + return -2; + } + + for(u64 offset = 0, blksize = BLOCK_SIZE; offset < shared_data->total_size; offset += blksize) + { + if (blksize > (shared_data->total_size - offset)) blksize = (shared_data->total_size - offset); + + /* Check if the transfer has been cancelled by the user */ + if (shared_data->transfer_cancelled) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Read current data chunk */ + shared_data->read_error = !gamecardReadStorage(buf, blksize, offset); + if (shared_data->read_error) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Remove certificate */ + if (!g_keepCertificate && offset == 0) memset(buf + GAMECARD_CERTIFICATE_OFFSET, 0xFF, sizeof(FsGameCardCertificate)); + + /* Wait until the previous data chunk has been written */ + mutexLock(&g_fileMutex); + + if (shared_data->data_size && !shared_data->write_error) condvarWait(&g_readCondvar, &g_fileMutex); + + if (shared_data->write_error) + { + mutexUnlock(&g_fileMutex); + break; + } + + /* Copy current file data chunk to the shared buffer */ + memcpy(shared_data->data, buf, blksize); + shared_data->data_size = blksize; + + /* Wake up the write thread to continue writing data */ + mutexUnlock(&g_fileMutex); + condvarWakeAll(&g_writeCondvar); + } + + free(buf); + + return (shared_data->read_error ? -3 : 0); +} + +static int write_thread_func(void *arg) +{ + ThreadSharedData *shared_data = (ThreadSharedData*)arg; + if (!shared_data || !shared_data->data) + { + shared_data->write_error = true; + return -1; + } + + while(shared_data->data_written < shared_data->total_size) + { + /* Wait until the current file data chunk has been read */ + mutexLock(&g_fileMutex); + + if (!shared_data->data_size && !shared_data->read_error) condvarWait(&g_writeCondvar, &g_fileMutex); + + if (shared_data->read_error || shared_data->transfer_cancelled) + { + mutexUnlock(&g_fileMutex); + break; + } + + /* Write current file data chunk */ + shared_data->write_error = !usbSendFileData(shared_data->data, shared_data->data_size); + if (!shared_data->write_error) + { + shared_data->data_written += shared_data->data_size; + shared_data->data_size = 0; + } + + /* Wake up the read thread to continue reading data */ + mutexUnlock(&g_fileMutex); + condvarWakeAll(&g_readCondvar); + + if (shared_data->write_error) break; + } + + return (shared_data->write_error ? -2 : 0); +} diff --git a/source/gamecard.h b/source/gamecard.h index c45da4d..1fb607f 100644 --- a/source/gamecard.h +++ b/source/gamecard.h @@ -32,6 +32,8 @@ #define GAMECARD_UPDATE_TID (u64)0x0100000000000816 +#define GAMECARD_CERTIFICATE_OFFSET 0x7000 + /// Encrypted using AES-128-ECB with the common titlekek generator key (stored in the .rodata segment from the Lotus firmware). typedef struct { u64 package_id; ///< Matches package_id from GameCardHeader. diff --git a/source/main.c b/source/main.c index 5d96641..16d8ac3 100644 --- a/source/main.c +++ b/source/main.c @@ -19,204 +19,150 @@ */ #include "utils.h" -#include "nca.h" -#include "title.h" -#include "pfs.h" -#include "romfs.h" +#include "gamecard.h" +#include "usb.h" -#define BLOCK_SIZE 0x800000 -#define OUTPATH "sdmc:/systitle_dumps" +#define BLOCK_SIZE USB_TRANSFER_BUFFER_SIZE -static u8 *buf = NULL; -static FILE *filefd = NULL; -static char path[FS_MAX_PATH * 2] = {0}; +/* Type definitions. */ -static void consolePrint(const char *text, ...) +typedef void (*MenuElementOptionFunction)(u32 idx); + +typedef struct { + u32 selected; ///< Used to keep track of the selected option. + MenuElementOptionFunction options_func; ///< Pointer to a function to be called each time a new option is selected. Should be set to NULL if not used. + const char **options; ///< Pointer to multiple char pointers with strings representing options. Last element must be set to NULL. +} MenuElementOption; + +typedef bool (*MenuElementFunction)(void); + +typedef struct { + const char *str; ///< Pointer to a string to be printed for this menu element. + void *child_menu; ///< Pointer to a child Menu element. Must be set to NULL if task_func != NULL. + MenuElementFunction task_func; ///< Pointer to a function to be called by this element. Must be set to NULL if child_menu != NULL. + MenuElementOption *element_options; ///< Options for this menu element. Should be set to NULL if not used. +} MenuElement; + +typedef struct _Menu { + struct _Menu *parent; ///< Set to NULL in the root menu element. + u32 selected, scroll; ///< Used to keep track of the selected element and scroll values. + MenuElement **elements; ///< Element info from this menu. Last element must be set to NULL. +} Menu; + +typedef struct { - va_list v; - va_start(v, text); - vfprintf(stdout, text, v); - va_end(v); - consoleUpdate(NULL); -} + void *data; + size_t data_size; + size_t data_written; + size_t total_size; + bool read_error; + bool write_error; + bool transfer_cancelled; +} ThreadSharedData; -static void dumpPartitionFs(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) -{ - if (!buf || !info || !nca_fs_ctx) return; - - u32 pfs_entry_count = 0; - PartitionFileSystemContext pfs_ctx = {0}; - PartitionFileSystemEntry *pfs_entry = NULL; - char *pfs_entry_name = NULL; - - size_t path_len = 0; - *path = '\0'; - - if (!pfsInitializeContext(&pfs_ctx, nca_fs_ctx)) - { - consolePrint("pfs initialize ctx failed!\n"); - goto end; - } - - if (!(pfs_entry_count = pfsGetEntryCount(&pfs_ctx))) - { - consolePrint("pfs entry count is zero!\n"); - goto end; - } - - snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)/Section %u (%s)", info->meta_key.id, info->app_metadata->lang_entry.name, ((NcaContext*)nca_fs_ctx->nca_ctx)->content_id_str, \ - titleGetNcmContentTypeName(((NcaContext*)nca_fs_ctx->nca_ctx)->content_type), nca_fs_ctx->section_num, ncaGetFsSectionTypeName(nca_fs_ctx->section_type)); - utilsCreateDirectoryTree(path, true); - path_len = strlen(path); - - for(u32 i = 0; i < pfs_entry_count; i++) - { - if (!(pfs_entry = pfsGetEntryByIndex(&pfs_ctx, i)) || !(pfs_entry_name = pfsGetEntryNameByIndex(&pfs_ctx, i)) || !strlen(pfs_entry_name)) - { - consolePrint("pfs get entry / get name #%u failed!\n", i); - goto end; - } - - path[path_len] = '\0'; - strcat(path, "/"); - strcat(path, pfs_entry_name); - utilsReplaceIllegalCharacters(path + path_len + 1, true); - - filefd = fopen(path, "wb"); - if (!filefd) - { - consolePrint("failed to create \"%s\"!\n", path); - goto end; - } - - consolePrint("dumping \"%s\"...\n", pfs_entry_name); - - u64 blksize = BLOCK_SIZE; - for(u64 j = 0; j < pfs_entry->size; j += blksize) - { - if (blksize > (pfs_entry->size - j)) blksize = (pfs_entry->size - j); - - if (!pfsReadEntryData(&pfs_ctx, pfs_entry, buf, blksize, j)) - { - consolePrint("failed to read 0x%lX block from offset 0x%lX!\n", blksize, j); - goto end; - } - - fwrite(buf, 1, blksize, filefd); - } - - fclose(filefd); - filefd = NULL; - } - - consolePrint("pfs dump complete\n"); - -end: - if (filefd) - { - fclose(filefd); - remove(path); - } - - if (*path) utilsCommitFileSystemChangesByPath(path); - - pfsFreeContext(&pfs_ctx); -} +/* Function prototypes. */ -static void dumpRomFs(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) -{ - if (!buf || !info || !nca_fs_ctx) return; - - u64 romfs_file_table_offset = 0; - RomFileSystemContext romfs_ctx = {0}; - RomFileSystemFileEntry *romfs_file_entry = NULL; - - size_t path_len = 0; - *path = '\0'; - - if (!romfsInitializeContext(&romfs_ctx, nca_fs_ctx)) - { - consolePrint("romfs initialize ctx failed!\n"); - goto end; - } - - snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)/Section %u (%s)", info->meta_key.id, info->app_metadata->lang_entry.name, ((NcaContext*)nca_fs_ctx->nca_ctx)->content_id_str, \ - titleGetNcmContentTypeName(((NcaContext*)nca_fs_ctx->nca_ctx)->content_type), nca_fs_ctx->section_num, ncaGetFsSectionTypeName(nca_fs_ctx->section_type)); - utilsCreateDirectoryTree(path, true); - path_len = strlen(path); - - while(romfs_file_table_offset < romfs_ctx.file_table_size) - { - if (!(romfs_file_entry = romfsGetFileEntryByOffset(&romfs_ctx, romfs_file_table_offset)) || \ - !romfsGeneratePathFromFileEntry(&romfs_ctx, romfs_file_entry, path + path_len, sizeof(path) - path_len, RomFileSystemPathIllegalCharReplaceType_KeepAsciiCharsOnly)) - { - consolePrint("romfs get entry / generate path failed for 0x%lX!\n", romfs_file_table_offset); - goto end; - } - - utilsCreateDirectoryTree(path, false); - - filefd = fopen(path, "wb"); - if (!filefd) - { - consolePrint("failed to create \"%s\"!\n", path); - goto end; - } - - consolePrint("dumping \"%s\"...\n", path + path_len); - - u64 blksize = BLOCK_SIZE; - for(u64 j = 0; j < romfs_file_entry->size; j += blksize) - { - if (blksize > (romfs_file_entry->size - j)) blksize = (romfs_file_entry->size - j); - - if (!romfsReadFileEntryData(&romfs_ctx, romfs_file_entry, buf, blksize, j)) - { - consolePrint("failed to read 0x%lX block from offset 0x%lX!\n", blksize, j); - goto end; - } - - fwrite(buf, 1, blksize, filefd); - } - - fclose(filefd); - filefd = NULL; - - romfs_file_table_offset += ALIGN_UP(sizeof(RomFileSystemFileEntry) + romfs_file_entry->name_length, 4); - } - - consolePrint("romfs dump complete\n"); - -end: - if (filefd) - { - fclose(filefd); - remove(path); - } - - if (*path) utilsCommitFileSystemChangesByPath(path); - - romfsFreeContext(&romfs_ctx); -} +static void consolePrint(const char *text, ...); -static void dumpFsSection(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) -{ - if (!buf || !info || !nca_fs_ctx) return; - - switch(nca_fs_ctx->section_type) - { - case NcaFsSectionType_PartitionFs: - dumpPartitionFs(info, nca_fs_ctx); - break; - case NcaFsSectionType_RomFs: - case NcaFsSectionType_Nca0RomFs: - dumpRomFs(info, nca_fs_ctx); - break; - default: - consolePrint("invalid section type!\n"); - break; - } -} +static u32 menuGetElementCount(const Menu *menu); + +static bool sendGameCardKeyAreaViaUsb(void); +static bool sendGameCardCertificateViaUsb(void); +static bool sendGameCardImageViaUsb(void); + +static void changeKeyAreaOption(u32 idx); +static void changeCertificateOption(u32 idx); +static void changeTrimOption(u32 idx); + +static int read_thread_func(void *arg); +static int write_thread_func(void *arg); + +/* Global variables. */ + +static bool g_appendKeyArea = false, g_keepCertificate = false, g_trimDump = false; + +static const char *g_xciOptions[] = { "no", "yes", NULL }; + +static MenuElement *g_xciMenuElements[] = { + &(MenuElement){ + .str = "start dump", + .child_menu = NULL, + .task_func = &sendGameCardImageViaUsb, + .element_options = NULL + }, + &(MenuElement){ + .str = "append key area", + .child_menu = NULL, + .task_func = NULL, + .element_options = &(MenuElementOption){ + .selected = 0, + .options_func = &changeKeyAreaOption, + .options = g_xciOptions + } + }, + &(MenuElement){ + .str = "keep certificate", + .child_menu = NULL, + .task_func = NULL, + .element_options = &(MenuElementOption){ + .selected = 0, + .options_func = &changeCertificateOption, + .options = g_xciOptions + } + }, + &(MenuElement){ + .str = "trim dump", + .child_menu = NULL, + .task_func = NULL, + .element_options = &(MenuElementOption){ + .selected = 0, + .options_func = &changeTrimOption, + .options = g_xciOptions + } + }, + NULL +}; + +static Menu g_xciMenu = { + .parent = NULL, + .selected = 0, + .scroll = 0, + .elements = g_xciMenuElements +}; + +static MenuElement *g_rootMenuElements[] = { + &(MenuElement){ + .str = "dump key area", + .child_menu = NULL, + .task_func = &sendGameCardKeyAreaViaUsb, + .element_options = NULL + }, + &(MenuElement){ + .str = "dump certificate", + .child_menu = NULL, + .task_func = &sendGameCardCertificateViaUsb, + .element_options = NULL + }, + &(MenuElement){ + .str = "dump xci", + .child_menu = &g_xciMenu, + .task_func = NULL, + .element_options = NULL + }, + NULL +}; + +static Menu g_rootMenu = { + .parent = NULL, + .selected = 0, + .scroll = 0, + .elements = g_rootMenuElements +}; + +static Mutex g_fileMutex = 0; +static CondVar g_readCondvar = 0, g_writeCondvar = 0; + +static char path[FS_MAX_PATH] = {0}; int main(int argc, char *argv[]) { @@ -225,6 +171,9 @@ int main(int argc, char *argv[]) int ret = 0; + Menu *cur_menu = &g_rootMenu; + u32 element_count = menuGetElementCount(cur_menu), page_size = 30; + LOGFILE(APP_TITLE " starting."); consoleInit(NULL); @@ -237,81 +186,36 @@ int main(int argc, char *argv[]) goto out; } - u32 app_count = 0; - TitleApplicationMetadata **app_metadata = NULL; - TitleInfo *cur_title_info = NULL; - - u32 selected_idx = 0, menu = 0, page_size = 30, scroll = 0; - u32 title_idx = 0, title_scroll = 0, nca_idx = 0; - char nca_id_str[0x21] = {0}; - - NcaContext *nca_ctx = NULL; - Ticket tik = {0}; - - app_metadata = titleGetApplicationMetadataEntries(true, &app_count); - if (!app_metadata || !app_count) - { - consolePrint("app metadata failed\n"); - goto out2; - } - - consolePrint("app metadata succeeded\n"); - - buf = malloc(BLOCK_SIZE); - if (!buf) - { - consolePrint("buf failed\n"); - goto out2; - } - - consolePrint("buf succeeded\n"); - - nca_ctx = calloc(1, sizeof(NcaContext)); - if (!nca_ctx) - { - consolePrint("nca ctx buf failed\n"); - goto out2; - } - - consolePrint("nca ctx buf succeeded\n"); - - utilsSleep(1); - - while(true) + while(appletMainLoop()) { consoleClear(); + printf("\npress b to %s.\n\n", cur_menu->parent ? "go back" : "exit"); - printf("select a %s.", menu == 0 ? "system title to view its contents" : (menu == 1 ? "content" : "fs section")); - printf("\npress b to %s.\n\n", menu == 0 ? "exit" : "go back"); + u32 limit = (cur_menu->scroll + page_size); + MenuElement *selected_element = cur_menu->elements[cur_menu->selected]; + MenuElementOption *selected_element_options = selected_element->element_options; - if (menu == 0) printf("title: %u / %u\n\n", selected_idx + 1, app_count); - if (menu >= 1) printf("selected title: %016lX - %s\n\n", app_metadata[title_idx]->title_id, app_metadata[title_idx]->lang_entry.name); - if (menu == 2) printf("selected content: %s (%s)\n\n", nca_id_str, titleGetNcmContentTypeName(cur_title_info->content_infos[nca_idx].content_type)); - - u32 max_val = (menu == 0 ? app_count : (menu == 1 ? cur_title_info->content_count : NCA_FS_HEADER_COUNT)); - for(u32 i = scroll; i < max_val; i++) + for(u32 i = cur_menu->scroll; cur_menu->elements[i] && i < element_count; i++) { - if (i >= (scroll + page_size)) break; + if (i >= limit) break; - printf("%s", i == selected_idx ? " -> " : " "); + MenuElement *cur_element = cur_menu->elements[i]; + MenuElementOption *cur_options = cur_menu->elements[i]->element_options; - if (menu == 0) + printf("%s%s", i == cur_menu->selected ? " -> " : " ", cur_element->str); + + if (cur_options) { - printf("%016lX - %s\n", app_metadata[i]->title_id, app_metadata[i]->lang_entry.name); - } else - if (menu == 1) - { - utilsGenerateHexStringFromData(nca_id_str, sizeof(nca_id_str), cur_title_info->content_infos[i].content_id.c, SHA256_HASH_SIZE / 2); - printf("%s (%s)\n", nca_id_str, titleGetNcmContentTypeName(cur_title_info->content_infos[i].content_type)); - } else - if (menu == 2) - { - printf("fs section #%u (%s)\n", i + 1, ncaGetFsSectionTypeName(nca_ctx->fs_contexts[i].section_type)); + printf(": "); + if (cur_options->selected > 0) printf("< "); + printf("%s", cur_options->options[cur_options->selected]); + if (cur_options->options[cur_options->selected + 1]) printf(" >"); } + + printf("\n"); } printf("\n"); - consoleUpdate(NULL); u64 btn_down = 0, btn_held = 0; @@ -324,120 +228,80 @@ int main(int argc, char *argv[]) if (btn_down & KEY_A) { - bool error = false; + Menu *child_menu = (Menu*)selected_element->child_menu; - if (menu == 0) + if (child_menu) { - title_idx = selected_idx; - title_scroll = scroll; + child_menu->parent = cur_menu; + cur_menu = child_menu; + element_count = menuGetElementCount(cur_menu); } else - if (menu == 1) + if (selected_element->task_func) { - nca_idx = selected_idx; - utilsGenerateHexStringFromData(nca_id_str, sizeof(nca_id_str), cur_title_info->content_infos[nca_idx].content_id.c, SHA256_HASH_SIZE / 2); - } - - menu++; - - if (menu == 1) - { - cur_title_info = titleGetInfoFromStorageByTitleId(NcmStorageId_BuiltInSystem, app_metadata[title_idx]->title_id); - if (!cur_title_info) - { - consolePrint("failed to get title info\n"); - error = true; - } - } else - if (menu == 2) - { - if (!ncaInitializeContext(nca_ctx, cur_title_info->storage_id, 0, &(cur_title_info->content_infos[nca_idx]), &tik)) - { - consolePrint("nca initialize ctx failed\n"); - error = true; - } - } else - if (menu == 3) - { - consoleClear(); - utilsChangeHomeButtonBlockStatus(true); - dumpFsSection(cur_title_info, &(nca_ctx->fs_contexts[selected_idx])); - utilsChangeHomeButtonBlockStatus(false); - } - - if (error || menu >= 3) - { - utilsSleep(3); - menu--; - } else { - selected_idx = scroll = 0; + selected_element->task_func(); } } else if ((btn_down & KEY_DDOWN) || (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN))) { - selected_idx++; + cur_menu->selected++; - if (selected_idx >= max_val) + if (!cur_menu->elements[cur_menu->selected]) { if (btn_down & KEY_DDOWN) { - selected_idx = scroll = 0; + cur_menu->selected = 0; + cur_menu->scroll = 0; } else { - selected_idx = (max_val - 1); + cur_menu->selected--; } } else - if (selected_idx >= (scroll + (page_size / 2)) && max_val > (scroll + page_size)) + if (cur_menu->selected >= limit && cur_menu->elements[cur_menu->selected + 1]) { - scroll++; + cur_menu->scroll++; } } else if ((btn_down & KEY_DUP) || (btn_held & (KEY_LSTICK_UP | KEY_RSTICK_UP))) { - selected_idx--; + cur_menu->selected--; - if (selected_idx == UINT32_MAX) + if (cur_menu->selected == UINT32_MAX) { if (btn_down & KEY_DUP) { - selected_idx = (max_val - 1); - scroll = (max_val >= page_size ? (max_val - page_size) : 0); + cur_menu->selected = (element_count - 1); + cur_menu->scroll = (element_count > page_size ? (element_count - page_size) : 0); } else { - selected_idx = 0; + cur_menu->selected = 0; } } else - if (selected_idx < (scroll + (page_size / 2)) && scroll > 0) + if (cur_menu->selected < cur_menu->scroll && cur_menu->scroll > 0) { - scroll--; + cur_menu->scroll--; } } else + if ((btn_down & (KEY_DRIGHT | KEY_LSTICK_RIGHT | KEY_RSTICK_RIGHT)) && selected_element_options) + { + selected_element_options->selected++; + if (!selected_element_options->options[selected_element_options->selected]) selected_element_options->selected--; + if (selected_element_options->options_func) selected_element_options->options_func(selected_element_options->selected); + } else + if ((btn_down & (KEY_DLEFT | KEY_LSTICK_LEFT | KEY_RSTICK_LEFT)) && selected_element_options) + { + selected_element_options->selected--; + if (selected_element_options->selected == UINT32_MAX) selected_element_options->selected = 0; + if (selected_element_options->options_func) selected_element_options->options_func(selected_element_options->selected); + } else if (btn_down & KEY_B) { - menu--; + if (!cur_menu->parent) break; - if (menu == UINT32_MAX) - { - break; - } else { - selected_idx = (menu == 0 ? title_idx : nca_idx); - scroll = (menu == 0 ? title_scroll : 0); - } + cur_menu = cur_menu->parent; + element_count = menuGetElementCount(cur_menu); } if (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN | KEY_LSTICK_UP | KEY_RSTICK_UP)) svcSleepThread(50000000); // 50 ms } -out2: - if (menu != UINT32_MAX) - { - consolePrint("press any button to exit\n"); - utilsWaitForButtonPress(KEY_NONE); - } - - if (nca_ctx) free(nca_ctx); - - if (buf) free(buf); - - if (app_metadata) free(app_metadata); - out: utilsCloseResources(); @@ -445,3 +309,396 @@ out: return ret; } + +static void consolePrint(const char *text, ...) +{ + va_list v; + va_start(v, text); + vfprintf(stdout, text, v); + va_end(v); + consoleUpdate(NULL); +} + +static u32 menuGetElementCount(const Menu *menu) +{ + if (!menu || !menu->elements || !menu->elements[0]) return 0; + + u32 cnt; + for(cnt = 0; menu->elements[cnt]; cnt++); + return cnt; +} + +static void waitForGameCardAndUsb(void) +{ + consoleClear(); + consolePrint("waiting for gamecard and usb session...\n"); + + while(true) + { + if (gamecardGetStatus() == GameCardStatus_InsertedAndInfoLoaded && usbIsReady()) break; + } +} + +static bool sendFileData(const char *path, void *data, size_t data_size) +{ + if (!path || !strlen(path) || !data || !data_size) + { + consolePrint("invalid parameters to send file data!\n"); + return false; + } + + if (!usbSendFileProperties(data_size, path)) + { + consolePrint("failed to send file properties for \"%s\"!\n", path); + return false; + } + + if (!usbSendFileData(data, data_size)) + { + consolePrint("failed to send file data for \"%s\"!\n", path); + return false; + } + + return true; +} + +static bool dumpGameCardKeyArea(GameCardKeyArea *out) +{ + if (!out) + { + consolePrint("invalid parameters to dump key area!\n"); + return false; + } + + if (!gamecardGetKeyArea(out)) + { + consolePrint("failed to get gamecard key area\n"); + return false; + } + + consolePrint("get gamecard key area ok\n"); + return true; +} + +static bool sendGameCardKeyAreaViaUsb(void) +{ + waitForGameCardAndUsb(); + + utilsChangeHomeButtonBlockStatus(false); + + GameCardKeyArea gc_key_area = {0}; + bool success = false; + + if (!dumpGameCardKeyArea(&gc_key_area)) goto end; + + sprintf(path, "card_key_area_%016lX.bin", gc_key_area.initial_data.key_source.package_id); + if (!sendFileData(path, &gc_key_area, sizeof(GameCardKeyArea))) goto end; + + consolePrint("successfully sent key area as \"%s\"\n", path); + success = true; + +end: + utilsChangeHomeButtonBlockStatus(false); + + consolePrint("press any button to continue"); + utilsWaitForButtonPress(KEY_NONE); + + return success; +} + +static bool sendGameCardCertificateViaUsb(void) +{ + waitForGameCardAndUsb(); + + utilsChangeHomeButtonBlockStatus(true); + + FsGameCardCertificate gc_cert = {0}; + char device_id_str[0x21] = {0}; + bool success = false; + + if (!gamecardGetCertificate(&gc_cert)) + { + consolePrint("failed to get gamecard certificate\n"); + goto end; + } + + consolePrint("get gamecard certificate ok\n"); + + utilsGenerateHexStringFromData(device_id_str, 0x21, gc_cert.device_id, 0x10); + sprintf(path, "card_certificate_%s.bin", device_id_str); + if (!sendFileData(path, &gc_cert, sizeof(FsGameCardCertificate))) goto end; + + consolePrint("successfully sent certificate as \"%s\"\n", path); + success = true; + +end: + utilsChangeHomeButtonBlockStatus(false); + + consolePrint("press any button to continue"); + utilsWaitForButtonPress(KEY_NONE); + + return success; +} + +static bool sendGameCardImageViaUsb(void) +{ + waitForGameCardAndUsb(); + + utilsChangeHomeButtonBlockStatus(true); + + u64 gc_size = 0; + GameCardKeyArea gc_key_area = {0}; + + ThreadSharedData shared_data = {0}; + thrd_t read_thread, write_thread; + + bool success = false; + + consolePrint("gamecard image dump\nappend key area: %s | keep certificate: %s | trim dump: %s\n\n", g_appendKeyArea ? "yes" : "no", g_keepCertificate ? "yes" : "no", g_trimDump ? "yes" : "no"); + + shared_data.data = usbAllocatePageAlignedBuffer(BLOCK_SIZE); + if (!shared_data.data) + { + consolePrint("failed to allocate memory for the dump procedure!\n"); + goto end; + } + + if ((!g_trimDump && !gamecardGetTotalSize(&gc_size)) || (g_trimDump && !gamecardGetTrimmedSize(&gc_size)) || !gc_size) + { + consolePrint("failed to get gamecard size!\n"); + goto end; + } + + shared_data.total_size = gc_size; + + consolePrint("gamecard size: 0x%lX\n", gc_size); + + if (g_appendKeyArea) + { + gc_size += sizeof(GameCardKeyArea); + if (!dumpGameCardKeyArea(&gc_key_area)) goto end; + consolePrint("gamecard size (with key area): 0x%lX\n", gc_size); + } + + sprintf(path, "gamecard (%s) (%s) (%s).xci", g_appendKeyArea ? "keyarea" : "keyarealess", g_keepCertificate ? "cert" : "certless", g_trimDump ? "trimmed" : "untrimmed"); + if (!usbSendFileProperties(gc_size, path)) + { + consolePrint("failed to send file properties for \"%s\"!\n", path); + goto end; + } + + if (g_appendKeyArea && !usbSendFileData(&gc_key_area, sizeof(GameCardKeyArea))) + { + consolePrint("failed to send gamecard key area data!\n"); + goto end; + } + + consolePrint("creating threads\n"); + thrd_create(&read_thread, read_thread_func, &shared_data); + thrd_create(&write_thread, write_thread_func, &shared_data); + + u8 prev_time = 0; + u64 prev_size = 0; + u8 percent = 0; + + time_t start = 0, btn_cancel_start_tmr = 0, btn_cancel_end_tmr = 0; + bool btn_cancel_cur_state = false, btn_cancel_prev_state = false; + + consolePrint("hold b to cancel\n\n"); + + start = time(NULL); + + while(shared_data.data_written < shared_data.total_size) + { + if (shared_data.read_error || shared_data.write_error) break; + + time_t now = time(NULL); + struct tm *ts = localtime(&now); + size_t size = shared_data.data_written; + + hidScanInput(); + btn_cancel_cur_state = (utilsHidKeysAllHeld() & KEY_B); + + if (btn_cancel_cur_state && btn_cancel_cur_state != btn_cancel_prev_state) + { + btn_cancel_start_tmr = now; + } else + if (btn_cancel_cur_state && btn_cancel_cur_state == btn_cancel_prev_state) + { + btn_cancel_end_tmr = now; + if ((btn_cancel_end_tmr - btn_cancel_start_tmr) >= 3) + { + mutexLock(&g_fileMutex); + usbCancelFileTransfer(); + shared_data.transfer_cancelled = true; + mutexUnlock(&g_fileMutex); + break; + } + } else { + btn_cancel_start_tmr = btn_cancel_end_tmr = 0; + } + + btn_cancel_prev_state = btn_cancel_cur_state; + + if (prev_time == ts->tm_sec || prev_size == size) continue; + + percent = (u8)((size * 100) / shared_data.total_size); + + prev_time = ts->tm_sec; + prev_size = size; + + printf("%lu / %lu (%u%%) | Time elapsed: %lu\n", size, shared_data.total_size, percent, (now - start)); + consoleUpdate(NULL); + } + + start = (time(NULL) - start); + + consolePrint("\nwaiting for threads to join\n"); + thrd_join(read_thread, NULL); + consolePrint("read_thread done: %lu\n", time(NULL)); + thrd_join(write_thread, NULL); + consolePrint("write_thread done: %lu\n", time(NULL)); + + if (shared_data.read_error || shared_data.write_error) + { + consolePrint("usb transfer error\n"); + goto end; + } + + if (shared_data.transfer_cancelled) + { + consolePrint("process cancelled\n"); + goto end; + } + + consolePrint("process completed in %lu seconds\n", start); + success = true; + +end: + if (shared_data.data) free(shared_data.data); + + utilsChangeHomeButtonBlockStatus(false); + + consolePrint("press any button to continue"); + utilsWaitForButtonPress(KEY_NONE); + + return success; +} + +static void changeKeyAreaOption(u32 idx) +{ + g_appendKeyArea = (idx > 0); +} + +static void changeCertificateOption(u32 idx) +{ + g_keepCertificate = (idx > 0); +} + +static void changeTrimOption(u32 idx) +{ + g_trimDump = (idx > 0); +} + +static int read_thread_func(void *arg) +{ + ThreadSharedData *shared_data = (ThreadSharedData*)arg; + if (!shared_data || !shared_data->data || !shared_data->total_size) + { + shared_data->read_error = true; + return -1; + } + + u8 *buf = malloc(BLOCK_SIZE); + if (!buf) + { + shared_data->read_error = true; + return -2; + } + + for(u64 offset = 0, blksize = BLOCK_SIZE; offset < shared_data->total_size; offset += blksize) + { + if (blksize > (shared_data->total_size - offset)) blksize = (shared_data->total_size - offset); + + /* Check if the transfer has been cancelled by the user */ + if (shared_data->transfer_cancelled) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Read current data chunk */ + shared_data->read_error = !gamecardReadStorage(buf, blksize, offset); + if (shared_data->read_error) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Remove certificate */ + if (!g_keepCertificate && offset == 0) memset(buf + GAMECARD_CERTIFICATE_OFFSET, 0xFF, sizeof(FsGameCardCertificate)); + + /* Wait until the previous data chunk has been written */ + mutexLock(&g_fileMutex); + + if (shared_data->data_size && !shared_data->write_error) condvarWait(&g_readCondvar, &g_fileMutex); + + if (shared_data->write_error) + { + mutexUnlock(&g_fileMutex); + break; + } + + /* Copy current file data chunk to the shared buffer */ + memcpy(shared_data->data, buf, blksize); + shared_data->data_size = blksize; + + /* Wake up the write thread to continue writing data */ + mutexUnlock(&g_fileMutex); + condvarWakeAll(&g_writeCondvar); + } + + free(buf); + + return (shared_data->read_error ? -3 : 0); +} + +static int write_thread_func(void *arg) +{ + ThreadSharedData *shared_data = (ThreadSharedData*)arg; + if (!shared_data || !shared_data->data) + { + shared_data->write_error = true; + return -1; + } + + while(shared_data->data_written < shared_data->total_size) + { + /* Wait until the current file data chunk has been read */ + mutexLock(&g_fileMutex); + + if (!shared_data->data_size && !shared_data->read_error) condvarWait(&g_writeCondvar, &g_fileMutex); + + if (shared_data->read_error || shared_data->transfer_cancelled) + { + mutexUnlock(&g_fileMutex); + break; + } + + /* Write current file data chunk */ + shared_data->write_error = !usbSendFileData(shared_data->data, shared_data->data_size); + if (!shared_data->write_error) + { + shared_data->data_written += shared_data->data_size; + shared_data->data_size = 0; + } + + /* Wake up the read thread to continue reading data */ + mutexUnlock(&g_fileMutex); + condvarWakeAll(&g_readCondvar); + + if (shared_data->write_error) break; + } + + return (shared_data->write_error ? -2 : 0); +} diff --git a/source/usb.c b/source/usb.c index 23d6072..d30d93f 100644 --- a/source/usb.c +++ b/source/usb.c @@ -29,7 +29,7 @@ #define USB_CMD_HEADER_MAGIC 0x4E584454 /* "NXDT". */ #define USB_TRANSFER_ALIGNMENT 0x1000 /* 4 KiB. */ -#define USB_TRANSFER_TIMEOUT 6 /* 6 seconds. */ +#define USB_TRANSFER_TIMEOUT 5 /* 5 seconds. */ /* Type definitions. */ @@ -89,7 +89,7 @@ typedef enum { typedef struct { u32 magic; - u32 status; + u32 status; ///< UsbStatusType. u8 reserved[0x8]; } UsbStatus; @@ -106,7 +106,8 @@ static bool g_usbHostAvailable = false, g_usbSessionStarted = false, g_usbDetect static atomic_bool g_usbDetectionThreadCreated = false; static u8 *g_usbTransferBuffer = NULL; -static u64 g_usbTransferRemainingSize = 0; +static u64 g_usbTransferRemainingSize = 0, g_usbTransferWrittenSize = 0; +static u32 g_usbUrbId = 0; /* Function prototypes. */ @@ -119,7 +120,7 @@ static void usbEndSession(void); NX_INLINE void usbPrepareCommandHeader(u32 cmd, u32 cmd_block_size); static u32 usbSendCommand(size_t cmd_size); -NX_INLINE void usbLogStatusDetail(u32 status); +static void usbLogStatusDetail(u32 status); NX_INLINE bool usbAllocateTransferBuffer(void); NX_INLINE void usbFreeTransferBuffer(void); @@ -135,8 +136,10 @@ static bool usbInitializeDeviceInterface1x(void); NX_INLINE bool usbIsHostAvailable(void); -NX_INLINE bool usbRead(void *buf, size_t size); -NX_INLINE bool usbWrite(void *buf, size_t size); +NX_INLINE void usbSetZltPacket(bool enable); + +NX_INLINE bool usbRead(void *buf, size_t size, bool reset_urb_id); +NX_INLINE bool usbWrite(void *buf, size_t size, bool reset_urb_id); static bool usbTransferData(void *buf, size_t size, UsbDsEndpoint *endpoint); bool usbInitialize(void) @@ -259,6 +262,7 @@ bool usbSendFileProperties(u64 file_size, const char *filename) { ret = true; g_usbTransferRemainingSize = file_size; + g_usbTransferWrittenSize = 0; } else { usbLogStatusDetail(status); } @@ -275,9 +279,9 @@ bool usbSendFileData(void *data, u64 data_size) rwlockWriteLock(&g_usbDeviceLock); rwlockWriteLock(&(g_usbDeviceInterface.lock)); - bool ret = false; void *buf = NULL; UsbStatus *cmd_status = NULL; + bool ret = false, zlt_required = false; if (!g_usbTransferBuffer || !g_usbDeviceInterfaceInitialized || !g_usbDeviceInterface.initialized || !g_usbHostAvailable || !g_usbSessionStarted || !g_usbTransferRemainingSize || !data || \ !data_size || data_size > USB_TRANSFER_BUFFER_SIZE || data_size > g_usbTransferRemainingSize) @@ -295,19 +299,47 @@ bool usbSendFileData(void *data, u64 data_size) memcpy(buf, data, data_size); } - if (!usbWrite(buf, data_size)) + /* Determine if we'll need to set a Zero Length Termination (ZLT) packet. */ + /* This is automatically handled by usbDsEndpoint_PostBufferAsync(), depending on the ZLT setting from the input (write) endpoint. */ + /* First, check if this is the last data chunk for this file. */ + if ((g_usbTransferRemainingSize - data_size) == 0) { - LOGFILE("Failed to write 0x%lX bytes long file data chunk!", data_size); + /* Enable ZLT if the last chunk size is aligned to the USB max packet size. */ + if (IS_ALIGNED(data_size, 0x40) || IS_ALIGNED(data_size, 0x200) || IS_ALIGNED(data_size, 0x400)) /* USB1, USB2 and USB3 max packet sizes, respectively. */ + { + zlt_required = true; + usbSetZltPacket(true); + //LOGFILE("ZLT enabled. Last chunk size: 0x%lX bytes.", data_size); + } + } else { + /* Disable ZLT if this is the first of multiple data chunks. */ + if (!g_usbTransferWrittenSize) + { + usbSetZltPacket(false); + //LOGFILE("ZLT disabled (first chunk)."); + } + } + + /* Send data chunk. */ + /* Make sure to reset the URB ID if this is the first chunk. */ + if (!usbWrite(buf, data_size, !g_usbTransferWrittenSize)) + { + LOGFILE("Failed to write 0x%lX bytes long file data chunk from offset 0x%lX! (total size: 0x%lX).", data_size, g_usbTransferWrittenSize, g_usbTransferRemainingSize + g_usbTransferWrittenSize); goto end; } ret = true; g_usbTransferRemainingSize -= data_size; + g_usbTransferWrittenSize += data_size; /* Check if this is the last chunk. */ if (!g_usbTransferRemainingSize) { - if (!usbRead(g_usbTransferBuffer, sizeof(UsbStatus))) + /* Disable ZLT if it was previously enabled. */ + if (zlt_required) usbSetZltPacket(false); + + /* Check response from host device. */ + if (!usbRead(g_usbTransferBuffer, sizeof(UsbStatus), true)) { LOGFILE("Failed to read 0x%lX bytes long status block!", sizeof(UsbStatus)); ret = false; @@ -328,7 +360,7 @@ bool usbSendFileData(void *data, u64 data_size) } end: - if (!ret) g_usbTransferRemainingSize = 0; + if (!ret) g_usbTransferRemainingSize = g_usbTransferWrittenSize = 0; rwlockWriteUnlock(&(g_usbDeviceInterface.lock)); rwlockWriteUnlock(&g_usbDeviceLock); @@ -336,6 +368,31 @@ end: return ret; } +void usbCancelFileTransfer(void) +{ + rwlockWriteLock(&g_usbDeviceLock); + rwlockWriteLock(&(g_usbDeviceInterface.lock)); + rwlockWriteLock(&(g_usbDeviceInterface.lock_in)); + + if (!g_usbTransferBuffer || !g_usbDeviceInterfaceInitialized || !g_usbDeviceInterface.initialized || !g_usbHostAvailable || !g_usbSessionStarted || !g_usbTransferRemainingSize) goto end; + + /* Disable ZLT, just in case it was previously enabled. */ + usbDsEndpoint_SetZlt(g_usbDeviceInterface.endpoint_in, false); + + /* Stall input (write) endpoint. */ + /* This will force the client to stop the current session, so a new one will have to be established. */ + usbDsEndpoint_Stall(g_usbDeviceInterface.endpoint_in); + + /* Signal usermode USB timeout event. */ + /* This will "reset" the USB connection by making the background thread wait until a new session is established. */ + ueventSignal(&g_usbTimeoutEvent); + +end: + rwlockWriteUnlock(&(g_usbDeviceInterface.lock_in)); + rwlockWriteUnlock(&(g_usbDeviceInterface.lock)); + rwlockWriteUnlock(&g_usbDeviceLock); +} + static bool usbCreateDetectionThread(void) { if (thrd_create(&g_usbDetectionThread, usbDetectionThreadFunc, NULL) != thrd_success) @@ -383,7 +440,7 @@ static int usbDetectionThreadFunc(void *arg) /* Only proceed if we're dealing with a status change. */ g_usbHostAvailable = usbIsHostAvailable(); g_usbSessionStarted = false; - g_usbTransferRemainingSize = 0; + g_usbTransferRemainingSize = g_usbTransferWrittenSize = 0; /* Start an USB session if we're connected to a host device. */ /* This will essentially hang this thread and all other threads that call USB-related functions until: */ @@ -406,7 +463,7 @@ static int usbDetectionThreadFunc(void *arg) /* Close USB session if needed. */ if (g_usbHostAvailable && g_usbSessionStarted) usbEndSession(); g_usbHostAvailable = g_usbSessionStarted = g_usbDetectionThreadExitFlag = false; - g_usbTransferRemainingSize = 0; + g_usbTransferRemainingSize = g_usbTransferWrittenSize = 0; rwlockWriteUnlock(&(g_usbDeviceInterface.lock)); rwlockWriteUnlock(&g_usbDeviceLock); @@ -454,7 +511,7 @@ static void usbEndSession(void) usbPrepareCommandHeader(UsbCommandType_EndSession, 0); - if (!usbWrite(g_usbTransferBuffer, sizeof(UsbCommandHeader))) LOGFILE("Failed to send EndSession command!"); + if (!usbWrite(g_usbTransferBuffer, sizeof(UsbCommandHeader), true)) LOGFILE("Failed to send EndSession command!"); } NX_INLINE void usbPrepareCommandHeader(u32 cmd, u32 cmd_block_size) @@ -478,14 +535,14 @@ static u32 usbSendCommand(size_t cmd_size) return UsbStatusType_InvalidCommandSize; } - if (!usbWrite(g_usbTransferBuffer, cmd_size)) + if (!usbWrite(g_usbTransferBuffer, cmd_size, true)) { /* Log error message only if the USB session has been started, or if thread exit flag hasn't been enabled. */ if (g_usbSessionStarted || !g_usbDetectionThreadExitFlag) LOGFILE("Failed to write 0x%lX bytes long block for type 0x%X command!", cmd_size, cmd); return UsbStatusType_WriteCommandFailed; } - if (!usbRead(g_usbTransferBuffer, sizeof(UsbStatus))) + if (!usbRead(g_usbTransferBuffer, sizeof(UsbStatus), true)) { /* Log error message only if the USB session has been started, or if thread exit flag hasn't been enabled. */ if (g_usbSessionStarted || !g_usbDetectionThreadExitFlag) LOGFILE("Failed to read 0x%lX bytes long status block for type 0x%X command!", sizeof(UsbStatus), cmd); @@ -503,7 +560,7 @@ static u32 usbSendCommand(size_t cmd_size) return cmd_status->status; } -NX_INLINE void usbLogStatusDetail(u32 status) +static void usbLogStatusDetail(u32 status) { switch(status) { @@ -625,7 +682,7 @@ static bool usbInitializeComms(void) } /* Super Speed is USB 3.0. */ - /* Upgrade packet size to 512. */ + /* Upgrade packet size to 512 (1 << 9). */ device_descriptor.bcdUSB = 0x0300; device_descriptor.bMaxPacketSize0 = 0x09; if (R_SUCCEEDED(rc)) @@ -984,19 +1041,27 @@ NX_INLINE bool usbIsHostAvailable(void) return (R_SUCCEEDED(rc) && state == 5); } -NX_INLINE bool usbRead(void *buf, u64 size) +NX_INLINE void usbSetZltPacket(bool enable) +{ + rwlockWriteLock(&(g_usbDeviceInterface.lock_in)); + usbDsEndpoint_SetZlt(g_usbDeviceInterface.endpoint_in, enable); + rwlockWriteUnlock(&(g_usbDeviceInterface.lock_in)); +} + +NX_INLINE bool usbRead(void *buf, u64 size, bool reset_urb_id) { rwlockWriteLock(&(g_usbDeviceInterface.lock_out)); + if (reset_urb_id) g_usbUrbId = 0; bool ret = usbTransferData(buf, size, g_usbDeviceInterface.endpoint_out); rwlockWriteUnlock(&(g_usbDeviceInterface.lock_out)); return ret; } -NX_INLINE bool usbWrite(void *buf, u64 size) +NX_INLINE bool usbWrite(void *buf, u64 size, bool reset_urb_id) { rwlockWriteLock(&(g_usbDeviceInterface.lock_in)); + if (reset_urb_id) g_usbUrbId = 0; bool ret = usbTransferData(buf, size, g_usbDeviceInterface.endpoint_in); - if (ret) usbDsEndpoint_SetZlt(g_usbDeviceInterface.endpoint_in, true); rwlockWriteUnlock(&(g_usbDeviceInterface.lock_in)); return ret; } @@ -1015,17 +1080,16 @@ static bool usbTransferData(void *buf, u64 size, UsbDsEndpoint *endpoint) return false; } - u32 urb_id = 0; Result rc = 0; UsbDsReportData report_data = {0}; u32 transferred_size = 0; bool thread_exit = false; /* Start an USB transfer using the provided endpoint. */ - rc = usbDsEndpoint_PostBufferAsync(endpoint, buf, size, &urb_id); + rc = usbDsEndpoint_PostBufferAsync(endpoint, buf, size, &g_usbUrbId); if (R_FAILED(rc)) { - LOGFILE("usbDsEndpoint_PostBufferAsync failed! (0x%08X).", rc); + LOGFILE("usbDsEndpoint_PostBufferAsync failed! (0x%08X) (URB ID %u).", rc, g_usbUrbId); return false; } @@ -1065,27 +1129,27 @@ static bool usbTransferData(void *buf, u64 size, UsbDsEndpoint *endpoint) /* This will "reset" the USB connection by making the background thread wait until a new session is established. */ if (g_usbSessionStarted) ueventSignal(&g_usbTimeoutEvent); - if (!thread_exit) LOGFILE("eventWait failed! (0x%08X).", rc); + if (!thread_exit) LOGFILE("eventWait failed! (0x%08X) (URB ID %u).", rc, g_usbUrbId); return false; } rc = usbDsEndpoint_GetReportData(endpoint, &report_data); if (R_FAILED(rc)) { - LOGFILE("usbDsEndpoint_GetReportData failed! (0x%08X).", rc); + LOGFILE("usbDsEndpoint_GetReportData failed! (0x%08X) (URB ID %u).", rc, g_usbUrbId); return false; } - rc = usbDsParseReportData(&report_data, urb_id, NULL, &transferred_size); + rc = usbDsParseReportData(&report_data, g_usbUrbId, NULL, &transferred_size); if (R_FAILED(rc)) { - LOGFILE("usbDsParseReportData failed! (0x%08X).", rc); + LOGFILE("usbDsParseReportData failed! (0x%08X) (URB ID %u).", rc, g_usbUrbId); return false; } if (transferred_size != size) { - LOGFILE("USB transfer failed! Expected 0x%lX bytes, got 0x%X bytes.", size, transferred_size); + LOGFILE("USB transfer failed! Expected 0x%lX bytes, got 0x%X bytes (URB ID %u).", size, transferred_size, g_usbUrbId); return false; } diff --git a/source/usb.h b/source/usb.h index 3ab5f7e..d842462 100644 --- a/source/usb.h +++ b/source/usb.h @@ -50,4 +50,8 @@ bool usbSendFileProperties(u64 file_size, const char *filename); /// Data chunk size must not exceed USB_TRANSFER_BUFFER_SIZE. bool usbSendFileData(void *data, u64 data_size); +/// Used to cancel an ongoing file transfer by stalling the input (write) USB endpoint. +/// A new USB session must be established afterwards if USB communication with a host device is required. +void usbCancelFileTransfer(void); + #endif /* __USB_H__ */