diff --git a/source/common/common.h b/source/common/common.h index 3095109..c54075b 100644 --- a/source/common/common.h +++ b/source/common/common.h @@ -52,6 +52,7 @@ // -> ncch.c seed setup // -> cia.c ticket / titlekey setup // -> gameutil.c various temporary stuff +// -> nandcmac.c for processing agbsave #define TEMP_BUFFER ((u8*)0x21100000) #define TEMP_BUFFER_SIZE (0x100000) // buffer area defines (in use by fsutil.c, fsinit.c and gameutil.c) diff --git a/source/godmode.c b/source/godmode.c index 081c82b..09e96e5 100644 --- a/source/godmode.c +++ b/source/godmode.c @@ -12,6 +12,7 @@ #include "nand.h" #include "virtual.h" #include "vcart.h" +#include "nandcmac.h" #include "ncchinfo.h" #include "image.h" #include "chainload.h" @@ -569,6 +570,28 @@ u32 Sha256Calculator(const char* path) { return 0; } +u32 CmacCalculator(const char* path) { + char pathstr[32 + 1]; + u8 cmac[16]; + TruncateString(pathstr, path, 32, 8); + if (CalculateFileCmac(path, cmac) != 0) { + ShowPrompt(false, "Calculating CMAC: failed!"); + return 1; + } else { + u8 cmac_file[16]; + bool identical = ((ReadFileCmac(path, cmac_file) == 0) && (memcmp(cmac, cmac_file, 16) == 0)); + if (ShowPrompt(!identical, "%s\n%016llX%016llX\n%s%s%s", + pathstr, getbe64(cmac + 0), getbe64(cmac + 8), + "CMAC verification: ", (identical) ? "passed!" : "failed!", + (!identical) ? "\n \nFix CMAC in file?" : "") && + !identical && (WriteFileCmac(path, cmac_file) != 0)) { + ShowPrompt(false, "Fixing CMAC: failed!"); + } + } + + return 0; +} + u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* current_dir, DirStruct* clipboard) { DirEntry* curr_entry = &(current_dir->entry[*cursor]); const char* optionstr[16]; @@ -586,7 +609,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur bool verificable = (FYTPE_VERIFICABLE(filetype)); bool decryptable = (FYTPE_DECRYPTABLE(filetype)); bool encryptable = (FYTPE_ENCRYPTABLE(filetype)); - bool cryptable_inplace = ((encryptable||decryptable) && (drvtype & DRV_FAT)); + bool cryptable_inplace = ((encryptable||decryptable) && !in_output_path && (drvtype & DRV_FAT)); bool buildable = (FTYPE_BUILDABLE(filetype)); bool buildable_legit = (FTYPE_BUILDABLE_L(filetype)); bool hsinjectable = (FTYPE_HSINJECTABLE(filetype)); @@ -596,7 +619,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur bool special_opt = mountable || verificable || decryptable || encryptable || buildable || buildable_legit || hsinjectable || restorable || xorpadable || launchable; - char pathstr[32 + 1]; + char pathstr[48]; TruncateString(pathstr, curr_entry->path, 32, 8); u32 n_marked = 0; @@ -610,6 +633,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur int special = (special_opt) ? ++n_opt : -1; int hexviewer = ++n_opt; int calcsha = ++n_opt; + int calccmac = (CheckCmacPath(curr_entry->path) == 0) ? ++n_opt : -1; int copystd = (!in_output_path) ? ++n_opt : -1; int inject = ((clipboard->n_entries == 1) && (clipboard->entry[0].type == T_FILE) && @@ -633,6 +657,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur (filetype & BIN_LAUNCH) ? "Launch as arm9 payload" : "???"; optionstr[hexviewer-1] = "Show in Hexeditor"; optionstr[calcsha-1] = "Calculate SHA-256"; + optionstr[calccmac-1] = "Calculate CMAC"; if (copystd > 0) optionstr[copystd-1] = "Copy to " OUTPUT_PATH; if (inject > 0) optionstr[inject-1] = "Inject data @offset"; if (searchdrv > 0) optionstr[searchdrv-1] = "Open containing folder"; @@ -645,6 +670,48 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur Sha256Calculator(curr_entry->path); GetDirContents(current_dir, current_path); return 0; + } else if (user_select == calccmac) { // -> calculate CMAC + optionstr[0] = "Check only current CMAC"; + optionstr[1] = "Verify CMAC for all files"; + optionstr[2] = "Fix CMAC for all files"; + user_select = (n_marked > 1) ? ShowSelectPrompt(3, optionstr, "%s\n%(lu files selected)", pathstr, n_marked) : 1; + if (user_select == 1) { + CmacCalculator(curr_entry->path); + return 0; + } else if ((user_select == 2) || (user_select == 3)) { + bool fix = (user_select == 3); + u32 n_processed = 0; + u32 n_success = 0; + u32 n_fixed = 0; + u32 n_nocmac = 0; + for (u32 i = 0; i < current_dir->n_entries; i++) { + const char* path = current_dir->entry[i].path; + if (!current_dir->entry[i].marked) continue; + if (!ShowProgress(n_processed++, n_marked, path)) break; + if (CheckCmacPath(path) != 0) { + n_nocmac++; + continue; + } + current_dir->entry[i].marked = false; + else if (fix && (FixFileCmac(path) == 0)) n_fixed++; + else { // on failure: set cursor on failed file + *cursor = i; + continue; + } + if (CheckFileCmac(path) == 0) n_success++; + } + if (n_fixed) { + if (n_nocmac) ShowPrompt(false, "%lu/%lu/%lu files ok/fixed/total\n%lu/%lu have no CMAC", + n_success, n_fixed, n_marked, n_nocmac, n_marked); + else ShowPrompt(false, "%lu/%lu files verified ok\n%lu/%lu files fixed", + n_success, n_marked, n_fixed, n_marked); + } else { + if (n_nocmac) ShowPrompt(false, "%lu/%lu files verified ok\n%lu/%lu have no CMAC", + n_success, n_marked, n_nocmac, n_marked); + else ShowPrompt(false, "%lu/%lu files verified ok", n_success, n_marked); + } + return 0; + } } else if (user_select == copystd) { // -> copy to OUTPUT_PATH u32 flags = 0; if ((n_marked > 1) && ShowPrompt(true, "Copy all %lu selected files?", n_marked)) { @@ -869,12 +936,12 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur const char* path = current_dir->entry[i].path; if (!current_dir->entry[i].marked) continue; + if (!(filetype & (GAME_CIA|GAME_TMD)) && + !ShowProgress(n_processed++, n_marked, path)) break; if (!(IdentifyFileType(path) & filetype & TYPE_BASE)) { n_other++; continue; } - if (!(filetype & (GAME_CIA|GAME_TMD)) && - !ShowProgress(n_processed++, n_marked, path)) break; current_dir->entry[i].marked = false; if (filetype & IMG_NAND) { if (ValidateNandDump(path) == 0) n_success++; diff --git a/source/nand/agbsave.c b/source/nand/agbsave.c index bd32c4a..8d967ef 100644 --- a/source/nand/agbsave.c +++ b/source/nand/agbsave.c @@ -15,6 +15,7 @@ u32 CheckAgbSaveCmac(u32 nand_src) { AgbSave* agbsave = (AgbSave*) NAND_BUFFER; if ((ReadNandSectors((u8*) agbsave, SECTOR_AGBSAVE, 1, 0x07, nand_src) != 0) || (memcmp(agbsave->magic, magic, sizeof(magic)) != 0) || + (0x200 + agbsave->save_size > SIZE_AGBSAVE * 0x200) || (ReadNandBytes(agbsave->savegame, (SECTOR_AGBSAVE+1) * 0x200, agbsave->save_size, 0x07, nand_src) != 0)) return 1; @@ -30,6 +31,7 @@ u32 CheckAgbSaveCmac(u32 nand_src) { u32 FixAgbSaveCmac(u32 nand_dst) { AgbSave* agbsave = (AgbSave*) NAND_BUFFER; if ((ReadNandSectors((u8*) agbsave, SECTOR_AGBSAVE, 1, 0x07, nand_dst) != 0) || + (0x200 + agbsave->save_size > SIZE_AGBSAVE * 0x200) || (ReadNandBytes(agbsave->savegame, (SECTOR_AGBSAVE+1) * 0x200, agbsave->save_size, 0x07, nand_dst) != 0)) return 1; diff --git a/source/nand/nandcmac.c b/source/nand/nandcmac.c new file mode 100644 index 0000000..7197c89 --- /dev/null +++ b/source/nand/nandcmac.c @@ -0,0 +1,235 @@ +#include "nandcmac.h" +#include "fsperm.h" +#include "agbsave.h" +#include "sha.h" +#include "aes.h" +#include "vff.h" + +// CMAC types, see: +// https://www.3dbrew.org/wiki/Savegames#AES_CMAC_header +// https://3dbrew.org/wiki/Nand/private/movable.sed +// https://3dbrew.org/wiki/3DS_Virtual_Console#NAND_Savegame +#define CMAC_EXTDATA_SD 1 +#define CMAC_EXTDATA_SYS 2 +#define CMAC_SAVEDATA_SYS 3 +#define CMAC_SAVE_GAMECARD 4 +#define CMAC_SAVEGAME 5 // this is not calclated into a CMAC +#define CMAC_SAVEDATA_SD 6 +#define CMAC_TITLEDB_SYS 7 +#define CMAC_TITLEDB_SD 8 +#define CMAC_MOVABLE 9 +#define CMAC_AGBSAVE 10 + +// see: https://www.3dbrew.org/wiki/Savegames#AES_CMAC_header +#define CMAC_SAVETYPE NULL, "CTR-EXT0", "CTR-EXT0", "CTR-SYS0", "CTR-NOR0", "CTR-SAV0", "CTR-SIGN", "CTR-9DB0", "CTR-9DB0", NULL, NULL + +// see: http://3dbrew.org/wiki/AES_Registers#Keyslots +#define CMAC_KEYSLOT 0xFF, 0x30 /*0x3A?*/, 0x30, 0x30, 0x33 /*0x19*/, 0xFF, 0x30, 0x0B, 0x30, 0x0B, 0x24 + +// see: https://www.3dbrew.org/wiki/Title_Database +#define SYS_DB_NAMES "ticket.db", "certs.db", "title.db", "import.db", "tmp_t.db", "tmp_i.db" + +// CMAC scan paths - just for reference, information used below +// see: https://www.3dbrew.org/wiki/Savegames#AES_CMAC_header +// see: https://www.3dbrew.org/wiki/Title_Database +// "%c:/extdata/%08lx/%08lx/%08lx/%08lx" SD extdata - drv / xid_high / xid_low / fid_low / fid_high +// "%c:/data/%016llx%016llx/extdata/%08lx/%08lx/%08lx/%08lx" SYS extdata - drv / id0_high / id0_low / xid_high / xid_low / fid_low / fid_high +// "%c:/data/%016llx%016llx/extdata/%08lx/%08lx/Quota.dat" SYS extdata - drv / id0_high / id0_low / xid_high / xid_low +// "%c:/data/%016llx%016llx/sysdata/%08lx/%08lx SYS savedata - drv / id0_high / id0_low / fid_low / fid_high +// "%c:/title/%08lx/%08lx/data/%08lx.sav" SD savedata - drv / tid_high / tid_low / sid +// "%c:/dbs/ticket.db" SYS database (0x0) +// "%c:/dbs/cert.db" SYS database (0x1) +// "%c:/dbs/title.db" SYS database (0x2) +// "%c:/dbs/import.db" SYS database (0x3) +// "%c:/dbs/tmp_t.db" SYS database (0x4) +// "%c:/dbs/tmp_i.db" SYS database (0x5) +// "%c:/private/movable.sed" movable.sed +// "%c:/agbsave.bin" virtual AGBSAVE file + + +u32 CheckCmacHeader(const char* path) { + u8 cmac_hdr[0x100]; + UINT br; + + if ((fvx_qread(path, cmac_hdr, 0, 0x100, &br) != FR_OK) || (br != 0x100)) + return 1; + for (u32 i = 0x10; i < 0x100; i++) + if (cmac_hdr[i] != 0x00) return 1; + + return 0; +} + +u32 CheckCmacPath(const char* path) { + return (CalculateFileCmac(path, NULL)) ? 0 : 1; +} + +u32 ReadFileCmac(const char* path, u8* cmac) { + u32 cmac_type = CalculateFileCmac(path, NULL); + u32 offset = 0; + UINT br; + + if (!cmac_type) return 1; + else if (cmac_type == CMAC_MOVABLE) offset = 0x130; + else if (cmac_type == CMAC_AGBSAVE) offset = 0x010; + else offset = 0x000; + + return ((fvx_qread(path, cmac, offset, 0x10, &br) != FR_OK) || (br != 0x10)) ? 1 : 0; +} + +u32 WriteFileCmac(const char* path, u8* cmac) { + u32 cmac_type = CalculateFileCmac(path, NULL); + u32 offset = 0; + UINT bw; + + if (!cmac_type) return 1; + else if (cmac_type == CMAC_MOVABLE) offset = 0x130; + else if (cmac_type == CMAC_AGBSAVE) offset = 0x010; + else offset = 0x000; + + if (!CheckWritePermissions(path)) return 1; + return ((fvx_qwrite(path, cmac, offset, 0x10, &bw) != FR_OK) || (bw != 0x10)) ? 1 : 0; +} + +u32 CalculateFileCmac(const char* path, u8* cmac) { + u32 cmac_type = 0; + char drv = *path; // drive letter + u32 xid_high, xid_low; // extdata ID + u32 fid_high, fid_low; // extfile ID + u32 tid_high, tid_low; // title ID + u32 sid; // save ID / various uses + char* name; + char* ext; + + name = strrchr(path, '/'); // filename + if (!name) return 0; // will not happen + name++; + ext = strrchr(name, '.'); // extension + if (ext) ext++; + + if ((drv == 'A') || (drv == 'B')) { // data installed on SD + if (sscanf(path, "%c:/extdata/%08lx/%08lx/%08lx/%08lx", &drv, &xid_high, &xid_low, &fid_high, &fid_low) == 5) { + sid = 1; + cmac_type = CMAC_EXTDATA_SD; + } else if ((sscanf(path, "%c:/title/%08lx/%08lx/data/%08lx.sav", &drv, &tid_high, &tid_low, &sid) == 4) && + ext && (strncmp(ext, "sav", 4) == 0) && (CheckCmacHeader(path) == 0)) { + cmac_type = CMAC_SAVEDATA_SD; + } + } else if ((drv == '1') || (drv == '4') || (drv == '7')) { // data on CTRNAND + u64 id0_high, id0_low; // ID0 + if (sscanf(path, "%c:/data/%016llx%016llx/extdata/%08lx/%08lx/%08lx/%08lx", &drv, &id0_high, &id0_low, &xid_high, &xid_low, &fid_high, &fid_low) == 7) { + sid = 1; + cmac_type = CMAC_EXTDATA_SYS; + } else if ((sscanf(path, "%c:/data/%016llx%016llx/extdata/%08lx/%08lx/Quota.dat", &drv, &id0_high, &id0_low, &xid_high, &xid_low) == 5) && (strncasecmp(name, "Quota.dat", 10) == 0)) { + sid = 0; + fid_low = fid_high = 0; + cmac_type = CMAC_EXTDATA_SYS; + } else if (sscanf(path, "%c:/data/%016llx%016llx/sysdata/%08lx/%08lx", &drv, &id0_high, &id0_low, &fid_low, &fid_high) == 5) + cmac_type = CMAC_SAVEDATA_SYS; + } + + if (!cmac_type) { // path independent stuff + const char* db_names[] = { SYS_DB_NAMES }; + for (sid = 0; sid < sizeof(db_names) / sizeof(char*); sid++) + if (strncmp(name, db_names[sid], 16) == 0) break; + if (sid < sizeof(db_names) / sizeof(char*)) + cmac_type = ((drv == 'A') || (drv == 'B')) ? CMAC_TITLEDB_SD : CMAC_TITLEDB_SYS; + else if (strncmp(name, "movable.sed", 16) == 0) + cmac_type = CMAC_MOVABLE; + /* else if (strncmp(name, "agbsave.bin", 16) == 0) + cmac_type = CMAC_AGBSAVE; */ + } + + // exit with cmac_type if (u8*) cmac is NULL + // somewhat hacky, but can be used to check if file has a CMAC + if (!cmac) return cmac_type; + else if (!cmac_type) return 1; + + const u32 cmac_keyslot[] = { CMAC_KEYSLOT }; + u8* data = (u8*) TEMP_BUFFER; + u32 keyslot = cmac_keyslot[cmac_type]; + u8* hashdata = NULL; + u32 hashsize = 0; + UINT br; + + // setup slot 0x30 via movable.sed + if (keyslot == 0x30) { + u8 keyy[16] __attribute__((aligned(32))); + char movable_path[32]; + snprintf(movable_path, 32, "%c:/private/movable.sed", (drv == 'A') ? '1' : (drv == 'B') ? '4' : drv); + if ((fvx_qread(movable_path, keyy, 0x110, 0x10, &br) != FR_OK) || (br != 0x10)) return 1; + setup_aeskeyY(0x30, keyy); + use_aeskey(0x30); + } + + // build hash data block, get size + if (cmac_type == CMAC_AGBSAVE) { // agbsaves + // see: https://www.3dbrew.org/wiki/Savegames#AES_CMAC_header + AgbSave* agbsave = (AgbSave*) (void*) data; + if ((fvx_qread(path, agbsave, 0, TEMP_BUFFER_SIZE, &br) != FR_OK) || + (br >= TEMP_BUFFER_SIZE) || (0x200 + agbsave->save_size > br)) + return 1; + hashsize = (0x200 - 0x30) + agbsave->save_size; + hashdata = data + 0x30; + } else if (cmac_type == CMAC_MOVABLE) { // movable.sed + // see: https://3dbrew.org/wiki/Nand/private/movable.sed + if ((fvx_qread(path, data, 0, 0x140, &br) != FR_OK) || (br != 0x140)) + return 1; + hashsize = 0x130; + hashdata = data; + } else { // "savegame" CMACs + // see: https://3dbrew.org/wiki/Savegames + const char* cmac_savetype[] = { CMAC_SAVETYPE }; + u8* disa = data; + hashdata = data + 0x400; + if ((fvx_qread(path, disa, 0x100, 0x100, &br) != FR_OK) || (br != 0x100)) + return 1; + memcpy(hashdata, cmac_savetype[cmac_type], 8); + if ((cmac_type == CMAC_EXTDATA_SD) || (cmac_type == CMAC_EXTDATA_SYS)) { + memcpy(hashdata + 0x08, &xid_low, 4); + memcpy(hashdata + 0x0C, &xid_high, 4); + memcpy(hashdata + 0x10, &sid, 4); + memcpy(hashdata + 0x14, &fid_low, 4); + memcpy(hashdata + 0x18, &fid_high, 4); + memcpy(hashdata + 0x1C, disa, 0x100); + hashsize = 0x11C; + } else if (cmac_type == CMAC_SAVEDATA_SYS) { + memcpy(hashdata + 0x08, &fid_low, 4); + memcpy(hashdata + 0x0C, &fid_high, 4); + memcpy(hashdata + 0x10, disa, 0x100); + hashsize = 0x110; + } else if (cmac_type == CMAC_SAVEDATA_SD) { + u8* hashdata0 = data + 0x200; + memcpy(hashdata0 + 0x00, cmac_savetype[CMAC_SAVEGAME], 8); + memcpy(hashdata0 + 0x08, disa, 0x100); + memcpy(hashdata + 0x08, &tid_low, 4); + memcpy(hashdata + 0x0C, &tid_high, 4); + sha_quick(hashdata + 0x10, hashdata0, 0x108, SHA256_MODE); + hashsize = 0x30; + } else if ((cmac_type == CMAC_TITLEDB_SD) || (cmac_type == CMAC_TITLEDB_SYS)) { + memcpy(hashdata + 0x08, &sid, 4); + memcpy(hashdata + 0x0C, disa, 0x100); + hashsize = 0x10C; + } + } + + // calculate CMAC + u8 shasum[32]; + if (!hashdata || !hashsize) return 1; + sha_quick(shasum, hashdata, hashsize, SHA256_MODE); + use_aeskey(keyslot); + aes_cmac(shasum, cmac, 2); + + return 0; +} + +u32 CheckFileCmac(const char* path) { + u8 fcmac[16]; + u8 ccmac[16]; + return ((ReadFileCmac(path, fcmac) == 0) && (CalculateFileCmac(path, ccmac) == 0) && + (memcmp(fcmac, ccmac, 16) == 0)) ? 0 : 1; +} + +u32 FixFileCmac(const char* path) { + u8 ccmac[16]; + return ((CalculateFileCmac(path, ccmac) == 0) && (WriteFileCmac(path, ccmac) == 0)) ? 0 : 1; +} diff --git a/source/nand/nandcmac.h b/source/nand/nandcmac.h new file mode 100644 index 0000000..9a92cae --- /dev/null +++ b/source/nand/nandcmac.h @@ -0,0 +1,10 @@ +#pragma once + +#include "common.h" + +u32 CheckCmacPath(const char* path); +u32 ReadFileCmac(const char* path, u8* cmac); +u32 WriteFileCmac(const char* path, u8* cmac); +u32 CalculateFileCmac(const char* path, u8* cmac); +u32 CheckFileCmac(const char* path); +u32 FixFileCmac(const char* path);