diff --git a/source/fs/filetype.c b/source/fs/filetype.c index c0f671f..5d0a6e7 100644 --- a/source/fs/filetype.c +++ b/source/fs/filetype.c @@ -1,9 +1,11 @@ #include "filetype.h" #include "fsutil.h" #include "game.h" +#include "firm.h" u32 IdentifyFileType(const char* path) { const u8 romfs_magic[] = { ROMFS_MAGIC }; + const u8 firm_magic[] = { FIRM_MAGIC }; u8 header[0x200] __attribute__((aligned(32))); // minimum required size size_t fsize = FileGetSize(path); if (FileGetData(path, header, 0x200, 0) != 0x200) return 0; @@ -43,6 +45,8 @@ u32 IdentifyFileType(const char* path) { } else if (strncmp(TMD_ISSUER, (char*) (header + 0x140), 0x40) == 0) { if (fsize >= TMD_SIZE_N(getbe16(header + 0x1DE))) return GAME_TMD; // TMD file + } else if (memcmp(header, firm_magic, sizeof(firm_magic)) == 0) { + return SYS_FIRM; // FIRM file } return 0; diff --git a/source/fs/filetype.h b/source/fs/filetype.h index d50a203..b7cecae 100644 --- a/source/fs/filetype.h +++ b/source/fs/filetype.h @@ -12,9 +12,11 @@ #define GAME_EXEFS (1<<6) #define GAME_ROMFS (1<<7) +#define SYS_FIRM (1<<8) + #define FTYPE_MOUNTABLE (IMG_FAT|IMG_NAND|GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_EXEFS|GAME_ROMFS) -#define FYTPE_VERIFICABLE (GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_TMD) -#define FYTPE_DECRYPTABLE (GAME_CIA|GAME_NCSD|GAME_NCCH) +#define FYTPE_VERIFICABLE (GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_TMD|SYS_FIRM) +#define FYTPE_DECRYPTABLE (GAME_CIA|GAME_NCSD|GAME_NCCH|SYS_FIRM) #define FTYPE_BUILDABLE (GAME_NCSD|GAME_NCCH|GAME_TMD) #define FTYPE_BUILDABLE_L (FTYPE_BUILDABLE&(GAME_TMD)) diff --git a/source/game/firm.c b/source/game/firm.c new file mode 100644 index 0000000..d87800d --- /dev/null +++ b/source/game/firm.c @@ -0,0 +1,213 @@ +#include "firm.h" +#include "aes.h" +#include "sha.h" +#include "nand.h" +#include "keydb.h" +#include "ff.h" + +// 0 -> pre 9.5 / 1 -> 9.5 / 2 -> post 9.5 +#define A9L_CRYPTO_TYPE(hdr) ((hdr->k9l[3] == 0xFF) ? 0 : (hdr->k9l[3] == '1') ? 1 : 2) + +u32 ValidateFirmHeader(FirmHeader* header) { + u8 magic[] = { FIRM_MAGIC }; + return memcmp(header->magic, magic, sizeof(magic)); // duh +} + +u32 ValidateFirmA9LHeader(FirmA9LHeader* header) { + const u8 enckeyX0x15hash[0x20] = { + 0x0A, 0x85, 0x20, 0x14, 0x8F, 0x7E, 0xB7, 0x21, 0xBF, 0xC6, 0xC8, 0x82, 0xDF, 0x37, 0x06, 0x3C, + 0x0E, 0x05, 0x1D, 0x1E, 0xF3, 0x41, 0xE9, 0x80, 0x1E, 0xC9, 0x97, 0x82, 0xA0, 0x84, 0x43, 0x08 + }; + u8 hash[0x20]; + sha_quick(hash, header->keyX0x15, 0x10, SHA256_MODE); + return memcmp(hash, enckeyX0x15hash, 0x20); +} + +FirmSectionHeader* FindFirmArm9Section(FirmHeader* firm) { + for (u32 i = 0; i < 4; i++) { + FirmSectionHeader* section = firm->sections + i; + if (section->size && (section->type == 0)) + return section; + } + return NULL; +} + +u32 GetArm9BinarySize(FirmA9LHeader* a9l) { + char* size_ascii = a9l->size_ascii; + u32 size = 0; + for (u32 i = 0; (i < 8) && *(size_ascii + i); i++) + size = (size * 10) + (*(size_ascii + i) - '0'); + return size; +} + +u32 SetupSecretKey(u32 keynum) { + const char* base[] = { INPUT_PATHS }; + // from: https://github.com/AuroraWright/SafeA9LHInstaller/blob/master/source/installer.c#L9-L17 + const u8 sectorHash[0x20] = { + 0x82, 0xF2, 0x73, 0x0D, 0x2C, 0x2D, 0xA3, 0xF3, 0x01, 0x65, 0xF9, 0x87, 0xFD, 0xCC, 0xAC, 0x5C, + 0xBA, 0xB2, 0x4B, 0x4E, 0x5F, 0x65, 0xC9, 0x81, 0xCD, 0x7B, 0xE6, 0xF4, 0x38, 0xE6, 0xD9, 0xD3 + }; + static u8 __attribute__((aligned(32))) sector[0x200]; + u8 hash[0x20]; + + // safety check + if (keynum >= 0x200/0x10) return 1; + + // secret sector already loaded? + sha_quick(hash, sector, 0x200, SHA256_MODE); + if (memcmp(hash, sectorHash, 0x20) == 0) { + setup_aeskey(0x11, sector + (keynum*0x10)); + use_aeskey(0x11); + return 0; + } + + // search for valid secret sector in SysNAND / EmuNAND + const u32 nand_src[] = { NAND_SYSNAND, NAND_EMUNAND }; + for (u32 i = 0; i < sizeof(nand_src) / sizeof(u32); i++) { + ReadNandSectors(sector, 0x96, 1, 0x11, nand_src[i]); + sha_quick(hash, sector, 0x200, SHA256_MODE); + if (memcmp(hash, sectorHash, 0x20) != 0) continue; + setup_aeskey(0x11, sector + (keynum*0x10)); + use_aeskey(0x11); + return 0; + } + + // no luck? try searching for a file + for (u32 i = 0; i < (sizeof(base)/sizeof(char*)); i++) { + char path[64]; + FIL fp; + UINT btr; + snprintf(path, 64, "%s/%s", base[i], SECTOR_NAME); + if (f_open(&fp, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) { + snprintf(path, 64, "%s/%s", base[i], SECRET_NAME); + if (f_open(&fp, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) continue; + } + f_read(&fp, sector, 0x200, &btr); + f_close(&fp); + sha_quick(hash, sector, 0x200, SHA256_MODE); + if (memcmp(hash, sectorHash, 0x20) != 0) continue; + setup_aeskey(0x11, sector + (keynum*0x10)); + use_aeskey(0x11); + return 0; + } + + // try to load from key database + if ((keynum < 2) && (LoadKeyFromFile(NULL, 0x11, 'N', (keynum == 0) ? "95" : "96"))) + return 0; // key found in keydb, done + + // out of options + return 1; +} + +u32 DecryptA9LHeader(FirmA9LHeader* header) { + u32 type = A9L_CRYPTO_TYPE(header); + + if (SetupSecretKey(0) != 0) return 1; + aes_decrypt(header->keyX0x15, header->keyX0x15, 1, AES_CNT_ECB_DECRYPT_MODE); + if (type) { + if (SetupSecretKey((type == 1) ? 0 : 1) != 0) return 1; + aes_decrypt(header->keyX0x16, header->keyX0x16, 1, AES_CNT_ECB_DECRYPT_MODE); + } + + return 0; +} + +u32 SetupArm9BinaryCrypto(FirmA9LHeader* header) { + u32 type = A9L_CRYPTO_TYPE(header); + + if (type == 0) { + u8 __attribute__((aligned(32))) keyX0x15[0x10]; + memcpy(keyX0x15, header->keyX0x15, 0x10); + if (SetupSecretKey(0) != 0) return 1; + aes_decrypt(keyX0x15, keyX0x15, 1, AES_CNT_ECB_DECRYPT_MODE); + setup_aeskeyX(0x15, keyX0x15); + setup_aeskeyY(0x15, header->keyY0x150x16); + use_aeskey(0x15); + } else { + u8 __attribute__((aligned(32))) keyX0x16[0x10]; + memcpy(keyX0x16, header->keyX0x16, 0x10); + if (SetupSecretKey((type == 1) ? 0 : 1) != 0) return 1; + aes_decrypt(keyX0x16, keyX0x16, 1, AES_CNT_ECB_DECRYPT_MODE); + setup_aeskeyX(0x16, keyX0x16); + setup_aeskeyY(0x16, header->keyY0x150x16); + use_aeskey(0x16); + } + + return 0; +} + +u32 DecryptArm9Binary(u8* data, u32 offset, u32 size, FirmA9LHeader* a9l) { + // offset == offset inside ARM9 binary + // ARM9 binary begins 0x800 byte after the ARM9 loader header + + // only process actual ARM9 binary + u32 size_bin = GetArm9BinarySize(a9l); + if (offset >= size_bin) return 0; + else if (size >= size_bin - offset) + size = size_bin - offset; + + // decrypt data + if (SetupArm9BinaryCrypto(a9l) != 0) return 1; + ctr_decrypt_byte(data, data, size, offset, AES_CNT_CTRNAND_MODE, a9l->ctr); + + return 0; +} + +u32 DecryptFirm(u8* data, u32 offset, u32 size, FirmHeader* firm, FirmA9LHeader* a9l) { + // ARM9 binary size / offset + FirmSectionHeader* arm9s = FindFirmArm9Section(firm); + u32 offset_arm9bin = arm9s->offset + ARM9BIN_OFFSET; + u32 size_arm9bin = GetArm9BinarySize(a9l); + + // sanity checks + if (!size_arm9bin || (size_arm9bin + ARM9BIN_OFFSET > arm9s->size)) + return 1; // bad header / data + + // check if ARM9 binary in data + if ((offset_arm9bin >= offset + size) || + (offset >= offset_arm9bin + size_arm9bin)) + return 0; // section not in data + + // determine data / offset / size + u8* data_i = data; + u32 offset_i = 0; + u32 size_i = size_arm9bin; + if (offset_arm9bin < offset) + offset_i = offset - offset_arm9bin; + else data_i = data + (offset_arm9bin - offset); + size_i = size_arm9bin - offset_i; + if (size_i > size - (data_i - data)) + size_i = size - (data_i - data); + + return DecryptArm9Binary(data_i, offset_i, size_i, a9l); +} + +u32 DecryptFirmSequential(u8* data, u32 offset, u32 size) { + // warning: this will only work for sequential processing + // unexpected results otherwise + static FirmHeader firm = { 0 }; + static FirmA9LHeader a9l = { 0 }; + static FirmHeader* firmptr = NULL; + static FirmA9LHeader* a9lptr = NULL; + static FirmSectionHeader* arm9s = NULL; + + // fetch firm header from data + if ((offset == 0) && (size >= sizeof(FirmHeader))) { + memcpy(&firm, data, sizeof(FirmHeader)); + firmptr = (ValidateFirmHeader(&firm) == 0) ? &firm : NULL; + arm9s = (firmptr) ? FindFirmArm9Section(firmptr) : NULL; + a9lptr = NULL; + } + + // safety check, firm header pointer + if (!firmptr) return 1; + + // fetch ARM9 loader header from data + if (arm9s && !a9lptr && (offset <= arm9s->offset) && + ((offset + size) >= arm9s->offset + sizeof(FirmA9LHeader))) { + memcpy(&a9l, data + arm9s->offset - offset, sizeof(FirmA9LHeader)); + a9lptr = (ValidateFirmA9LHeader(&a9l) == 0) ? &a9l : NULL; + } + + return (a9lptr) ? DecryptFirm(data, offset, size, firmptr, a9lptr) : 0; +} diff --git a/source/game/firm.h b/source/game/firm.h new file mode 100644 index 0000000..ddc26f9 --- /dev/null +++ b/source/game/firm.h @@ -0,0 +1,52 @@ +#pragma once + +#include "common.h" + +#define FIRM_MAGIC 'F', 'I', 'R', 'M' + +#define SECTOR_NAME "sector0x96.bin" +#define SECRET_NAME "secret_sector.bin" + +#define ARM9BIN_OFFSET 0x800 + +// see: https://www.3dbrew.org/wiki/FIRM#Firmware_Section_Headers +typedef struct { + u32 offset; + u32 address; + u32 size; + u32 type; + u8 hash[0x20]; +} __attribute__((packed)) FirmSectionHeader; + +// see: https://www.3dbrew.org/wiki/FIRM#FIRM_Header +typedef struct { + u8 magic[4]; + u8 dec_magic[4]; + u32 entry_arm11; + u32 entry_arm9; + u8 reserved1[0x30]; + FirmSectionHeader sections[4]; + u8 signature[0x100]; +} __attribute__((packed, aligned(16))) FirmHeader; + +// see: https://www.3dbrew.org/wiki/FIRM#New_3DS_FIRM +typedef struct { + u8 keyX0x15[0x10]; // this is encrypted + u8 keyY0x150x16[0x10]; + u8 ctr[0x10]; + char size_ascii[0x8]; + u8 reserved[0x8]; + u8 control[0x10]; + u8 k9l[0x10]; + u8 keyX0x16[0x10]; // this is encrypted + u8 padding[0x1A0]; +} __attribute__((packed, aligned(16))) FirmA9LHeader; + +u32 ValidateFirmHeader(FirmHeader* header); +u32 ValidateFirmA9LHeader(FirmA9LHeader* header); +FirmSectionHeader* FindFirmArm9Section(FirmHeader* firm); + +u32 DecryptA9LHeader(FirmA9LHeader* header); +u32 DecryptFirm(u8* data, u32 offset, u32 size, FirmHeader* firm, FirmA9LHeader* a9l); +u32 DecryptArm9Binary(u8* data, u32 offset, u32 size, FirmA9LHeader* a9l); +u32 DecryptFirmSequential(u8* data, u32 offset, u32 size); diff --git a/source/game/gameutil.c b/source/game/gameutil.c index 367ee89..1b4e36e 100644 --- a/source/game/gameutil.c +++ b/source/game/gameutil.c @@ -1,5 +1,6 @@ #include "gameutil.h" #include "game.h" +#include "firm.h" #include "ui.h" #include "fsperm.h" #include "filetype.h" @@ -462,6 +463,48 @@ u32 VerifyTmdFile(const char* path) { return 0; } +u32 VerifyFirmFile(const char* path) { + FirmHeader header; + FIL file; + UINT btr; + + // open file, get FIRM header + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&file, 0); + if ((fx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) || + (ValidateFirmHeader(&header) != 0)) { + fx_close(&file); + return 1; + } + + // hash verify all available sections + for (u32 i = 0; i < 4; i++) { + FirmSectionHeader* section = header.sections + i; + u32 size = section->size; + if (!size) continue; + f_lseek(&file, section->offset); + sha_init(SHA256_MODE); + for (u32 i = 0; i < size; i += MAIN_BUFFER_SIZE) { + u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i)); + fx_read(&file, MAIN_BUFFER, read_bytes, &btr); + sha_update(MAIN_BUFFER, read_bytes); + } + u8 hash[0x20]; + sha_get(hash); + if (memcmp(hash, section->hash, 0x20) != 0) { + char pathstr[32 + 1]; + TruncateString(pathstr, path, 32, 8); + ShowPrompt(false, "%s\nSection %u hash mismatch", pathstr, i); + fx_close(&file); + return 1; + } + } + fx_close(&file); + + return 0; +} + u32 VerifyGameFile(const char* path) { u32 filetype = IdentifyFileType(path); if (filetype == GAME_CIA) @@ -472,25 +515,15 @@ u32 VerifyGameFile(const char* path) { return VerifyNcchFile(path, 0, 0); else if (filetype == GAME_TMD) return VerifyTmdFile(path); + else if (filetype == SYS_FIRM) + return VerifyFirmFile(path); else return 1; } u32 CheckEncryptedNcchFile(const char* path, u32 offset) { NcchHeader ncch; - FIL file; - UINT btr; - - // open file, get NCCH header - if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + if (LoadNcchHeaders(&ncch, NULL, NULL, path, offset) != 0) return 1; - f_lseek(&file, offset); - if ((fx_read(&file, &ncch, sizeof(NcchHeader), &btr) != FR_OK) || - (ValidateNcchHeader(&ncch) != 0)) { - fx_close(&file); - return 1; - } - fx_close(&file); - return (NCCH_ENCRYPTED(&ncch)) ? 0 : 1; } @@ -535,6 +568,37 @@ u32 CheckEncryptedCiaFile(const char* path) { return 1; } +u32 CheckEncryptedFirmFile(const char* path) { + FirmHeader header; + FIL file; + UINT btr; + + // open file, get FIRM header + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&file, 0); + if ((fx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) || + (ValidateFirmHeader(&header) != 0)) { + fx_close(&file); + return 1; + } + + // check ARM9 binary for ARM9 loader + FirmSectionHeader* arm9s = FindFirmArm9Section(&header); + if (arm9s) { + FirmA9LHeader a9l; + f_lseek(&file, arm9s->offset); + if ((fx_read(&file, &a9l, sizeof(FirmA9LHeader), &btr) == FR_OK) && + (ValidateFirmA9LHeader(&a9l) == 0)) { + fx_close(&file); + return 0; + } + } + + fx_close(&file); + return 1; +} + u32 CheckEncryptedGameFile(const char* path) { u32 filetype = IdentifyFileType(path); if (filetype == GAME_CIA) @@ -543,10 +607,12 @@ u32 CheckEncryptedGameFile(const char* path) { return CheckEncryptedNcsdFile(path); else if (filetype == GAME_NCCH) return CheckEncryptedNcchFile(path, 0); + else if (filetype == SYS_FIRM) + return CheckEncryptedFirmFile(path); else return 1; } -u32 DecryptNcchNcsdFile(const char* orig, const char* dest, u32 mode, +u32 DecryptNcchNcsdFirmFile(const char* orig, const char* dest, u32 mode, u32 offset, u32 size, TmdContentChunk* chunk, const u8* titlekey) { // this line only for CIA contents // this will do a simple copy for unencrypted files bool inplace = (strncmp(orig, dest, 256) == 0); @@ -576,14 +642,15 @@ u32 DecryptNcchNcsdFile(const char* orig, const char* dest, u32 mode, if (!size) size = fsize; u32 ret = 0; - if (mode & (GAME_NCCH|GAME_NCSD)) { // for NCCH / NCSD files - if (!ShowProgress(offset, fsize, dest)) ret = 1; + if (!ShowProgress(offset, fsize, dest)) ret = 1; + if (mode & (GAME_NCCH|GAME_NCSD|SYS_FIRM)) { // for NCCH / NCSD / FIRM files for (u32 i = 0; (i < size) && (ret == 0); i += MAIN_BUFFER_SIZE) { u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i)); UINT bytes_read, bytes_written; if (fx_read(ofp, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1; if (((mode & GAME_NCCH) && (DecryptNcchSequential(MAIN_BUFFER, i, read_bytes) != 0)) || - ((mode & GAME_NCSD) && (DecryptNcsdSequential(MAIN_BUFFER, i, read_bytes) != 0))) + ((mode & GAME_NCSD) && (DecryptNcsdSequential(MAIN_BUFFER, i, read_bytes) != 0)) || + ((mode & SYS_FIRM) && (DecryptFirmSequential(MAIN_BUFFER, i, read_bytes) != 0))) ret = 1; if (inplace) f_lseek(ofp, f_tell(ofp) - read_bytes); if (fx_write(dfp, MAIN_BUFFER, read_bytes, &bytes_written) != FR_OK) ret = 1; @@ -591,7 +658,6 @@ u32 DecryptNcchNcsdFile(const char* orig, const char* dest, u32 mode, if (!ShowProgress(offset + i + read_bytes, fsize, dest)) ret = 1; } } else if (mode & GAME_CIA) { // for NCCHs inside CIAs - if (!ShowProgress(offset, fsize, dest)) ret = 1; bool cia_crypto = getbe16(chunk->type) & 0x1; bool ncch_crypto; // find out by decrypting the NCCH header UINT bytes_read, bytes_written; @@ -654,7 +720,7 @@ u32 DecryptCiaFile(const char* orig, const char* dest) { for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++) { TmdContentChunk* chunk = &(cia->content_list[i]); u64 size = getbe64(chunk->size); - if (DecryptNcchNcsdFile(orig, dest, GAME_CIA, next_offset, size, chunk, titlekey) != 0) + if (DecryptNcchNcsdFirmFile(orig, dest, GAME_CIA, next_offset, size, chunk, titlekey) != 0) return 1; next_offset += size; } @@ -666,6 +732,65 @@ u32 DecryptCiaFile(const char* orig, const char* dest) { return 0; } +u32 DecryptFirmFile(const char* orig, const char* dest) { + const u8 dec_magic[] = { 'D', 'E', 'C', '\0' }; // insert to decrypted firms + FirmHeader firm; + FIL file; + UINT btr; + + // actual decryption + if (DecryptNcchNcsdFirmFile(orig, dest, SYS_FIRM, 0, 0, NULL, NULL) != 0) + return 1; + + // open destination file, get FIRM header + if (fx_open(&file, dest, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&file, 0); + if ((fx_read(&file, &firm, sizeof(FirmHeader), &btr) != FR_OK) || + (ValidateFirmHeader(&firm) != 0)) { + fx_close(&file); + return 1; + } + + // find ARM9 section + FirmSectionHeader* arm9s = FindFirmArm9Section(&firm); + if (!arm9s || !arm9s->size) return 1; + + // decrypt ARM9 loader header + FirmA9LHeader a9l; + f_lseek(&file, arm9s->offset); + if ((fx_read(&file, &a9l, sizeof(FirmA9LHeader), &btr) != FR_OK) || + (DecryptA9LHeader(&a9l) != 0) || (f_lseek(&file, arm9s->offset) != FR_OK) || + (fx_write(&file, &a9l, sizeof(FirmA9LHeader), &btr) != FR_OK)) { + fx_close(&file); + return 1; + } + + // calculate new hash for ARM9 section + f_lseek(&file, arm9s->offset); + sha_init(SHA256_MODE); + for (u32 i = 0; i < arm9s->size; i += MAIN_BUFFER_SIZE) { + u32 read_bytes = min(MAIN_BUFFER_SIZE, (arm9s->size - i)); + if ((fx_read(&file, MAIN_BUFFER, read_bytes, &btr) != FR_OK) || (btr != read_bytes)) { + fx_close(&file); + return 1; + } + sha_update(MAIN_BUFFER, read_bytes); + } + sha_get(arm9s->hash); + + // write back FIRM header + f_lseek(&file, 0); + memcpy(firm.dec_magic, dec_magic, sizeof(dec_magic)); + if (fx_write(&file, &firm, sizeof(FirmHeader), &btr) != FR_OK) { + fx_close(&file); + return 1; + } + + fx_close(&file); + return 0; +} + u32 DecryptGameFile(const char* path, bool inplace) { u32 filetype = IdentifyFileType(path); char dest[256]; @@ -688,8 +813,10 @@ u32 DecryptGameFile(const char* path, bool inplace) { if (filetype & GAME_CIA) ret = DecryptCiaFile(path, destptr); + else if (filetype & SYS_FIRM) + ret = DecryptFirmFile(path, destptr); else if (filetype & (GAME_NCCH|GAME_NCSD)) - ret = DecryptNcchNcsdFile(path, destptr, filetype, 0, 0, NULL, NULL); + ret = DecryptNcchNcsdFirmFile(path, destptr, filetype, 0, 0, NULL, NULL); else ret = 1; if (!inplace && (ret != 0)) diff --git a/source/godmode.c b/source/godmode.c index fc52378..e3f16ff 100644 --- a/source/godmode.c +++ b/source/godmode.c @@ -599,7 +599,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur (filetype == GAME_NCCH ) ? "NCCH image options..." : (filetype == GAME_EXEFS) ? "Mount as EXEFS image" : (filetype == GAME_ROMFS) ? "Mount as ROMFS image" : - (filetype == GAME_TMD) ? "TMD file options..." : "???"; + (filetype == GAME_TMD) ? "TMD file options..." : + (filetype == SYS_FIRM) ? "FIRM image options..." : "???"; optionstr[hexviewer-1] = "Show in Hexeditor"; optionstr[calcsha-1] = "Calculate SHA-256"; if (inject > 0) optionstr[inject-1] = "Inject data @offset";