From fc97df6466a5cb0aecb3cb59cc8ae23b3add9acf Mon Sep 17 00:00:00 2001 From: d0k3 Date: Fri, 10 Nov 2017 04:14:38 +0100 Subject: [PATCH] On-the-fly decryption for DSiWare Exports --- arm9/source/filesys/filetype.c | 3 + arm9/source/filesys/filetype.h | 23 +++--- arm9/source/filesys/sddata.c | 125 ++++++++++++++++++++++++++++----- arm9/source/game/dsiwareexp.c | 24 +++++++ arm9/source/game/dsiwareexp.h | 59 ++++++++++++++++ arm9/source/game/game.h | 1 + 6 files changed, 208 insertions(+), 27 deletions(-) create mode 100644 arm9/source/game/dsiwareexp.c create mode 100644 arm9/source/game/dsiwareexp.h diff --git a/arm9/source/filesys/filetype.c b/arm9/source/filesys/filetype.c index 217fc8c..77a7a19 100644 --- a/arm9/source/filesys/filetype.c +++ b/arm9/source/filesys/filetype.c @@ -128,6 +128,9 @@ u64 IdentifyFileType(const char* path) { if (fsize < TEMP_BUFFER_SIZE) type |= TXT_GENERIC; return type; } else if ((strnlen(fname, 16) == 8) && (sscanf(fname, "%08lx", &id) == 1)) { + if (strncmp(path + 2, "/Nintendo DSiWare/", 18) == 0) + return GAME_DSIWE; + char path_cdn[256]; char* name_cdn = path_cdn + (fname - path); strncpy(path_cdn, path, 256); diff --git a/arm9/source/filesys/filetype.h b/arm9/source/filesys/filetype.h index 7e392cf..32f162b 100644 --- a/arm9/source/filesys/filetype.h +++ b/arm9/source/filesys/filetype.h @@ -17,17 +17,18 @@ #define GAME_3DSX (1ULL<<12) #define GAME_NDS (1ULL<<13) #define GAME_GBA (1ULL<<14) -#define SYS_FIRM (1ULL<<15) -#define SYS_AGBSAVE (1ULL<<16) -#define SYS_TICKDB (1ULL<<17) -#define BIN_NCCHNFO (1ULL<<18) -#define BIN_TIKDB (1ULL<<19) -#define BIN_KEYDB (1ULL<<20) -#define BIN_LEGKEY (1ULL<<21) -#define TXT_SCRIPT (1ULL<<22) -#define TXT_GENERIC (1ULL<<23) -#define NOIMG_NAND (1ULL<<24) -#define HDR_NAND (1ULL<<25) +#define GAME_DSIWE (1ULL<<15) +#define SYS_FIRM (1ULL<<16) +#define SYS_AGBSAVE (1ULL<<17) +#define SYS_TICKDB (1ULL<<18) +#define BIN_NCCHNFO (1ULL<<19) +#define BIN_TIKDB (1ULL<<20) +#define BIN_KEYDB (1ULL<<21) +#define BIN_LEGKEY (1ULL<<22) +#define TXT_SCRIPT (1ULL<<23) +#define TXT_GENERIC (1ULL<<24) +#define NOIMG_NAND (1ULL<<25) +#define HDR_NAND (1ULL<<26) #define TYPE_BASE 0xFFFFFFFFULL // 32 bit reserved for base types // #define FLAG_FIRM (1ULL<<58) // <--- for CXIs containing FIRMs diff --git a/arm9/source/filesys/sddata.c b/arm9/source/filesys/sddata.c index f9dfd41..f4cd370 100644 --- a/arm9/source/filesys/sddata.c +++ b/arm9/source/filesys/sddata.c @@ -1,7 +1,9 @@ #include "sddata.h" +#include "dsiwareexp.h" #include "aes.h" #include "sha.h" +#define DSIWARE_MAGIC "Nintendo DSiWare" // must be exactly 16 chars #define NUM_ALIAS_DRV 2 #define NUM_FILCRYPTINFO 16 @@ -52,28 +54,117 @@ FilCryptInfo* fx_find_cryptinfo(FIL* fptr) { return info; } +FRESULT fx_decrypt_dsiware (FIL* fp, void* buff, FSIZE_t ofs, UINT len) { + const u32 mode = AES_CNT_TITLEKEY_DECRYPT_MODE; + const u32 num_tbl = sizeof(DsiWareExpContentTable) / sizeof(u32); + const FSIZE_t ofs0 = f_tell(fp); + + u8 __attribute__((aligned(16))) iv[AES_BLOCK_SIZE]; + u32 tbl[num_tbl]; + u8 hdr[DSIWEXP_HEADER_LEN]; + + FRESULT res; + UINT br; + + + // read and decrypt header + if ((res = f_lseek(fp, DSIWEXP_HEADER_OFFSET)) != FR_OK) return res; + if ((res = f_read(fp, hdr, DSIWEXP_HEADER_LEN, &br)) != FR_OK) return res; + if (br != DSIWEXP_HEADER_LEN) return FR_DENIED; + memcpy(iv, hdr + DSIWEXP_HEADER_LEN - AES_BLOCK_SIZE, AES_BLOCK_SIZE); + cbc_decrypt(hdr, hdr, sizeof(DsiWareExpHeader) / AES_BLOCK_SIZE, mode, iv); + + // setup the table + if (BuildDsiWareExportContentTable(tbl, hdr) != 0) return FR_DENIED; + if (tbl[num_tbl-1] > f_size(fp)) return FR_DENIED; // obviously missing data + + + // process sections + u32 sct_start = 0; + u32 sct_end = 0; + for (u32 i = 0; i < num_tbl; i++, sct_start = sct_end) { + sct_end = tbl[i]; + if (sct_start == sct_end) continue; // nothing in section + if ((ofs + len <= sct_start) || (ofs >= sct_end)) continue; // section not in data + + const u32 crypt_end = sct_end - (AES_BLOCK_SIZE * 2); + const u32 data_end = min(crypt_end, ofs + len); + u32 data_pos = max(ofs, sct_start); + if (ofs >= crypt_end) continue; // nothing to do + + if ((sct_start < ofs) || (sct_end > ofs + len)) { // incomplete section, ugh + u8 __attribute__((aligned(16))) block[AES_BLOCK_SIZE]; + + // load iv0 + FSIZE_t block0_ofs = data_pos - (data_pos % AES_BLOCK_SIZE); + FSIZE_t iv0_ofs = ((block0_ofs > sct_start) ? block0_ofs : sct_end) - AES_BLOCK_SIZE; + if ((res = f_lseek(fp, iv0_ofs)) != FR_OK) return res; + if ((res = f_read(fp, iv, AES_BLOCK_SIZE, &br)) != FR_OK) return res; + + // load and decrypt block0 (if misaligned) + if (data_pos % AES_BLOCK_SIZE) { + if ((res = f_lseek(fp, block0_ofs)) != FR_OK) return res; + if ((res = f_read(fp, block, AES_BLOCK_SIZE, &br)) != FR_OK) return res; + cbc_decrypt(block, block, 1, mode, iv); + data_pos = min(block0_ofs + AES_BLOCK_SIZE, data_end); + memcpy(buff, block + (ofs - block0_ofs), data_pos - ofs); + } + + // decrypt blocks in between + u32 num_blocks = (data_end - data_pos) / AES_BLOCK_SIZE; + if (num_blocks) { + u8* blocks = (u8*) buff + (data_pos - ofs); + cbc_decrypt(blocks, blocks, num_blocks, mode, iv); + data_pos += num_blocks * AES_BLOCK_SIZE; + } + + // decrypt last block + if (data_pos < data_end) { + u8* lbuff = (u8*) buff + (data_pos - ofs); + memcpy(block, lbuff, data_end - data_pos); + cbc_decrypt(block, block, 1, mode, iv); + memcpy(lbuff, block, data_end - data_pos); + data_pos = data_end; + } + } else { // complete section (thank god for these!) + u8* blocks = (u8*) buff + (sct_start - ofs); + u8* iv0 = (u8*) buff + (sct_end - ofs) - AES_BLOCK_SIZE; + u32 num_blocks = (crypt_end - sct_start) / AES_BLOCK_SIZE; + memcpy(iv, iv0, AES_BLOCK_SIZE); + cbc_decrypt(blocks, blocks, num_blocks, mode, iv); + } + } + + return f_lseek(fp, ofs0); +} + FRESULT fx_open (FIL* fp, const TCHAR* path, BYTE mode) { int num = alias_num(path); FilCryptInfo* info = fx_find_cryptinfo(fp); if (info) info->fptr = NULL; if (info && (num >= 0)) { - // get AES counter, see: http://www.3dbrew.org/wiki/Extdata#Encryption - // path is the part of the full path after //Nintendo 3DS// - u8 hashstr[256]; - u8 sha256sum[32]; - u32 plen = 0; - // poor man's UTF-8 -> UTF-16 / uppercase -> lowercase - for (plen = 0; plen < 128; plen++) { - u8 symbol = path[2 + plen]; - if ((symbol >= 'A') && (symbol <= 'Z')) symbol += ('a' - 'A'); - hashstr[2*plen] = symbol; - hashstr[2*plen+1] = 0; - if (symbol == 0) break; + // DSIWare Export, mark with the magic number + if (strncmp(path + 2, "/" DSIWARE_MAGIC, 1 + 16) == 0) { + memcpy(info->ctr, DSIWARE_MAGIC, 16); + } else { + // get AES counter, see: http://www.3dbrew.org/wiki/Extdata#Encryption + // path is the part of the full path after //Nintendo 3DS// + u8 hashstr[256]; + u8 sha256sum[32]; + u32 plen = 0; + // poor man's UTF-8 -> UTF-16 / uppercase -> lowercase + for (plen = 0; plen < 128; plen++) { + u8 symbol = path[2 + plen]; + if ((symbol >= 'A') && (symbol <= 'Z')) symbol += ('a' - 'A'); + hashstr[2*plen] = symbol; + hashstr[2*plen+1] = 0; + if (symbol == 0) break; + } + sha_quick(sha256sum, hashstr, (plen + 1) * 2, SHA256_MODE); + for (u32 i = 0; i < 16; i++) + info->ctr[i] = sha256sum[i] ^ sha256sum[i+16]; } - sha_quick(sha256sum, hashstr, (plen + 1) * 2, SHA256_MODE); - for (u32 i = 0; i < 16; i++) - info->ctr[i] = sha256sum[i] ^ sha256sum[i+16]; // copy over key, FIL pointer memcpy(info->keyy, sd_keyy[num], 16); info->fptr = fp; @@ -89,7 +180,8 @@ FRESULT fx_read (FIL* fp, void* buff, UINT btr, UINT* br) { if (info && info->fptr) { setup_aeskeyY(0x34, info->keyy); use_aeskey(0x34); - ctr_decrypt_byte(buff, buff, btr, off, AES_CNT_CTRNAND_MODE, info->ctr); + if (memcmp(info->ctr, DSIWARE_MAGIC, 16) == 0) fx_decrypt_dsiware(fp, buff, off, btr); + else ctr_decrypt_byte(buff, buff, btr, off, AES_CNT_CTRNAND_MODE, info->ctr); } return res; } @@ -99,6 +191,7 @@ FRESULT fx_write (FIL* fp, const void* buff, UINT btw, UINT* bw) { FSIZE_t off = f_tell(fp); FRESULT res = FR_OK; if (info && info->fptr) { + if (memcmp(info->ctr, DSIWARE_MAGIC, 16) == 0) return FR_DENIED; setup_aeskeyY(0x34, info->keyy); use_aeskey(0x34); *bw = 0; diff --git a/arm9/source/game/dsiwareexp.c b/arm9/source/game/dsiwareexp.c new file mode 100644 index 0000000..71f9117 --- /dev/null +++ b/arm9/source/game/dsiwareexp.c @@ -0,0 +1,24 @@ +#include "dsiwareexp.h" + + +u32 BuildDsiWareExportContentTable(void* table, void* header) { + DsiWareExpHeader* hdr = (DsiWareExpHeader*) header; + DsiWareExpContentTable* tbl = (DsiWareExpContentTable*) table; + + if (strncmp(hdr->magic, DSIWEXP_HEADER_MAGIC, strlen(DSIWEXP_HEADER_MAGIC)) != 0) + return 1; + + tbl->banner_end = 0 + sizeof(DsiWareExpBanner) + sizeof(DsiWareExpBlockMetaData); + tbl->header_end = tbl->banner_end + sizeof(DsiWareExpHeader) + sizeof(DsiWareExpBlockMetaData); + tbl->footer_end = tbl->header_end + sizeof(DsiWareExpFooter) + sizeof(DsiWareExpBlockMetaData); + + u32 content_end_last = tbl->footer_end; + for (u32 i = 0; i < DSIWEXP_NUM_CONTENT; i++) { + tbl->content_end[i] = content_end_last; + if (!hdr->content_size[i]) continue; // non-existant section + tbl->content_end[i] += align(hdr->content_size[i], 0x10) + sizeof(DsiWareExpBlockMetaData); + content_end_last = tbl->content_end[i]; + } + + return 0; +} diff --git a/arm9/source/game/dsiwareexp.h b/arm9/source/game/dsiwareexp.h new file mode 100644 index 0000000..bd74f7f --- /dev/null +++ b/arm9/source/game/dsiwareexp.h @@ -0,0 +1,59 @@ +#pragma once + +#include "common.h" +#include "nds.h" + +#define DSIWEXP_NUM_CONTENT 11 +#define DSIWEXP_HEADER_MAGIC "3FDT" +#define DSIWEXP_BANNER_OFFSET 0 +#define DSIWEXP_BANNER_LEN (sizeof(DsiWareExpBanner) + sizeof(DsiWareExpBlockMetaData)) +#define DSIWEXP_HEADER_OFFSET (DSIWEXP_BANNER_OFFSET + DSIWEXP_BANNER_LEN) +#define DSIWEXP_HEADER_LEN (sizeof(DsiWareExpHeader) + sizeof(DsiWareExpBlockMetaData)) + + +typedef struct { + u32 banner_end; + u32 header_end; + u32 footer_end; + u32 content_end[DSIWEXP_NUM_CONTENT]; +} __attribute__((packed)) DsiWareExpContentTable; + +// see: https://www.3dbrew.org/wiki/DSiWare_Exports#Block_Metadata +typedef struct { + u8 cmac[16]; + u8 iv0[16]; +} __attribute__((packed)) DsiWareExpBlockMetaData; + +// see: https://www.3dbrew.org/wiki/DSiWare_Exports#File_Structure_v2 +typedef struct { + TwlIconData icon_data; + u8 unknown[0x4000 - sizeof(TwlIconData)]; +} __attribute__((packed)) DsiWareExpBanner; + +// see: https://www.3dbrew.org/wiki/DSiWare_Exports#Header_2 +typedef struct { + char magic[4]; // "3FDT" + u16 group_id; + u16 title_version; + u8 movable_enc_sha256[0x20]; + u8 cbc_test_block[0x10]; + u64 title_id; + u64 unknown0; + u32 content_size[DSIWEXP_NUM_CONTENT]; + u8 unknown1[0x30]; + u8 tmd_reserved[0x3E]; + u8 padding[0x0E]; +} __attribute__((packed)) DsiWareExpHeader; + +// see: https://www.3dbrew.org/wiki/DSiWare_Exports#Footer +typedef struct { + u8 banner_sha256[0x20]; + u8 header_sha256[0x20]; + u8 content_sha256[DSIWEXP_NUM_CONTENT][0x20]; + u8 ecdsa_signature[0x3C]; + u8 ecdsa_apcert[0x180]; + u8 ecdsa_ctcert[0x180]; + u8 padding[0x4]; +} __attribute__((packed)) DsiWareExpFooter; + +u32 BuildDsiWareExportContentTable(void* table, void* header); diff --git a/arm9/source/game/game.h b/arm9/source/game/game.h index fdfdb57..8130b36 100644 --- a/arm9/source/game/game.h +++ b/arm9/source/game/game.h @@ -11,5 +11,6 @@ #include "codelzss.h" #include "nds.h" #include "gba.h" +#include "dsiwareexp.h" #include "3dsx.h" #include "ncchinfo.h"