diff --git a/include/core/gamecard.h b/include/core/gamecard.h index 2cc97e2..79ffffe 100644 --- a/include/core/gamecard.h +++ b/include/core/gamecard.h @@ -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 challenge–response 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]; diff --git a/include/core/keys.h b/include/core/keys.h index 1ed9a73..586b547 100644 --- a/include/core/keys.h +++ b/include/core/keys.h @@ -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 diff --git a/source/core/gamecard.c b/source/core/gamecard.c index bb8a747..751d022 100644 --- a/source/core/gamecard.c +++ b/source/core/gamecard.c @@ -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; diff --git a/source/core/keys.c b/source/core/keys.c index 4c67693..8c92bdf 100644 --- a/source/core/keys.c +++ b/source/core/keys.c @@ -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; +} diff --git a/source/core/nxdt_utils.c b/source/core/nxdt_utils.c index 01aae29..3480a76 100644 --- a/source/core/nxdt_utils.c +++ b/source/core/nxdt_utils.c @@ -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()) diff --git a/todo.txt b/todo.txt index f612489..846267d 100644 --- a/todo.txt +++ b/todo.txt @@ -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