From f477da087ca77cf10088b3753958054b5da104ae Mon Sep 17 00:00:00 2001 From: ZeroSkill <18742654+ZeroSkill1@users.noreply.github.com> Date: Sat, 23 May 2026 13:13:22 +0300 Subject: [PATCH] Add support for decrypting gamecart saves (#963) This also fixes #958 Original commits: * initial work on cart save crypto/wearleveling * add support for n3ds only save crypto * use existing crc16 impl for save blockmap * fix missing aeskey select for card2 save decrypt * fix old/new cart media being swapped * fix 1 MiB save chips / blockmap type 2 * move cart save wearleveling/crypto to separate file * try 2nd wearlevel header on fail + minor fixes * save_ctr: zero out savectx before reading fallback header --- arm9/source/crypto/aes.h | 2 + arm9/source/game/ncsd.h | 3 +- arm9/source/gamecart/gamecart.c | 12 +- arm9/source/gamecart/protocol.c | 10 +- arm9/source/gamecart/save_ctr.c | 490 ++++++++++++++++++++++++++++++++ arm9/source/gamecart/save_ctr.h | 6 + arm9/source/godmode.c | 7 +- arm9/source/virtual/vcart.c | 43 ++- 8 files changed, 553 insertions(+), 20 deletions(-) create mode 100644 arm9/source/gamecart/save_ctr.c create mode 100644 arm9/source/gamecart/save_ctr.h diff --git a/arm9/source/crypto/aes.h b/arm9/source/crypto/aes.h index df3672f..2fe614a 100644 --- a/arm9/source/crypto/aes.h +++ b/arm9/source/crypto/aes.h @@ -40,6 +40,8 @@ extern "C" { #define AES_CNT_FLUSH_WRITE 0x00000400u #define AES_CNT_CTRNAND_MODE (AES_CTR_MODE | AES_CNT_INPUT_ORDER | AES_CNT_OUTPUT_ORDER | AES_CNT_INPUT_ENDIAN | AES_CNT_OUTPUT_ENDIAN) +// gamecard save crypto and NAND crypto use the same options for AES-CTR. +#define AES_CNT_CART_SAVE_MODE AES_CNT_CTRNAND_MODE #define AES_CNT_TWLNAND_MODE AES_CTR_MODE #define AES_CNT_TITLEKEY_DECRYPT_MODE (AES_CBC_DECRYPT_MODE | AES_CNT_INPUT_ORDER | AES_CNT_OUTPUT_ORDER | AES_CNT_INPUT_ENDIAN | AES_CNT_OUTPUT_ENDIAN) #define AES_CNT_TITLEKEY_ENCRYPT_MODE (AES_CBC_ENCRYPT_MODE | AES_CNT_INPUT_ORDER | AES_CNT_OUTPUT_ORDER | AES_CNT_INPUT_ENDIAN | AES_CNT_OUTPUT_ENDIAN) diff --git a/arm9/source/game/ncsd.h b/arm9/source/game/ncsd.h index 0a9801b..d5cf806 100644 --- a/arm9/source/game/ncsd.h +++ b/arm9/source/game/ncsd.h @@ -33,7 +33,8 @@ typedef struct { u8 sector_zero_offset[0x4]; u8 partition_flags[8]; u8 partitionId_table[8][8]; - u8 reserved[0x30]; + u8 reserved[0x2F]; + u8 extra_save_keysel; } PACKED_STRUCT NcsdHeader; u32 ValidateNcsdHeader(NcsdHeader* header); diff --git a/arm9/source/gamecart/gamecart.c b/arm9/source/gamecart/gamecart.c index b6577ed..6be0642 100644 --- a/arm9/source/gamecart/gamecart.c +++ b/arm9/source/gamecart/gamecart.c @@ -80,8 +80,9 @@ u32 GetCartInfoString(char* info, size_t info_size, CartData* cdata) { "Product Code : %.10s\n" "Revision : %lu\n" "Cart ID : %08lX\n" + "Cart ID2 : %08lX\n" "Platform : %s\n", - ncsd->mediaId, ncch->productcode, cdata_i->rom_version, cdata_i->cart_id, + ncsd->mediaId, ncch->productcode, cdata_i->rom_version, cdata_i->cart_id, cdata_i->cart_id2, (ncch->flags[4] == 0x2) ? "N3DS" : "O3DS"); } else if (cdata->cart_type & CART_NTR) { CartDataNtrTwl* cdata_i = (CartDataNtrTwl*)cdata; @@ -220,9 +221,10 @@ u32 InitCartRead(CartData* cdata) { memcpy(priv_header + 0x44, &(cdata->cart_id2), 4); memset(priv_header + 0x48, 0xFF, 8); - // save data + bool is_card2 = cdata->cart_id & 0x8000000; u32 card2_offset = getle32(cdata->header + 0x200); - if (card2_offset != 0xFFFFFFFF) { + + if (is_card2 && card2_offset != 0xFFFFFFFF) { cdata->save_type = CARD_SAVE_CARD2; cdata->save_size = GetCtrCartSaveSize(cdata); // Sanity checks @@ -433,11 +435,11 @@ u32 ReadCartPrivateHeader(void* buffer, u64 offset, u64 count, CartData* cdata) } u32 ReadCartInfo(u8* buffer, u64 offset, u64 count, CartData* cdata) { - char info[256]; + char info[301]; u32 len; GetCartInfoString(info, sizeof(info), cdata); - len = strnlen(info, 255); + len = strnlen(info, 300); if (offset >= len) return 0; if (offset + count > len) count = len - offset; diff --git a/arm9/source/gamecart/protocol.c b/arm9/source/gamecart/protocol.c index 718ccff..07533b0 100644 --- a/arm9/source/gamecart/protocol.c +++ b/arm9/source/gamecart/protocol.c @@ -211,10 +211,12 @@ void Cart_Secure_Init(u32 *buf, u32 *out) CTR_SendCommand(C5_cmd, 0, 1, 0x100002C, NULL); } - for (int i = 0; i < 5; ++i) { - CTR_SendCommand(A2_cmd, 4, 1, 0x701002C, &test); - ARM_WaitCycles(0xF0000 * 8); - } + // Process9 (or at least the latest version of it, in NATIVE_FIRM v32673) does not use this in its init routine. + // leaving this in adds an unnecessary 2.4-2.6s of init time. + //for (int i = 0; i < 5; ++i) { + // CTR_SendCommand(A2_cmd, 4, 1, 0x701002C, &test); + // ARM_WaitCycles(0xF0000 * 8); + //} } void Cart_Dummy(void) { diff --git a/arm9/source/gamecart/save_ctr.c b/arm9/source/gamecart/save_ctr.c new file mode 100644 index 0000000..96719d7 --- /dev/null +++ b/arm9/source/gamecart/save_ctr.c @@ -0,0 +1,490 @@ +#include "aes.h" +#include "gamecart.h" +#include "ncch.h" +#include "ncsd.h" +#include "crc16.h" +#include "sha.h" +#include "types.h" +#include "unittype.h" +#include + +typedef struct SaveBlockmapHeader { + u32 flushcount; // how many times the blockmap was entirely rewritten + u32 common_min_remap_count; // every physical sector in the flash has been rewritten at least `common_min_remap_count` times + // that is, the total remap count for physical sector x representing virtual sector n = blockmap[n].remap_count + common_min_remap_count +} SaveBlockmapHeader; + +typedef struct SaveBlockmapEntryV1 { + struct { + u8 phys_sec : 7; // physical sector representing this virtual sector + u8 used : 1; // whether or not this virtual sector is in use + }; + + u8 remap_count; // amount of times phys_sec has been rewritten + + u8 checksums[8]; // checksum values (if applicable) for each 0x200 sized sector in the full 0x1000-sized physical flash sector this structure covers +} SaveBlockmapEntryV1; + +typedef struct SaveBlockmapEntryV2 { + struct { + u8 remap_count : 7; // amount of times phys_sec has been rewritten + u8 used : 1; // whether or not this virtual sector is in use + }; + u8 phys_sec; // physical sector representing this virtual sector +} SaveBlockmapEntryV2; + +typedef struct SaveBlockmapEntry { + u8 remap_count; // how many times phys_sec was remapped (written to) + u8 used; // whether or not this virtual sector is used + u8 phys_sec; // the physical sector this virtual sector is mapped to + u8 checksums[8]; // checksum values (if applicable) for each 0x200 sized sector in the full 0x1000-sized physical flash sector this structure covers +} SaveBlockmapEntry; + +typedef struct SaveJournalEntryData +{ + u8 src_virt_sec; // virtual sector that is being remapped + u8 dst_virt_sec; // virtual sector whose corresponding physical sector was used to remap src_virt_sec to + u8 dst_phys_sec; // the physical sector of dst_virt_sec the data of src_virt_sec now resides in + u8 src_phys_sec; // the previous physical sector where the data in src_virt_sec resided in before the remapping + u8 new_dst_remap_count; // the new remap count (including this remap write) of the destination physical sector + u8 src_remap_count; // the remap count of the source physical sector (unchanged) + u8 new_dst_checksums[8]; // checksums for the data that was written to the destination physical sector, pending to be written to blockmap +} SaveJournalEntryData; + +typedef struct SaveJournalEntry +{ + SaveJournalEntryData main; + SaveJournalEntryData backup; // compared to main to check for integrity before applying to blockmap + u32 pad; +} SaveJournalEntry; + +typedef enum CardSaveWearLevelingType { + CARD_SAVE_WEAR_LEVELING_V1 = 10, + CARD_SAVE_WEAR_LEVELING_V2 = 2, + + CARD_SAVE_WEAR_LEVELING_NONE = -1, +} CardSaveWearLevelingType; + +typedef enum CardSaveCryptoType { + CARD_SAVE_CRYPTO_V0 = 0, + CARD_SAVE_CRYPTO_V1 = 1, + CARD_SAVE_CRYPTO_V1_N3DS = 2, + CARD_SAVE_CRYPTO_V2 = 3, + + CARD_SAVE_CRYPTO_INVALID = 0x7FFFFFFF, +} CardSaveCryptoType; + +typedef enum CardSaveCryptoKeyslot { + CARD_SAVE_CMAC_KEYSLOT_O3DS = 0x33, + CARD_SAVE_CMAC_KEYSLOT_N3DS = 0x19, + CARD_SAVE_CRYPTO_KEYSLOT_O3DS = 0x37, + CARD_SAVE_CRYPTO_KEYSLOT_N3DS = 0x1A, +} CardSaveCryptoKeyslot; + +typedef struct CartSaveContext { + // wear leveling + + /* min. 87, max 118 journal entries */ + SaveJournalEntry journal[118]; + /* max case is header + 127 v1 blockmap entries + crc16 */ + u8 blockmap[sizeof(SaveBlockmapHeader) + 127 * sizeof(SaveBlockmapEntryV1) + 2]; + + CardSaveWearLevelingType type; + u32 num_journal_entries; // number of journal entries + u32 num_blockmap_sectors; // number of sectors covered by the blockmap (nsectors - 1) + u32 blockmap_size; // size of the blockmap, in bytes + u32 logical_size; // actual number of available data sectors for the DISA save image (nsectors - 2) + + // crypto + + u32 crypto_type; + bool repeating_ctr; + +} CartSaveContext; + +static CartSaveContext savectx; + +// wear leveling + +static void SetBlockmapEntry(u32 index, SaveBlockmapEntry *in_entry) { + u8 *base_ptr = &savectx.blockmap[sizeof(SaveBlockmapHeader)]; + if (savectx.type == CARD_SAVE_WEAR_LEVELING_V2) { + SaveBlockmapEntryV2 *entry = &((SaveBlockmapEntryV2 *)base_ptr)[index]; + entry->phys_sec = in_entry->phys_sec; + entry->remap_count = in_entry->remap_count; + entry->used = in_entry->used; + } else { + SaveBlockmapEntryV1 *entry = &((SaveBlockmapEntryV1 *)base_ptr)[index]; + entry->phys_sec = in_entry->phys_sec; + entry->remap_count = in_entry->remap_count; + entry->used = in_entry->used; + memcpy(entry->checksums, in_entry->checksums, sizeof(entry->checksums)); + } +} + +static SaveBlockmapEntry GetBlockmapEntry(u32 index) { + u8 *base_ptr = &savectx.blockmap[sizeof(SaveBlockmapHeader)]; + if (savectx.type == CARD_SAVE_WEAR_LEVELING_V2) { + SaveBlockmapEntryV2 *entries = (SaveBlockmapEntryV2 *)base_ptr; + return (SaveBlockmapEntry) { .checksums = { 0 }, .phys_sec = entries[index].phys_sec, .remap_count = entries[index].remap_count, .used = entries[index].used }; + } else { + SaveBlockmapEntryV1 *entries = (SaveBlockmapEntryV1 *)base_ptr; + SaveBlockmapEntry out = (SaveBlockmapEntry) { .phys_sec = entries[index].phys_sec, .remap_count = entries[index].remap_count, .used = entries[index].used }; + memcpy(out.checksums, entries[index].checksums, sizeof(out.checksums)); + return out; + } +} + +static void ApplyJournalEntryToBlockmap(SaveJournalEntry *entry) { + SaveBlockmapEntry ent = { + .used = 1, + .phys_sec = entry->main.dst_phys_sec, + .remap_count = entry->main.new_dst_remap_count + }; + memcpy(ent.checksums, entry->main.new_dst_checksums, sizeof(ent.checksums)); + + // remap the virtual sector to the new physical sector + SetBlockmapEntry(entry->main.src_virt_sec, &ent); + + // unless an actual remap didn't occur, we must mark (the physical sector + // whose virtual sector number we used for remapping this virtual sector to) + // as free and ready to use for another remap + if (entry->main.dst_phys_sec != entry->main.src_phys_sec) { + ent.used = 0; + ent.phys_sec = entry->main.src_phys_sec; + ent.remap_count = entry->main.src_remap_count; + memset(ent.checksums, 0, sizeof(ent.checksums)); + SetBlockmapEntry(entry->main.dst_virt_sec, &ent); + } +} + +static u32 InitSaveWearLeveling(CartData *cdata, u32 header_offset) { + // CARD2 does not implement wear leveling for the writable portion of the "ROM" + if (cdata->cart_id & 0x8000000) { + savectx.type = CARD_SAVE_WEAR_LEVELING_NONE; + return 0; + } + + // SPI flash savedata implements wear leveling + u32 num_physical_sectors = cdata->save_size / 0x1000; + // for some reason, the blockmap also covers the sector for the backup blockmap + journal header, + // despite the logical size calculation (see below) removing this sector from the available logical space + savectx.num_blockmap_sectors = num_physical_sectors - 1; + // the blockmap + journal header (combined size 0x1000) exists twice, the first being the main one and the second being the "backup" one. + // therefore, the actual number of usable data sectors for save data is num_sectors - 2. + savectx.logical_size = (num_physical_sectors - 2) * 0x1000; + + if (num_physical_sectors > 128) { + // V2, header + blockmap_entries_v2[511] + crc + savectx.blockmap_size = 0x400; // fixed size + savectx.type = CARD_SAVE_WEAR_LEVELING_V2; + } else { + // V1, header + blockmap_entries_v1[n] + crc + savectx.blockmap_size = sizeof(SaveBlockmapHeader) + savectx.num_blockmap_sectors * sizeof(SaveBlockmapEntryV1) + sizeof(u16); + savectx.type = CARD_SAVE_WEAR_LEVELING_V1; + } + + // TODO: add CMAC verification ability for DISA + + if (ReadCartSave(savectx.blockmap, header_offset, savectx.blockmap_size, cdata) != 0) + return 1; + + u32 journal_size = 0x1000 - savectx.blockmap_size; + savectx.num_journal_entries = journal_size / sizeof(SaveJournalEntry); + + if (ReadCartSave((u8 *)savectx.journal, header_offset + savectx.blockmap_size, journal_size, cdata) != 0) + return 1; + + u8 *crcLoc = &savectx.blockmap[savectx.blockmap_size - 2]; + u16 expected_bmap_crc = crcLoc[0] | crcLoc[1] << 8; + u16 calc_bmap_crc = crc16_quick(savectx.blockmap, savectx.blockmap_size - 2); + if (expected_bmap_crc != calc_bmap_crc) { + return 1; + } + + u32 journal_idx = 0; + + for (; journal_idx < savectx.num_journal_entries; journal_idx++) { + SaveJournalEntry *ent = &savectx.journal[journal_idx]; + + // journal entry should point to a valid physical sector + if (ent->main.dst_phys_sec) { + // reached the end of the journal + if (ent->main.src_virt_sec >= savectx.num_blockmap_sectors) + break; + + ApplyJournalEntryToBlockmap(ent); + } + } + + u8 blank[sizeof(SaveJournalEntry)]; + memset(blank, 0xFF, sizeof(blank)); + u32 num_bad_journal_entries = 0; + for (; journal_idx < savectx.num_journal_entries; journal_idx++) { + num_bad_journal_entries += memcmp((u8 *)&savectx.journal[journal_idx], blank, sizeof(SaveJournalEntry)) != 0; + } + + if (num_bad_journal_entries) { + return 1; + } + + return 0; +} + +// crypto + +static u32 InitCtrCardSaveCryptoKey(CartData *cdata) { + NcsdHeader *ncsd = (NcsdHeader *) (void *) cdata->header; + // save data crypto (if supported) + u32 save_media_old = ncsd->partition_flags[7]; + u32 save_media_new = ncsd->partition_flags[3]; + u8 save_crypto_keysel_base = ncsd->partition_flags[1]; + u8 save_crypto_keysel_extra = ncsd->extra_save_keysel; + s32 save_crypto_keysel = 0; + bool is_card2 = cdata->cart_id & 0x8000000; + + bool supported_save_crypto = + (save_media_old == 0 && save_media_new == 0) || /* SPI save flash (very old carts) */ + (save_media_old == 1) || /* old SPI flash */ + (save_media_old == 0 && save_media_new == 1) || /* newer SPI flash */ + is_card2 /* CARD2 */; + + // skip everything if there isn't save data or its crypto is not supported + if (cdata->save_type == CARD_SAVE_NONE || !supported_save_crypto) { + savectx.crypto_type = CARD_SAVE_CRYPTO_INVALID; + return 1; + } + + if (save_media_old == 0 && save_media_new == 0 && !is_card2) + savectx.repeating_ctr = true; + + if (is_card2) + save_crypto_keysel = save_crypto_keysel_base + save_crypto_keysel_extra; + else if (save_media_old != 0) + save_crypto_keysel = 0; + else if (save_media_new != 0) + save_crypto_keysel = save_crypto_keysel_base + save_crypto_keysel_extra; + else + save_crypto_keysel = -1; + + save_crypto_keysel += 1; + + u8 save_key_y[16] = { 0 }; + + struct { + NcchHeader ncch; + NcchExtHeader exthdr; + } hdrs; + + ReadCartBytes(&hdrs, ncsd->partitions[0].offset * NCSD_MEDIA_UNIT, sizeof(hdrs), cdata, false); + DecryptNcch(&hdrs.exthdr, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), &hdrs.ncch, NULL); + u8 *cart_unique_id = cdata->header + 0x4000; + + switch (save_crypto_keysel) { + case 0: // "backup security version" -1 + memcpy(save_key_y, hdrs.exthdr.signature, 8); + memcpy(&save_key_y[8], &cdata->cart_id, 4); + memcpy(&save_key_y[12], &cdata->cart_id2, 4); + savectx.crypto_type = CARD_SAVE_CRYPTO_V0; + break; + + case 1: // "backup security version" 0 + case 11: // "backup security version" 10 + { + // version 10 (11) is not supported on O3DS + if (IS_O3DS && save_crypto_keysel == 11) { + savectx.crypto_type = CARD_SAVE_CRYPTO_INVALID; + return 1; + } + + u8 tmpbuf[0x48]; + u8 hash[0x20]; + memcpy(tmpbuf, hdrs.exthdr.signature, 8); + memcpy(&tmpbuf[8], cart_unique_id, 0x40); + + sha_quick(hash, tmpbuf, 0x48, SHA256_MODE); + memcpy(save_key_y, hash, 16); + savectx.crypto_type = save_crypto_keysel == 11 ? CARD_SAVE_CRYPTO_V1_N3DS : CARD_SAVE_CRYPTO_V1; + break; + } + + case 2: // "backup security version" 1 + { + static bool save60KeyYSetup = false; + + if (!save60KeyYSetup) { + static const u8 save60KeyY[16] = { 0xC3, 0x69, 0xBA, 0xA2, 0x1E, 0x18, 0x8A, 0x88, 0xA9, 0xAA, 0x94, 0xE5, 0x50, 0x6A, 0x9F, 0x16 }; + setup_aeskeyY(0x2F, save60KeyY); + save60KeyYSetup = true; + } + + u8 hash[0x20]; + u8 tmpbuf[0x70]; + + u32 in_ncch_offset = hdrs.ncch.offset_exefs * NCCH_MEDIA_UNIT + 0x1E0; + ReadCartBytes(&hash, ncsd->partitions[0].offset * NCSD_MEDIA_UNIT + in_ncch_offset, sizeof(hash), cdata, false); + DecryptNcch(hash, in_ncch_offset, sizeof(hash), &hdrs.ncch, NULL); + + memcpy(tmpbuf, hdrs.exthdr.signature, 8); + memcpy(&tmpbuf[8], cart_unique_id, 0x40); + memcpy(&tmpbuf[0x48], &hdrs.ncch.programId, 8); + memcpy(&tmpbuf[0x50], hash, 0x20); + sha_quick(hash, tmpbuf, 0x70, SHA256_MODE); + + use_aeskey(0x2F); + aes_cmac(hash, save_key_y, 2); + savectx.crypto_type = CARD_SAVE_CRYPTO_V2; + break; + } + default: + savectx.crypto_type = CARD_SAVE_CRYPTO_INVALID; + return 1; + }; + + u32 cmac_keyslot = savectx.crypto_type == CARD_SAVE_CRYPTO_V1_N3DS ? CARD_SAVE_CMAC_KEYSLOT_N3DS : CARD_SAVE_CMAC_KEYSLOT_O3DS; + u32 crypto_keyslot = savectx.crypto_type == CARD_SAVE_CRYPTO_V1_N3DS ? CARD_SAVE_CRYPTO_KEYSLOT_N3DS : CARD_SAVE_CRYPTO_KEYSLOT_O3DS; + + setup_aeskeyY(cmac_keyslot, save_key_y); // savedata CMAC key + setup_aeskeyY(crypto_keyslot, save_key_y); // savedata crypto key + return 0; +} + +u32 InitCtrCardSave(CartData *cdata) { + // the wear leveling header exists twice: + // the one at 0x0 is the main one + // the one at 0x1000 is used as a failsafe if the one above is corrupt + if (InitSaveWearLeveling(cdata, 0) != 0) { + memset(&savectx, 0, sizeof(savectx)); + if (InitSaveWearLeveling(cdata, 0x1000) != 0) + return 1; + } + + if (InitCtrCardSaveCryptoKey(cdata) != 0) + return 1; + + return 0; +} + +static u32 ReadDecryptedCard1Save(u8 *buffer, u64 offset, u64 count, CartData* cdata) { + if (offset >= savectx.logical_size) return 1; + if (offset + count > savectx.logical_size) return 1; + + u32 first_sector = offset / 0x1000; + u32 last_sector = (offset + count - 1) / 0x1000; + u32 outbuf_offset = 0; + + SaveBlockmapEntry ent; + u8 sectorbuf[0x1000]; + u8 ctr[16]; + memset(ctr, 0, sizeof(ctr)); + + u32 crypto_keyslot = savectx.crypto_type == CARD_SAVE_CRYPTO_V1_N3DS ? CARD_SAVE_CRYPTO_KEYSLOT_N3DS : CARD_SAVE_CRYPTO_KEYSLOT_O3DS; + use_aeskey(crypto_keyslot); + + // the minimum one can read from the flash is 4K sectors anyway, and blockmap scatters it, so we do it sector by sector + for (u32 cur_sector = first_sector; cur_sector < last_sector + 1; cur_sector++) { + ent = GetBlockmapEntry(cur_sector); + + // where in this sector we need to start reading data from + u32 in_sector_start = cur_sector == first_sector ? offset % 0x1000 : 0; + // how much data we can read in this sector assuming the start offset above + u32 in_sector_size = 0x1000 - in_sector_start; + // how much data we actually need to read from the sector + u32 chunksize = min(count, in_sector_size); + + if (!ent.used) { + memset(§orbuf, 0xFF, sizeof(sectorbuf)); + } else { + if (ReadCartSave(sectorbuf, ent.phys_sec * 0x1000, sizeof(sectorbuf), cdata) != 0) + return 1; + } + + if (savectx.repeating_ctr) { + for (u32 cursect = 0; cursect < 8; cursect++) { + memset(ctr, 0, sizeof(ctr)); + ctr_decrypt_byte(§orbuf[0x200 * cursect], §orbuf[0x200 * cursect], 0x200, 0, AES_CNT_CART_SAVE_MODE, ctr); + } + } else { + ctr_decrypt_byte(sectorbuf, sectorbuf, sizeof(sectorbuf), cur_sector * 0x1000, AES_CNT_CART_SAVE_MODE, ctr); + } + + memcpy(&buffer[outbuf_offset], §orbuf[in_sector_start], chunksize); + outbuf_offset += chunksize; + count -= chunksize; + offset += chunksize; + } + + return 0; +} + +static u32 ReadDecryptedCard2Save(u8 *buffer, u64 offset, u64 count, CartData* cdata) { + if (offset >= cdata->save_size) return 1; + if (offset + count > cdata->save_size) count = cdata->save_size - offset; + + u32 first_sector = offset / 0x200; + u32 outbuf_offset = 0; + + u8 ctr[16]; + memset(ctr, 0, sizeof(ctr)); + + u32 crypto_keyslot = savectx.crypto_type == CARD_SAVE_CRYPTO_V1_N3DS ? CARD_SAVE_CRYPTO_KEYSLOT_N3DS : CARD_SAVE_CRYPTO_KEYSLOT_O3DS; + use_aeskey(crypto_keyslot); + + u8 sector_tmp[0x200]; + + // handle misalignment at the start + u32 in_first_sector_offset = offset % 0x200; + if (in_first_sector_offset) { + u32 sector_remain = 0x200 - in_first_sector_offset; + u32 misalignsize = min(sector_remain, count); + + if (ReadCartSave(sector_tmp, first_sector * 0x200, sizeof(sector_tmp), cdata) != 0) + return 1; + + ctr_decrypt_byte(sector_tmp, sector_tmp, sizeof(sector_tmp), first_sector * 0x200, AES_CNT_CART_SAVE_MODE, ctr); + memcpy(buffer, §or_tmp[in_first_sector_offset], misalignsize); + outbuf_offset += misalignsize; + offset += misalignsize; + count -= misalignsize; + } + + // offset is now aligned, size may still not be, though + u32 aligned_size = count & ~(0x200-1); + if (aligned_size) { + if (ReadCartSave(&buffer[outbuf_offset], offset, aligned_size, cdata) != 0) + return 1; + + ctr_decrypt_byte(&buffer[outbuf_offset], &buffer[outbuf_offset], aligned_size, offset, AES_CNT_CART_SAVE_MODE, ctr); + + count -= aligned_size; + offset += aligned_size; + outbuf_offset += aligned_size; + } + + // only ending misalignment remains, if applicable + if (count) { + if (ReadCartSave(sector_tmp, offset, sizeof(sector_tmp), cdata) != 0) + return 1; + + ctr_decrypt_byte(sector_tmp, sector_tmp, sizeof(sector_tmp), offset, AES_CNT_CART_SAVE_MODE, ctr); + + memcpy(&buffer[outbuf_offset], §or_tmp, count); + } + + return 0; +} + +u32 ReadDecryptedCtrCardSave(u8* buffer, u64 offset, u64 count, CartData* cdata) { + switch (cdata->save_type) { + case CARD_SAVE_SPI: + return ReadDecryptedCard1Save(buffer, offset, count, cdata); + break; + + case CARD_SAVE_CARD2: + return ReadDecryptedCard2Save(buffer, offset, count, cdata); + break; + + default: + return 1; + } +} \ No newline at end of file diff --git a/arm9/source/gamecart/save_ctr.h b/arm9/source/gamecart/save_ctr.h new file mode 100644 index 0000000..d9ce6ba --- /dev/null +++ b/arm9/source/gamecart/save_ctr.h @@ -0,0 +1,6 @@ +#pragma once + +#include "gamecart.h" + +u32 InitCtrCardSave(CartData *); +u32 ReadDecryptedCtrCardSave(u8* buffer, u64 offset, u64 count, CartData* cdata); \ No newline at end of file diff --git a/arm9/source/godmode.c b/arm9/source/godmode.c index d5af973..5839f69 100644 --- a/arm9/source/godmode.c +++ b/arm9/source/godmode.c @@ -3079,8 +3079,13 @@ u32 GodMode(int entrypoint) { if (!InitVCartDrive() && (pad_state & CART_INSERT) && (curr_drvtype & DRV_CART)) // reinit virtual cart drive ShowPrompt(false, "%s", STR_CART_INIT_FAILED); - if (!(*current_path) || (curr_drvtype & DRV_CART)) + // unmount any gamecart image if the gamecart was removed + if (pad_state & CART_EJECT && *GetMountPath() && DriveType(GetMountPath()) & DRV_CART) { + InitImgFS(NULL); + } + if (!(*current_path) || (curr_drvtype & DRV_CART)) { GetDirContents(current_dir, current_path); // refresh dir contents + } } else if (pad_state & SD_INSERT) { while (!InitSDCardFS() && ShowPrompt(true, "%s", STR_INITIALIZING_SD_FAILED_RETRY)); ClearScreenF(true, true, COLOR_STD_BG); diff --git a/arm9/source/virtual/vcart.c b/arm9/source/virtual/vcart.c index e6807ef..ba3da85 100644 --- a/arm9/source/virtual/vcart.c +++ b/arm9/source/virtual/vcart.c @@ -1,15 +1,21 @@ #include "vcart.h" +#include "fsdrive.h" +#include "fsinit.h" #include "gamecart.h" +#include "image.h" +#include "save_ctr.h" #define FAT_LIMIT 0x100000000 -#define VFLAG_SECURE_AREA_ENC (1UL<<28) -#define VFLAG_GAMECART_NFO (1UL<<29) -#define VFLAG_SAVEGAME (1UL<<30) -#define VFLAG_PRIV_HDR (1UL<<31) +#define VFLAG_DECRYPTED_SAVEGAME (1UL<<27) +#define VFLAG_SECURE_AREA_ENC (1UL<<28) +#define VFLAG_GAMECART_NFO (1UL<<29) +#define VFLAG_SAVEGAME (1UL<<30) +#define VFLAG_PRIV_HDR (1UL<<31) static CartData* cdata = NULL; static bool cart_init = false; static bool cart_checked = false; +static bool enable_dec_ctr_save = false; u32 InitVCartDrive(void) { if (!cart_checked) cart_checked = true; @@ -19,6 +25,10 @@ u32 InitVCartDrive(void) { free(cdata); cdata = NULL; } + if (cart_init && (cdata->cart_type & CART_CTR)) { + // for compatibility purposes save crypto and wear leveling init are optional + enable_dec_ctr_save = InitCtrCardSave(cdata) == 0; + } return cart_init ? cdata->cart_id : 0; } @@ -34,7 +44,7 @@ bool ReadVCartDir(VirtualFile* vfile, VirtualDir* vdir) { vfile->keyslot = 0xFF; // unused vfile->flags = VFLAG_READONLY; - while (++vdir->index <= 9) { + while (++vdir->index <= 10) { if ((vdir->index == 0) && (cdata->data_size < FAT_LIMIT)) { // standard full rom snprintf(vfile->name, 32, "%s.%s", name, ext); vfile->size = cdata->cart_size; @@ -73,11 +83,16 @@ bool ReadVCartDir(VirtualFile* vfile, VirtualDir* vdir) { vfile->flags |= VFLAG_READONLY; } return true; - } else if (vdir->index == 8) { // gamecart info - char info[256]; + } else if ((vdir->index == 8) && enable_dec_ctr_save) { + snprintf(vfile->name, 32, "%s.dec.sav", name); + vfile->size = (cdata->cart_id & 0x8000000) /* card2 */ ? cdata->save_size : cdata->save_size - 0x2000; + vfile->flags = VFLAG_DECRYPTED_SAVEGAME | VFLAG_READONLY /* for now */; + return true; + } else if (vdir->index == 9) { // gamecart info + char info[301]; GetCartInfoString(info, sizeof(info), cdata); snprintf(vfile->name, 32, "%s.txt", name); - vfile->size = strnlen(info, 255); + vfile->size = strnlen(info, 300); vfile->flags |= VFLAG_GAMECART_NFO; return true; } @@ -94,6 +109,8 @@ int ReadVCartFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 count) return ReadCartPrivateHeader(buffer, foffset, count, cdata); else if (vfile->flags & VFLAG_SAVEGAME) return ReadCartSave(buffer, foffset, count, cdata); + else if (vfile->flags & VFLAG_DECRYPTED_SAVEGAME) + return ReadDecryptedCtrCardSave(buffer, foffset, count, cdata); else if (vfile->flags & VFLAG_GAMECART_NFO) return ReadCartInfo(buffer, foffset, count, cdata); @@ -104,7 +121,15 @@ int ReadVCartFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 count) int WriteVCartFile(const VirtualFile* vfile, const void* buffer, u64 offset, u64 count) { if (!cdata) return -1; if (vfile->flags & VFLAG_SAVEGAME) { - return WriteCartSave(buffer, offset, count, cdata); + int res = WriteCartSave(buffer, offset, count, cdata); + if (cdata->cart_type & CART_CTR) { + enable_dec_ctr_save = InitCtrCardSave(cdata) == 0; + if (*GetMountPath() && !enable_dec_ctr_save && (DriveType(GetMountPath()) & DRV_CART) && strstr(GetMountPath(), ".sav")) { + // unmount the virtual DISA archive if user invalidated the encrypted and wear-leveled source + InitImgFS(NULL); + } + } + return res; } return -1; }