diff --git a/source/fs/filetype.c b/source/fs/filetype.c index b6eb094..d67a958 100644 --- a/source/fs/filetype.c +++ b/source/fs/filetype.c @@ -51,7 +51,10 @@ u32 IdentifyFileType(const char* path) { return SYS_FIRM; // FIRM file } } - if ((fsize > sizeof(NcchInfoHeader)) && + if ((fsize > sizeof(BossHeader)) && + (ValidateBossHeader((BossHeader*) (void*) header, fsize) == 0)) { + return GAME_BOSS; // BOSS (SpotPass) file + } else if ((fsize > sizeof(NcchInfoHeader)) && (GetNcchInfoVersion((NcchInfoHeader*) (void*) header)) && fname && (strncasecmp(fname, NCCHINFO_NAME, 32) == 0)) { return BIN_NCCHNFO; // ncchinfo.bin file diff --git a/source/fs/filetype.h b/source/fs/filetype.h index 3b9093e..353e7ee 100644 --- a/source/fs/filetype.h +++ b/source/fs/filetype.h @@ -10,16 +10,17 @@ #define GAME_TMD (1<<5) #define GAME_EXEFS (1<<6) #define GAME_ROMFS (1<<7) -#define SYS_FIRM (1<<8) -#define BIN_NCCHNFO (1<<9) -#define BIN_LAUNCH (1<<10) +#define GAME_BOSS (1<<8) +#define SYS_FIRM (1<<9) +#define BIN_NCCHNFO (1<<10) +#define BIN_LAUNCH (1<<11) #define FLAG_CXI (1<<30) #define FLAG_ENCRYPTED (1<<31) #define FTYPE_MOUNTABLE(tp) (tp&(IMG_FAT|IMG_NAND|GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_EXEFS|GAME_ROMFS|SYS_FIRM)) -#define FYTPE_VERIFICABLE(tp) (tp&(IMG_NAND|GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_TMD|SYS_FIRM)) -#define FYTPE_DECRYPTABLE(tp) (tp&(GAME_CIA|GAME_NCSD|GAME_NCCH|SYS_FIRM)) +#define FYTPE_VERIFICABLE(tp) (tp&(IMG_NAND|GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_TMD|GAME_BOSS|SYS_FIRM)) +#define FYTPE_DECRYPTABLE(tp) (tp&(GAME_CIA|GAME_NCSD|GAME_NCCH|GAME_BOSS|SYS_FIRM)) #define FTYPE_BUILDABLE(tp) (tp&(GAME_NCSD|GAME_NCCH|GAME_TMD)) #define FTYPE_BUILDABLE_L(tp) (FTYPE_BUILDABLE(tp) && (tp&(GAME_TMD))) #define FTYPE_HSINJECTABLE(tp) ((tp&(GAME_NCCH|FLAG_CXI|FLAG_ENCRYPTED)) == (GAME_NCCH|FLAG_CXI)) diff --git a/source/game/boss.c b/source/game/boss.c new file mode 100644 index 0000000..5ab83bd --- /dev/null +++ b/source/game/boss.c @@ -0,0 +1,96 @@ +#include "boss.h" +#include "sha.h" +#include "aes.h" + +// http://3dbrew.org/wiki/SpotPass#Content_Header +u32 CheckBossHash(BossHeader* boss, bool encrypted) { + u8 hash_area[0x14] = { 0 }; + u8 boss_sha256[0x20]; + u8 l_sha256[0x20]; + + // calculate hash + memcpy(hash_area, ((u8*) boss) + 0x28, 0x12); + memcpy(boss_sha256, boss->hash_header, 0x20); + if (encrypted) { + CryptBoss(hash_area, 0x28, 0x12, boss); + CryptBoss(boss_sha256, 0x28 + 0x12, 0x20, boss); + } + sha_quick(l_sha256, hash_area, 0x14, SHA256_MODE); + + return (memcmp(boss_sha256, l_sha256, 0x20) == 0) ? 0 : 1; +} + +u32 ValidateBossHeader(BossHeader* header, u32 fsize) { + u8 boss_magic[] = { BOSS_MAGIC }; + + // base checks + if ((memcmp(header->magic, boss_magic, sizeof(boss_magic)) != 0) || + (fsize && (fsize != getbe32(header->filesize))) || + (getbe32(header->filesize) > BOSS_MAX_SIZE) || + (getbe32(header->filesize) < sizeof(BossHeader)) || + (getbe16(header->unknown0) != 0x0001) || + (getbe16(header->cnthdr_hash_type) != 0x0002) || + (getbe16(header->cnthdr_rsa_size) != 0x0002)) + return 1; + + // hash check + if ((CheckBossHash(header, false) != 0) && + (CheckBossHash(header, true) != 0)) + return 1; + + return 0; +} + +u32 GetBossPayloadHashHeader(u8* header, BossHeader* boss) { + memset(header, 0, BOSS_SIZE_PAYLOAD_HEADER); + memcpy(header, ((u8*) boss) + 0x15A, 0x1C); + return 0; +} + +u32 CheckBossEncrypted(BossHeader* boss) { + return CheckBossHash(boss, true); +} + +// on the fly de-/encryptor for BOSS +u32 CryptBoss(u8* data, u32 offset, u32 size, BossHeader* boss) { + // check data area (encrypted area starts @0x28) + if (offset + size < 0x28) return 0; + else if (offset < 0x28) { + data += 0x28 - offset; + size -= 0x28 - offset; + offset = 0x28; + } + + // decrypt BOSS data + u8 ctr[16] = { 0 }; + memcpy(ctr, boss->ctr12, 12); + ctr[15] = 0x01; + use_aeskey(0x38); + ctr_decrypt_byte(data, data, size, offset - 0x28, AES_CNT_CTRNAND_MODE, ctr); + + return 0; +} + +// on the fly de-/encryptor for BOSS - sequential +u32 CryptBossSequential(u8* data, u32 offset, u32 size) { + // warning: this will only work for sequential processing + // unexpected results otherwise + static BossHeader boss = { 0 }; + static BossHeader* bossptr = NULL; + + // fetch boss header from data + if ((offset == 0) && (size >= sizeof(BossHeader))) { + bossptr = NULL; + memcpy(&boss, data, sizeof(BossHeader)); + if (((CheckBossEncrypted(&boss) == 0) && + (CryptBoss((u8*) &boss, 0, sizeof(BossHeader), &boss) != 0)) || + (ValidateBossHeader(&boss, 0) != 0)) + return 1; + bossptr = &boss; + } + + // safety check, boss pointer + if (!bossptr) return 1; + + return CryptBoss(data, offset, size, bossptr); +} diff --git a/source/game/boss.h b/source/game/boss.h new file mode 100644 index 0000000..74ef9c7 --- /dev/null +++ b/source/game/boss.h @@ -0,0 +1,45 @@ +#pragma once + +#include "common.h" + +#define BOSS_MAGIC 0x62, 0x6F, 0x73, 0x73, 0x00, 0x01, 0x00, 0x01 +#define BOSS_MAX_SIZE 0xF0000 // 960 kB, should be more than enough + +#define BOSS_OFFSET_PAYLOAD sizeof(BossHeader) +#define BOSS_SIZE_PAYLOAD_HEADER (0x1C + 2) + +// see: http://3dbrew.org/wiki/SpotPass#BOSS_Header +// and: http://3dbrew.org/wiki/SpotPass#Content_Header +// and: http://3dbrew.org/wiki/SpotPass#Payload_Content_Header +// everything is in big endian +typedef struct { + // actual BOSS header + u8 magic[8]; // "boss" + 0x00010001, see above + u8 filesize[4]; // big endian + u8 release_date[8]; + u8 unknown0[2]; // always 0x0001 + u8 padding[2]; + u8 cnthdr_hash_type[2]; // always 0x0002 + u8 cnthdr_rsa_size[2]; // always 0x0002 + u8 ctr12[12]; // first 12 byte of ctr + // content header, encryption starts here (0x28) + u8 unknown1[0x10]; // usually 0x80 followed by 0x00 + u8 ext_info[2]; // for generating extdata filepath + u8 hash_header[0x20]; + u8 signature_header[0x100]; + // payload header, first 0x1C byte used for hash (0x15A) + u8 programId[8]; + u8 unknown2[4]; // typically zero + u8 data_type[4]; + u8 size_payload[4]; + u8 ns_dataId[4]; + u8 unknown3[4]; + u8 hash_payload[0x20]; + u8 signature_payload[0x100]; +} __attribute__((packed)) BossHeader; + +u32 ValidateBossHeader(BossHeader* header, u32 fsize); +u32 GetBossPayloadHashHeader(u8* header, BossHeader* boss); +u32 CheckBossEncrypted(BossHeader* boss); +u32 CryptBoss(u8* data, u32 offset, u32 size, BossHeader* boss); +u32 CryptBossSequential(u8* data, u32 offset, u32 size); diff --git a/source/game/game.h b/source/game/game.h index bca1b5c..4e4e508 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -6,4 +6,5 @@ #include "exefs.h" #include "romfs.h" #include "firm.h" +#include "boss.h" #include "ncchinfo.h" diff --git a/source/game/gameutil.c b/source/game/gameutil.c index 1f410a6..c19a1dd 100644 --- a/source/game/gameutil.c +++ b/source/game/gameutil.c @@ -504,6 +504,48 @@ u32 VerifyFirmFile(const char* path) { return 0; } +u32 VerifyBossFile(const char* path) { + BossHeader* boss = (BossHeader*) TEMP_BUFFER; + u8* payload_hdr = MAIN_BUFFER; + u8* payload = MAIN_BUFFER + BOSS_SIZE_PAYLOAD_HEADER; + u32 payload_size; + bool encrypted = false; + + char pathstr[32 + 1]; + TruncateString(pathstr, path, 32, 8); + + // read file header + UINT btr; + if ((fvx_qread(path, boss, 0, sizeof(BossHeader), &btr) != FR_OK) || + (btr != sizeof(BossHeader)) || (ValidateBossHeader(boss, 0) != 0)) { + ShowPrompt(false, "%s\nError: Not a BOSS file", pathstr); + return 1; + } + + // get / check size + payload_size = getbe32(boss->filesize) - sizeof(BossHeader); + if ((payload_size + BOSS_SIZE_PAYLOAD_HEADER > MAIN_BUFFER_SIZE) || !payload_size) + return 1; + + // check if encrypted, decrypt if required + encrypted = (CheckBossEncrypted(boss) == 0); + if (encrypted) CryptBoss((u8*) boss, 0, sizeof(BossHeader), boss); + + // actual hash calculation & compare + u8 hash[32]; + memset(MAIN_BUFFER, 0, MAIN_BUFFER_SIZE); + GetBossPayloadHashHeader(payload_hdr, boss); + fvx_qread(path, payload, sizeof(BossHeader), payload_size, &btr); + if (encrypted) CryptBoss(payload, sizeof(BossHeader), payload_size, boss); + sha_quick(hash, MAIN_BUFFER, payload_size + BOSS_SIZE_PAYLOAD_HEADER, SHA256_MODE); + if (memcmp(hash, boss->hash_payload, 0x20) != 0) { + ShowPrompt(false, "%s\nBOSS payload hash mismatch", pathstr); + return 1; + } + + return 0; +} + u32 VerifyGameFile(const char* path) { u32 filetype = IdentifyFileType(path); if (filetype & GAME_CIA) @@ -514,6 +556,8 @@ u32 VerifyGameFile(const char* path) { return VerifyNcchFile(path, 0, 0); else if (filetype & GAME_TMD) return VerifyTmdFile(path); + else if (filetype & GAME_BOSS) + return VerifyBossFile(path); else if (filetype & SYS_FIRM) return VerifyFirmFile(path); else return 1; @@ -598,6 +642,19 @@ u32 CheckEncryptedFirmFile(const char* path) { return 1; } +u32 CheckEncryptedBossFile(const char* path) { + BossHeader* boss = (BossHeader*) TEMP_BUFFER; + UINT btr; + + // get boss header + if ((fvx_qread(path, boss, 0, sizeof(BossHeader), &btr) != FR_OK) || + (btr != sizeof(BossHeader))) { + return 1; + } + + return CheckBossEncrypted(boss); +} + u32 CheckEncryptedGameFile(const char* path) { u32 filetype = IdentifyFileType(path); if (filetype & GAME_CIA) @@ -606,12 +663,14 @@ u32 CheckEncryptedGameFile(const char* path) { return CheckEncryptedNcsdFile(path); else if (filetype & GAME_NCCH) return CheckEncryptedNcchFile(path, 0); + else if (filetype & GAME_BOSS) + return CheckEncryptedBossFile(path); else if (filetype & SYS_FIRM) return CheckEncryptedFirmFile(path); else return 1; } -u32 CryptNcchNcsdFirmFile(const char* orig, const char* dest, u32 mode, u16 crypto, // crypto only relevant for NCCH +u32 CryptNcchNcsdBossFirmFile(const char* orig, const char* dest, u32 mode, u16 crypto, // crypto only relevant for NCCH 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); @@ -642,13 +701,14 @@ u32 CryptNcchNcsdFirmFile(const char* orig, const char* dest, u32 mode, u16 cryp u32 ret = 0; if (!ShowProgress(offset, fsize, dest)) ret = 1; - if (mode & (GAME_NCCH|GAME_NCSD|SYS_FIRM)) { // for NCCH / NCSD / FIRM files + if (mode & (GAME_NCCH|GAME_NCSD|GAME_BOSS|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 (fvx_read(ofp, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1; if (((mode & GAME_NCCH) && (CryptNcchSequential(MAIN_BUFFER, i, read_bytes, crypto) != 0)) || ((mode & GAME_NCSD) && (DecryptNcsdSequential(MAIN_BUFFER, i, read_bytes) != 0)) || + ((mode & GAME_BOSS) && (CryptBossSequential(MAIN_BUFFER, i, read_bytes) != 0)) || ((mode & SYS_FIRM) && (DecryptFirmSequential(MAIN_BUFFER, i, read_bytes) != 0))) ret = 1; if (inplace) fvx_lseek(ofp, fvx_tell(ofp) - read_bytes); @@ -719,7 +779,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 (CryptNcchNcsdFirmFile(orig, dest, GAME_CIA, NCCH_NOCRYPTO, next_offset, size, chunk, titlekey) != 0) + if (CryptNcchNcsdBossFirmFile(orig, dest, GAME_CIA, NCCH_NOCRYPTO, next_offset, size, chunk, titlekey) != 0) return 1; next_offset += size; } @@ -738,7 +798,7 @@ u32 DecryptFirmFile(const char* orig, const char* dest) { UINT btr; // actual decryption - if (CryptNcchNcsdFirmFile(orig, dest, SYS_FIRM, NCCH_NOCRYPTO, 0, 0, NULL, NULL) != 0) + if (CryptNcchNcsdBossFirmFile(orig, dest, SYS_FIRM, 0, 0, 0, NULL, NULL) != 0) return 1; // open destination file, get FIRM header @@ -814,8 +874,8 @@ u32 DecryptGameFile(const char* path, bool inplace) { ret = DecryptCiaFile(path, destptr); else if (filetype & SYS_FIRM) ret = DecryptFirmFile(path, destptr); - else if (filetype & (GAME_NCCH|GAME_NCSD)) - ret = CryptNcchNcsdFirmFile(path, destptr, filetype, NCCH_NOCRYPTO, 0, 0, NULL, NULL); + else if (filetype & (GAME_NCCH|GAME_NCSD|GAME_BOSS)) + ret = CryptNcchNcsdBossFirmFile(path, destptr, filetype, NCCH_NOCRYPTO, 0, 0, NULL, NULL); else ret = 1; if (!inplace && (ret != 0)) @@ -1239,9 +1299,9 @@ u32 InjectHealthAndSafety(const char* path, const char* destdrv) { if (f_rename(path_cxi, path_bak) != FR_OK) return 1; } else f_unlink(path_cxi); - // copy the source CXI + // copy / decrypt the source CXI u32 ret = 0; - if (CryptNcchNcsdFirmFile(path, path_cxi, GAME_NCCH, NCCH_NOCRYPTO, 0, 0, NULL, NULL) != 0) + if (CryptNcchNcsdBossFirmFile(path, path_cxi, GAME_NCCH, NCCH_NOCRYPTO, 0, 0, NULL, NULL) != 0) ret = 1; // fix up the injected H&S NCCH header (copy H&S signature, title ID) @@ -1256,10 +1316,10 @@ u32 InjectHealthAndSafety(const char* path, const char* destdrv) { } else ret = 1; // encrypt the CXI in place - if (CryptNcchNcsdFirmFile(path_cxi, path_cxi, GAME_NCCH, crypto, 0, 0, NULL, NULL) != 0) + if (CryptNcchNcsdBossFirmFile(path_cxi, path_cxi, GAME_NCCH, crypto, 0, 0, NULL, NULL) != 0) ret = 1; - if (ret != 0) { // try recover + if (ret != 0) { // in case of failure: try recover f_unlink(path_cxi); f_rename(path_bak, path_cxi); } diff --git a/source/godmode.c b/source/godmode.c index 206b43b..5330a5b 100644 --- a/source/godmode.c +++ b/source/godmode.c @@ -625,8 +625,9 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, DirStruct* cur (filetype & GAME_EXEFS) ? "Mount as EXEFS image" : (filetype & GAME_ROMFS) ? "Mount as ROMFS image" : (filetype & GAME_TMD) ? "TMD file options..." : + (filetype & GAME_BOSS) ? "BOSS file options..." : (filetype & SYS_FIRM) ? "FIRM image options..." : - (filetype & BIN_NCCHNFO) ? "NCCHinfo options..." : + (filetype & BIN_NCCHNFO)? "NCCHinfo options..." : (filetype & BIN_LAUNCH) ? "Launch as arm9 payload" : "???"; optionstr[hexviewer-1] = "Show in Hexeditor"; optionstr[calcsha-1] = "Calculate SHA-256";