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:
parent
15431ec2c8
commit
974790944f
19 changed files with 1398 additions and 187 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
1009
code_templates/nsp_dumper.c
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 || \
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
34
source/nca.c
34
source/nca.c
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
320
source/tik.c
320
source/tik.c
|
@ -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. */
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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);
|
||||
|
|
31
todo.txt
31
todo.txt
|
@ -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?
|
||||
|
||||
|
|
Loading…
Reference in a new issue