Added GBA VC save dumper / injector

... this also removes the gbavc.sav file from the virtual NAND drives
and does some general cleanup. Fixes #205.
This commit is contained in:
d0k3 2017-09-20 23:41:07 +02:00
parent 73d4ba0ab8
commit c9f9745495
10 changed files with 180 additions and 145 deletions

View File

@ -3,6 +3,7 @@
#include "fatmbr.h"
#include "nand.h"
#include "game.h"
#include "agbsave.h"
#include "keydb.h"
#include "ctrtransfer.h"
#include "scripting.h"
@ -64,6 +65,8 @@ u32 IdentifyFileType(const char* path) {
return GAME_TICKET; // Ticket file (not used for anything right now)
} else if (ValidateFirmHeader((FirmHeader*) data, fsize) == 0) {
return SYS_FIRM; // FIRM file
} else if ((ValidateAgbSaveHeader((AgbSaveHeader*) data) == 0) && (fsize >= AGBSAVE_MAX_SIZE)) {
return SYS_AGBSAVE; // AGBSAVE file
} else if (memcmp(header + 0x100, tickdb_magic, sizeof(tickdb_magic)) == 0) {
return SYS_TICKDB; // ticket.db
} else if (memcmp(header, smdh_magic, sizeof(smdh_magic)) == 0) {

View File

@ -16,15 +16,16 @@
#define GAME_SMDH (1UL<<11)
#define GAME_NDS (1UL<<12)
#define SYS_FIRM (1UL<<13)
#define SYS_TICKDB (1UL<<14)
#define BIN_NCCHNFO (1UL<<15)
#define BIN_TIKDB (1UL<<16)
#define BIN_KEYDB (1UL<<17)
#define BIN_LEGKEY (1UL<<18)
#define TXT_SCRIPT (1UL<<19)
#define TXT_GENERIC (1UL<<20)
#define NOIMG_NAND (1UL<<21)
#define HDR_NAND (1UL<<22)
#define SYS_AGBSAVE (1UL<<14)
#define SYS_TICKDB (1UL<<15)
#define BIN_NCCHNFO (1UL<<16)
#define BIN_TIKDB (1UL<<17)
#define BIN_KEYDB (1UL<<18)
#define BIN_LEGKEY (1UL<<19)
#define TXT_SCRIPT (1UL<<20)
#define TXT_GENERIC (1UL<<21)
#define NOIMG_NAND (1UL<<22)
#define HDR_NAND (1UL<<23)
#define TYPE_BASE 0x00FFFFFF // 24 bit reserved for base types
#define FLAG_ENC (1UL<<28)
@ -53,5 +54,6 @@
#define FTYPE_SCRIPT(tp) (tp&(TXT_SCRIPT))
#define FTYPE_BOOTABLE(tp) (tp&(SYS_FIRM))
#define FTYPE_INSTALLABLE(tp) (tp&(SYS_FIRM))
#define FTPYE_AGBSAVE(tp) (tp&(SYS_AGBSAVE))
u32 IdentifyFileType(const char* path);

View File

@ -837,7 +837,9 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
bool scriptable = (FTYPE_SCRIPT(filetype));
bool bootable = (FTYPE_BOOTABLE(filetype) && !(drvtype & DRV_VIRTUAL));
bool installable = (FTYPE_INSTALLABLE(filetype) && !(drvtype & DRV_VIRTUAL));
bool special_opt = mountable || verificable || decryptable || encryptable || cia_buildable || cia_buildable_legit || cxi_dumpable || tik_buildable || key_buildable || titleinfo || renamable || transferable || hsinjectable || restorable || xorpadable || ebackupable || ncsdfixable || extrcodeable || keyinitable || bootable || scriptable || installable;
bool agbexportable = (FTPYE_AGBSAVE(filetype));
bool agbimportable = (FTPYE_AGBSAVE(filetype) && (drvtype & DRV_VIRTUAL) && (drvtype & DRV_SYSNAND));
bool special_opt = mountable || verificable || decryptable || encryptable || cia_buildable || cia_buildable_legit || cxi_dumpable || tik_buildable || key_buildable || titleinfo || renamable || transferable || hsinjectable || restorable || xorpadable || ebackupable || ncsdfixable || extrcodeable || keyinitable || bootable || scriptable || installable || agbexportable || agbimportable;
char pathstr[32+1];
TruncateString(pathstr, curr_entry->path, 32, 8);
@ -878,6 +880,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
(filetype & GAME_NDS) ? "NDS image options..." :
(filetype & GAME_TICKET)? "Ticket options..." :
(filetype & SYS_FIRM ) ? "FIRM image options..." :
(filetype & SYS_AGBSAVE)? (agbimportable) ? "AGBSAVE options..." : "Dump GBA VC save" :
(filetype & SYS_TICKDB) ? (tik_buildable) ? "Ticket.db options..." : "Mount as ticket.db" :
(filetype & BIN_TIKDB) ? "Titlekey options..." :
(filetype & BIN_KEYDB) ? "AESkeydb options..." :
@ -901,14 +904,17 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
FileHexViewer(curr_entry->path);
GetDirContents(current_dir, current_path);
return 0;
} else if (user_select == textviewer) { // -> show in text viewer
}
else if (user_select == textviewer) { // -> show in text viewer
FileTextViewer(curr_entry->path, scriptable);
return 0;
} else if (user_select == calcsha) { // -> calculate SHA-256
}
else if (user_select == calcsha) { // -> calculate SHA-256
Sha256Calculator(curr_entry->path);
GetDirContents(current_dir, current_path);
return 0;
} else if (user_select == calccmac) { // -> calculate CMAC
}
else if (user_select == calccmac) { // -> calculate CMAC
optionstr[0] = "Check current CMAC only";
optionstr[1] = "Verify CMAC for all";
optionstr[2] = "Fix CMAC for all";
@ -951,7 +957,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
return 0;
}
return FileHandlerMenu(current_path, cursor, scroll, current_dir, clipboard);
} else if (user_select == fileinfo) { // -> show file info
}
else if (user_select == fileinfo) { // -> show file info
FILINFO fno;
if (fvx_stat(curr_entry->path, &fno) != FR_OK) {
ShowPrompt(false, "%s\nFile info failed!", pathstr);
@ -968,10 +975,12 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
(fno.fattrib & AM_RDO) ? 'X' : ' ', (fno.fattrib & AM_HID) ? 'X' : ' ', (fno.fattrib & AM_SYS) ? 'X' : ' ' ,
(fno.fattrib & AM_ARC) ? 'X' : ' ', (fno.fattrib & AM_VRT) ? 'X' : ' ');
return 0;
} else if (user_select == copystd) { // -> copy to OUTPUT_PATH
}
else if (user_select == copystd) { // -> copy to OUTPUT_PATH
StandardCopy(cursor, scroll, current_dir);
return 0;
} else if (user_select == inject) { // -> inject data from clipboard
}
else if (user_select == inject) { // -> inject data from clipboard
char origstr[18 + 1];
TruncateString(origstr, clipboard->entry[0].name, 18, 10);
u64 offset = ShowHexPrompt(0, 8, "Inject data from %s?\nSpecifiy offset below.", origstr);
@ -981,7 +990,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
clipboard->n_entries = 0;
}
return 0;
} else if (user_select == searchdrv) { // -> search drive, open containing path
}
else if (user_select == searchdrv) { // -> search drive, open containing path
char* last_slash = strrchr(curr_entry->path, '/');
if (last_slash) {
snprintf(current_path, last_slash - curr_entry->path + 1, "%s", curr_entry->path);
@ -990,7 +1000,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
*scroll = 0;
}
return 0;
} else if (user_select != special) {
}
else if (user_select != special) {
return 1;
}
@ -1020,6 +1031,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
int install = (installable) ? ++n_opt : -1;
int boot = (bootable) ? ++n_opt : -1;
int script = (scriptable) ? ++n_opt : -1;
int agbexport = (agbexportable) ? ++n_opt : -1;
int agbimport = (agbimportable) ? ++n_opt : -1;
if (mount > 0) optionstr[mount-1] = "Mount image to drive";
if (restore > 0) optionstr[restore-1] = "Restore SysNAND (safe)";
if (ebackup > 0) optionstr[ebackup-1] = "Update embedded backup";
@ -1044,6 +1057,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
if (install > 0) optionstr[install-1] = "Install FIRM";
if (boot > 0) optionstr[boot-1] = "Boot FIRM";
if (script > 0) optionstr[script-1] = "Execute GM9 script";
if (agbexport > 0) optionstr[agbexport-1] = "Dump GBA VC save";
if (agbimport > 0) optionstr[agbimport-1] = "Inject GBA VC save";
// auto select when there is only one option
user_select = (n_opt <= 1) ? n_opt : (int) ShowSelectPrompt(n_opt, optionstr, (n_marked > 1) ?
@ -1070,7 +1085,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
}
}
return 0;
} else if (user_select == decrypt) { // -> decrypt game file
}
else if (user_select == decrypt) { // -> decrypt game file
if (cryptable_inplace) {
optionstr[0] = "Decrypt to " OUTPUT_PATH;
optionstr[1] = "Decrypt inplace";
@ -1122,7 +1138,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
}
}
return 0;
} else if (user_select == encrypt) { // -> encrypt game file
}
else if (user_select == encrypt) { // -> encrypt game file
if (cryptable_inplace) {
optionstr[0] = "Encrypt to " OUTPUT_PATH;
optionstr[1] = "Encrypt inplace";
@ -1165,7 +1182,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
else ShowPrompt(false, "%s\nEncrypted to %s", pathstr, OUTPUT_PATH);
}
return 0;
} else if ((user_select == cia_build) || (user_select == cia_build_legit) || (user_select == cxi_dump)) { // -> build CIA / dump CXI
}
else if ((user_select == cia_build) || (user_select == cia_build_legit) || (user_select == cxi_dump)) { // -> build CIA / dump CXI
char* type = (user_select == cxi_dump) ? "CXI" : "CIA";
bool force_legit = (user_select == cia_build_legit);
if ((n_marked > 1) && ShowPrompt(true, "Try to process all %lu selected files?", n_marked)) {
@ -1202,7 +1220,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
} else ShowPrompt(false, "%s\n%s build failed", pathstr, type);
}
return 0;
} else if (user_select == verify) { // -> verify game / nand file
}
else if (user_select == verify) { // -> verify game / nand file
if ((n_marked > 1) && ShowPrompt(true, "Try to verify all %lu selected files?", n_marked)) {
u32 n_success = 0;
u32 n_other = 0;
@ -1239,7 +1258,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
(VerifyGameFile(curr_entry->path) == 0) ? "success" : "failed");
}
return 0;
} else if ((user_select == tik_build_enc) || (user_select == tik_build_dec)) { // -> (Re)Build titlekey database
}
else if ((user_select == tik_build_enc) || (user_select == tik_build_dec)) { // -> (Re)Build titlekey database
bool dec = (user_select == tik_build_dec);
const char* path_out = (dec) ? OUTPUT_PATH "/" TIKDB_NAME_DEC : OUTPUT_PATH "/" TIKDB_NAME_ENC;
if (BuildTitleKeyInfo(NULL, dec, false) != 0) return 1; // init database
@ -1266,7 +1286,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
} else ShowPrompt(false, "%s\nBuild database %s.", path_out,
(BuildTitleKeyInfo(curr_entry->path, dec, true) == 0) ? "success" : "failed");
return 0;
} else if (user_select == key_build) { // -> (Re)Build AES key database
}
else if (user_select == key_build) { // -> (Re)Build AES key database
const char* path_out = OUTPUT_PATH "/" KEYDB_NAME;
if (BuildKeyDb(NULL, false) != 0) return 1; // init database
ShowString("Building %s...", KEYDB_NAME);
@ -1292,7 +1313,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
} else ShowPrompt(false, "%s\nBuild database %s.", path_out,
(BuildKeyDb(curr_entry->path, true) == 0) ? "success" : "failed");
return 0;
} else if (user_select == rename) { // -> Game file renamer
}
else if (user_select == rename) { // -> Game file renamer
if ((n_marked > 1) && ShowPrompt(true, "Try to rename all %lu selected files?", n_marked)) {
u32 n_success = 0;
ShowProgress(0, 0, "");
@ -1309,11 +1331,13 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
ShowPrompt(false, "%s\nCould not rename\n(Maybe try decrypt?)", pathstr);
}
return 0;
} else if (user_select == show_info) { // -> Show title info
}
else if (user_select == show_info) { // -> Show title info
if (ShowGameFileTitleInfo(curr_entry->path) != 0)
ShowPrompt(false, "Title info: not found");
return 0;
} else if (user_select == hsinject) { // -> Inject to Health & Safety
}
else if (user_select == hsinject) { // -> Inject to Health & Safety
char* destdrv[2] = { NULL };
n_opt = 0;
if (DriveType("1:")) {
@ -1330,13 +1354,15 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
(InjectHealthAndSafety(curr_entry->path, destdrv[user_select-1]) == 0) ? "success" : "failed");
}
return 0;
} else if (user_select == extrcode) { // -> Extract code
}
else if (user_select == extrcode) { // -> Extract code
ShowString("%s\nExtracting .code, please wait...", pathstr);
if (ExtractCodeFromCxiFile(curr_entry->path, NULL) == 0) {
ShowPrompt(false, "%s\n.code extracted to " OUTPUT_PATH, pathstr);
} else ShowPrompt(false, "%s\n.code extract failed", pathstr);
return 0;
} else if (user_select == ctrtransfer) { // -> transfer CTRNAND image to SysNAND
}
else if (user_select == ctrtransfer) { // -> transfer CTRNAND image to SysNAND
char* destdrv[2] = { NULL };
n_opt = 0;
if (DriveType("1:")) {
@ -1355,17 +1381,20 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
}
} else ShowPrompt(false, "%s\nNo valid destination found");
return 0;
} else if (user_select == restore) { // -> restore SysNAND (A9LH preserving)
}
else if (user_select == restore) { // -> restore SysNAND (A9LH preserving)
ShowPrompt(false, "%s\nNAND restore %s", pathstr,
(SafeRestoreNandDump(curr_entry->path) == 0) ? "success" : "failed");
return 0;
} else if (user_select == ncsdfix) { // -> inject sighaxed NCSD
}
else if (user_select == ncsdfix) { // -> inject sighaxed NCSD
ShowPrompt(false, "%s\nRebuild NCSD %s", pathstr,
(FixNandHeader(curr_entry->path, !(filetype == HDR_NAND)) == 0) ? "success" : "failed");
GetDirContents(current_dir, current_path);
InitExtFS(); // this might have fixed something, so try this
return 0;
} else if ((user_select == xorpad) || (user_select == xorpad_inplace)) { // -> build xorpads
}
else if ((user_select == xorpad) || (user_select == xorpad_inplace)) { // -> build xorpads
bool inplace = (user_select == xorpad_inplace);
bool success = (BuildNcchInfoXorpads((inplace) ? current_path : OUTPUT_PATH, curr_entry->path) == 0);
ShowPrompt(false, "%s\nNCCHinfo padgen %s%s", pathstr,
@ -1381,7 +1410,8 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
*cursor = 1;
}
return 0;
} else if (user_select == ebackup) { // -> update embedded backup
}
else if (user_select == ebackup) { // -> update embedded backup
ShowString("%s\nUpdating embedded backup...", pathstr);
bool required = (CheckEmbeddedBackup(curr_entry->path) != 0);
bool success = (required && (EmbedEssentialBackup(curr_entry->path) == 0));
@ -1389,11 +1419,13 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
(success) ? "completed" : "failed!");
GetDirContents(current_dir, current_path);
return 0;
} else if ((user_select == keyinit)) { // -> initialise keys from aeskeydb.bin
}
else if (user_select == keyinit) { // -> initialise keys from aeskeydb.bin
if (ShowPrompt(true, "Warning: Keys are not verified.\nContinue on your own risk?"))
ShowPrompt(false, "%s\nAESkeydb init %s", pathstr, (InitKeyDb(curr_entry->path) == 0) ? "success" : "failed");
return 0;
} else if ((user_select == install)) { // -> install FIRM
}
else if (user_select == install) { // -> install FIRM
size_t firm_size = FileGetSize(curr_entry->path);
u32 slots = 1;
if (GetNandPartitionInfo(NULL, NP_TYPE_FIRM, NP_SUBTYPE_CTR, 1, NAND_SYSNAND) == 0) {
@ -1406,16 +1438,31 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur
if (slots) ShowPrompt(false, "%s (%dkB)\nInstall %s", pathstr, firm_size / 1024,
(SafeInstallFirm(curr_entry->path, slots) == 0) ? "success" : "failed!");
return 0;
} else if ((user_select == boot)) { // -> boot FIRM
}
else if (user_select == boot) { // -> boot FIRM
BootFirmHandler(curr_entry->path, true, false);
return 0;
} else if ((user_select == script)) { // execute script
}
else if (user_select == script) { // execute script
if (ShowPrompt(true, "%s\nWarning: Do not run scripts\nfrom untrusted sources.\n \nExecute script?", pathstr))
ShowPrompt(false, "%s\nScript execute %s", pathstr, ExecuteGM9Script(curr_entry->path) ? "success" : "failure");
GetDirContents(current_dir, current_path);
ClearScreenF(true, true, COLOR_STD_BG);
return 0;
}
else if (user_select == agbexport) { // export GBA VC save
if (DumpGbaVcSavegame(curr_entry->path) == 0)
ShowPrompt(false, "Savegame dumped to " OUTPUT_PATH);
else ShowPrompt(false, "Savegame dump failed!");
return 0;
}
else if (user_select == agbimport) { // import GBA VC save
if (clipboard->n_entries != 1) {
ShowPrompt(false, "GBA VC savegame has to\nbe in the clipboard.");
} else ShowPrompt(false, "Savegame inject %s",
(InjectGbaVcSavegame(curr_entry->path, clipboard->entry[0].path) == 0) ? "success" : "failed!");
return 0;
}
return FileHandlerMenu(current_path, cursor, scroll, current_dir, clipboard);
}
@ -1459,14 +1506,16 @@ u32 HomeMoreMenu(char* current_path, DirStruct* current_dir, DirStruct* clipboar
InitExtFS();
GetDirContents(current_dir, current_path);
return 0;
} else if (user_select == bonus) { // setup bonus drive
}
else if (user_select == bonus) { // setup bonus drive
if (clipboard->n_entries && (DriveType(clipboard->entry[0].path) & (DRV_BONUS|DRV_IMAGE)))
clipboard->n_entries = 0; // remove bonus drive clipboard entries
if (!SetupBonusDrive()) ShowPrompt(false, "Setup failed!");
ClearScreenF(true, true, COLOR_STD_BG);
GetDirContents(current_dir, current_path);
return 0;
} else if (user_select == multi) { // switch EmuNAND offset
}
else if (user_select == multi) { // switch EmuNAND offset
while (ShowPrompt(true, "Current EmuNAND offset is %06X.\nSwitch to next offset?", GetEmuNandBase())) {
if (clipboard->n_entries && (DriveType(clipboard->entry[0].path) & DRV_EMUNAND))
clipboard->n_entries = 0; // remove EmuNAND clipboard entries
@ -1476,7 +1525,8 @@ u32 HomeMoreMenu(char* current_path, DirStruct* current_dir, DirStruct* clipboar
}
GetDirContents(current_dir, current_path);
return 0;
} else if (user_select == bsupport) { // build support files
}
else if (user_select == bsupport) { // build support files
bool tik_enc_sys = false;
bool tik_enc_emu = false;
if (BuildTitleKeyInfo(NULL, false, false) == 0) {
@ -1510,7 +1560,8 @@ u32 HomeMoreMenu(char* current_path, DirStruct* current_dir, DirStruct* clipboar
SEEDDB_NAME, seed_sys ? seed_emu ? "OK (Sys&Emu)" : "OK (Sys)" : "Failed");
GetDirContents(current_dir, current_path);
return 0;
} else if (user_select == hsrestore) { // restore Health & Safety
}
else if (user_select == hsrestore) { // restore Health & Safety
n_opt = 0;
int sys = (CheckHealthAndSafetyInject("1:") == 0) ? (int) ++n_opt : -1;
int emu = (CheckHealthAndSafetyInject("4:") == 0) ? (int) ++n_opt : -1;
@ -1522,7 +1573,8 @@ u32 HomeMoreMenu(char* current_path, DirStruct* current_dir, DirStruct* clipboar
GetDirContents(current_dir, current_path);
return 0;
}
} else if (user_select == clock) { // RTC clock setter
}
else if (user_select == clock) { // RTC clock setter
DsTime dstime;
get_dstime(&dstime);
if (ShowRtcSetterPrompt(&dstime, "Set RTC date&time:")) {
@ -1533,7 +1585,8 @@ u32 HomeMoreMenu(char* current_path, DirStruct* current_dir, DirStruct* clipboar
timestr);
}
return 0;
} else if (user_select == sysinfo) { // Myria's system info
}
else if (user_select == sysinfo) { // Myria's system info
char* sysinfo_txt = (char*) TEMP_BUFFER;
MyriaSysinfo(sysinfo_txt);
MemTextViewer(sysinfo_txt, strnlen(sysinfo_txt, TEMP_BUFFER_SIZE), false);

View File

@ -9,7 +9,7 @@ u32 ValidateAgbSaveHeader(AgbSaveHeader* header) {
// basic checks
if ((memcmp(header->magic, magic, sizeof(magic)) != 0) ||
(header->unknown0 != 1) || (header->save_start != 0x200) ||
(header->save_size > AGBSAVE_MAX_SSIZE))
(header->save_size > AGBSAVE_MAX_SSIZE) || !(GBASAVE_VALID(header->save_size)))
return 1;
// reserved area checks
@ -21,72 +21,3 @@ u32 ValidateAgbSaveHeader(AgbSaveHeader* header) {
// all fine if arriving here
return 0;
}
u32 LoadAgbSave(u32 nand_src, u8* agbsave, u32 max_size, NandPartitionInfo* info, bool header_only) {
AgbSaveHeader* header = (AgbSaveHeader*) agbsave;
// need at least room for the header
if (max_size < sizeof(AgbSaveHeader)) return 1;
// load the header
if ((GetNandPartitionInfo(info, NP_TYPE_AGB, NP_SUBTYPE_CTR, 0, nand_src) != 0) ||
(ReadNandSectors(agbsave, info->sector, 1, info->keyslot, nand_src) != 0) ||
(ValidateAgbSaveHeader(header) != 0) ||
(sizeof(AgbSaveHeader) + header->save_size > info->count * 0x200))
return 1;
// done if we only want the header
if (header_only) return 0;
// load the savegame
if ((sizeof(AgbSaveHeader) + header->save_size > max_size) ||
(ReadNandBytes(agbsave + 0x200, (info->sector+1) * 0x200, header->save_size, info->keyslot, nand_src) != 0))
return 1;
return 0;
}
u32 GetAgbSaveSize(u32 nand_src) {
AgbSaveHeader* header = (AgbSaveHeader*) NAND_BUFFER;
NandPartitionInfo info;
if (LoadAgbSave(nand_src, NAND_BUFFER, NAND_BUFFER_SIZE, &info, true) != 0)
return 1;
return header->save_size; // it's recommended to also check the CMAC
}
u32 CheckAgbSaveCmac(u32 nand_src) {
u8* agbsave = (u8*) NAND_BUFFER;
AgbSaveHeader* header = (AgbSaveHeader*) agbsave;
NandPartitionInfo info;
if (LoadAgbSave(nand_src, agbsave, NAND_BUFFER_SIZE, &info, false) != 0)
return 1;
u8 cmac[16] __attribute__((aligned(32)));
u8 shasum[32];
sha_quick(shasum, agbsave + 0x30, (0x200 - 0x30) + header->save_size, SHA256_MODE);
use_aeskey(0x24);
aes_cmac(shasum, cmac, 2);
return (memcmp(cmac, header->cmac, 16) == 0) ? 0 : 1;
}
u32 FixAgbSaveCmac(u32 nand_dst) {
u8* agbsave = (u8*) NAND_BUFFER;
AgbSaveHeader* header = (AgbSaveHeader*) agbsave;
NandPartitionInfo info;
if (LoadAgbSave(nand_dst, agbsave, NAND_BUFFER_SIZE, &info, false) != 0)
return 1;
u8 cmac[16] __attribute__((aligned(32)));
u8 shasum[32];
sha_quick(shasum, agbsave + 0x30, (0x200 - 0x30) + header->save_size, SHA256_MODE);
use_aeskey(0x24);
aes_cmac(shasum, cmac, 2);
memcpy(header->cmac, cmac, 16);
// set CFG_BOOTENV to 0x7 so the save is taken over
// https://www.3dbrew.org/wiki/CONFIG_Registers#CFG_BOOTENV
*(u32*) 0x10010000 = 0x7;
return (WriteNandSectors(header, info.sector, 1, info.keyslot, nand_dst) == 0) ? 0 : 1;
}

View File

@ -7,6 +7,19 @@
#define AGBSAVE_MAX_SIZE (0x000180 * 0x200) // standard size of the NAND partition
#define AGBSAVE_MAX_SSIZE (AGBSAVE_MAX_SIZE - sizeof(AgbSaveHeader))
// see: http://3dbrew.org/wiki/3DS_Virtual_Console#GBA_VC
#define GBASAVE_EEPROM_512 (512)
#define GBASAVE_EEPROM_8K (8 * 1024)
#define GBASAVE_SRAM_32K (32 * 1024)
#define GBASAVE_FLASH_64K (64 * 1024)
#define GBASAVE_FLASH_128K (128 * 1024)
#define GBASAVE_VALID(size) \
(((size) == GBASAVE_EEPROM_512) || \
((size) == GBASAVE_EEPROM_8K) || \
((size) == GBASAVE_SRAM_32K) || \
((size) == GBASAVE_FLASH_64K) || \
((size) == GBASAVE_FLASH_128K))
// see: http://3dbrew.org/wiki/3DS_Virtual_Console#NAND_Savegame
typedef struct {
u8 magic[4]; // ".SAV"
@ -26,6 +39,3 @@ typedef struct {
} __attribute__((packed)) AgbSaveHeader;
u32 ValidateAgbSaveHeader(AgbSaveHeader* header);
u32 GetAgbSaveSize(u32 nand_src);
u32 CheckAgbSaveCmac(u32 nand_src);
u32 FixAgbSaveCmac(u32 nand_dst);

View File

@ -534,19 +534,6 @@ u32 GetNandPartitionInfo(NandPartitionInfo* info, u32 type, u32 subtype, u32 ind
return 0;
}
// OTP hash is 32 byte in size
u32 GetOtpHash(void* hash) {
if (!CheckSector0x96Crypto()) return 1;
memcpy(hash, OtpSha256, 0x20);
return 0;
}
// NAND CID is 16 byte in size
u32 GetNandCid(void* cid) {
sdmmc_get_cid(1, (u32*) cid);
return 0;
}
bool CheckMultiEmuNand(void)
{
// this only checks for the theoretical possibility

View File

@ -76,9 +76,6 @@ u32 GetNandNcsdPartitionInfo(NandPartitionInfo* info, u32 type, u32 subtype, u32
u32 GetNandPartitionInfo(NandPartitionInfo* info, u32 type, u32 subtype, u32 index, u32 nand_src);
u32 ValidateSecretSector(u8* sector);
u32 GetOtpHash(void* hash);
u32 GetNandCid(void* cid);
bool CheckMultiEmuNand(void);
u32 AutoEmuNandBase(bool reset);
u32 GetEmuNandBase(void);

View File

@ -3,11 +3,14 @@
#include "firm.h"
#include "fatmbr.h"
#include "essentials.h" // for essential backup struct
#include "nandcmac.h"
#include "agbsave.h"
#include "image.h"
#include "fsinit.h"
#include "fsperm.h"
#include "sighax.h"
#include "unittype.h"
#include "sdmmc.h"
#include "sha.h"
#include "ui.h"
#include "vff.h"
@ -81,7 +84,7 @@ u32 BuildEssentialBackup(const char* path, EssentialBackup* essential) {
InitImgFS(path_bak);
// fill nand cid / otp hash
if (GetNandCid(&(essential->nand_cid)) != 0) return 1;
sdmmc_get_cid(1, (u32*) (void*) &(essential->nand_cid));
if (!IS_UNLOCKED) memset(&(filelist[5]), 0, 3 * sizeof(ExeFsFileHeader));
else memcpy(&(essential->otp), (u8*) 0x10012000, 0x100);
@ -117,6 +120,62 @@ u32 EmbedEssentialBackup(const char* path) {
return 0;
}
u32 DumpGbaVcSavegame(const char* path) {
if (TEMP_BUFFER_SIZE < AGBSAVE_MAX_SIZE) return 1;
AgbSaveHeader* agbsave = (AgbSaveHeader*) (void*) TEMP_BUFFER;
u8* savegame = (u8*) (agbsave + 1);
// read full AGBsave to memory
if ((fvx_qread(path, agbsave, 0, sizeof(AgbSaveHeader), NULL) != FR_OK) || (ValidateAgbSaveHeader(agbsave) != 0) ||
(fvx_qread(path, savegame, sizeof(AgbSaveHeader), agbsave->save_size, NULL) != FR_OK)) return 1; // not a proper AGBSAVE file
// byteswap for eeprom type saves (512 byte / 8 kB)
if ((agbsave->save_size == GBASAVE_EEPROM_512) || (agbsave->save_size == GBASAVE_EEPROM_8K)) {
for (u8* ptr = savegame; (ptr - savegame) < (int) agbsave->save_size; ptr += 8)
*(u64*) (void*) ptr = getbe64(ptr);
}
// generate output path
char path_vcsav[64];
snprintf(path_vcsav, 64, OUTPUT_PATH "/%016llX.gbavc.sav", agbsave->title_id);
if (fvx_qwrite(path_vcsav, savegame, 0, agbsave->save_size, NULL) != FR_OK) return 1; // write fail
return 0;
}
u32 InjectGbaVcSavegame(const char* path, const char* path_vcsave) {
if (TEMP_BUFFER_SIZE < AGBSAVE_MAX_SIZE) return 1;
AgbSaveHeader* agbsave = (AgbSaveHeader*) (void*) TEMP_BUFFER;
u8* savegame = (u8*) (agbsave + 1);
// basic sanity checks for path_vcsave
FILINFO fno;
char* ext = strrchr(path_vcsave, '.');
if (!ext || (strncasecmp(++ext, "sav", 4) != 0)) return 1; // bad extension
if ((fvx_stat(path_vcsave, &fno) != FR_OK) || !GBASAVE_VALID(fno.fsize))
return 1; // bad size
// read AGBsave header, savegame to memory
if ((fvx_qread(path, agbsave, 0, sizeof(AgbSaveHeader), NULL) != FR_OK) || (ValidateAgbSaveHeader(agbsave) != 0) ||
(fvx_qread(path_vcsave, savegame, 0, agbsave->save_size, NULL) != FR_OK)) return 1; // not a proper savegame for header
// byteswap for eeprom type saves (512 byte / 8 kB)
if ((agbsave->save_size == GBASAVE_EEPROM_512) || (agbsave->save_size == GBASAVE_EEPROM_8K)) {
for (u8* ptr = savegame; (ptr - savegame) < (int) agbsave->save_size; ptr += 8)
*(u64*) (void*) ptr = getbe64(ptr);
}
// rewrite AGBSAVE file, fix CMAC
if (fvx_qwrite(path, agbsave, 0, sizeof(AgbSaveHeader) + agbsave->save_size, NULL) != FR_OK) return 1; // write fail
if (FixFileCmac(path) != 0) return 1; // cmac fail (this is not efficient, but w/e)
// set CFG_BOOTENV to 0x7 so the save is taken over
// https://www.3dbrew.org/wiki/CONFIG_Registers#CFG_BOOTENV
if (strncasecmp(path, "S:/agbsave.bin", 256) == 0) *(u32*) 0x10010000 = 0x7;
return 0;
}
u32 RebuildNandNcsdHeader(NandNcsdHeader* ncsd) {
// signature (retail or dev)
u8* signature = (IS_DEVKIT) ? sig_nand_ncsd_dev : sig_nand_ncsd_retail;

View File

@ -8,3 +8,5 @@ u32 FixNandHeader(const char* path, bool check_size);
u32 ValidateNandDump(const char* path);
u32 SafeRestoreNandDump(const char* path);
u32 SafeInstallFirm(const char* path, u32 slots);
u32 DumpGbaVcSavegame(const char* path);
u32 InjectGbaVcSavegame(const char* path, const char* path_vcsave);

View File

@ -4,9 +4,8 @@
#include "essentials.h"
#include "unittype.h"
#define VFLAG_MBR (1UL<<27)
#define VFLAG_ESSENTIAL (1UL<<28)
#define VFLAG_GBA_VC (1UL<<29)
#define VFLAG_MBR (1UL<<28)
#define VFLAG_ESSENTIAL (1UL<<29)
#define VFLAG_NEEDS_OTP (1UL<<30)
#define VFLAG_NAND_SIZE (1UL<<31)
@ -27,7 +26,6 @@ static const VirtualNandTemplate vNandTemplates[] = {
{ "twln.bin" , NP_TYPE_FAT , NP_SUBTYPE_TWL , 0, 0 },
{ "twlp.bin" , NP_TYPE_FAT , NP_SUBTYPE_TWL , 1, 0 },
{ "agbsave.bin" , NP_TYPE_AGB , NP_SUBTYPE_CTR , 0, VFLAG_DELETABLE },
{ "gbavc.sav" , NP_TYPE_AGB , NP_SUBTYPE_CTR , 0, VFLAG_DELETABLE | VFLAG_GBA_VC },
{ "firm0.bin" , NP_TYPE_FIRM , NP_SUBTYPE_CTR , 0, 0 },
{ "firm1.bin" , NP_TYPE_FIRM , NP_SUBTYPE_CTR , 1, 0 },
{ "firm2.bin" , NP_TYPE_FIRM , NP_SUBTYPE_CTR , 2, 0 },
@ -73,7 +71,7 @@ bool ReadVNandDir(VirtualFile* vfile, VirtualDir* vdir) { // uses a generic vdir
// handle special cases
if (!vfile->size) continue;
if ((nand_src == VRT_XORPAD) && ((vfile->keyslot == 0x11) || (vfile->keyslot >= 0x40) || (vfile->flags & VFLAG_GBA_VC)))
if ((nand_src == VRT_XORPAD) && ((vfile->keyslot == 0x11) || (vfile->keyslot >= 0x40)))
continue;
if ((vfile->keyslot == 0x05) && !CheckSlot0x05Crypto())
continue; // keyslot 0x05 not properly set up
@ -90,12 +88,6 @@ bool ReadVNandDir(VirtualFile* vfile, VirtualDir* vdir) { // uses a generic vdir
if (memcmp(data, magic, sizeof(magic)) != 0) continue;
vfile->size = sizeof(EssentialBackup);
}
if (vfile->flags & VFLAG_GBA_VC) {
if (CheckAgbSaveCmac(nand_src) != 0) continue;
vfile->offset += 0x200;
vfile->size = GetAgbSaveSize(nand_src);
}
// found if arriving here
vfile->flags |= nand_src;
@ -113,7 +105,6 @@ int ReadVNandFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 count)
int WriteVNandFile(const VirtualFile* vfile, const void* buffer, u64 offset, u64 count) {
u32 nand_dst = vfile->flags & (VRT_SYSNAND|VRT_EMUNAND|VRT_IMGNAND|VRT_XORPAD);
int res = WriteNandBytes(buffer, vfile->offset + offset, count, vfile->keyslot, nand_dst);
if ((res == 0) && (vfile->flags & VFLAG_GBA_VC)) res = FixAgbSaveCmac(nand_dst);
return res;
}