1
0
Fork 0
mirror of https://github.com/DarkMatterCore/nxdumptool.git synced 2024-12-23 00:52:10 +00:00

More changes.

* Added NSP dumper PoC (SD card only atm, single-threaded).
* Cert: replaced a wrong strcmp() with a proper strncmp().
* CNMT: added functions to update content info entries and generate/write Partition FS patches.
* NCA: encrypt key area right after removing titlekey crypto.
* NPDM/ProgramInfo: changed function names.
* NPDM: check if the NCA has been modified before attempting to patch ACID data + calculate RSA-PSS signature *after* generating the PFS patch, not before. lol
* PFS: restore name table size value before writing the header padding.
* Tik: reworked the ticket lookup algorithm. Now uses information from ticket_list.bin to properly calculate the offset to the requested ticket in ticket.bin.
* Title: changed title type strings used for filename generation.
* Updated to-do list.
This commit is contained in:
Pablo Curiel 2020-10-22 00:38:14 -04:00
parent 15431ec2c8
commit 974790944f
19 changed files with 1398 additions and 187 deletions

1
.gitignore vendored
View file

@ -7,7 +7,6 @@ build
/*.pfs0
/*.lst
/*.tar.bz2
/code_templates/nsp_dumper.c
/code_templates/tmp/*
/source/main.c
/*.log

1009
code_templates/nsp_dumper.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -79,7 +79,7 @@ bool certRetrieveCertificateChainBySignatureIssuer(CertificateChain *dst, const
bool ret = false;
size_t issuer_len = 0;
if (!dst || !issuer || !(issuer_len = strlen(issuer)) || issuer_len <= 5 || strcmp(issuer, "Root-") != 0)
if (!dst || !issuer || (issuer_len = strlen(issuer)) <= 5 || strncmp(issuer, "Root-", 5) != 0)
{
LOGFILE("Invalid parameters!");
goto end;

View file

@ -247,6 +247,90 @@ end:
return success;
}
bool cnmtUpdateContentInfo(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx)
{
if (!cnmtIsValidContext(cnmt_ctx) || !nca_ctx || !*(nca_ctx->content_id_str) || !*(nca_ctx->hash_str) || nca_ctx->content_type > NcmContentType_DeltaFragment || !nca_ctx->content_size)
{
LOGFILE("Invalid parameters!");
return false;
}
/* Return right away if we're dealing with a Meta NCA. */
if (nca_ctx->content_type == NcmContentType_Meta) return true;
bool success = false;
for(u16 i = 0; i < cnmt_ctx->packaged_header->content_count; i++)
{
NcmPackagedContentInfo *packaged_content_info = &(cnmt_ctx->packaged_content_info[i]);
NcmContentInfo *content_info = &(packaged_content_info->info);
u64 content_size = 0;
titleConvertNcmContentSizeToU64(content_info->size, &content_size);
if (content_size == nca_ctx->content_size && content_info->content_type == nca_ctx->content_type && content_info->id_offset == nca_ctx->id_offset)
{
/* Jackpot. Copy content ID and hash to our raw CNMT. */
memcpy(packaged_content_info->hash, nca_ctx->hash, sizeof(nca_ctx->hash));
memcpy(&(content_info->content_id), &(nca_ctx->content_id), sizeof(NcmContentId));
LOGFILE("Updated CNMT content record #%u (size 0x%lX, type 0x%02X, ID offset 0x%02X).", i, content_size, content_info->content_type, content_info->id_offset);
success = true;
break;
}
}
if (!success) LOGFILE("Unable to find CNMT content info entry for \"%s\" NCA! (size 0x%lX, type 0x%02X, ID offset 0x%02X).", nca_ctx->content_id_str, nca_ctx->content_size, nca_ctx->content_type, \
nca_ctx->id_offset);
return success;
}
bool cnmtGenerateNcaPatch(ContentMetaContext *cnmt_ctx)
{
if (!cnmtIsValidContext(cnmt_ctx))
{
LOGFILE("Invalid parameters!");
return false;
}
/* Check if we really need to generate this patch. */
u8 cnmt_hash[SHA256_HASH_SIZE] = {0};
sha256CalculateHash(cnmt_hash, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size);
if (!memcmp(cnmt_hash, cnmt_ctx->raw_data_hash, sizeof(cnmt_hash)))
{
LOGFILE("Skipping CNMT patching - no content records have been changed.");
return true;
}
/* Generate Partition FS entry patch. */
if (!pfsGenerateEntryPatch(&(cnmt_ctx->pfs_ctx), cnmt_ctx->pfs_entry, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size, 0, &(cnmt_ctx->nca_patch)))
{
LOGFILE("Failed to generate Partition FS entry patch!");
return false;
}
/* Update NCA content type context patch status. */
cnmt_ctx->nca_ctx->content_type_ctx_patch = true;
return true;
}
void cnmtWriteNcaPatch(ContentMetaContext *cnmt_ctx, void *buf, u64 buf_size, u64 buf_offset)
{
/* Using cnmtIsValidContext() here would probably take up precious CPU cycles. */
if (!cnmt_ctx || !cnmt_ctx->nca_ctx || cnmt_ctx->nca_ctx->content_type != NcmContentType_Meta || !cnmt_ctx->nca_ctx->content_type_ctx_patch || cnmt_ctx->nca_patch.written) return;
/* Attempt to write Partition FS entry. */
pfsWriteEntryPatchToMemoryBuffer(&(cnmt_ctx->pfs_ctx), &(cnmt_ctx->nca_patch), buf, buf_size, buf_offset);
/* Check if we need to update the NCA content type context patch status. */
if (cnmt_ctx->nca_patch.written)
{
cnmt_ctx->nca_ctx->content_type_ctx_patch = false;
LOGFILE("CNMT Partition FS file entry patch successfully written to NCA \"%s\"!", cnmt_ctx->nca_ctx->content_id_str);
}
}
bool cnmtGenerateAuthoringToolXml(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx, u32 nca_ctx_count)
{
if (!cnmtIsValidContext(cnmt_ctx) || !nca_ctx || nca_ctx_count != ((u32)cnmt_ctx->packaged_header->content_count + 1))

View file

@ -241,6 +241,15 @@ typedef struct {
/// Initializes a ContentMetaContext using a previously initialized NcaContext (which must belong to a Meta NCA).
bool cnmtInitializeContext(ContentMetaContext *out, NcaContext *nca_ctx);
/// Updates NcmPackagedContentInfo data for the content entry with size, type and ID offset values that match the ones from the input NcaContext.
bool cnmtUpdateContentInfo(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx);
/// Generates a Partition FS entry patch for the NcaContext pointed to by the input ContentMetaContext, using its raw CNMT data.
bool cnmtGenerateNcaPatch(ContentMetaContext *cnmt_ctx);
/// Writes data from the Partition FS patch in the input ContentMetaContext to the provided buffer.
void cnmtWriteNcaPatch(ContentMetaContext *cnmt_ctx, void *buf, u64 buf_size, u64 buf_offset);
/// Generates an AuthoringTool-like XML using information from a previously initialized ContentMetaContext, as well as a pointer to 'nca_ctx_count' NcaContext with content information.
/// If the function succeeds, XML data and size will get saved to the 'authoring_tool_xml' and 'authoring_tool_xml_size' members from the ContentMetaContext.
bool cnmtGenerateAuthoringToolXml(ContentMetaContext *cnmt_ctx, NcaContext *nca_ctx, u32 nca_ctx_count);
@ -266,19 +275,6 @@ NX_INLINE bool cnmtIsValidContext(ContentMetaContext *cnmt_ctx)
((cnmt_ctx->extended_data_size && cnmt_ctx->extended_data) || (!cnmt_ctx->extended_data_size && !cnmt_ctx->extended_data)) && cnmt_ctx->digest);
}
NX_INLINE bool cnmtIsNcaPatchRequired(ContentMetaContext *cnmt_ctx)
{
if (!cnmtIsValidContext(cnmt_ctx)) return false;
u8 tmp_hash[SHA256_HASH_SIZE] = {0};
sha256CalculateHash(tmp_hash, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size);
return (memcmp(tmp_hash, cnmt_ctx->raw_data_hash, SHA256_HASH_SIZE) != 0);
}
NX_INLINE bool cnmtGenerateNcaPatch(ContentMetaContext *cnmt_ctx)
{
return (cnmtIsValidContext(cnmt_ctx) && pfsGenerateEntryPatch(&(cnmt_ctx->pfs_ctx), cnmt_ctx->pfs_entry, cnmt_ctx->raw_data, cnmt_ctx->raw_data_size, 0, &(cnmt_ctx->nca_patch)));
}
NX_INLINE u64 cnmtGetRequiredTitleId(ContentMetaContext *cnmt_ctx)
{
return ((cnmtIsValidContext(cnmt_ctx) && (cnmt_ctx->packaged_header->content_meta_type == NcmContentMetaType_Application || \

View file

@ -23,7 +23,13 @@
#ifndef __COMMON_H__
#define __COMMON_H__
#define SYSTEM_UPDATE_TID (u64)0x0100000000000816
#define FS_SYSMODULE_TID (u64)0x0100000000000000
#define BOOT_SYSMODULE_TID (u64)0x0100000000000005
#define SPL_SYSMODULE_TID (u64)0x0100000000000028
#define ES_SYSMODULE_TID (u64)0x0100000000000033
#define SYSTEM_UPDATE_TID (u64)0x0100000000000816
#define FAT32_FILESIZE_LIMIT (u64)0xFFFFFFFF /* 4 GiB - 1 (4294967295 bytes). */
/// Used to store version numbers expressed in dot notation: "{major}.{minor}.{micro}-{major_relstep}.{minor_relstep}".
/// Referenced by multiple header files.

View file

@ -24,11 +24,6 @@
#ifndef __MEM_H__
#define __MEM_H__
#define FS_SYSMODULE_TID (u64)0x0100000000000000
#define BOOT_SYSMODULE_TID (u64)0x0100000000000005
#define SPL_SYSMODULE_TID (u64)0x0100000000000028
#define ES_SYSMODULE_TID (u64)0x0100000000000033
typedef enum {
MemoryProgramSegmentType_Text = BIT(0),
MemoryProgramSegmentType_Rodata = BIT(1),

View file

@ -334,15 +334,20 @@ void ncaSetDownloadDistributionType(NcaContext *ctx)
{
if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !*(ctx->content_id_str) || ctx->content_type > NcmContentType_DeltaFragment || \
ctx->header.distribution_type == NcaDistributionType_Download) return;
LOGFILE("Setting download distribution type to %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str);
ctx->header.distribution_type = NcaDistributionType_Download;
LOGFILE("Set download distribution type to %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str);
}
void ncaRemoveTitlekeyCrypto(NcaContext *ctx)
bool ncaRemoveTitlekeyCrypto(NcaContext *ctx)
{
if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !*(ctx->content_id_str) || ctx->content_type > NcmContentType_DeltaFragment || !ctx->rights_id_available || !ctx->titlekey_retrieved) return;
if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !*(ctx->content_id_str) || ctx->content_type > NcmContentType_DeltaFragment)
{
LOGFILE("Invalid parameters!");
return false;
}
LOGFILE("Removing titlekey crypto from %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str);
/* Don't proceed if we're not dealing with a NCA with a populated rights ID field, or if we couldn't retrieve the titlekey for it. */
if (!ctx->rights_id_available || !ctx->titlekey_retrieved) return true;
/* Copy decrypted titlekey to the decrypted NCA key area. */
/* This will be reencrypted at a later stage. */
@ -356,11 +361,22 @@ void ncaRemoveTitlekeyCrypto(NcaContext *ctx)
memcpy(key_ptr, ctx->titlekey, AES_128_KEY_SIZE);
}
/* Encrypt NCA key area. */
if (!ncaEncryptKeyArea(ctx))
{
LOGFILE("Error encrypting %s NCA \"%s\" key area!", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str);
return false;
}
/* Wipe Rights ID. */
memset(&(ctx->header.rights_id), 0, sizeof(FsRightsId));
/* Update context flags. */
ctx->rights_id_available = false;
LOGFILE("Removed titlekey crypto from %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str);
return true;
}
bool ncaEncryptHeader(NcaContext *ctx)
@ -378,13 +394,6 @@ bool ncaEncryptHeader(NcaContext *ctx)
const u8 *header_key = keysGetNcaHeaderKey();
Aes128XtsContext hdr_aes_ctx = {0}, nca0_fs_header_ctx = {0};
/* Encrypt NCA key area. */
if (!ctx->rights_id_available && !ncaEncryptKeyArea(ctx))
{
LOGFILE("Error encrypting NCA \"%s\" key area!", ctx->content_id_str);
return false;
}
/* Prepare AES-128-XTS contexts. */
aes128XtsContextCreate(&hdr_aes_ctx, header_key, header_key + AES_128_KEY_SIZE, true);
if (ctx->format_version == NcaVersion_Nca0) aes128XtsContextCreate(&nca0_fs_header_ctx, ctx->decrypted_key_area.aes_xts_1, ctx->decrypted_key_area.aes_xts_2, true);
@ -1129,7 +1138,8 @@ static bool ncaWritePatchToMemoryBuffer(NcaContext *ctx, const void *patch, u64
memcpy((u8*)buf + buf_block_offset, (const u8*)patch + patch_block_offset, buf_block_size);
LOGFILE("Overwrote 0x%lX bytes block at offset 0x%lX from raw NCA \"%s\" buffer (size 0x%lX, NCA offset 0x%lX).", buf_block_size, buf_block_offset, ctx->content_id_str, buf_size, buf_offset);
LOGFILE("Overwrote 0x%lX bytes block at offset 0x%lX from raw %s NCA \"%s\" buffer (size 0x%lX, NCA offset 0x%lX).", buf_block_size, buf_block_offset, titleGetNcmContentTypeName(ctx->content_type), \
ctx->content_id_str, buf_size, buf_offset);
return ((patch_block_offset + buf_block_size) == patch_size);
}

View file

@ -410,7 +410,7 @@ void ncaWriteHierarchicalIntegrityPatchToMemoryBuffer(NcaContext *ctx, NcaHierar
void ncaSetDownloadDistributionType(NcaContext *ctx);
/// Removes titlekey crypto dependency from a NCA context by wiping the Rights ID from the underlying NCA header and copying the decrypted titlekey to the NCA key area.
void ncaRemoveTitlekeyCrypto(NcaContext *ctx);
bool ncaRemoveTitlekeyCrypto(NcaContext *ctx);
/// Encrypts NCA header and NCA FS headers.
/// The 'encrypted_header' member from the NCA context and its underlying NCA FS section contexts is updated by this function.

View file

@ -258,7 +258,7 @@ end:
return success;
}
bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx)
bool npdmGenerateNcaPatch(NpdmContext *npdm_ctx)
{
NcaContext *nca_ctx = NULL;
@ -268,16 +268,16 @@ bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx)
return false;
}
/* Check if we really need to generate this patch. */
if (!ncaIsHeaderDirty(nca_ctx))
{
LOGFILE("Skipping NPDM patching - NCA header hasn't been modified.");
return true;
}
/* Update NPDM ACID public key. */
memcpy(npdm_ctx->acid_header->public_key, rsa2048GetCustomPublicKey(), RSA2048_PUBKEY_SIZE);
/* Update NCA ACID signature. */
if (!rsa2048GenerateSha256BasedPssSignature(nca_ctx->header.acid_signature, &(nca_ctx->header.magic), NCA_ACID_SIGNATURE_AREA_SIZE))
{
LOGFILE("Failed to generate RSA-2048-PSS NCA ACID signature!");
return false;
}
/* Generate Partition FS entry patch. */
if (!pfsGenerateEntryPatch(npdm_ctx->pfs_ctx, npdm_ctx->pfs_entry, npdm_ctx->raw_data, npdm_ctx->raw_data_size, 0, &(npdm_ctx->nca_patch)))
{
@ -285,6 +285,13 @@ bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx)
return false;
}
/* Update NCA ACID signature. */
if (!rsa2048GenerateSha256BasedPssSignature(nca_ctx->header.acid_signature, &(nca_ctx->header.magic), NCA_ACID_SIGNATURE_AREA_SIZE))
{
LOGFILE("Failed to generate RSA-2048-PSS NCA ACID signature!");
return false;
}
/* Update NCA content type context patch status. */
nca_ctx->content_type_ctx_patch = true;

View file

@ -555,7 +555,7 @@ typedef struct {
bool npdmInitializeContext(NpdmContext *out, PartitionFileSystemContext *pfs_ctx);
/// Changes the ACID public key from the NPDM in the input NpdmContext, updates the ACID signature from the NCA header in the underlying NCA context and generates a Partition FS entry patch.
bool npdmChangeAcidPublicKeyAndNcaSignature(NpdmContext *npdm_ctx);
bool npdmGenerateNcaPatch(NpdmContext *npdm_ctx);
/// Writes data from the Partition FS patch in the input NpdmContext to the provided buffer.
void npdmWriteNcaPatch(NpdmContext *npdm_ctx, void *buf, u64 buf_size, u64 buf_offset);

View file

@ -336,13 +336,12 @@ bool pfsWriteFileContextHeaderToMemoryBuffer(PartitionFileSystemFileContext *ctx
return false;
}
/* Update name table size in Partition FS header to make it reflect the padding. */
header->name_table_size += padding_size;
/* Write full header. */
header->name_table_size += padding_size;
block_size = sizeof(PartitionFileSystemHeader);
memcpy(buf_u8 + block_offset, header, block_size);
block_offset += block_size;
header->name_table_size -= padding_size;
block_size = (header->entry_count * sizeof(PartitionFileSystemEntry));
memcpy(buf_u8 + block_offset, ctx->entries, block_size);
@ -357,8 +356,5 @@ bool pfsWriteFileContextHeaderToMemoryBuffer(PartitionFileSystemFileContext *ctx
/* Update output header size. */
*out_header_size = full_header_size;
/* Restore name table size in Partition FS header. */
header->name_table_size -= padding_size;
return true;
}

View file

@ -70,9 +70,9 @@ NX_INLINE bool programInfoIsValidContext(ProgramInfoContext *program_info_ctx)
program_info_ctx->nso_count && program_info_ctx->nso_ctx);
}
NX_INLINE bool programInfoChangeAcidPublicKeyAndNcaSignature(ProgramInfoContext *program_info_ctx)
NX_INLINE bool programInfoGenerateNcaPatch(ProgramInfoContext *program_info_ctx)
{
return (programInfoIsValidContext(program_info_ctx) && npdmChangeAcidPublicKeyAndNcaSignature(&(program_info_ctx->npdm_ctx)));
return (programInfoIsValidContext(program_info_ctx) && npdmGenerateNcaPatch(&(program_info_ctx->npdm_ctx)));
}
NX_INLINE void programInfoWriteNcaPatch(ProgramInfoContext *program_info_ctx, void *buf, u64 buf_size, u64 buf_offset)

View file

@ -93,8 +93,8 @@ bool rsa2048GenerateSha256BasedPssSignature(void *dst, const void *src, size_t s
return false;
}
u8 hash[SHA256_HASH_SIZE];
u8 buf[MBEDTLS_MPI_MAX_SIZE];
u8 hash[SHA256_HASH_SIZE] = {0};
u8 buf[MBEDTLS_MPI_MAX_SIZE] = {0};
const char *pers = "rsa_sign_pss";
size_t olen = 0;

View file

@ -32,7 +32,9 @@
#define TIK_COMMON_SAVEFILE_PATH BIS_SYSTEM_PARTITION_MOUNT_NAME "/save/80000000000000e1"
#define TIK_PERSONALIZED_SAVEFILE_PATH BIS_SYSTEM_PARTITION_MOUNT_NAME "/save/80000000000000e2"
#define TIK_SAVEFILE_STORAGE_PATH "/ticket.bin"
#define TIK_LIST_STORAGE_PATH "/ticket_list.bin"
#define TIK_DB_STORAGE_PATH "/ticket.bin"
#define ETICKET_DEVKEY_PUBLIC_EXPONENT 0x10001
@ -40,6 +42,31 @@
/* Type definitions. */
/// Used to parse ticket_list.bin entries.
typedef struct {
FsRightsId rights_id;
u64 ticket_id;
u32 account_id;
u8 reserved[0x04];
} TikListEntry;
/// 9.x+ CTR key entry in ES .data segment. Used to store CTR key/IV data for encrypted volatile tickets in ticket.bin and/or encrypted entries in ticket_list.bin.
/// This is always stored in pairs. The first entry holds the key/IV for the encrypted volatile ticket, while the second entry holds the key/IV for the encrypted entry in ticket_list.bin.
/// First index in this list is always 0, and it's aligned to ES_CTRKEY_ENTRY_ALIGNMENT.
typedef struct {
u32 idx; ///< Entry index.
u8 key[AES_BLOCK_SIZE]; ///< AES-128-CTR key.
u8 ctr[AES_BLOCK_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out.
} TikEsCtrKeyEntry9x;
/// Lookup pattern for TikEsCtrKeyEntry9x.
typedef struct {
u32 idx1; ///< Always set to 0 (first entry).
u8 ctrdata[AES_BLOCK_SIZE * 2];
u32 idx2; ///< Always set to 1 (second entry).
} TikEsCtrKeyPattern9x;
/// Used to parse the eTicket device key retrieved from PRODINFO via setcalGetEticketDeviceKey().
/// Everything after the AES CTR is encrypted.
typedef struct {
u8 ctr[0x10];
@ -49,23 +76,7 @@ typedef struct {
u8 padding[0x14];
u64 device_id;
u8 ghash[0x10];
} tikEticketDeviceKeyData;
/// 9.x+ CTR key entry in ES .data segment. Used to store CTR key/IV data for encrypted volatile tickets in ticket.bin and/or encrypted entries in ticket_list.bin.
/// This is always stored in pairs. The first entry holds the key/IV for the encrypted volatile ticket, while the second entry holds the key/IV for the encrypted entry in ticket_list.bin.
/// First index in this list is always 0, and it's aligned to ES_CTRKEY_ENTRY_ALIGNMENT.
typedef struct {
u32 idx; ///< Entry index.
u8 key[AES_BLOCK_SIZE]; ///< AES-128-CTR key.
u8 ctr[AES_BLOCK_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out.
} tikEsCtrKeyEntry9x;
/// Lookup pattern for tikEsCtrKeyEntry9x.
typedef struct {
u32 idx1; ///< Always set to 0 (first entry).
u8 ctrdata[AES_BLOCK_SIZE * 2];
u32 idx2; ///< Always set to 1 (second entry).
} tikEsCtrKeyPattern9x;
} TikEticketDeviceKeyData;
/* Global variables. */
@ -101,7 +112,9 @@ static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_g
static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out);
static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, bool personalized);
static u8 *tikRetrieveTicketEntryFromTicketBin(allocation_table_storage_ctx_t *fat_storage, u64 ticket_bin_size, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type);
static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 *out_offset, u8 titlekey_type);
static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 ticket_offset, u8 titlekey_type);
static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size);
@ -288,67 +301,64 @@ static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRight
u8 titlekey_type = 0;
save_ctx_t *save_ctx = NULL;
allocation_table_storage_ctx_t fat_storage = {0};
u64 ticket_bin_size = 0;
u64 buf_size = (SIGNED_TIK_MAX_SIZE * 0x10);
u8 *buf = NULL, *ticket_entry = NULL;
u64 buf_size = (SIGNED_TIK_MAX_SIZE * 0x100);
u8 *buf = NULL;
u64 ticket_offset = 0;
bool success = false;
/* Allocate memory to retrieve the ticket. */
if (!(buf = malloc(buf_size)))
{
LOGFILE("Unable to allocate 0x%lX bytes block for temporary read buffer!", buf_size);
return false;
}
/* Get titlekey type. */
if (!tikGetTitleKeyTypeFromRightsId(id, &titlekey_type))
{
LOGFILE("Unable to retrieve ticket titlekey type!");
return false;
goto end;
}
save_ctx = save_open_savefile(titlekey_type == TikTitleKeyType_Common ? TIK_COMMON_SAVEFILE_PATH : TIK_PERSONALIZED_SAVEFILE_PATH, 0);
if (!save_ctx)
/* Open ES common/personalized system savefile. */
if (!(save_ctx = save_open_savefile(titlekey_type == TikTitleKeyType_Common ? TIK_COMMON_SAVEFILE_PATH : TIK_PERSONALIZED_SAVEFILE_PATH, 0)))
{
LOGFILE("Failed to open ES %s ticket system savefile!", g_tikTitleKeyTypeStrings[titlekey_type]);
return false;
}
if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_SAVEFILE_STORAGE_PATH, &fat_storage, &ticket_bin_size))
{
LOGFILE("Failed to locate \"%s\" in ES %s ticket system save!", TIK_SAVEFILE_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
goto end;
}
if (ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0)
/* Get ticket entry offset from ticket_list.bin. */
if (!tikGetTicketEntryOffsetFromTicketList(save_ctx, buf, buf_size, id, &ticket_offset, titlekey_type))
{
LOGFILE("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_SAVEFILE_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_bin_size);
LOGFILE("Unable to find an entry with a matching Rights ID in \"%s\" from ES %s ticket system save!", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
goto end;
}
buf = malloc(buf_size);
if (!buf)
{
LOGFILE("Unable to allocate 0x%lX bytes block for temporary read buffer!", buf_size);
goto end;
}
if (!(ticket_entry = tikRetrieveTicketEntryFromTicketBin(&fat_storage, ticket_bin_size, buf, buf_size, id, titlekey_type)))
/* Get ticket entry from ticket.bin. */
if (!tikRetrieveTicketEntryFromTicketBin(save_ctx, buf, buf_size, id, ticket_offset, titlekey_type))
{
LOGFILE("Unable to find a matching %s ticket entry for the provided Rights ID!", g_tikTitleKeyTypeStrings[titlekey_type]);
goto end;
}
if (!tikGetTicketTypeAndSize(ticket_entry, SIGNED_TIK_MAX_SIZE, &(dst->type), &(dst->size)))
/* Get ticket type and size. */
if (!tikGetTicketTypeAndSize(buf, SIGNED_TIK_MAX_SIZE, &(dst->type), &(dst->size)))
{
LOGFILE("Unable to determine ticket type and size!");
goto end;
}
memcpy(dst->data, ticket_entry, dst->size);
memcpy(dst->data, buf, dst->size);
success = true;
end:
if (buf) free(buf);
if (save_ctx) save_close_savefile(save_ctx);
if (buf) free(buf);
return success;
}
@ -365,7 +375,7 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik)
size_t out_keydata_size = 0;
u8 out_keydata[0x100] = {0};
tikEticketDeviceKeyData *eticket_devkey = NULL;
TikEticketDeviceKeyData *eticket_devkey = NULL;
switch(tik_common_block->titlekey_type)
{
@ -381,7 +391,7 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik)
return false;
}
eticket_devkey = (tikEticketDeviceKeyData*)g_eTicketDeviceKey.key;
eticket_devkey = (TikEticketDeviceKeyData*)g_eTicketDeviceKey.key;
/* Perform a RSA-OAEP decrypt operation to get the titlekey. */
if (!rsa2048OaepDecryptAndVerify(out_keydata, 0x100, tik_common_block->titlekey_block, eticket_devkey->modulus, eticket_devkey->exponent, 0x100, g_nullHash, &out_keydata_size) || \
@ -520,104 +530,184 @@ static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count,
return true;
}
static u8 *tikRetrieveTicketEntryFromTicketBin(allocation_table_storage_ctx_t *fat_storage, u64 ticket_bin_size, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type)
static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 *out_offset, u8 titlekey_type)
{
if (!fat_storage || ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0 || !buf || !buf_size || (buf_size % SIGNED_TIK_MAX_SIZE) != 0 || !id)
if (!save_ctx || !buf || !buf_size || (buf_size % sizeof(TikListEntry)) != 0 || !id || !out_offset)
{
LOGFILE("Invalid parameters!");
return NULL;
return false;
}
u64 br = 0, total_br = 0;
u8 *out_tik = NULL;
allocation_table_storage_ctx_t fat_storage = {0};
u64 ticket_list_bin_size = 0, br = 0, total_br = 0;
Aes128CtrContext ctr_ctx = {0};
u8 null_ctr[AES_BLOCK_SIZE] = {0}, ctr[AES_BLOCK_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0};
u8 last_rights_id[0x10];
memset(last_rights_id, 0xFF, sizeof(last_rights_id));
bool is_9x = hosversionAtLeast(9, 0, 0);
bool last_entry_found = false, success = false;
if (is_9x && !memRetrieveFullProgramMemory(&g_esMemoryLocation))
/* Get FAT storage info for the ticket_list.bin stored within the opened system savefile. */
if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_LIST_STORAGE_PATH, &fat_storage, &ticket_list_bin_size))
{
LOGFILE("Failed to retrieve ES program memory!");
return NULL;
LOGFILE("Failed to locate \"%s\" in ES %s ticket system save!", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
return false;
}
while(total_br < ticket_bin_size)
/* Check ticket_list.bin size. */
if (ticket_list_bin_size < sizeof(TikListEntry) || (ticket_list_bin_size % sizeof(TikListEntry)) != 0)
{
if (buf_size > (ticket_bin_size - total_br)) buf_size = (ticket_bin_size - total_br);
LOGFILE("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_list_bin_size);
return false;
}
/* Look for an entry matching our rights ID in ticket_list.bin. */
while(total_br < ticket_list_bin_size)
{
if (buf_size > (ticket_list_bin_size - total_br)) buf_size = (ticket_list_bin_size - total_br);
br = save_allocation_table_storage_read(fat_storage, buf, total_br, buf_size);
if (br != buf_size)
if ((br = save_allocation_table_storage_read(&fat_storage, buf, total_br, buf_size)) != buf_size)
{
LOGFILE("Failed to read 0x%lX bytes chunk at offset 0x%lX from \"%s\" in ES %s ticket system save!", buf_size, total_br, TIK_SAVEFILE_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
LOGFILE("Failed to read 0x%lX bytes chunk at offset 0x%lX from \"%s\" in ES %s ticket system save!", buf_size, total_br, TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
break;
}
for(u64 i = 0; i < buf_size; i += SIGNED_TIK_MAX_SIZE)
for(u64 i = 0; i < buf_size; i += sizeof(TikListEntry))
{
if ((buf_size - i) < SIGNED_TIK_MIN_SIZE) break;
if ((buf_size - i) < sizeof(TikListEntry)) break;
u8 *cur_tik = (buf + i);
u64 tik_offset = (total_br + i);
TikCommonBlock *tik_common_block = tikGetCommonBlock(cur_tik);
u64 entry_offset = (total_br + i);
TikListEntry *entry = (TikListEntry*)(buf + i);
if (!tik_common_block)
/* Check if we found the last entry. */
if (!memcmp(entry->rights_id.c, last_rights_id, sizeof(last_rights_id)))
{
/* Check if we're dealing with a padding block. */
if (!memcmp(cur_tik, null_ctr, sizeof(null_ctr))) continue;
/* We're most likely dealing with an encrypted ticket. Don't proceed if HOS version isn't at least 9.0.0. */
if (!is_9x) continue;
/* Sad path. We need to retrieve the CTR key/IV from ES program memory in order to decrypt this ticket. */
for(u64 j = 0; j < g_esMemoryLocation.data_size; j += ES_CTRKEY_ENTRY_ALIGNMENT)
{
if ((g_esMemoryLocation.data_size - j) < (sizeof(tikEsCtrKeyEntry9x) * 2)) break;
/* Check if the key indexes are valid. idx2 should always be an odd number.*/
tikEsCtrKeyPattern9x *pattern = (tikEsCtrKeyPattern9x*)(g_esMemoryLocation.data + j);
if (pattern->idx2 != (pattern->idx1 + 1) || !(pattern->idx2 & 1)) continue;
/* Seems like indexes are valid. Check if the key is not null and if the CTR is. */
tikEsCtrKeyEntry9x *key_entry = (tikEsCtrKeyEntry9x*)pattern;
if (!memcmp(key_entry->key, null_ctr, sizeof(null_ctr)) || memcmp(key_entry->ctr, null_ctr, sizeof(null_ctr)) != 0) continue;
/* Check if we can decrypt the current ticket with this data. */
memset(&ctr_ctx, 0, sizeof(Aes128CtrContext));
aes128CtrInitializePartialCtr(ctr, key_entry->ctr, tik_offset);
aes128CtrContextCreate(&ctr_ctx, key_entry->key, ctr);
aes128CtrCrypt(&ctr_ctx, dec_tik, cur_tik, SIGNED_TIK_MAX_SIZE);
if ((tik_common_block = tikGetCommonBlock(dec_tik)) != NULL && !strncmp(tik_common_block->issuer, "Root", 4))
{
/* Ticket successfully decrypted. */
memcpy(cur_tik, dec_tik, SIGNED_TIK_MAX_SIZE);
tik_common_block = tikGetCommonBlock(cur_tik);
break;
}
}
/* Don't proceed if we couldn't decrypt the ticket. */
if (!tik_common_block || strncmp(tik_common_block->issuer, "Root", 4) != 0) continue;
last_entry_found = true;
break;
}
/* Check if the rights ID from the ticket common block matches the one we're looking for. */
if (!memcmp(tik_common_block->rights_id.c, id->c, 0x10))
/* Check if this is the entry we're looking for. */
if (!memcmp(entry->rights_id.c, id->c, sizeof(id->c)))
{
/* Jackpot. */
out_tik = cur_tik;
*out_offset = (entry_offset << 5); /* (entry_offset / 0x20) * 0x400 */
success = true;
break;
}
}
total_br += br;
if (out_tik) break;
if (last_entry_found || success) break;
}
if (is_9x) memFreeMemoryLocation(&g_esMemoryLocation);
return success;
}
static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 ticket_offset, u8 titlekey_type)
{
if (!save_ctx || !buf || buf_size < SIGNED_TIK_MAX_SIZE || !id || (ticket_offset % SIGNED_TIK_MAX_SIZE) != 0)
{
LOGFILE("Invalid parameters!");
return false;
}
return out_tik;
allocation_table_storage_ctx_t fat_storage = {0};
u64 ticket_bin_size = 0, br = 0;
TikCommonBlock *tik_common_block = NULL;
Aes128CtrContext ctr_ctx = {0};
u8 null_ctr[AES_BLOCK_SIZE] = {0}, ctr[AES_BLOCK_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0};
bool is_volatile = false, success = false;
/* Get FAT storage info for the ticket.bin stored within the opened system savefile. */
if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_DB_STORAGE_PATH, &fat_storage, &ticket_bin_size))
{
LOGFILE("Failed to locate \"%s\" in ES %s ticket system save!", TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
return false;
}
/* Check ticket.bin size. */
if (ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0 || ticket_bin_size < (ticket_offset + SIGNED_TIK_MAX_SIZE))
{
LOGFILE("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_bin_size);
return false;
}
/* Read ticket data. */
if ((br = save_allocation_table_storage_read(&fat_storage, buf, ticket_offset, SIGNED_TIK_MAX_SIZE)) != SIGNED_TIK_MAX_SIZE)
{
LOGFILE("Failed to read 0x%lX bytes long ticket at offset 0x%lX from \"%s\" in ES %s ticket system save!", SIGNED_TIK_MAX_SIZE, ticket_offset, TIK_DB_STORAGE_PATH, \
g_tikTitleKeyTypeStrings[titlekey_type]);
return false;
}
/* Check if we're dealing with a volatile (encrypted) ticket. */
if (!(tik_common_block = tikGetCommonBlock(buf)) || strncmp(tik_common_block->issuer, "Root", 4) != 0)
{
tik_common_block = NULL;
is_volatile = true;
/* Don't proceed if HOS version isn't at least 9.0.0. */
if (!hosversionAtLeast(9, 0, 0))
{
LOGFILE("Unable to retrieve ES key entry for volatile tickets under HOS versions below 9.0.0!");
return false;
}
/* Retrieve ES program memory. */
if (!memRetrieveFullProgramMemory(&g_esMemoryLocation))
{
LOGFILE("Failed to retrieve ES program memory!");
return false;
}
/* Retrieve the CTR key/IV from ES program memory in order to decrypt this ticket. */
for(u64 i = 0; i < g_esMemoryLocation.data_size; i += ES_CTRKEY_ENTRY_ALIGNMENT)
{
if ((g_esMemoryLocation.data_size - i) < (sizeof(TikEsCtrKeyEntry9x) * 2)) break;
/* Check if the key indexes are valid. idx2 should always be an odd number equal to idx + 1. */
TikEsCtrKeyPattern9x *pattern = (TikEsCtrKeyPattern9x*)(g_esMemoryLocation.data + i);
if (pattern->idx2 != (pattern->idx1 + 1) || !(pattern->idx2 & 1)) continue;
/* Check if the key is not null and if the CTR is. */
TikEsCtrKeyEntry9x *key_entry = (TikEsCtrKeyEntry9x*)pattern;
if (!memcmp(key_entry->key, null_ctr, sizeof(null_ctr)) || memcmp(key_entry->ctr, null_ctr, sizeof(null_ctr)) != 0) continue;
/* Check if we can decrypt the current ticket with this data. */
memset(&ctr_ctx, 0, sizeof(Aes128CtrContext));
aes128CtrInitializePartialCtr(ctr, key_entry->ctr, ticket_offset);
aes128CtrContextCreate(&ctr_ctx, key_entry->key, ctr);
aes128CtrCrypt(&ctr_ctx, dec_tik, buf, SIGNED_TIK_MAX_SIZE);
/* Check if we successfully decrypted this ticket. */
if ((tik_common_block = tikGetCommonBlock(dec_tik)) != NULL && !strncmp(tik_common_block->issuer, "Root", 4))
{
memcpy(buf, dec_tik, SIGNED_TIK_MAX_SIZE);
tik_common_block = tikGetCommonBlock(buf);
break;
}
tik_common_block = NULL;
}
/* Check if we were able to decrypt the ticket. */
if (!tik_common_block)
{
LOGFILE("Unable to decrypt volatile ticket at offset 0x%lX in \"%s\" from ES %s ticket system save!", ticket_offset, TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]);
goto end;
}
}
/* Check if the rights ID from the ticket common block matches the one we're looking for. */
if (!(success = (memcmp(tik_common_block->rights_id.c, id->c, 0x10) == 0))) LOGFILE("Retrieved ticket doesn't hold a matching Rights ID!");
end:
if (is_volatile) memFreeMemoryLocation(&g_esMemoryLocation);
return success;
}
static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size)
@ -673,7 +763,7 @@ static bool tikRetrieveEticketDeviceKey(void)
Result rc = 0;
u32 public_exponent = 0;
tikEticketDeviceKeyData *eticket_devkey = NULL;
TikEticketDeviceKeyData *eticket_devkey = NULL;
Aes128CtrContext eticket_aes_ctx = {0};
rc = setcalGetEticketDeviceKey(&g_eTicketDeviceKey);
@ -684,9 +774,9 @@ static bool tikRetrieveEticketDeviceKey(void)
}
/* Decrypt eTicket RSA key. */
eticket_devkey = (tikEticketDeviceKeyData*)g_eTicketDeviceKey.key;
eticket_devkey = (TikEticketDeviceKeyData*)g_eTicketDeviceKey.key;
aes128CtrContextCreate(&eticket_aes_ctx, keysGetEticketRsaKek(g_eTicketDeviceKey.generation > 0), eticket_devkey->ctr);
aes128CtrCrypt(&eticket_aes_ctx, &(eticket_devkey->exponent), &(eticket_devkey->exponent), sizeof(tikEticketDeviceKeyData) - 0x10);
aes128CtrCrypt(&eticket_aes_ctx, &(eticket_devkey->exponent), &(eticket_devkey->exponent), sizeof(TikEticketDeviceKeyData) - 0x10);
/* Public exponent value must be 0x10001. */
/* It is stored using big endian byte order. */

View file

@ -73,6 +73,13 @@ static const char *g_titleNcmContentMetaTypeNames[] = {
[NcmContentMetaType_Delta - 0x7A] = "Delta"
};
static const char *g_filenameTypeStrings[] = {
[NcmContentMetaType_Application - 0x80] = "BASE",
[NcmContentMetaType_Patch - 0x80] = "UPD",
[NcmContentMetaType_AddOnContent - 0x80] = "DLC",
[NcmContentMetaType_Delta - 0x80] = "DELTA"
};
/* Info retrieved from https://switchbrew.org/wiki/Title_list. */
/* Titles bundled with the kernel are excluded. */
static const SystemTitleName g_systemTitles[] = {
@ -735,13 +742,15 @@ char *titleGenerateFileName(const TitleInfo *title_info, u8 name_convention, u8
char title_name[0x400] = {0};
TitleApplicationMetadata *app_metadata = NULL;
if (!title_info || name_convention > TitleFileNameConvention_IdAndVersionOnly || \
if (!title_info || title_info->meta_key.type < NcmContentMetaType_Application || title_info->meta_key.type > NcmContentMetaType_Delta || name_convention > TitleFileNameConvention_IdAndVersionOnly || \
(name_convention == TitleFileNameConvention_Full && illegal_char_replace_type > TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly))
{
LOGFILE("Invalid parameters!");
goto end;
}
u8 type = (title_info->meta_key.type - 0x80);
/* Retrieve application metadata. */
/* System titles and user applications: just retrieve the app_metadata pointer from the input TitleInfo. */
/* Patches and add-on contents: retrieve the app_metadata pointer from the parent TitleInfo if it's available. */
@ -756,11 +765,11 @@ char *titleGenerateFileName(const TitleInfo *title_info, u8 name_convention, u8
if (illegal_char_replace_type) utilsReplaceIllegalCharacters(title_name, illegal_char_replace_type == TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly);
}
sprintf(title_name + strlen(title_name), "[%016lX][v%u][%s]", title_info->meta_key.id, title_info->meta_key.version, titleGetNcmContentMetaTypeName(title_info->meta_key.type));
sprintf(title_name + strlen(title_name), "[%016lX][v%u][%s]", title_info->meta_key.id, title_info->meta_key.version, g_filenameTypeStrings[type]);
} else
if (name_convention == TitleFileNameConvention_IdAndVersionOnly)
{
sprintf(title_name, "%016lX_v%u_%s", title_info->meta_key.id, title_info->meta_key.version, titleGetNcmContentMetaTypeName(title_info->meta_key.type));
sprintf(title_name, "%016lX_v%u_%s", title_info->meta_key.id, title_info->meta_key.version, g_filenameTypeStrings[type]);
}
/* Duplicate generated filename. */

View file

@ -633,6 +633,13 @@ bool utilsCheckIfFileExists(const char *path)
return false;
}
void utilsRemoveConcatenationFile(const char *path)
{
if (!path || !*path) return;
remove(path);
fsdevDeleteDirectoryRecursively(path);
}
bool utilsCreateConcatenationFile(const char *path)
{
if (!path || !*path)
@ -642,8 +649,7 @@ bool utilsCreateConcatenationFile(const char *path)
}
/* Safety check: remove any existant file/directory at the destination path. */
remove(path);
fsdevDeleteDirectoryRecursively(path);
utilsRemoveConcatenationFile(path);
/* Create ConcatenationFile */
/* If the call succeeds, the caller function will be able to operate on this file using stdio calls. */

View file

@ -111,6 +111,7 @@ bool utilsCommitSdCardFileSystemChanges(void);
bool utilsCheckIfFileExists(const char *path);
void utilsRemoveConcatenationFile(const char *path);
bool utilsCreateConcatenationFile(const char *path);
void utilsCreateDirectoryTree(const char *path, bool create_last_element);

View file

@ -2,7 +2,7 @@ reminder:
list of top level functions designed to alter nca data in order of (possible) usage:
out of loop:
out of dump loop:
* ncaSetDownloadDistributionType (instead of always using it like legacy, offer it as an option)
* ncaRemoveTitlekeyCrypto (can be used with digital titles + game updates in gamecards)
@ -10,17 +10,11 @@ list of top level functions designed to alter nca data in order of (possible) us
* cnmtGenerateNcaPatch (Meta)
* calls pfsGenerateEntryPatch
* calls ncaGenerateHierarchicalSha256Patch
* cnmtIsNcaPatchRequired -> not sure if i'll keep this
* missing wrapper for pfsWriteEntryPatchToMemoryBuffer !!!
* programInfoChangeAcidPublicKeyAndNcaSignature (Program)
* programInfoGenerateNcaPatch (Program)
* calls npdmChangeAcidPublicKeyAndNcaSignature
* calls pfsGenerateEntryPatch
* calls ncaGenerateHierarchicalSha256Patch
* needs programInfoWriteNcaPatch to write patched data
* calls npdmWriteNcaPatch
* calls pfsWriteEntryPatchToMemoryBuffer
* calls ncaWriteHierarchicalSha256PatchToMemoryBuffer
* nacpGenerateNcaPatch (Control)
* calls romfsGenerateFileEntryPatch
@ -29,15 +23,26 @@ list of top level functions designed to alter nca data in order of (possible) us
* missing wrapper for romfsWriteFileEntryPatchToMemoryBuffer !!!
* missing functions for nacp mods !!!
* ncaIsHeaderDirty (doesn't modify anything per se, but it's used to check if any of the functions above has been used, basically)
* ncaEncryptHeader (doesn't modify anything per se, but it's used to generate new encrypted header data if needed)
inside loop:
* ncaWriteEncryptedHeaderDataToMemoryBuffer
inside dump loop:
* ncaIsHeaderDirty (doesn't modify anything per se, but it's used to check if any of the functions above has been used, basically - and by extension, if the functions below need to be used)
* ncaWriteEncryptedHeaderDataToMemoryBuffer (write encrypted nca header data)
* cnmtUpdateContentInfo (used to update content entry info in the raw cnmt copy after dumping each one)
* cnmtWriteNcaPatch (writes cnmt patch)
* calls pfsWriteEntryPatchToMemoryBuffer
* calls ncaWriteHierarchicalSha256PatchToMemoryBuffer
* programInfoWriteNcaPatch (writes ndpm patch)
* calls npdmWriteNcaPatch
* calls pfsWriteEntryPatchToMemoryBuffer
* calls ncaWriteHierarchicalSha256PatchToMemoryBuffer
* pfsWriteEntryPatchToMemoryBuffer
* calls ncaWriteHierarchicalSha256PatchToMemoryBuffer
* missing cnmt, program wrappers
* romfsWriteFileEntryPatchToMemoryBuffer
* calls ncaWriteHierarchicalSha256PatchToMemoryBuffer / ncaWriteHierarchicalIntegrityPatchToMemoryBuffer
@ -60,8 +65,6 @@ todo:
nca: support for compressed fs sections?
nca: support for sparse sections?
tik: option to wipe elicense property mask (otherwise, the console will attempt to connect to the Internet to perform elicense verification before launching the title the ticket belongs to)
tik: option to wipe volatile property mask (otherwise, the imported ticket will use an additional aes-ctr crypto layer in ticket.bin)
tik: automatically dump tickets to the SD card?
tik: use dumped tickets when the original ones can't be found in the ES savefile?