mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-22 18:26:39 +00:00
Bunch of changes.
* Updated NCA structs (including NcaSparseInfo). * Changed the way NCA header + NCA FS header decryption is handled. * Changed the way the NCA encrypted key area is handled. * Unified hierarchical patch generation functions. * Updated PFS, RomFS and BKTR functions accordingly to reflect NCA handling changes. * Logfile path is now relative. * Gamecard initial data lookup code now uses the initial data hash from the gamecard header (a tad bit slower, but way more failproof).
This commit is contained in:
parent
cddf57363c
commit
90e0f057bc
13 changed files with 738 additions and 794 deletions
|
@ -37,14 +37,14 @@ bool bktrInitializeContext(BktrContext *out, NcaFsSectionContext *base_nca_fs_ct
|
|||
{
|
||||
NcaContext *base_nca_ctx = NULL, *update_nca_ctx = NULL;
|
||||
|
||||
if (!out || !base_nca_fs_ctx || !base_nca_fs_ctx->enabled || !(base_nca_ctx = (NcaContext*)base_nca_fs_ctx->nca_ctx) || !base_nca_fs_ctx->header || \
|
||||
base_nca_fs_ctx->section_type != NcaFsSectionType_RomFs || base_nca_fs_ctx->encryption_type == NcaEncryptionType_AesCtrEx || !update_nca_fs_ctx || !update_nca_fs_ctx->enabled || \
|
||||
!update_nca_fs_ctx->header || !(update_nca_ctx = (NcaContext*)update_nca_fs_ctx->nca_ctx) || update_nca_fs_ctx->section_type != NcaFsSectionType_PatchRomFs || \
|
||||
update_nca_fs_ctx->encryption_type != NcaEncryptionType_AesCtrEx || base_nca_ctx->header.program_id != update_nca_ctx->header.program_id || \
|
||||
base_nca_ctx->header.content_type != update_nca_ctx->header.content_type || __builtin_bswap32(update_nca_fs_ctx->header->patch_info.indirect_header.magic) != NCA_BKTR_MAGIC || \
|
||||
__builtin_bswap32(update_nca_fs_ctx->header->patch_info.aes_ctr_ex_header.magic) != NCA_BKTR_MAGIC || \
|
||||
(update_nca_fs_ctx->header->patch_info.indirect_offset + update_nca_fs_ctx->header->patch_info.indirect_size) != update_nca_fs_ctx->header->patch_info.aes_ctr_ex_offset || \
|
||||
(update_nca_fs_ctx->header->patch_info.aes_ctr_ex_offset + update_nca_fs_ctx->header->patch_info.aes_ctr_ex_size) != update_nca_fs_ctx->section_size)
|
||||
if (!out || !base_nca_fs_ctx || !base_nca_fs_ctx->enabled || !(base_nca_ctx = (NcaContext*)base_nca_fs_ctx->nca_ctx) || base_nca_fs_ctx->section_type != NcaFsSectionType_RomFs || \
|
||||
base_nca_fs_ctx->encryption_type == NcaEncryptionType_AesCtrEx || !update_nca_fs_ctx || !update_nca_fs_ctx->enabled || !(update_nca_ctx = (NcaContext*)update_nca_fs_ctx->nca_ctx) || \
|
||||
update_nca_fs_ctx->section_type != NcaFsSectionType_PatchRomFs || update_nca_fs_ctx->encryption_type != NcaEncryptionType_AesCtrEx || \
|
||||
base_nca_ctx->header.program_id != update_nca_ctx->header.program_id || base_nca_ctx->header.content_type != update_nca_ctx->header.content_type || \
|
||||
__builtin_bswap32(update_nca_fs_ctx->header.patch_info.indirect_bucket.header.magic) != NCA_BKTR_MAGIC || \
|
||||
__builtin_bswap32(update_nca_fs_ctx->header.patch_info.aes_ctr_ex_bucket.header.magic) != NCA_BKTR_MAGIC || \
|
||||
(update_nca_fs_ctx->header.patch_info.indirect_bucket.offset + update_nca_fs_ctx->header.patch_info.indirect_bucket.size) != update_nca_fs_ctx->header.patch_info.aes_ctr_ex_bucket.offset || \
|
||||
(update_nca_fs_ctx->header.patch_info.aes_ctr_ex_bucket.offset + update_nca_fs_ctx->header.patch_info.aes_ctr_ex_bucket.size) != update_nca_fs_ctx->section_size)
|
||||
{
|
||||
LOGFILE("Invalid parameters!");
|
||||
return false;
|
||||
|
@ -59,10 +59,10 @@ bool bktrInitializeContext(BktrContext *out, NcaFsSectionContext *base_nca_fs_ct
|
|||
|
||||
/* Fill context. */
|
||||
bool success = false;
|
||||
NcaPatchInfo *patch_info = &(update_nca_fs_ctx->header->patch_info);
|
||||
NcaPatchInfo *patch_info = &(update_nca_fs_ctx->header.patch_info);
|
||||
|
||||
/* Allocate space for an extra (fake) indirect storage entry, to simplify our logic. */
|
||||
out->indirect_block = calloc(1, patch_info->indirect_size + ((0x3FF0 / sizeof(u64)) * sizeof(BktrIndirectStorageEntry)));
|
||||
out->indirect_block = calloc(1, patch_info->indirect_bucket.size + ((0x3FF0 / sizeof(u64)) * sizeof(BktrIndirectStorageEntry)));
|
||||
if (!out->indirect_block)
|
||||
{
|
||||
LOGFILE("Unable to allocate memory for the BKTR Indirect Storage Block!");
|
||||
|
@ -70,14 +70,14 @@ bool bktrInitializeContext(BktrContext *out, NcaFsSectionContext *base_nca_fs_ct
|
|||
}
|
||||
|
||||
/* Read indirect storage block data. */
|
||||
if (!ncaReadFsSection(update_nca_fs_ctx, out->indirect_block, patch_info->indirect_size, patch_info->indirect_offset))
|
||||
if (!ncaReadFsSection(update_nca_fs_ctx, out->indirect_block, patch_info->indirect_bucket.size, patch_info->indirect_bucket.offset))
|
||||
{
|
||||
LOGFILE("Failed to read BKTR Indirect Storage Block data!");
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Allocate space for an extra (fake) AesCtrEx storage entry, to simplify our logic. */
|
||||
out->aes_ctr_ex_block = calloc(1, patch_info->aes_ctr_ex_size + (((0x3FF0 / sizeof(u64)) + 1) * sizeof(BktrAesCtrExStorageEntry)));
|
||||
out->aes_ctr_ex_block = calloc(1, patch_info->aes_ctr_ex_bucket.size + (((0x3FF0 / sizeof(u64)) + 1) * sizeof(BktrAesCtrExStorageEntry)));
|
||||
if (!out->aes_ctr_ex_block)
|
||||
{
|
||||
LOGFILE("Unable to allocate memory for the BKTR AesCtrEx Storage Block!");
|
||||
|
@ -85,13 +85,13 @@ bool bktrInitializeContext(BktrContext *out, NcaFsSectionContext *base_nca_fs_ct
|
|||
}
|
||||
|
||||
/* Read AesCtrEx storage block data. */
|
||||
if (!ncaReadFsSection(update_nca_fs_ctx, out->aes_ctr_ex_block, patch_info->aes_ctr_ex_size, patch_info->aes_ctr_ex_offset))
|
||||
if (!ncaReadFsSection(update_nca_fs_ctx, out->aes_ctr_ex_block, patch_info->aes_ctr_ex_bucket.size, patch_info->aes_ctr_ex_bucket.offset))
|
||||
{
|
||||
LOGFILE("Failed to read BKTR AesCtrEx Storage Block data!");
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (out->aes_ctr_ex_block->physical_size != patch_info->aes_ctr_ex_offset)
|
||||
if (out->aes_ctr_ex_block->physical_size != patch_info->aes_ctr_ex_bucket.offset)
|
||||
{
|
||||
LOGFILE("Invalid BKTR AesCtrEx Storage Block size!");
|
||||
goto end;
|
||||
|
@ -129,16 +129,16 @@ bool bktrInitializeContext(BktrContext *out, NcaFsSectionContext *base_nca_fs_ct
|
|||
BktrIndirectStorageBucket *last_indirect_bucket = bktrGetIndirectStorageBucket(out->indirect_block, out->indirect_block->bucket_count - 1);
|
||||
BktrAesCtrExStorageBucket *last_aes_ctr_ex_bucket = bktrGetAesCtrExStorageBucket(out->aes_ctr_ex_block, out->aes_ctr_ex_block->bucket_count - 1);
|
||||
last_indirect_bucket->indirect_storage_entries[last_indirect_bucket->entry_count].virtual_offset = out->indirect_block->virtual_size;
|
||||
last_aes_ctr_ex_bucket->aes_ctr_ex_storage_entries[last_aes_ctr_ex_bucket->entry_count].offset = patch_info->indirect_offset;
|
||||
last_aes_ctr_ex_bucket->aes_ctr_ex_storage_entries[last_aes_ctr_ex_bucket->entry_count].generation = update_nca_fs_ctx->header->generation;
|
||||
last_aes_ctr_ex_bucket->aes_ctr_ex_storage_entries[last_aes_ctr_ex_bucket->entry_count].offset = patch_info->indirect_bucket.offset;
|
||||
last_aes_ctr_ex_bucket->aes_ctr_ex_storage_entries[last_aes_ctr_ex_bucket->entry_count].generation = update_nca_fs_ctx->header.aes_ctr_upper_iv.generation;
|
||||
last_aes_ctr_ex_bucket->aes_ctr_ex_storage_entries[last_aes_ctr_ex_bucket->entry_count + 1].offset = update_nca_fs_ctx->section_size;
|
||||
last_aes_ctr_ex_bucket->aes_ctr_ex_storage_entries[last_aes_ctr_ex_bucket->entry_count + 1].generation = 0;
|
||||
|
||||
/* Initialize update NCA RomFS context. */
|
||||
/* Don't verify offsets from Patch RomFS sections, because they reflect the full, patched RomFS image. */
|
||||
out->patch_romfs_ctx.nca_fs_ctx = update_nca_fs_ctx;
|
||||
out->patch_romfs_ctx.offset = out->offset = update_nca_fs_ctx->header->hash_info.hierarchical_integrity.hash_target_layer_info.offset;
|
||||
out->patch_romfs_ctx.size = out->size = update_nca_fs_ctx->header->hash_info.hierarchical_integrity.hash_target_layer_info.size;
|
||||
out->patch_romfs_ctx.offset = out->offset = update_nca_fs_ctx->header.hash_data.integrity_meta_info.info_level_hash.level_information[NCA_IVFC_LEVEL_COUNT - 1].offset;
|
||||
out->patch_romfs_ctx.size = out->size = update_nca_fs_ctx->header.hash_data.integrity_meta_info.info_level_hash.level_information[NCA_IVFC_LEVEL_COUNT - 1].size;
|
||||
|
||||
/* Read update NCA RomFS header. */
|
||||
if (!bktrPhysicalSectionRead(out, &(out->patch_romfs_ctx.header), sizeof(RomFileSystemHeader), out->patch_romfs_ctx.offset))
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
#ifndef __FS_EXT_H__
|
||||
#define __FS_EXT_H__
|
||||
|
||||
#define GAMECARD_CERT_MAGIC 0x43455254 /* "CERT". */
|
||||
|
||||
/// Located at offset 0x7000 in the gamecard image.
|
||||
typedef struct {
|
||||
u8 signature[0x100]; ///< RSA-2048 PKCS #1 signature over the rest of the data.
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
|
||||
/* Type definitions. */
|
||||
|
||||
/// Only kept for documentation purposes, not really used.
|
||||
/// A copy of the gamecard header without the RSA-2048 signature and a plaintext GameCardHeaderEncryptedArea precedes this struct in FS program memory.
|
||||
typedef struct {
|
||||
u32 memory_interface_mode;
|
||||
u32 asic_status;
|
||||
|
@ -106,6 +108,8 @@ static u64 g_gameCardCapacity = 0;
|
|||
static u8 *g_gameCardHfsRootHeader = NULL; /// GameCardHashFileSystemHeader + (entry_count * GameCardHashFileSystemEntry) + Name Table.
|
||||
static GameCardHashFileSystemPartitionInfo *g_gameCardHfsPartitions = NULL;
|
||||
|
||||
static GameCardKeyArea g_gameCardKeyArea = {0};
|
||||
|
||||
static MemoryLocation g_fsProgramMemory = {
|
||||
.program_id = FS_SYSMODULE_TID,
|
||||
.mask = 0,
|
||||
|
@ -113,9 +117,6 @@ static MemoryLocation g_fsProgramMemory = {
|
|||
.data_size = 0
|
||||
};
|
||||
|
||||
static GameCardSecurityInformation g_gameCardSecurityInfo = {0};
|
||||
static GameCardKeyArea g_gameCardKeyArea = {0};
|
||||
|
||||
/* Function prototypes. */
|
||||
|
||||
static bool gamecardCreateDetectionThread(void);
|
||||
|
@ -127,7 +128,7 @@ NX_INLINE bool gamecardIsInserted(void);
|
|||
static void gamecardLoadInfo(void);
|
||||
static void gamecardFreeInfo(void);
|
||||
|
||||
static bool gamecardReadSecurityInformation(void);
|
||||
static bool gamecardReadInitialData(void);
|
||||
|
||||
static bool gamecardGetHandleAndStorage(u32 partition);
|
||||
NX_INLINE void gamecardCloseHandle(void);
|
||||
|
@ -713,13 +714,13 @@ static void gamecardLoadInfo(void)
|
|||
}
|
||||
}
|
||||
|
||||
/* Read full FS program memory to retrieve the GameCardSecurityInformation data, which holds the gamecard initial data area. */
|
||||
/* This must be performed while the gamecard is in secure mode, which is already taken care of in the gamecardReadStorageArea() calls from the last iteration in the previous for() loop. */
|
||||
/* GameCardSecurityInformation data is returned by Lotus command "ChangeToSecureMode" (0xF), and kept in FS program memory only after the gamecard secure area has been both mounted and read from. */
|
||||
/* Under some circumstances, the gamecard initial data is located *after* the GameCardSecurityInformation area (offset 0x600), instead of its common location at offset 0x400. */
|
||||
if (!gamecardReadSecurityInformation())
|
||||
/* Read full FS program memory to retrieve the GameCardInitialData block, which is part of the GameCardKeyArea block. */
|
||||
/* In FS program memory, this is stored as part of the GameCardSecurityInformation struct, which is returned by Lotus command "ChangeToSecureMode" (0xF). */
|
||||
/* This means it is only available *after* the gamecard secure area has been both mounted and read from, which has already been taken care of in the last iteration from the previous for() loop. */
|
||||
/* The GameCardSecurityInformation struct is only kept for documentation purposes. It isn't used at all to retrieve the GameCardInitialData block. */
|
||||
if (!gamecardReadInitialData())
|
||||
{
|
||||
LOGFILE("Failed to read gamecard security information area from FS program memory!");
|
||||
LOGFILE("Failed to read gamecard initial data area from FS program memory!");
|
||||
goto end;
|
||||
}
|
||||
|
||||
|
@ -731,10 +732,8 @@ end:
|
|||
|
||||
static void gamecardFreeInfo(void)
|
||||
{
|
||||
memset(&g_gameCardHeader, 0, sizeof(GameCardHeader));
|
||||
|
||||
memset(&g_gameCardSecurityInfo, 0, sizeof(GameCardSecurityInformation));
|
||||
memset(&g_gameCardKeyArea, 0, sizeof(GameCardKeyArea));
|
||||
memset(&g_gameCardHeader, 0, sizeof(GameCardHeader));
|
||||
|
||||
g_gameCardStorageNormalAreaSize = 0;
|
||||
g_gameCardStorageSecureAreaSize = 0;
|
||||
|
@ -768,9 +767,10 @@ static void gamecardFreeInfo(void)
|
|||
g_gameCardInfoLoaded = false;
|
||||
}
|
||||
|
||||
static bool gamecardReadSecurityInformation(void)
|
||||
static bool gamecardReadInitialData(void)
|
||||
{
|
||||
bool found = false;
|
||||
u8 tmp_hash[SHA256_HASH_SIZE] = {0};
|
||||
|
||||
/* Retrieve full FS program memory dump. */
|
||||
if (!memRetrieveFullProgramMemory(&g_fsProgramMemory))
|
||||
|
@ -779,35 +779,20 @@ static bool gamecardReadSecurityInformation(void)
|
|||
return false;
|
||||
}
|
||||
|
||||
/* Look for the gamecard header in the FS memory dump. */
|
||||
/* Look for the initial data block in the FS memory dump using the package ID and the initial data hash from the gamecard header. */
|
||||
for(u64 offset = 0; offset < g_fsProgramMemory.data_size; offset++)
|
||||
{
|
||||
if (memcmp(&(g_gameCardHeader.magic), g_fsProgramMemory.data + offset, 0x90) != 0) continue;
|
||||
if (memcmp(g_fsProgramMemory.data + offset, &(g_gameCardHeader.package_id), sizeof(g_gameCardHeader.package_id)) != 0) continue;
|
||||
|
||||
/* Found the gamecard header. Let's read the GameCardSecurityInformation element. */
|
||||
offset += 0x100;
|
||||
memcpy(&g_gameCardSecurityInfo, g_fsProgramMemory.data + offset, sizeof(GameCardSecurityInformation));
|
||||
sha256CalculateHash(tmp_hash, g_fsProgramMemory.data + offset, sizeof(GameCardInitialData));
|
||||
|
||||
/* Check the key_source / package_id value. */
|
||||
if (g_gameCardSecurityInfo.initial_data.package_id == g_gameCardHeader.package_id)
|
||||
if (!memcmp(tmp_hash, g_gameCardHeader.initial_data_hash, SHA256_HASH_SIZE))
|
||||
{
|
||||
/* Jackpot. */
|
||||
memcpy(&(g_gameCardKeyArea.initial_data), g_fsProgramMemory.data + offset, sizeof(GameCardInitialData));
|
||||
found = true;
|
||||
} else {
|
||||
/* Copy the sector right after the GameCardSecurityInformation element from the memory dump, since it may hold the gamecard initial data. */
|
||||
offset += sizeof(GameCardSecurityInformation);
|
||||
memcpy(&(g_gameCardSecurityInfo.initial_data), g_fsProgramMemory.data + offset, sizeof(GameCardInitialData));
|
||||
found = (g_gameCardSecurityInfo.initial_data.package_id == g_gameCardHeader.package_id);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (found)
|
||||
{
|
||||
memcpy(&(g_gameCardKeyArea.initial_data), &(g_gameCardSecurityInfo.initial_data), sizeof(GameCardInitialData));
|
||||
} else {
|
||||
LOGFILE("Failed to locate gamecard initial data area!");
|
||||
}
|
||||
|
||||
/* Free FS memory dump. */
|
||||
|
@ -824,29 +809,29 @@ static bool gamecardGetHandleAndStorage(u32 partition)
|
|||
return false;
|
||||
}
|
||||
|
||||
Result rc1 = 0, rc2 = 0;
|
||||
Result rc = 0;
|
||||
|
||||
/* 10 tries. */
|
||||
for(u8 i = 0; i < 10; i++)
|
||||
{
|
||||
/* 100 ms wait in case there was an error in the previous loop. */
|
||||
if (R_FAILED(rc1) || R_FAILED(rc2)) svcSleepThread(100000000);
|
||||
if (R_FAILED(rc)) svcSleepThread(100000000);
|
||||
|
||||
/* First, let's try to retrieve a gamecard handle. */
|
||||
/* This can return 0x140A02 if the "nogc" patch is enabled by the running CFW. */
|
||||
rc1 = fsDeviceOperatorGetGameCardHandle(&g_deviceOperator, &g_gameCardHandle);
|
||||
if (R_FAILED(rc1))
|
||||
rc = fsDeviceOperatorGetGameCardHandle(&g_deviceOperator, &g_gameCardHandle);
|
||||
if (R_FAILED(rc))
|
||||
{
|
||||
LOGFILE("fsDeviceOperatorGetGameCardHandle failed on try #%u! (0x%08X).", i + 1, rc1);
|
||||
LOGFILE("fsDeviceOperatorGetGameCardHandle failed on try #%u! (0x%08X).", i + 1, rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If the previous call succeeded, let's try to open the desired gamecard storage area. */
|
||||
rc2 = fsOpenGameCardStorage(&g_gameCardStorage, &g_gameCardHandle, partition);
|
||||
if (R_FAILED(rc2))
|
||||
rc = fsOpenGameCardStorage(&g_gameCardStorage, &g_gameCardHandle, partition);
|
||||
if (R_FAILED(rc))
|
||||
{
|
||||
gamecardCloseHandle(); /* Close invalid gamecard handle. */
|
||||
LOGFILE("fsOpenGameCardStorage failed to open %s storage area on try #%u! (0x%08X).", GAMECARD_STORAGE_AREA_NAME(partition + 1), i + 1, rc2);
|
||||
LOGFILE("fsOpenGameCardStorage failed to open %s storage area on try #%u! (0x%08X).", GAMECARD_STORAGE_AREA_NAME(partition + 1), i + 1, rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -854,7 +839,7 @@ static bool gamecardGetHandleAndStorage(u32 partition)
|
|||
break;
|
||||
}
|
||||
|
||||
return (R_SUCCEEDED(rc1) && R_SUCCEEDED(rc2));
|
||||
return R_SUCCEEDED(rc);
|
||||
}
|
||||
|
||||
NX_INLINE void gamecardCloseHandle(void)
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
#include "fs_ext.h"
|
||||
|
||||
#define GAMECARD_HEAD_MAGIC 0x48454144 /* "HEAD". */
|
||||
#define GAMECARD_CERT_MAGIC 0x43455254 /* "CERT". */
|
||||
|
||||
#define GAMECARD_MEDIA_UNIT_SIZE 0x200
|
||||
#define GAMECARD_MEDIA_UNIT_OFFSET(x) ((u64)(x) * GAMECARD_MEDIA_UNIT_SIZE)
|
||||
|
@ -37,18 +36,18 @@
|
|||
((x) == GameCardHashFileSystemPartitionType_Logo ? "logo" : ((x) == GameCardHashFileSystemPartitionType_Normal ? "normal" : \
|
||||
((x) == GameCardHashFileSystemPartitionType_Secure ? "secure" : ((x) == GameCardHashFileSystemPartitionType_Boot ? "boot" : "unknown"))))))
|
||||
|
||||
/// Encrypted using AES-128-ECB with the common titlekek generator key (stored in the .rodata segment from the Lotus firmware).
|
||||
typedef struct {
|
||||
u64 package_id; ///< Matches package_id from GameCardHeader.
|
||||
u8 reserved[0x8]; ///< Just zeroes.
|
||||
} GameCardKeySource;
|
||||
|
||||
/// Plaintext area. Dumped from FS program memory.
|
||||
typedef struct {
|
||||
union {
|
||||
u8 key_source[0x10]; ///< Encrypted using AES-128-ECB with the common titlekek generator key (stored in the .rodata segment from the Lotus firmware).
|
||||
struct {
|
||||
u64 package_id; ///< Matches package_id from GameCardHeader.
|
||||
u64 padding; ///< Just zeroes.
|
||||
};
|
||||
};
|
||||
GameCardKeySource key_source;
|
||||
u8 encrypted_titlekey[0x10]; ///< Encrypted using AES-128-CCM with the decrypted key_source and the nonce from this section.
|
||||
u8 mac[0x10]; ///< Used to verify the validity of the decrypted titlekey.
|
||||
u8 nonce[0xC]; ///< Used as the IV to decrypt the key_source using AES-128-CCM.
|
||||
u8 nonce[0xC]; ///< Used as the IV to decrypt encrypted_titlekey using AES-128-CCM.
|
||||
u8 reserved[0x1C4];
|
||||
} GameCardInitialData;
|
||||
|
||||
|
@ -81,7 +80,7 @@ typedef enum {
|
|||
typedef struct {
|
||||
u8 kek_index : 4; ///< GameCardKekIndex.
|
||||
u8 titlekey_dec_index : 4;
|
||||
} GameCardKeyFlags;
|
||||
} GameCardKeyIndex;
|
||||
|
||||
typedef enum {
|
||||
GameCardRomSize_1GiB = 0xFA,
|
||||
|
@ -101,20 +100,20 @@ typedef enum {
|
|||
} GameCardFlags;
|
||||
|
||||
typedef enum {
|
||||
GameCardSelSec_ForT1 = 0,
|
||||
GameCardSelSec_ForT2 = 1
|
||||
GameCardSelSec_ForT1 = 1,
|
||||
GameCardSelSec_ForT2 = 2
|
||||
} GameCardSelSec;
|
||||
|
||||
typedef enum {
|
||||
GameCardFwVersion_Dev = 0,
|
||||
GameCardFwVersion_Prod = 1,
|
||||
GameCardFwVersion_Since400NUP = 2
|
||||
GameCardFwVersion_ForDev = 0,
|
||||
GameCardFwVersion_Before400NUP = 1, ///< cup_version < 268435456 (4.0.0-0.0) in GameCardHeaderEncryptedArea.
|
||||
GameCardFwVersion_Since400NUP = 2 ///< cup_version >= 268435456 (4.0.0-0.0) in GameCardHeaderEncryptedArea.
|
||||
} GameCardFwVersion;
|
||||
|
||||
typedef enum {
|
||||
GameCardAccCtrl_25MHz = 0xA10011,
|
||||
GameCardAccCtrl_50MHz = 0xA10010
|
||||
} GameCardAccCtrl;
|
||||
GameCardAccCtrl1_25MHz = 0xA10011,
|
||||
GameCardAccCtrl1_50MHz = 0xA10010 ///< GameCardRomSize_8GiB or greater.
|
||||
} GameCardAccCtrl1;
|
||||
|
||||
typedef enum {
|
||||
GameCardCompatibilityType_Normal = 0,
|
||||
|
@ -129,27 +128,27 @@ typedef struct {
|
|||
} GameCardFwMode;
|
||||
|
||||
typedef struct {
|
||||
u32 GameCardUppVersion_MinorRelstep : 8;
|
||||
u32 GameCardUppVersion_MajorRelstep : 8;
|
||||
u32 GameCardUppVersion_Micro : 4;
|
||||
u32 GameCardUppVersion_Minor : 6;
|
||||
u32 GameCardUppVersion_Major : 6;
|
||||
} GameCardUppVersion;
|
||||
u32 GameCardCupVersion_MinorRelstep : 8;
|
||||
u32 GameCardCupVersion_MajorRelstep : 8;
|
||||
u32 GameCardCupVersion_Micro : 4;
|
||||
u32 GameCardCupVersion_Minor : 6;
|
||||
u32 GameCardCupVersion_Major : 6;
|
||||
} GameCardCupVersion;
|
||||
|
||||
/// Encrypted using AES-128-CBC with the `xci_header_key` (which can't dumped through current methods) and the IV from `GameCardHeader`.
|
||||
typedef struct {
|
||||
u64 fw_version; ///< GameCardFwVersion.
|
||||
u32 acc_ctrl; ///< GameCardAccCtrl.
|
||||
u32 acc_ctrl_1; ///< GameCardAccCtrl1.
|
||||
u32 wait_1_time_read; ///< Always 0x1388.
|
||||
u32 wait_2_time_read; ///< Always 0.
|
||||
u32 wait_1_time_write; ///< Always 0.
|
||||
u32 wait_2_time_write; ///< Always 0.
|
||||
GameCardFwMode fw_mode;
|
||||
GameCardUppVersion upp_version;
|
||||
GameCardCupVersion cup_version;
|
||||
u8 compatibility_type; ///< GameCardCompatibilityType.
|
||||
u8 reserved_1[0x3];
|
||||
u64 upp_hash;
|
||||
u64 upp_id; ///< Must match GAMECARD_UPDATE_TID.
|
||||
u64 cup_hash;
|
||||
u64 cup_id; ///< Must match GAMECARD_UPDATE_TID.
|
||||
u8 reserved_2[0x38];
|
||||
} GameCardHeaderEncryptedArea;
|
||||
|
||||
|
@ -159,9 +158,9 @@ typedef struct {
|
|||
u32 magic; ///< "HEAD".
|
||||
u32 secure_area_start_address; ///< Expressed in GAMECARD_MEDIA_UNIT_SIZE blocks.
|
||||
u32 backup_area_start_address; ///< Always 0xFFFFFFFF.
|
||||
GameCardKeyFlags key_flags;
|
||||
GameCardKeyIndex key_index;
|
||||
u8 rom_size; ///< GameCardRomSize.
|
||||
u8 header_version;
|
||||
u8 header_version; ///< Always 0.
|
||||
u8 flags; ///< GameCardFlags.
|
||||
u64 package_id;
|
||||
u32 valid_data_end_address; ///< Expressed in GAMECARD_MEDIA_UNIT_SIZE blocks.
|
||||
|
@ -172,8 +171,8 @@ typedef struct {
|
|||
u8 partition_fs_header_hash[SHA256_HASH_SIZE];
|
||||
u8 initial_data_hash[SHA256_HASH_SIZE];
|
||||
u32 sel_sec; ///< GameCardSelSec.
|
||||
u32 sel_t1_key_index;
|
||||
u32 sel_key_index;
|
||||
u32 sel_t1_key; ///< Always 2.
|
||||
u32 sel_key; ///> Always 0.
|
||||
u32 normal_area_end_address; ///< Expressed in GAMECARD_MEDIA_UNIT_SIZE blocks.
|
||||
GameCardHeaderEncryptedArea encrypted_area;
|
||||
} GameCardHeader;
|
||||
|
|
|
@ -200,7 +200,7 @@ int main(int argc, char *argv[])
|
|||
|
||||
int ret = 0;
|
||||
|
||||
LOGFILE("nxdumptool starting.");
|
||||
LOGFILE(APP_TITLE " starting.");
|
||||
|
||||
consoleInit(NULL);
|
||||
|
||||
|
@ -216,7 +216,7 @@ int main(int argc, char *argv[])
|
|||
|
||||
u8 *buf = NULL;
|
||||
|
||||
u64 base_tid = (u64)0x01006F8002326000; // ACNH 0x01006F8002326000 | Smash 0x01006A800016E000 | Dark Souls 0x01004AB00A260000 | BotW 0x01007EF00011E000
|
||||
u64 base_tid = (u64)0x01006A800016E000; // ACNH 0x01006F8002326000 | Smash 0x01006A800016E000 | Dark Souls 0x01004AB00A260000 | BotW 0x01007EF00011E000
|
||||
u64 update_tid = (base_tid | 0x800);
|
||||
|
||||
Ticket base_tik = {0}, update_tik = {0};
|
||||
|
|
919
source/nca.c
919
source/nca.c
File diff suppressed because it is too large
Load diff
309
source/nca.h
309
source/nca.h
|
@ -25,29 +25,28 @@
|
|||
|
||||
#include "tik.h"
|
||||
|
||||
#define NCA_HEADER_LENGTH 0x400
|
||||
#define NCA_FS_HEADER_LENGTH 0x200
|
||||
#define NCA_FS_HEADER_COUNT 4
|
||||
#define NCA_FULL_HEADER_LENGTH (NCA_HEADER_LENGTH + (NCA_FS_HEADER_LENGTH * NCA_FS_HEADER_COUNT))
|
||||
#define NCA_FULL_HEADER_LENGTH (sizeof(NcaHeader) + (sizeof(NcaFsHeader) * NCA_FS_HEADER_COUNT))
|
||||
|
||||
#define NCA_NCA0_MAGIC 0x4E434130 /* "NCA0" */
|
||||
#define NCA_NCA2_MAGIC 0x4E434132 /* "NCA2" */
|
||||
#define NCA_NCA3_MAGIC 0x4E434133 /* "NCA3" */
|
||||
#define NCA_NCA0_MAGIC 0x4E434130 /* "NCA0". */
|
||||
#define NCA_NCA2_MAGIC 0x4E434132 /* "NCA2". */
|
||||
#define NCA_NCA3_MAGIC 0x4E434133 /* "NCA3". */
|
||||
|
||||
#define NCA_HIERARCHICAL_SHA256_LAYER_COUNT 2
|
||||
#define NCA_USED_KEY_AREA_SIZE sizeof(NcaDecryptedKeyArea) /* Four keys, 0x40 bytes. */
|
||||
|
||||
#define NCA_IVFC_MAGIC 0x49564643 /* "IVFC" */
|
||||
#define NCA_IVFC_LAYER_COUNT 7
|
||||
#define NCA_IVFC_HASH_DATA_LAYER_COUNT 5
|
||||
#define NCA_IVFC_BLOCK_SIZE(x) (1 << (x))
|
||||
#define NCA_HIERARCHICAL_SHA256_MAX_REGION_COUNT 5
|
||||
|
||||
#define NCA_BKTR_MAGIC 0x424B5452 /* "BKTR" */
|
||||
#define NCA_IVFC_MAGIC 0x49564643 /* "IVFC". */
|
||||
#define NCA_IVFC_MAX_LEVEL_COUNT 7
|
||||
#define NCA_IVFC_LEVEL_COUNT (NCA_IVFC_MAX_LEVEL_COUNT - 1)
|
||||
#define NCA_IVFC_BLOCK_SIZE(x) (1U << (x))
|
||||
|
||||
#define NCA_FS_ENTRY_BLOCK_SIZE 0x200
|
||||
#define NCA_FS_ENTRY_BLOCK_OFFSET(x) ((u64)(x) * NCA_FS_ENTRY_BLOCK_SIZE)
|
||||
#define NCA_BKTR_MAGIC 0x424B5452 /* "BKTR". */
|
||||
|
||||
#define NCA_FS_SECTOR_SIZE 0x200
|
||||
#define NCA_FS_SECTOR_OFFSET(x) ((u64)(x) * NCA_FS_SECTOR_SIZE)
|
||||
|
||||
#define NCA_AES_XTS_SECTOR_SIZE 0x200
|
||||
#define NCA_NCA0_FS_HEADER_AES_XTS_SECTOR(x) (((x) - NCA_HEADER_LENGTH) >> 9)
|
||||
|
||||
typedef enum {
|
||||
NcaDistributionType_Download = 0,
|
||||
|
@ -96,19 +95,49 @@ typedef enum {
|
|||
} NcaKeyGeneration;
|
||||
|
||||
typedef struct {
|
||||
u32 start_block_offset; ///< Expressed in NCA_FS_ENTRY_BLOCK_SIZE blocks.
|
||||
u32 end_block_offset; ///< Expressed in NCA_FS_ENTRY_BLOCK_SIZE blocks.
|
||||
u8 enable_entry;
|
||||
u8 reserved[0x7];
|
||||
} NcaFsEntry;
|
||||
u32 start_sector; ///< Expressed in NCA_FS_SECTOR_SIZE sectors.
|
||||
u32 end_sector; ///< Expressed in NCA_FS_SECTOR_SIZE sectors.
|
||||
u32 hash_sector;
|
||||
u8 reserved[0x4];
|
||||
} NcaFsInfo;
|
||||
|
||||
typedef struct {
|
||||
u8 hash[SHA256_HASH_SIZE];
|
||||
} NcaFsHash;
|
||||
} NcaFsHeaderHash;
|
||||
|
||||
/// Encrypted NCA key area used to hold NCA FS section encryption keys. Zeroed out if the NCA uses titlekey crypto.
|
||||
/// Only the first 4 key entries are encrypted.
|
||||
/// If a particular key entry is unused, it is zeroed out before this area is encrypted.
|
||||
typedef struct {
|
||||
u8 key[0x10];
|
||||
} NcaKey;
|
||||
u8 aes_xts_1[AES_128_KEY_SIZE]; ///< AES-128-XTS key 0 used for NCA FS sections with NcaEncryptionType_AesXts crypto.
|
||||
u8 aes_xts_2[AES_128_KEY_SIZE]; ///< AES-128-XTS key 1 used for NCA FS sections with NcaEncryptionType_AesXts crypto.
|
||||
u8 aes_ctr[AES_128_KEY_SIZE]; ///< AES-128-CTR key used for NCA FS sections with NcaEncryptionType_AesCtr crypto.
|
||||
u8 aes_ctr_ex[AES_128_KEY_SIZE]; ///< AES-128-CTR key used for NCA FS sections with NcaEncryptionType_AesCtrEx crypto.
|
||||
u8 aes_ctr_hw[AES_128_KEY_SIZE]; ///< Unused AES-128-CTR key.
|
||||
u8 reserved[0xB0];
|
||||
} NcaEncryptedKeyArea;
|
||||
|
||||
/// First 0x400 bytes from every NCA.
|
||||
typedef struct {
|
||||
u8 main_signature[0x100]; ///< RSA-PSS signature over header with fixed key.
|
||||
u8 acid_signature[0x100]; ///< RSA-PSS signature over header with key in NPDM.
|
||||
u32 magic; ///< "NCA0" / "NCA2" / "NCA3".
|
||||
u8 distribution_type; ///< NcaDistributionType.
|
||||
u8 content_type; ///< NcaContentType.
|
||||
u8 key_generation_old; ///< NcaKeyGenerationOld.
|
||||
u8 kaek_index; ///< NcaKeyAreaEncryptionKeyIndex.
|
||||
u64 content_size;
|
||||
u64 program_id;
|
||||
u32 content_index;
|
||||
NcaSdkAddOnVersion sdk_addon_version;
|
||||
u8 key_generation; ///< NcaKeyGeneration.
|
||||
u8 main_signature_key_generation;
|
||||
u8 reserved_1[0xE];
|
||||
FsRightsId rights_id; ///< Used for titlekey crypto.
|
||||
NcaFsInfo fs_info[NCA_FS_HEADER_COUNT]; ///< Start and end sectors for each NCA FS section.
|
||||
NcaFsHeaderHash fs_header_hash[NCA_FS_HEADER_COUNT]; ///< SHA-256 hashes calculated over each NCA FS section header.
|
||||
NcaEncryptedKeyArea encrypted_key_area;
|
||||
} NcaHeader;
|
||||
|
||||
typedef enum {
|
||||
NcaFsType_RomFs = 0,
|
||||
|
@ -133,121 +162,111 @@ typedef enum {
|
|||
typedef struct {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
} NcaHierarchicalSha256LayerInfo;
|
||||
} NcaRegion;
|
||||
|
||||
/// Used for NcaFsType_PartitionFs and NCA0 NcaFsType_RomFsRomFS.
|
||||
/// Used by NcaFsType_PartitionFs and NCA0 NcaFsType_RomFs.
|
||||
typedef struct {
|
||||
u8 master_hash[SHA256_HASH_SIZE];
|
||||
u32 hash_block_size;
|
||||
u32 layer_count;
|
||||
NcaHierarchicalSha256LayerInfo hash_data_layer_info;
|
||||
NcaHierarchicalSha256LayerInfo hash_target_layer_info;
|
||||
} NcaHierarchicalSha256;
|
||||
u32 hash_region_count;
|
||||
NcaRegion hash_region[NCA_HIERARCHICAL_SHA256_MAX_REGION_COUNT];
|
||||
} NcaHierarchicalSha256Data;
|
||||
|
||||
typedef struct {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
u32 block_size; ///< Use NCA_IVFC_BLOCK_SIZE to calculate the actual block size using this value.
|
||||
u32 block_order; ///< Use NCA_IVFC_BLOCK_SIZE to calculate the actual block size using this value.
|
||||
u8 reserved[0x4];
|
||||
} NcaHierarchicalIntegrityLayerInfo;
|
||||
} NcaHierarchicalIntegrityVerificationLevelInformation;
|
||||
|
||||
/// Used for NcaFsType_RomFs.
|
||||
typedef struct {
|
||||
u8 value[0x20];
|
||||
} NcaSignatureSalt;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
u32 max_level_count; ///< Always NCA_IVFC_MAX_LEVEL_COUNT.
|
||||
NcaHierarchicalIntegrityVerificationLevelInformation level_information[NCA_IVFC_LEVEL_COUNT];
|
||||
NcaSignatureSalt signature_salt;
|
||||
} NcaInfoLevelHash;
|
||||
#pragma pack(pop)
|
||||
|
||||
/// Used by NcaFsType_RomFs.
|
||||
typedef struct {
|
||||
u32 magic; ///< "IVFC".
|
||||
u32 version;
|
||||
u32 master_hash_size;
|
||||
u32 layer_count;
|
||||
NcaHierarchicalIntegrityLayerInfo hash_data_layer_info[NCA_IVFC_HASH_DATA_LAYER_COUNT];
|
||||
NcaHierarchicalIntegrityLayerInfo hash_target_layer_info;
|
||||
u8 signature_salt[0x20];
|
||||
u8 master_hash[0x20];
|
||||
} NcaHierarchicalIntegrity;
|
||||
u32 master_hash_size; ///< Always SHA256_HASH_SIZE.
|
||||
NcaInfoLevelHash info_level_hash;
|
||||
u8 master_hash[SHA256_HASH_SIZE];
|
||||
} NcaIntegrityMetaInfo;
|
||||
|
||||
typedef struct {
|
||||
union {
|
||||
struct {
|
||||
///< Used if hash_type == NcaHashType_HierarchicalSha256 (NcaFsType_PartitionFs and NCA0 NcaFsType_RomFs).
|
||||
NcaHierarchicalSha256 hierarchical_sha256;
|
||||
u8 reserved_1[0xB0];
|
||||
NcaHierarchicalSha256Data hierarchical_sha256_data;
|
||||
u8 reserved_1[0x80];
|
||||
};
|
||||
struct {
|
||||
///< Used if hash_type == NcaHashType_HierarchicalIntegrity (NcaFsType_RomFs).
|
||||
NcaHierarchicalIntegrity hierarchical_integrity;
|
||||
NcaIntegrityMetaInfo integrity_meta_info;
|
||||
u8 reserved_2[0x18];
|
||||
};
|
||||
};
|
||||
} NcaHashInfo;
|
||||
} NcaHashData;
|
||||
|
||||
typedef struct {
|
||||
u32 magic; ///< "BKTR".
|
||||
u32 bucket_count;
|
||||
u32 version; ///< offset_count / node_count ?
|
||||
u32 entry_count;
|
||||
u8 reserved[0x4];
|
||||
} NcaBucketTreeHeader;
|
||||
|
||||
typedef struct {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
NcaBucketTreeHeader header;
|
||||
} NcaBucketInfo;
|
||||
|
||||
/// Only used for NcaEncryptionType_AesCtrEx (PatchRomFs).
|
||||
typedef struct {
|
||||
u64 indirect_offset;
|
||||
u64 indirect_size;
|
||||
NcaBucketTreeHeader indirect_header;
|
||||
u64 aes_ctr_ex_offset;
|
||||
u64 aes_ctr_ex_size;
|
||||
NcaBucketTreeHeader aes_ctr_ex_header;
|
||||
NcaBucketInfo indirect_bucket;
|
||||
NcaBucketInfo aes_ctr_ex_bucket;
|
||||
} NcaPatchInfo;
|
||||
|
||||
/// Format unknown.
|
||||
typedef struct {
|
||||
u8 unknown[0x30];
|
||||
union {
|
||||
u8 value[0x8];
|
||||
struct {
|
||||
u32 generation;
|
||||
u32 secure_value;
|
||||
};
|
||||
};
|
||||
} NcaAesCtrUpperIv;
|
||||
|
||||
/// Used in NCAs with sparse storage.
|
||||
typedef struct {
|
||||
NcaBucketInfo sparse_bucket;
|
||||
u64 physical_offset;
|
||||
u16 generation;
|
||||
u8 reserved[0x6];
|
||||
} NcaSparseInfo;
|
||||
|
||||
/// Four NCA FS headers are placed right after the 0x400 byte long NCA header in NCA2 and NCA3.
|
||||
/// NCA0 place the FS headers at the start sector from the NcaFsInfo entries.
|
||||
typedef struct {
|
||||
u16 version;
|
||||
u8 fs_type; ///< NcaFsType.
|
||||
u8 hash_type; ///< NcaHashType.
|
||||
u8 encryption_type; ///< NcaEncryptionType.
|
||||
u8 reserved_1[0x3];
|
||||
NcaHashInfo hash_info;
|
||||
NcaHashData hash_data;
|
||||
NcaPatchInfo patch_info;
|
||||
union {
|
||||
u8 section_ctr[0x8];
|
||||
struct {
|
||||
u32 generation;
|
||||
u32 secure_value;
|
||||
};
|
||||
};
|
||||
NcaAesCtrUpperIv aes_ctr_upper_iv;
|
||||
NcaSparseInfo sparse_info;
|
||||
u8 reserved_2[0x88];
|
||||
} NcaFsHeader;
|
||||
|
||||
typedef struct {
|
||||
u8 main_signature[0x100]; ///< RSA-PSS signature over header with fixed key.
|
||||
u8 acid_signature[0x100]; ///< RSA-PSS signature over header with key in NPDM.
|
||||
u32 magic; ///< "NCA0" / "NCA2" / "NCA3".
|
||||
u8 distribution_type; ///< NcaDistributionType.
|
||||
u8 content_type; ///< NcaContentType.
|
||||
u8 key_generation_old; ///< NcaKeyGenerationOld.
|
||||
u8 kaek_index; ///< NcaKeyAreaEncryptionKeyIndex.
|
||||
u64 content_size;
|
||||
u64 program_id;
|
||||
u32 content_index;
|
||||
NcaSdkAddOnVersion sdk_addon_version;
|
||||
u8 key_generation; ///< NcaKeyGeneration.
|
||||
u8 main_signature_key_generation;
|
||||
u8 reserved_1[0xE];
|
||||
FsRightsId rights_id; ///< Used for titlekey crypto.
|
||||
NcaFsEntry fs_entries[4]; ///< Start and end offsets for each NCA FS section.
|
||||
NcaFsHash fs_hashes[4]; ///< SHA-256 hashes calculated over each NCA FS section header.
|
||||
NcaKey encrypted_keys[4]; ///< Only the encrypted key at index #2 is used. The other three are zero filled before the key area is encrypted.
|
||||
u8 reserved_2[0xC0];
|
||||
NcaFsHeader fs_headers[4]; /// NCA FS section headers.
|
||||
} NcaHeader;
|
||||
|
||||
typedef enum {
|
||||
NcaVersion_Nca0 = 0,
|
||||
NcaVersion_Nca2 = 1,
|
||||
NcaVersion_Nca3 = 2
|
||||
} NcaVersion;
|
||||
|
||||
typedef enum {
|
||||
NcaFsSectionType_PartitionFs = 0, ///< NcaFsType_PartitionFs + NcaHashType_HierarchicalSha256.
|
||||
NcaFsSectionType_RomFs = 1, ///< NcaFsType_RomFs + NcaHashType_HierarchicalIntegrity.
|
||||
|
@ -259,25 +278,38 @@ typedef enum {
|
|||
typedef struct {
|
||||
bool enabled;
|
||||
void *nca_ctx; ///< NcaContext. Used to perform NCA reads.
|
||||
NcaFsHeader header; ///< NCA FS section header.
|
||||
u8 section_num;
|
||||
u64 section_offset;
|
||||
u64 section_size;
|
||||
u8 section_type; ///< NcaFsSectionType.
|
||||
u8 encryption_type; ///< NcaEncryptionType.
|
||||
NcaFsHeader *header;
|
||||
u8 ctr[0x10]; ///< Used to update the AES CTR context IV based on the desired offset.
|
||||
u8 ctr[AES_BLOCK_SIZE]; ///< Used to update the AES CTR context IV based on the desired offset.
|
||||
Aes128CtrContext ctr_ctx;
|
||||
Aes128XtsContext xts_decrypt_ctx;
|
||||
Aes128XtsContext xts_encrypt_ctx;
|
||||
} NcaFsSectionContext;
|
||||
|
||||
typedef enum {
|
||||
NcaVersion_Nca0 = 0,
|
||||
NcaVersion_Nca2 = 2,
|
||||
NcaVersion_Nca3 = 3
|
||||
} NcaVersion;
|
||||
|
||||
typedef struct {
|
||||
u8 aes_xts_1[AES_128_KEY_SIZE]; ///< AES-128-XTS key 0 used for NCA FS sections with NcaEncryptionType_AesXts crypto.
|
||||
u8 aes_xts_2[AES_128_KEY_SIZE]; ///< AES-128-XTS key 1 used for NCA FS sections with NcaEncryptionType_AesXts crypto.
|
||||
u8 aes_ctr[AES_128_KEY_SIZE]; ///< AES-128-CTR key used for NCA FS sections with NcaEncryptionType_AesCtr crypto.
|
||||
u8 aes_ctr_ex[AES_128_KEY_SIZE]; ///< AES-128-CTR key used for NCA FS sections with NcaEncryptionType_AesCtrEx crypto.
|
||||
} NcaDecryptedKeyArea;
|
||||
|
||||
typedef struct {
|
||||
u8 storage_id; ///< NcmStorageId.
|
||||
NcmContentStorage *ncm_storage; ///< Pointer to a NcmContentStorage instance. Used to read NCA data.
|
||||
u64 gamecard_offset; ///< Used to read NCA data from a gamecard using a FsStorage instance when storage_id == NcmStorageId_GameCard.
|
||||
NcmContentId content_id; ///< Also used to read NCA data.
|
||||
char content_id_str[0x21];
|
||||
u8 hash[0x20]; ///< Manually calculated (if needed).
|
||||
u8 hash[SHA256_HASH_SIZE]; ///< Manually calculated (if needed).
|
||||
char hash_str[0x41];
|
||||
u8 format_version; ///< NcaVersion.
|
||||
u8 content_type; ///< NcmContentType. Retrieved from NcmContentInfo.
|
||||
|
@ -286,27 +318,26 @@ typedef struct {
|
|||
u8 id_offset; ///< Retrieved from NcmContentInfo.
|
||||
bool rights_id_available;
|
||||
bool titlekey_retrieved;
|
||||
u8 titlekey[0x10];
|
||||
u8 titlekey[AES_128_KEY_SIZE]; ///< Decrypted titlekey from the ticket.
|
||||
bool dirty_header;
|
||||
NcaHeader header;
|
||||
NcaFsSectionContext fs_contexts[4];
|
||||
NcaKey decrypted_keys[4];
|
||||
NcaHeader header; ///< NCA header.
|
||||
NcaFsSectionContext fs_contexts[NCA_FS_HEADER_COUNT];
|
||||
NcaDecryptedKeyArea decrypted_key_area;
|
||||
} NcaContext;
|
||||
|
||||
typedef struct {
|
||||
u64 offset; ///< New layer data offset (relative to the start of the NCA content file).
|
||||
u64 size; ///< New layer data size.
|
||||
u8 *data; ///< New layer data.
|
||||
} NcaHashInfoLayerPatch;
|
||||
u64 offset; ///< New data offset (relative to the start of the NCA content file).
|
||||
u64 size; ///< New data size.
|
||||
u8 *data; ///< New data.
|
||||
} NcaHashDataPatch;
|
||||
|
||||
typedef struct {
|
||||
NcaHashInfoLayerPatch hash_data_layer_patch;
|
||||
NcaHashInfoLayerPatch hash_target_layer_patch;
|
||||
u32 hash_region_count;
|
||||
NcaHashDataPatch hash_region_patch[NCA_HIERARCHICAL_SHA256_MAX_REGION_COUNT];
|
||||
} NcaHierarchicalSha256Patch;
|
||||
|
||||
typedef struct {
|
||||
NcaHashInfoLayerPatch hash_data_layer_patch[NCA_IVFC_HASH_DATA_LAYER_COUNT];
|
||||
NcaHashInfoLayerPatch hash_target_layer_patch;
|
||||
NcaHashDataPatch hash_level_patch[NCA_IVFC_LEVEL_COUNT];
|
||||
} NcaHierarchicalIntegrityPatch;
|
||||
|
||||
/// Functions to control the internal heap buffer used by NCA FS section crypto operations.
|
||||
|
@ -318,7 +349,7 @@ void ncaFreeCryptoBuffer(void);
|
|||
/// If 'storage_id' != NcmStorageId_GameCard, the 'ncm_storage' argument must point to a valid NcmContentStorage instance, previously opened using the same NcmStorageId value.
|
||||
/// If 'storage_id' == NcmStorageId_GameCard, the 'hfs_partition_type' argument must be a valid GameCardHashFileSystemPartitionType value.
|
||||
/// If the NCA holds a populated Rights ID field, and if the Ticket element pointed to by 'tik' hasn't been filled, ticket data will be retrieved.
|
||||
/// If ticket data can't be retrieved, the context will still be initialized, but anything that involves working with plaintext FS section blocks won't be possible (e.g. ncaReadFsSection()).
|
||||
/// If ticket data can't be retrieved, the context will still be initialized, but anything that involves working with encrypted NCA FS section blocks won't be possible (e.g. ncaReadFsSection()).
|
||||
bool ncaInitializeContext(NcaContext *out, u8 storage_id, NcmContentStorage *ncm_storage, u8 hfs_partition_type, const NcmContentInfo *content_info, Ticket *tik);
|
||||
|
||||
/// Reads raw encrypted data from a NCA using an input context, previously initialized by ncaInitializeContext().
|
||||
|
@ -341,24 +372,28 @@ bool ncaReadAesCtrExStorageFromBktrSection(NcaFsSectionContext *ctx, void *out,
|
|||
/// This function isn't compatible with Patch RomFS sections.
|
||||
void *ncaGenerateEncryptedFsSectionBlock(NcaFsSectionContext *ctx, const void *data, u64 data_size, u64 data_offset, u64 *out_block_size, u64 *out_block_offset);
|
||||
|
||||
/// Generates HierarchicalSha256 FS section patch data, which can be used to replace NCA data in content dumping operations.
|
||||
/// Input offset must be relative to the start of the HierarchicalSha256 hash target layer (actual underlying FS).
|
||||
/// Bear in mind that this function recalculates both the NcaHashInfo block master hash and the NCA FS header hash from the NCA header, and enables the 'dirty_header' flag from the NCA context.
|
||||
/// Generates HierarchicalSha256 FS section patch data, which can be used to seamlessly replace NCA data.
|
||||
/// Input offset must be relative to the start of the last HierarchicalSha256 hash region (actual underlying FS).
|
||||
/// Bear in mind that this function recalculates both the NcaHashData block master hash and the NCA FS header hash from the NCA header, and enables the 'dirty_header' flag from the NCA context.
|
||||
/// As such, this function is not designed to generate more than one patch per HierarchicalSha256 FS section.
|
||||
bool ncaGenerateHierarchicalSha256Patch(NcaFsSectionContext *ctx, const void *data, u64 data_size, u64 data_offset, NcaHierarchicalSha256Patch *out);
|
||||
|
||||
/// Cleanups a previously generated NcaHierarchicalSha256Patch.
|
||||
NX_INLINE void ncaFreeHierarchicalSha256Patch(NcaHierarchicalSha256Patch *patch)
|
||||
{
|
||||
if (!patch) return;
|
||||
if (patch->hash_data_layer_patch.data) free(patch->hash_data_layer_patch.data);
|
||||
if (patch->hash_target_layer_patch.data) free(patch->hash_target_layer_patch.data);
|
||||
if (!patch || !patch->hash_region_count || patch->hash_region_count > NCA_HIERARCHICAL_SHA256_MAX_REGION_COUNT) return;
|
||||
|
||||
for(u8 i = 0; i < patch->hash_region_count; i++)
|
||||
{
|
||||
if (patch->hash_region_patch[i].data) free(patch->hash_region_patch[i].data);
|
||||
}
|
||||
|
||||
memset(patch, 0, sizeof(NcaHierarchicalSha256Patch));
|
||||
}
|
||||
|
||||
/// Generates HierarchicalIntegrity FS section patch data, which can be used to replace NCA data in content dumping operations.
|
||||
/// Input offset must be relative to the start of the HierarchicalIntegrity hash target layer (actual underlying FS).
|
||||
/// Bear in mind that this function recalculates both the NcaHashInfo block master hash and the NCA FS header hash from the NCA header, and enables the 'dirty_header' flag from the NCA context.
|
||||
/// Generates HierarchicalIntegrity FS section patch data, which can be used to seamlessly replace NCA data.
|
||||
/// Input offset must be relative to the start of the last HierarchicalIntegrity hash level (actual underlying FS).
|
||||
/// Bear in mind that this function recalculates both the NcaHashData block master hash and the NCA FS header hash from the NCA header, and enables the 'dirty_header' flag from the NCA context.
|
||||
/// As such, this function is not designed to generate more than one patch per HierarchicalIntegrity FS section.
|
||||
bool ncaGenerateHierarchicalIntegrityPatch(NcaFsSectionContext *ctx, const void *data, u64 data_size, u64 data_offset, NcaHierarchicalIntegrityPatch *out);
|
||||
|
||||
|
@ -367,15 +402,22 @@ NX_INLINE void ncaFreeHierarchicalIntegrityPatch(NcaHierarchicalIntegrityPatch *
|
|||
{
|
||||
if (!patch) return;
|
||||
|
||||
for(u8 i = 0; i < (NCA_IVFC_HASH_DATA_LAYER_COUNT + 1); i++)
|
||||
for(u8 i = 0; i < NCA_IVFC_LEVEL_COUNT; i++)
|
||||
{
|
||||
NcaHashInfoLayerPatch *layer_patch = (i < NCA_IVFC_HASH_DATA_LAYER_COUNT ? &(patch->hash_data_layer_patch[i]) : &(patch->hash_target_layer_patch));
|
||||
if (layer_patch->data) free(layer_patch->data);
|
||||
if (patch->hash_level_patch[i].data) free(patch->hash_level_patch[i].data);
|
||||
}
|
||||
|
||||
memset(patch, 0, sizeof(NcaHierarchicalIntegrityPatch));
|
||||
}
|
||||
|
||||
/// Removes titlekey crypto dependency from a NCA context by wiping the Rights ID from the underlying NCA header copy and copying the decrypted titlekey to the NCA key area.
|
||||
void ncaRemoveTitlekeyCrypto(NcaContext *ctx);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -384,8 +426,6 @@ NX_INLINE void ncaFreeHierarchicalIntegrityPatch(NcaHierarchicalIntegrityPatch *
|
|||
|
||||
|
||||
|
||||
bool ncaEncryptKeyArea(NcaContext *nca_ctx);
|
||||
bool ncaEncryptHeader(NcaContext *ctx);
|
||||
|
||||
|
||||
|
||||
|
@ -412,35 +452,30 @@ NX_INLINE void ncaSetDownloadDistributionType(NcaContext *ctx)
|
|||
ctx->dirty_header = true;
|
||||
}
|
||||
|
||||
NX_INLINE void ncaWipeRightsId(NcaContext *ctx)
|
||||
NX_INLINE bool ncaValidateHierarchicalSha256Offsets(NcaHierarchicalSha256Data *hierarchical_sha256_data, u64 section_size)
|
||||
{
|
||||
if (!ctx || !ctx->rights_id_available) return;
|
||||
memset(&(ctx->header.rights_id), 0, sizeof(FsRightsId));
|
||||
ctx->dirty_header = true;
|
||||
}
|
||||
if (!hierarchical_sha256_data || !section_size || !hierarchical_sha256_data->hash_block_size || !hierarchical_sha256_data->hash_region_count || \
|
||||
hierarchical_sha256_data->hash_region_count > NCA_HIERARCHICAL_SHA256_MAX_REGION_COUNT) return false;
|
||||
|
||||
NX_INLINE bool ncaValidateHierarchicalSha256Offsets(NcaHierarchicalSha256 *hierarchical_sha256, u64 section_size)
|
||||
{
|
||||
if (!hierarchical_sha256 || !section_size || !hierarchical_sha256->hash_block_size || hierarchical_sha256->layer_count != NCA_HIERARCHICAL_SHA256_LAYER_COUNT) return false;
|
||||
|
||||
for(u8 i = 0; i < NCA_HIERARCHICAL_SHA256_LAYER_COUNT; i++)
|
||||
for(u8 i = 0; i < hierarchical_sha256_data->hash_region_count; i++)
|
||||
{
|
||||
NcaHierarchicalSha256LayerInfo *layer_info = (i == 0 ? &(hierarchical_sha256->hash_data_layer_info) : &(hierarchical_sha256->hash_target_layer_info));
|
||||
if (layer_info->offset >= section_size || !layer_info->size || (layer_info->offset + layer_info->size) > section_size) return false;
|
||||
if (hierarchical_sha256_data->hash_region[i].offset >= section_size || !hierarchical_sha256_data->hash_region[i].size || \
|
||||
(hierarchical_sha256_data->hash_region[i].offset + hierarchical_sha256_data->hash_region[i].size) > section_size) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NX_INLINE bool ncaValidateHierarchicalIntegrityOffsets(NcaHierarchicalIntegrity *hierarchical_integrity, u64 section_size)
|
||||
NX_INLINE bool ncaValidateHierarchicalIntegrityOffsets(NcaIntegrityMetaInfo *integrity_meta_info, u64 section_size)
|
||||
{
|
||||
if (!hierarchical_integrity || !section_size || __builtin_bswap32(hierarchical_integrity->magic) != NCA_IVFC_MAGIC || !hierarchical_integrity->master_hash_size || \
|
||||
hierarchical_integrity->layer_count != NCA_IVFC_LAYER_COUNT) return false;
|
||||
if (!integrity_meta_info || !section_size || __builtin_bswap32(integrity_meta_info->magic) != NCA_IVFC_MAGIC || integrity_meta_info->master_hash_size != SHA256_HASH_SIZE || \
|
||||
integrity_meta_info->info_level_hash.max_level_count != NCA_IVFC_MAX_LEVEL_COUNT) return false;
|
||||
|
||||
for(u8 i = 0; i < (NCA_IVFC_HASH_DATA_LAYER_COUNT + 1); i++)
|
||||
for(u8 i = 0; i < NCA_IVFC_LEVEL_COUNT; i++)
|
||||
{
|
||||
NcaHierarchicalIntegrityLayerInfo *layer_info = (i < NCA_IVFC_HASH_DATA_LAYER_COUNT ? &(hierarchical_integrity->hash_data_layer_info[i]) : &(hierarchical_integrity->hash_target_layer_info));
|
||||
if (layer_info->offset >= section_size || !layer_info->size || !layer_info->block_size || (layer_info->offset + layer_info->size) > section_size) return false;
|
||||
if (integrity_meta_info->info_level_hash.level_information[i].offset >= section_size || !integrity_meta_info->info_level_hash.level_information[i].size || \
|
||||
!integrity_meta_info->info_level_hash.level_information[i].block_order || \
|
||||
(integrity_meta_info->info_level_hash.level_information[i].offset + integrity_meta_info->info_level_hash.level_information[i].size) > section_size) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
17
source/pfs.c
17
source/pfs.c
|
@ -23,31 +23,38 @@
|
|||
|
||||
bool pfsInitializeContext(PartitionFileSystemContext *out, NcaFsSectionContext *nca_fs_ctx)
|
||||
{
|
||||
if (!out || !nca_fs_ctx || !nca_fs_ctx->enabled || nca_fs_ctx->section_type != NcaFsSectionType_PartitionFs || !nca_fs_ctx->header || nca_fs_ctx->header->fs_type != NcaFsType_PartitionFs || \
|
||||
nca_fs_ctx->header->hash_type != NcaHashType_HierarchicalSha256)
|
||||
if (!out || !nca_fs_ctx || !nca_fs_ctx->enabled || nca_fs_ctx->section_type != NcaFsSectionType_PartitionFs || nca_fs_ctx->header.fs_type != NcaFsType_PartitionFs || \
|
||||
nca_fs_ctx->header.hash_type != NcaHashType_HierarchicalSha256)
|
||||
{
|
||||
LOGFILE("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
u32 magic = 0;
|
||||
|
||||
PartitionFileSystemHeader pfs_header = {0};
|
||||
PartitionFileSystemEntry *main_npdm_entry = NULL;
|
||||
|
||||
u32 hash_region_count = 0;
|
||||
NcaRegion *hash_region = NULL;
|
||||
|
||||
/* Clear output partition FS context. */
|
||||
memset(out, 0, sizeof(PartitionFileSystemContext));
|
||||
|
||||
/* Fill context. */
|
||||
out->nca_fs_ctx = nca_fs_ctx;
|
||||
|
||||
if (!ncaValidateHierarchicalSha256Offsets(&(nca_fs_ctx->header->hash_info.hierarchical_sha256), nca_fs_ctx->section_size))
|
||||
if (!ncaValidateHierarchicalSha256Offsets(&(nca_fs_ctx->header.hash_data.hierarchical_sha256_data), nca_fs_ctx->section_size))
|
||||
{
|
||||
LOGFILE("Invalid HierarchicalSha256 block!");
|
||||
return false;
|
||||
}
|
||||
|
||||
out->offset = nca_fs_ctx->header->hash_info.hierarchical_sha256.hash_target_layer_info.offset;
|
||||
out->size = nca_fs_ctx->header->hash_info.hierarchical_sha256.hash_target_layer_info.size;
|
||||
hash_region_count = nca_fs_ctx->header.hash_data.hierarchical_sha256_data.hash_region_count;
|
||||
hash_region = &(nca_fs_ctx->header.hash_data.hierarchical_sha256_data.hash_region[hash_region_count - 1]);
|
||||
|
||||
out->offset = hash_region->offset;
|
||||
out->size = hash_region->size;
|
||||
|
||||
/* Read partial PFS header. */
|
||||
if (!ncaReadFsSection(nca_fs_ctx, &pfs_header, sizeof(PartitionFileSystemHeader), out->offset))
|
||||
|
|
|
@ -31,14 +31,18 @@ bool romfsInitializeContext(RomFileSystemContext *out, NcaFsSectionContext *nca_
|
|||
NcaContext *nca_ctx = NULL;
|
||||
u64 dir_table_offset = 0, file_table_offset = 0;
|
||||
|
||||
if (!out || !nca_fs_ctx || !nca_fs_ctx->enabled || !nca_fs_ctx->header || !(nca_ctx = (NcaContext*)nca_fs_ctx->nca_ctx) || (nca_ctx->format_version == NcaVersion_Nca0 && \
|
||||
(nca_fs_ctx->section_type != NcaFsSectionType_Nca0RomFs || nca_fs_ctx->header->hash_type != NcaHashType_HierarchicalSha256)) || (nca_ctx->format_version != NcaVersion_Nca0 && \
|
||||
(nca_fs_ctx->section_type != NcaFsSectionType_RomFs || nca_fs_ctx->header->hash_type != NcaHashType_HierarchicalIntegrity)))
|
||||
if (!out || !nca_fs_ctx || !nca_fs_ctx->enabled || !(nca_ctx = (NcaContext*)nca_fs_ctx->nca_ctx) || (nca_ctx->format_version == NcaVersion_Nca0 && \
|
||||
(nca_fs_ctx->section_type != NcaFsSectionType_Nca0RomFs || nca_fs_ctx->header.hash_type != NcaHashType_HierarchicalSha256)) || (nca_ctx->format_version != NcaVersion_Nca0 && \
|
||||
(nca_fs_ctx->section_type != NcaFsSectionType_RomFs || nca_fs_ctx->header.hash_type != NcaHashType_HierarchicalIntegrity)))
|
||||
{
|
||||
LOGFILE("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
u32 layer_count = 0;
|
||||
NcaRegion *hash_region = NULL;
|
||||
NcaHierarchicalIntegrityVerificationLevelInformation *level_information = NULL;
|
||||
|
||||
/* Clear output RomFS context. */
|
||||
memset(out, 0, sizeof(RomFileSystemContext));
|
||||
|
||||
|
@ -47,23 +51,29 @@ bool romfsInitializeContext(RomFileSystemContext *out, NcaFsSectionContext *nca_
|
|||
|
||||
if (nca_fs_ctx->section_type == NcaFsSectionType_Nca0RomFs)
|
||||
{
|
||||
if (!ncaValidateHierarchicalSha256Offsets(&(nca_fs_ctx->header->hash_info.hierarchical_sha256), nca_fs_ctx->section_size))
|
||||
if (!ncaValidateHierarchicalSha256Offsets(&(nca_fs_ctx->header.hash_data.hierarchical_sha256_data), nca_fs_ctx->section_size))
|
||||
{
|
||||
LOGFILE("Invalid HierarchicalSha256 block!");
|
||||
return false;
|
||||
}
|
||||
|
||||
out->offset = nca_fs_ctx->header->hash_info.hierarchical_sha256.hash_target_layer_info.offset;
|
||||
out->size = nca_fs_ctx->header->hash_info.hierarchical_sha256.hash_target_layer_info.size;
|
||||
layer_count = nca_fs_ctx->header.hash_data.hierarchical_sha256_data.hash_region_count;
|
||||
hash_region = &(nca_fs_ctx->header.hash_data.hierarchical_sha256_data.hash_region[layer_count - 1]);
|
||||
|
||||
out->offset = hash_region->offset;
|
||||
out->size = hash_region->size;
|
||||
} else {
|
||||
if (!ncaValidateHierarchicalIntegrityOffsets(&(nca_fs_ctx->header->hash_info.hierarchical_integrity), nca_fs_ctx->section_size))
|
||||
if (!ncaValidateHierarchicalIntegrityOffsets(&(nca_fs_ctx->header.hash_data.integrity_meta_info), nca_fs_ctx->section_size))
|
||||
{
|
||||
LOGFILE("Invalid HierarchicalIntegrity block!");
|
||||
return false;
|
||||
}
|
||||
|
||||
out->offset = nca_fs_ctx->header->hash_info.hierarchical_integrity.hash_target_layer_info.offset;
|
||||
out->size = nca_fs_ctx->header->hash_info.hierarchical_integrity.hash_target_layer_info.size;
|
||||
layer_count = NCA_IVFC_LEVEL_COUNT;
|
||||
level_information = &(nca_fs_ctx->header.hash_data.integrity_meta_info.info_level_hash.level_information[layer_count - 1]);
|
||||
|
||||
out->offset = level_information->offset;
|
||||
out->size = level_information->size;
|
||||
}
|
||||
|
||||
/* Read RomFS header. */
|
||||
|
|
|
@ -156,7 +156,7 @@ bool romfsGeneratePathFromDirectoryEntry(RomFileSystemContext *ctx, RomFileSyste
|
|||
/// Generates a path string from a RomFS file entry.
|
||||
bool romfsGeneratePathFromFileEntry(RomFileSystemContext *ctx, RomFileSystemFileEntry *file_entry, char *out_path, size_t out_path_size, u8 illegal_char_replace_type);
|
||||
|
||||
/// Generates HierarchicalSha256 (NCA0) / HierarchicalIntegrity (NCA2/NCA3) FS section patch data using a RomFS context + file entry, which can be used to replace NCA data in content dumping operations.
|
||||
/// Generates HierarchicalSha256 (NCA0) / HierarchicalIntegrity (NCA2/NCA3) FS section patch data using a RomFS context + file entry, which can be used to seamlessly replace NCA data.
|
||||
/// Input offset must be relative to the start of the RomFS file entry data.
|
||||
/// This function shares the same limitations as ncaGenerateHierarchicalSha256Patch() / ncaGenerateHierarchicalIntegrityPatch().
|
||||
bool romfsGenerateFileEntryPatch(RomFileSystemContext *ctx, RomFileSystemFileEntry *file_entry, const void *data, u64 data_size, u64 data_offset, RomFileSystemFileEntryPatch *out);
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
#include "usb.h"
|
||||
#include "fatfs/ff.h"
|
||||
|
||||
#define LOGFILE_PATH "./nxdumptool.log"
|
||||
#define LOGFILE_PATH "./" APP_TITLE ".log"
|
||||
|
||||
/* Global variables. */
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
#include <stdatomic.h>
|
||||
#include <switch.h>
|
||||
|
||||
#define APP_BASE_PATH "sdmc:/switch/nxdumptool/"
|
||||
#define APP_BASE_PATH "sdmc:/switch/" APP_TITLE "/"
|
||||
|
||||
#define MEMBER_SIZE(type, member) sizeof(((type*)NULL)->member)
|
||||
|
||||
|
|
15
todo.txt
15
todo.txt
|
@ -7,6 +7,7 @@ todo:
|
|||
tik: use dumped tickets when the original ones can't be found in the ES savefile?
|
||||
|
||||
nca: function to write encrypted nca headers / nca fs headers (don't forget nca0 please)
|
||||
nca: function to write hashdata patches
|
||||
|
||||
pfs0: filelist generation methods
|
||||
pfs0: full header aligned to 0x20 (nsp)
|
||||
|
@ -31,17 +32,3 @@ todo:
|
|||
fclose(content_info);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Result txIsFat32(bool *mode) {
|
||||
Result rc = serviceDispatch(&g_tx, 137);
|
||||
if (rc == 0xa08) {
|
||||
*mode = false;
|
||||
return 0;
|
||||
} else if (rc == 0) {
|
||||
*mode = true;
|
||||
}
|
||||
return rc;
|
||||
}
|
Loading…
Reference in a new issue