1
0
Fork 0
mirror of https://github.com/DarkMatterCore/nxdumptool.git synced 2024-11-08 11:51:48 +00:00

LAFW shenanigans.

* Added custom key sources to derive CardInfo keys at runtime using SPL.

* Implemented CardInfo area decryption.

* Implemented LAFW blob lookup in FS .data segment to retrieve the current LAFW version.

P.S.: still need to move around code to perform the LAFW version check at the places we need. But the current code is good enough for a test.
This commit is contained in:
Pablo Curiel 2021-05-22 04:45:40 -04:00
parent 0ce6d244e5
commit 21d1b001ee
6 changed files with 290 additions and 27 deletions

View file

@ -126,7 +126,8 @@ typedef enum {
GameCardFwVersion_ForProdSince400NUP = 2, ///< upp_version >= 268435456 (4.0.0-0.0) in GameCardInfo.
GameCardFwVersion_ForDevSince1100NUP = 3, ///< upp_version >= 738197504 (11.0.0-0.0) in GameCardInfo.
GameCardFwVersion_ForProdSince1100NUP = 4, ///< upp_version >= 738197504 (11.0.0-0.0) in GameCardInfo.
GameCardFwVersion_ForProdSince1200NUP = 5 ///< upp_version >= 805306368 (12.0.0-0.0) in GameCardInfo.
GameCardFwVersion_ForProdSince1200NUP = 5, ///< upp_version >= 805306368 (12.0.0-0.0) in GameCardInfo.
GameCardFwVersion_Count = 6
} GameCardFwVersion;
typedef enum {
@ -139,7 +140,7 @@ typedef enum {
GameCardCompatibilityType_Terra = 1
} GameCardCompatibilityType;
/// Encrypted using AES-128-CBC with the XCI header key (found in FS program memory under newer versions of HOS) and the IV from `GameCardHeader`.
/// Encrypted using AES-128-CBC with the XCI header key (found in FS program memory under HOS 9.0.0+) and the IV from `GameCardHeader`.
/// Key hashes for documentation purposes:
/// Production XCI header key hash: 2E36CC55157A351090A73E7AE77CF581F69B0B6E48FB066C984879A6ED7D2E96
/// Development XCI header key hash: 61D5C02244188810E2E3DE69341AC0F3C7653D370C6D3F77CA82B0B7E59F39AD
@ -174,7 +175,7 @@ typedef struct {
u64 package_id; ///< Used for challengeresponse authentication.
u32 valid_data_end_address; ///< Expressed in GAMECARD_PAGE_SIZE units.
u8 reserved[0x4];
u8 iv[0x10];
u8 card_info_iv[AES_128_KEY_SIZE]; ///< AES-128-CBC IV for the CardInfo area (reversed).
u64 partition_fs_header_address; ///< Root Hash File System header offset.
u64 partition_fs_header_size; ///< Root Hash File System header size.
u8 partition_fs_header_hash[SHA256_HASH_SIZE];

View file

@ -30,9 +30,9 @@
extern "C" {
#endif
/// Loads (and derives) NCA keydata from sysmodule program memory and the Lockpick_RCM keys file.
/// Loads (and derives) keydata from sysmodule program memory, the Lockpick_RCM keys file and hardcoded/obfuscated information.
/// Must be called (and succeed) before calling any of the functions below.
bool keysLoadNcaKeyset(void);
bool keysLoadKeyset(void);
/// Returns a pointer to the AES-128-XTS NCA header key, or NULL if keydata hasn't been loaded.
const u8 *keysGetNcaHeaderKey(void);
@ -57,6 +57,9 @@ bool keysDecryptRsaOaepWrappedTitleKey(const void *rsa_wrapped_titlekey, void *o
/// Returns a pointer to an AES-128-ECB ticket common key using the provided key generation value, or NULL if keydata hasn't been loaded.
const u8 *keysGetTicketCommonKey(u8 key_generation);
/// Returns a pointer to the AES-128-CBC CardInfo area key for gamecard headers, or NULL if keydata hasn't been loaded.
const u8 *keysGetGameCardInfoKey(void);
#ifdef __cplusplus
}
#endif

View file

@ -22,6 +22,7 @@
#include "nxdt_utils.h"
#include "mem.h"
#include "gamecard.h"
#include "keys.h"
#define GAMECARD_HFS0_MAGIC 0x48465330 /* "HFS0". */
@ -34,6 +35,8 @@
#define GAMECARD_STORAGE_AREA_NAME(x) ((x) == GameCardStorageArea_Normal ? "normal" : ((x) == GameCardStorageArea_Secure ? "secure" : "none"))
#define LAFW_MAGIC 0x4C414657 /* "LAFW". */
/* Type definitions. */
typedef enum {
@ -64,6 +67,39 @@ typedef struct {
NXDT_ASSERT(GameCardSecurityInformation, 0x600);
typedef enum {
LotusAsicFirmwareType_ReadFw = 0xFF,
LotusAsicFirmwareType_ReadDevFw = 0xFFFF,
LotusAsicFirmwareType_WriterFw = 0xFFFFFF,
LotusAsicFirmwareType_RmaFw = 0xFFFFFFFF
} LotusAsicFirmwareType;
typedef enum {
LotusAsicDeviceType_Test = 0,
LotusAsicDeviceType_Dev = 1,
LotusAsicDeviceType_Prod = 2,
LotusAsicDeviceType_Prod2Dev = 3
} LotusAsicDeviceType;
typedef struct {
u8 signature[0x100];
u32 magic; ///< "LAFW".
u32 fw_type; ///< LotusAsicFirmwareType.
u8 reserved_1[0x8];
struct {
u64 fw_version : 62; ///< Stored using a bitmask.
u64 device_type : 2; ///< LotusAsicDeviceType.
};
u32 data_size;
u8 reserved_2[0x4];
u8 data_iv[AES_128_KEY_SIZE];
char placeholder_str[0x10]; ///< "IDIDIDIDIDIDIDID".
u8 reserved_3[0x40];
u8 data[0x7680];
} LotusAsicFirmwareBlob;
NXDT_ASSERT(LotusAsicFirmwareBlob, 0x7800);
/* Global variables. */
static Mutex g_gameCardMutex = 0;
@ -74,6 +110,8 @@ static FsEventNotifier g_gameCardEventNotifier = {0};
static Event g_gameCardKernelEvent = {0};
static bool g_openDeviceOperator = false, g_openEventNotifier = false, g_loadKernelEvent = false;
static u64 g_lafwVersion = 0;
static Thread g_gameCardDetectionThread = {0};
static UEvent g_gameCardDetectionThreadExitEvent = {0}, g_gameCardStatusChangeEvent = {0};
static bool g_gameCardDetectionThreadCreated = false, g_gameCardInserted = false, g_gameCardInfoLoaded = false;
@ -84,6 +122,7 @@ static u8 g_gameCardStorageCurrentArea = GameCardStorageArea_None;
static u8 *g_gameCardReadBuf = NULL;
static GameCardHeader g_gameCardHeader = {0};
static GameCardInfo g_gameCardInfoArea = {0};
static u64 g_gameCardStorageNormalAreaSize = 0, g_gameCardStorageSecureAreaSize = 0, g_gameCardStorageTotalSize = 0;
static u64 g_gameCardCapacity = 0;
@ -108,6 +147,8 @@ static const char *g_gameCardHfsPartitionNames[] = {
/* Function prototypes. */
static bool gamecardGetLotusAsicFirmwareVersion(void);
static bool gamecardCreateDetectionThread(void);
static void gamecardDestroyDetectionThread(void);
static void gamecardDetectionThreadFunc(void *arg);
@ -129,6 +170,8 @@ static void gamecardCloseStorageArea(void);
static bool gamecardGetStorageAreasSizes(void);
NX_INLINE u64 gamecardGetCapacityFromRomSizeValue(u8 rom_size);
static bool gamecardGetDecryptedCardInfoArea(void);
static HashFileSystemContext *gamecardInitializeHashFileSystemContext(const char *name, u64 offset, u64 size, u8 *hash, u64 hash_target_offset, u32 hash_target_size);
static HashFileSystemContext *_gamecardGetHashFileSystemContext(u8 hfs_partition_type);
@ -186,6 +229,9 @@ bool gamecardInitialize(void)
/* Create user-mode gamecard status change event. */
ueventCreate(&g_gameCardStatusChangeEvent, true);
/* Retrieve LAFW version. */
if (!gamecardGetLotusAsicFirmwareVersion()) break;
/* Create gamecard detection thread. */
if (!(g_gameCardDetectionThreadCreated = gamecardCreateDetectionThread())) break;
@ -459,6 +505,69 @@ bool gamecardGetHashFileSystemEntryInfoByName(u8 hfs_partition_type, const char
return ret;
}
static bool gamecardGetLotusAsicFirmwareVersion(void)
{
u64 fw_version = 0;
bool ret = false, found = false, dev_unit = utilsIsDevelopmentUnit();
/* Temporarily set the segment mask to .data. */
g_fsProgramMemory.mask = MemoryProgramSegmentType_Data;
/* Retrieve full FS program memory dump. */
ret = memRetrieveProgramMemorySegment(&g_fsProgramMemory);
/* Clear segment mask. */
g_fsProgramMemory.mask = 0;
if (!ret)
{
LOG_MSG("Failed to retrieve FS .data segment dump!");
goto end;
}
/* Look for the LAFW ReadFw blob in the FS .data memory dump. */
for(u64 offset = 0; offset < g_fsProgramMemory.data_size; offset++)
{
if ((g_fsProgramMemory.data_size - offset) < sizeof(LotusAsicFirmwareBlob)) break;
LotusAsicFirmwareBlob *lafw_blob = (LotusAsicFirmwareBlob*)(g_fsProgramMemory.data + offset);
u32 magic = __builtin_bswap32(lafw_blob->magic), fw_type = lafw_blob->fw_type;
if (magic == LAFW_MAGIC && ((!dev_unit && fw_type == LotusAsicFirmwareType_ReadFw) || (dev_unit && fw_type == LotusAsicFirmwareType_ReadDevFw)))
{
/* Jackpot. */
fw_version = lafw_blob->fw_version;
found = true;
break;
}
}
if (!found)
{
LOG_MSG("Unable to locate Lotus ReadFw blob in FS .data segment!");
goto end;
}
/* Convert LAFW version bitmask to an integer. */
g_lafwVersion = 0;
while(fw_version)
{
g_lafwVersion += (fw_version & 1);
fw_version >>= 1;
}
LOG_MSG("LAFW version: %lu.", g_lafwVersion);
/* Update flag. */
ret = true;
end:
memFreeMemoryLocation(&g_fsProgramMemory);
return ret;
}
static bool gamecardCreateDetectionThread(void)
{
if (!utilsCreateThread(&g_gameCardDetectionThread, gamecardDetectionThreadFunc, NULL, 1))
@ -577,6 +686,51 @@ static void gamecardLoadInfo(void)
goto end;
}
/* Get decrypted CardInfo area. */
if (!gamecardGetDecryptedCardInfoArea()) goto end;
LOG_DATA(&g_gameCardInfoArea, sizeof(GameCardInfo), "Gamecard CardInfo area dump:");
/* Check if we meet the Lotus ASIC firmware (LAFW) version requirement. */
/* Lotus treats the GameCardFwVersion field as the maximum unsupported LAFW version, instead of treating it as the minimum supported version. */
/* TODO: move this check somewhere else. We're supposed to do it only if things go south while reading gamecard data. */
if (g_lafwVersion <= g_gameCardInfoArea.fw_version)
{
LOG_MSG("LAFW version not supported by the inserted gamecard (%lu <= %lu).", g_lafwVersion, g_gameCardInfoArea.fw_version);
goto end;
}
/* Get gamecard capacity. */
g_gameCardCapacity = gamecardGetCapacityFromRomSizeValue(g_gameCardHeader.rom_size);
if (!g_gameCardCapacity)
@ -654,6 +808,8 @@ static void gamecardFreeInfo(void)
{
memset(&g_gameCardHeader, 0, sizeof(GameCardHeader));
memset(&g_gameCardInfoArea, 0, sizeof(GameCardInfo));
g_gameCardStorageNormalAreaSize = g_gameCardStorageSecureAreaSize = g_gameCardStorageTotalSize = 0;
g_gameCardCapacity = 0;
@ -976,6 +1132,39 @@ NX_INLINE u64 gamecardGetCapacityFromRomSizeValue(u8 rom_size)
return capacity;
}
static bool gamecardGetDecryptedCardInfoArea(void)
{
const u8 *card_info_key = NULL;
u8 card_info_iv[AES_128_KEY_SIZE] = {0};
Aes128CbcContext aes_ctx = {0};
/* Retrieve CardInfo area key. */
card_info_key = keysGetGameCardInfoKey();
if (!card_info_key)
{
LOG_MSG("Failed to retrieve CardInfo area key!");
return false;
}
/* Reverse CardInfo IV. */
for(u8 i = 0; i < AES_128_KEY_SIZE; i++) card_info_iv[i] = g_gameCardHeader.card_info_iv[AES_128_KEY_SIZE - i - 1];
/* Initialize AES-128-CBC context. */
aes128CbcContextCreate(&aes_ctx, card_info_key, card_info_iv, false);
/* Decrypt CardInfo area. */
aes128CbcDecrypt(&aes_ctx, &g_gameCardInfoArea, &(g_gameCardHeader.card_info), sizeof(GameCardInfo));
/* Verify update ID. */
if (utilsGetCustomFirmwareType() != UtilsCustomFirmwareType_SXOS && g_gameCardInfoArea.upp_id != GAMECARD_UPDATE_TID)
{
LOG_MSG("Failed to decrypt CardInfo area!");
return false;
}
return true;
}
static HashFileSystemContext *gamecardInitializeHashFileSystemContext(const char *name, u64 offset, u64 size, u8 *hash, u64 hash_target_offset, u32 hash_target_size)
{
u32 i = 0, magic = 0;

View file

@ -73,13 +73,24 @@ typedef struct {
u8 ticket_common_keys[NcaKeyGeneration_Max][AES_128_KEY_SIZE]; ///< Retrieved from the Lockpick_RCM keys file.
} KeysNcaKeyset;
typedef struct {
/// AES-128-CBC keys needed to decrypt the CardInfo area from gamecard headers.
const u8 gc_cardinfo_kek_source[AES_128_KEY_SIZE]; ///< Randomly generated KEK source to decrypt official CardInfo area keys.
const u8 gc_cardinfo_key_prod_source[AES_128_KEY_SIZE]; ///< CardInfo area key used in retail units. Obfuscated using the above KEK source and SMC AES engine keydata.
const u8 gc_cardinfo_key_dev_source[AES_128_KEY_SIZE]; ///< CardInfo area key used in development units. Obfuscated using the above KEK source and SMC AES engine keydata.
u8 gc_cardinfo_kek_sealed[AES_128_KEY_SIZE]; ///< Generated from gc_cardinfo_kek_source. Sealed by the SMC AES engine.
u8 gc_cardinfo_key_prod[AES_128_KEY_SIZE]; ///< Generated from gc_cardinfo_kek_sealed and gc_cardinfo_key_prod_source.
u8 gc_cardinfo_key_dev[AES_128_KEY_SIZE]; ///< Generated from gc_cardinfo_kek_sealed and gc_cardinfo_key_dev_source.
} KeysGameCardKeyset;
/// Used to parse the eTicket RSA device key retrieved from PRODINFO via setcalGetEticketDeviceKey().
/// Everything after the AES CTR is encrypted using the eTicket RSA device key encryption key.
typedef struct {
u8 ctr[AES_128_KEY_SIZE];
u8 private_exponent[RSA2048_BYTES];
u8 modulus[RSA2048_BYTES];
u32 public_exponent; ///< Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT. Stored using big endian byte order.
u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT.
u8 padding[0x14];
u64 device_id;
u8 ghash[0x10];
@ -108,11 +119,23 @@ static bool keysReadKeysFromFile(void);
static bool keysGetDecryptedEticketRsaDeviceKey(void);
static bool keysTestEticketRsaDeviceKey(const void *e, const void *d, const void *n);
static bool keysDeriveGameCardKeys(void);
/* Global variables. */
static KeysNcaKeyset g_ncaKeyset = {0};
static bool g_ncaKeysetLoaded = false;
static Mutex g_ncaKeysetMutex = 0;
static KeysGameCardKeyset g_gameCardKeyset = {
.gc_cardinfo_kek_source = { 0xDE, 0xC6, 0x3F, 0x6A, 0xBF, 0x37, 0x72, 0x0B, 0x7E, 0x54, 0x67, 0x6A, 0x2D, 0xEF, 0xDD, 0x97 },
.gc_cardinfo_key_prod_source = { 0xF4, 0x92, 0x06, 0x52, 0xD6, 0x37, 0x70, 0xAF, 0xB1, 0x9C, 0x6F, 0x63, 0x09, 0x01, 0xF6, 0x29 },
.gc_cardinfo_key_dev_source = { 0x0B, 0x7D, 0xBB, 0x2C, 0xCF, 0x64, 0x1A, 0xF4, 0xD7, 0x38, 0x81, 0x3F, 0x0C, 0x33, 0xF4, 0x1C },
.gc_cardinfo_kek_sealed = {0},
.gc_cardinfo_key_prod = {0},
.gc_cardinfo_key_dev = {0}
};
static bool g_keysetLoaded = false;
static Mutex g_keysetMutex = 0;
static SetCalRsa2048DeviceKey g_eTicketRsaDeviceKey = {0};
@ -230,13 +253,13 @@ static KeysMemoryInfo g_fsDataMemoryInfo = {
}
};
bool keysLoadNcaKeyset(void)
bool keysLoadKeyset(void)
{
bool ret = false;
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
ret = g_ncaKeysetLoaded;
ret = g_keysetLoaded;
if (ret) break;
/* Retrieve FS .rodata keys. */
@ -273,14 +296,18 @@ bool keysLoadNcaKeyset(void)
/* Get decrypted eTicket RSA device key. */
if (!keysGetDecryptedEticketRsaDeviceKey()) break;
/* Derive gamecard keys. */
if (!keysDeriveGameCardKeys()) break;
/* Update flags. */
ret = g_ncaKeysetLoaded = true;
ret = g_keysetLoaded = true;
}
/*if (ret)
{
LOG_DATA(&g_ncaKeyset, sizeof(KeysNcaKeyset), "NCA keyset dump:");
LOG_DATA(&g_eTicketRsaDeviceKey, sizeof(SetCalRsa2048DeviceKey), "eTicket RSA device key dump:");
LOG_DATA(&g_gameCardKeyset, sizeof(KeysGameCardKeyset), "Gamecard keyset dump:");
}*/
return ret;
@ -290,9 +317,9 @@ const u8 *keysGetNcaHeaderKey(void)
{
const u8 *ret = NULL;
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
if (g_ncaKeysetLoaded) ret = (const u8*)(g_ncaKeyset.nca_header_key);
if (g_keysetLoaded) ret = (const u8*)(g_ncaKeyset.nca_header_key);
}
return ret;
@ -309,9 +336,9 @@ const u8 *keysGetNcaMainSignatureModulus(u8 key_generation)
bool dev_unit = utilsIsDevelopmentUnit();
const u8 *ret = NULL, null_modulus[RSA2048_PUBKEY_SIZE] = {0};
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
if (!g_ncaKeysetLoaded) break;
if (!g_keysetLoaded) break;
ret = (const u8*)(dev_unit ? g_ncaKeyset.nca_main_signature_moduli_dev[key_generation] : g_ncaKeyset.nca_main_signature_moduli_prod[key_generation]);
@ -348,9 +375,9 @@ bool keysDecryptNcaKeyAreaEntry(u8 kaek_index, u8 key_generation, void *dst, con
goto end;
}
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
if (!g_ncaKeysetLoaded) break;
if (!g_keysetLoaded) break;
Result rc = splCryptoGenerateAesKey(g_ncaKeyset.nca_kaek_sealed[kaek_index][key_gen_val], src, dst);
if (!(ret = R_SUCCEEDED(rc))) LOG_MSG("splCryptoGenerateAesKey failed! (0x%08X).", rc);
}
@ -376,9 +403,9 @@ const u8 *keysGetNcaKeyAreaEncryptionKey(u8 kaek_index, u8 key_generation)
goto end;
}
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
if (g_ncaKeysetLoaded) ret = (const u8*)(g_ncaKeyset.nca_kaek[kaek_index][key_gen_val]);
if (g_keysetLoaded) ret = (const u8*)(g_ncaKeyset.nca_kaek[kaek_index][key_gen_val]);
}
end:
@ -395,9 +422,9 @@ bool keysDecryptRsaOaepWrappedTitleKey(const void *rsa_wrapped_titlekey, void *o
bool ret = false;
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
if (!g_ncaKeysetLoaded) break;
if (!g_keysetLoaded) break;
size_t out_keydata_size = 0;
u8 out_keydata[RSA2048_BYTES] = {0};
@ -432,15 +459,27 @@ const u8 *keysGetTicketCommonKey(u8 key_generation)
goto end;
}
SCOPED_LOCK(&g_ncaKeysetMutex)
SCOPED_LOCK(&g_keysetMutex)
{
if (g_ncaKeysetLoaded) ret = (const u8*)(g_ncaKeyset.ticket_common_keys[key_gen_val]);
if (g_keysetLoaded) ret = (const u8*)(g_ncaKeyset.ticket_common_keys[key_gen_val]);
}
end:
return ret;
}
const u8 *keysGetGameCardInfoKey(void)
{
const u8 *ret = NULL;
SCOPED_LOCK(&g_keysetMutex)
{
if (g_keysetLoaded) ret = (const u8*)(utilsIsDevelopmentUnit() ? g_gameCardKeyset.gc_cardinfo_key_dev : g_gameCardKeyset.gc_cardinfo_key_prod);
}
return ret;
}
static bool keysIsProductionModulus1xMandatory(void)
{
return !utilsIsDevelopmentUnit();
@ -941,3 +980,34 @@ static bool keysTestEticketRsaDeviceKey(const void *e, const void *d, const void
return true;
}
static bool keysDeriveGameCardKeys(void)
{
Result rc = 0;
/* Derive gc_cardinfo_kek_sealed from gc_cardinfo_kek_source. */
rc = splCryptoGenerateAesKek(g_gameCardKeyset.gc_cardinfo_kek_source, 0, 0, g_gameCardKeyset.gc_cardinfo_kek_sealed);
if (R_FAILED(rc))
{
LOG_MSG("splCryptoGenerateAesKek failed! (0x%08X) (gc_cardinfo_kek_sealed).", rc);
return false;
}
/* Derive gc_cardinfo_key_prod from gc_cardinfo_kek_sealed and gc_cardinfo_key_prod_source. */
rc = splCryptoGenerateAesKey(g_gameCardKeyset.gc_cardinfo_kek_sealed, g_gameCardKeyset.gc_cardinfo_key_prod_source, g_gameCardKeyset.gc_cardinfo_key_prod);
if (R_FAILED(rc))
{
LOG_MSG("splCryptoGenerateAesKey failed! (0x%08X) (gc_cardinfo_key_prod).", rc);
return false;
}
/* Derive gc_cardinfo_key_dev from gc_cardinfo_kek_sealed and gc_cardinfo_key_dev_source. */
rc = splCryptoGenerateAesKey(g_gameCardKeyset.gc_cardinfo_kek_sealed, g_gameCardKeyset.gc_cardinfo_key_dev_source, g_gameCardKeyset.gc_cardinfo_key_dev);
if (R_FAILED(rc))
{
LOG_MSG("splCryptoGenerateAesKey failed! (0x%08X) (gc_cardinfo_key_dev).", rc);
return false;
}
return true;
}

View file

@ -129,8 +129,8 @@ bool utilsInitializeResources(const int program_argc, const char **program_argv)
/* Initialize USB Mass Storage interface. */
if (!umsInitialize()) break;
/* Load NCA keyset. */
if (!keysLoadNcaKeyset()) break;
/* Load keyset. */
if (!keysLoadKeyset()) break;
/* Allocate NCA crypto buffer. */
if (!ncaAllocateCryptoBuffer())

View file

@ -15,7 +15,7 @@ todo:
title: parse the update partition from gamecards (if available) to generate ncmcontentinfo data for all update titles
gamecard: functions to display filelist
gamecard: check cardinfo's lafw version
gamecard: move around code to perform the lafw version check at the places we need
pfs0: functions to display filelist