diff --git a/code_templates/nsp_dumper_stor.c b/code_templates/nsp_dumper_stor.c index 404e7e4..e5d9391 100644 --- a/code_templates/nsp_dumper_stor.c +++ b/code_templates/nsp_dumper_stor.c @@ -279,7 +279,7 @@ static void nspDump(TitleInfo *title_info, u64 free_space) // remove titlekey crypto // has no effect if this nca doesn't use titlekey crypto - if (remove_titlekey_crypto && !ncaRemoveTitlekeyCrypto(cur_nca_ctx)) + if (remove_titlekey_crypto && !ncaRemoveTitleKeyCrypto(cur_nca_ctx)) { consolePrint("nca remove titlekey crypto failed\n"); goto end; diff --git a/code_templates/nsp_dumper_usb.c b/code_templates/nsp_dumper_usb.c index 69937ef..e209def 100644 --- a/code_templates/nsp_dumper_usb.c +++ b/code_templates/nsp_dumper_usb.c @@ -269,7 +269,7 @@ static void dump_thread_func(void *arg) // remove titlekey crypto // has no effect if this nca doesn't use titlekey crypto - if (remove_titlekey_crypto && !ncaRemoveTitlekeyCrypto(cur_nca_ctx)) + if (remove_titlekey_crypto && !ncaRemoveTitleKeyCrypto(cur_nca_ctx)) { consolePrint("nca remove titlekey crypto failed\n"); goto end; diff --git a/host/README.md b/host/README.md new file mode 100644 index 0000000..e5abd31 --- /dev/null +++ b/host/README.md @@ -0,0 +1,218 @@ +# nxdumptool USB Application Binary Interface (ABI) Technical Specification + +This Markdown document aims to explain the technical details behind the ABI used by nxdumptool to communicate with a USB host device connected to the console. As of this writing (May 11th, 2021), the current ABI version is `1`. + +In order to avoid unnecessary clutter, this document assumes the reader is already familiar with homebrew launching on the Nintendo Switch, as well as USB concepts such as device/configuration/interface/endpoint descriptors and bulk mode transfers. Shall this not be the case, a small list of helpful resources is available at the end of this document. + +## Table of contents + +* [USB device interface details](#usb-device-interface-details). +* [USB driver](#usb-driver). + * [Unix-like operating systems](#unix-like-operating-systems). + * [Windows](#windows). +* [USB communication details](#usb-communication-details). + * [Command header](#command-header). + * [Command IDs](#command-ids). + * [Command blocks](#command-blocks). + * [StartSession](#startsession). + * [SendFileProperties](#sendfileproperties). + * [Zero Length Termination (ZLT)](#zero-length-termination-zlt). + * [CancelFileTransfer](#cancelfiletransfer). + * [SendNspHeader](#sendnspheader). + * [EndSession](#endsession). + * [Status response](#status-response). + * [Status codes](#status-codes). + * [NSP transfer mode](#nsp-transfer-mode). + * [Why is there such thing as a 'NSP transfer mode'?](#why-is-there-such-thing-as-a-nsp-transfer-mode) +* [Additional resources](#additional-resources). + +## USB device interface details + +Right after launching nxdumptool on the target Nintendo Switch, the application configures the console's USB interface using the following information: + +* Device descriptor: + * Class / Subclass / Protocol: all set to `0x00` (defined at the interface level). + * Vendor ID: `0x057E`. + * Product ID: `0x3000`. + * BCD release number: `0x0100`. + * Product string: `nxdumptool`. + * Manufacturer string: `DarkMatterCore`. + * Multiple device descriptors are used to support USB 1.1, 2.0 and 3.0 speeds, each one with slightly different properties. The underlying USB stack from a USB host device usually takes care of automatically choosing one of these, depending on the capabilities of the USB host. + * USB 3.0 support depends on the `usb30_force_enabled` setting from Horizon OS to be manually enabled before launching a custom firmware (CFW) on the target console. Otherwise, only 1.1 and 2.0 speeds will be made available to the USB host device. +* Configuration descriptor: + * A single configuration descriptor is provided, regardless of the USB speed selected by the USB host. +* Interface descriptor: + * A single interface descriptor with no alternate setting is provided as part of the configuration descriptor. + * Class / Subclass / Protocol: all set to `0xFF` (vendor-specific). +* Endpoint descriptors: + * Only two bulk endpoint descriptors are provided as part of the interface descriptor. + * The max packet size varies depending on the USB speed selected by the USB host: + * USB 1.1: 64 bytes. + * USB 2.0: 512 bytes. + * USB 3.0: 1024 bytes. + * SuperSpeed endpoint companion descriptors are also provided if USB 3.0 is used. +* Binary Object Store (BOS) descriptor: + * Holds a USB 2.0 extension descriptor for Link Power Management (LPM) support, as well as a SuperSpeed device capability descriptor to indicate the supported speeds. + +Communication is performed through the bulk input and output endpoints using 5-second timeouts. + +## USB driver + +A USB driver is needed to actually communicate to the target console running nxdumptool. + +### Unix-like operating systems + +A package manager can be used to install [libusb](https://libusb.info), which in turn can be used by programs to enumerate and interact with the target console. Under some operating systems, this step may not even be needed. + +### Windows + +A tool such as [Zadig](https://zadig.akeo.ie) must be used to manually install a USB driver for the target console. + +Even though it's possible to use the `WinUSB` driver, we suggest to use `libusbK` instead - the provided Python script in this directory depends on [PyUSB](https://github.com/pyusb/pyusb), which only provides a backend for `libusb` devices. If you intend to write your own `WinUSB`-based ABI host implementation for Windows based on this document, you may be able to use that driver. + +Furthermore, even though it's possible for USB devices to work right out of the box using [Microsoft OS descriptors](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors), the `usb:ds` API available to homebrew applications on the Nintendo Switch doesn't provide any way to set them. Thus, it's not possible to interact with the target console without installing a USB driver first. + +## USB communication details + +The USB host device essentially acts as a storage server for nxdumptool. This means all commands are initially issued by the target console, leading to data transfer stages for which status responses are expected to be sent by the USB host device. + +This intends to minimize the overhead on the USB host device by letting nxdumptool take care of the full dump process - the host only needs to take care of storing the received data. This also heavily simplifies the work required to write ABI host implementations from scratch, regardless of the programming language being used. + +Command handling can be broken down in three different transfer stages: command header (from nxdumptool), command block (from nxdumptool) and status response (from USB host). Certain commands may lead to an additional data transfer stage after the status response is received from the USB host device. + +### Command header + +| Offset | Size | Description | +|:------:|:----:|------------------------------| +| 0x00 | 0x04 | Magic word (`NXDT`). | +| 0x04 | 0x04 | Command ID (LE u32). | +| 0x08 | 0x04 | Command block size (LE u32). | +| 0x0C | 0x04 | Reserved. | + +While handling ABI commands, nxdumptool first issues the command header - this way, the USB host device knows both the command ID and the command block size before attempting to receive the command block. + +Certain commands yield no command block at all, leading to a command block size of zero - this is considered defined behaviour. Nonetheless, a status response is still expected to be sent by the USB host. + +#### Command IDs + +| Value | Name | Description | +|:-----:|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | StartSession. | Starts a USB session between the target console and the USB host device. | +| 1 | SendFileProperties. | Sends file metadata and starts a data transfer process. | +| 2 | CancelFileTransfer. | Cancels an ongoing data transfer process started by a previously issued SendFileProperties command. | +| 3 | SendNspHeader. | Sends the `PFS0` header from a Nintendo Submission Package (NSP). Only issued under NSP transfer mode, and after the data for all NSP file entries has been transferred. | +| 4 | EndSession. | Ends a previously stablished USB session between the target console and the USB host device. | + +### Command blocks + +All commands, with the exception of `CancelFileTransfer` and `EndSession`, yield a command block. Each command block follows its own distinctive structure. + +#### StartSession + +| Offset | Size | Description | +|:------:|------|-------------------------------------------| +| 0x00 | 0x01 | nxdumptool version (major). | +| 0x01 | 0x01 | nxdumptool version (minor). | +| 0x02 | 0x01 | nxdumptool version (micro). | +| 0x03 | 0x01 | nxdumptool USB ABI version. | +| 0x04 | 0x08 | Git commit hash (NULL terminated string). | +| 0x0C | 0x04 | Reserved. | + +This is the first USB command issued by nxdumptool upon connection to a USB host device. If it succeeds, further USB commands may be sent. + +#### SendFileProperties + +| Offset | Size | Description | +|:------:|:-----:|--------------------------------------------------| +| 0x000 | 0x08 | File size (LE u64). | +| 0x008 | 0x04 | Filename length (LE u32). | +| 0x00C | 0x04 | NSP header size (LE u32). | +| 0x010 | 0x301 | UTF-8 encoded filename (NULL terminated string). | +| 0x311 | 0x0F | Reserved. | + +Sent right before starting a file transfer. If it succeeds, file data will be transferred using 8 MiB (0x800000) chunks. The last chunk will be truncated, if needed. + +A status response is expected from the USB host right after receiving this command block, which is also right before starting the file data transfer stage. Furthermore, an additional status response is expected right after the last file data chunk has been sent. + +In the case of extracted RomFS transfers, the `filename` field may actually hold an absolute filepath - this will always start with a `/`. The USB host is free to decide how to handle this (e.g. create full directory tree, etc.). + +##### Zero Length Termination (ZLT) + +As per USB bulk transfer specification, when a USB host/device receives a data packet smaller than the endpoint max packet size, it shall consider the transfer is complete and no more data packets are left. This is called a transaction completion mechanism. + +However, if the last data chunk is aligned to the endpoint max packet size, an alternate completion mechanism is needed - this is where Zero Length Termination (ZLT) packets come into play. If this condition is met, the USB host device should expect a single ZLT packet from nxdumptool right after the last data chunk has been transferred. + +If no ZLT packet were issued, the USB stack from the host device wouldn't be capable of knowing the ongoing transfer has been completed, making it expect further data to be sent by the target console - which in turn leads to a timeout error on the USB host side. Furthermore, if the ZLT packet is left unhandled by the USB host device, a timeout error will be raised on the target console's side. + +Most USB backend implementations require the program to provide a bigger transfer length (+1 byte at least) if a ZLT packet is to be expected. This should be more than enough. + +#### CancelFileTransfer + +Yields no command block. Expects a status response, just like the rest of the commands. + +This command can only be issued during the file data transfer stage from a [SendFileProperties](#sendfileproperties) command. It is used to gracefully cancel an ongoing file transfer while also keeping the USB session alive. It's up to the USB host to decide what to do with the incomplete data. + +The easiest way to detect this command during a file transfer is by checking the length of the last received block and then parse it to see if it matches a `CancelFileTransfer` command header. + +#### SendNspHeader + +Variable length. The command block size from the command header represents the NSP header size, while the command block data represents the `PFS0` header from a NSP. + +If the NSP header size is aligned to the endpoint max packet size, the USB host should expect a [ZLT packet](#zero-length-termination-zlt). + +#### EndSession + +Yields no command block. Expects a status response, just like the rest of the commands. + +This command is only issued while exiting nxdumptool, as long as the target console is connected to a host device and a USB session has been successfully established. + +### Status response + +| Offset | Size | Description | +|:------:|:----:|------------------------------------| +| 0x00 | 0x04 | Magic word (`NXDT`). | +| 0x04 | 0x04 | Status code (LE u32). | +| 0x08 | 0x02 | Endpoint max packet size (LE u16). | +| 0x0A | 0x06 | Reserved. | + +Status responses are expected by nxdumptool at certain points throughout the command handling steps: + +* Right after receiving a command header and/or command block. +* Right after receiving the last file data chunk from a [SendFileProperties](#sendfileproperties) command. + +The endpoint max packet size must be sent back to the target console using status responses because the `usb:ds` API provides no way for homebrew applications to know which device descriptor and/or speed was selected by the USB host device. + +#### Status codes + +| Value | Description | +|:-----:|------------------------------------------------------------------| +| 0 | Success. | +| 1 | Invalid command size. Reserved for internal nxdumptool usage. | +| 2 | Failed to write command. Reserved for internal nxdumptool usage. | +| 3 | Failed to read status. Reserved for internal nxdumptool usage. | +| 4 | Invalid magic word. | +| 5 | Unsupported command. | +| 6 | Unsupported USB ABI version. | +| 7 | Malformed command. | +| 8 | USB host I/O error (write error, insufficient space, etc.). | + +### NSP transfer mode + +If the NSP header size field from a [SendFileProperties](#sendfileproperties) command block is greater than zero, the USB host should enter NSP transfer mode. The file size field from this block represents, then, the full NSP size (including the NSP header). + +In this mode, the USB host should immediately create the output file, write `NSP header size` bytes of padding to it, reply with a status response as usual and expect further [SendFileProperties](#sendfileproperties) commands. No file data is transferred for this very first [SendFileProperties](#sendfileproperties) command block. + +Each further [SendFileProperties](#sendfileproperties) command block will hold the filename and size for a specific NSP file entry, and the NSP header size field will always be set to zero. The file data received for each one of these file entries must be written to the output file created during the first [SendFileProperties](#sendfileproperties) command. The sum of all file entry sizes should be equal to the full NSP size minus the NSP header size received during the first [SendFileProperties](#sendfileproperties) command. + +Finally, the USB host will receive a [SendNspHeader](#sendnspheader) command with the NSP header data, which should be written at the start of the output file. The command block size in the command header should match the NSP header size received in the first [SendFileProperties](#sendfileproperties) command. + +#### Why is there such thing as a 'NSP transfer mode'? + +This is because the `PFS0` header from NSPs holds the filenames for all file entries written into the package, which are mostly [Nintendo Content Archives (NCA)](https://switchbrew.org/wiki/NCA). + +NCA filenames represent the first half of the NCA SHA-256 checksum, in lowercase. This fact alone makes it impossible to send a NSP header right from the beginning - SHA-256 checksums are calculated by nxdumptool while dumping each NCA. + +## Additional resources + +* [USB in a NutShell](https://www.beyondlogic.org/usbnutshell/usb1.shtml). +* [USB Made Simple](https://www.usbmadesimple.co.uk). diff --git a/host/usb_abi_specs.txt b/host/usb_abi_specs.txt deleted file mode 100644 index 19b1783..0000000 --- a/host/usb_abi_specs.txt +++ /dev/null @@ -1,162 +0,0 @@ -USB command header: - -*--------*--------*------------------------------* -| Offset | Size | Description | -*--------*--------*------------------------------* -| 0x00 | 0x04 | Magic word ("NXDT"). | -*--------*--------*------------------------------* -| 0x04 | 0x04 | Command ID (LE u32). | -*--------*--------*------------------------------* -| 0x08 | 0x04 | Command Block Size (LE u32). | -*--------*--------*------------------------------* -| 0x0C | 0x04 | Reserved. | -*--------*--------*------------------------------* - -USB command IDs: - -*--------*--------------------------------------* -| Value | Description | -*--------*--------------------------------------* -| 0 | StartSession. | -*--------*--------------------------------------* -| 1 | SendFileProperties. | -*--------*--------------------------------------* -| 2 | CancelFileTransfer. | -*--------*--------------------------------------* -| 3 | SendNspHeader. | -*--------*--------------------------------------* -| 4 | EndSession. | -*--------*--------------------------------------* - -____________________________________________________________________________________________________________________________________ - - -StartSession Command Block: 0x10 byte-long, set by nxdumptool as the value for the Command Block Size field in the command header (along with its matching command ID). - -This is the first USB command issued by nxdumptool. If it succeeds, further USB commands may be sent. - -*--------*--------*-----------------------------* -| Offset | Size | Description | -*--------*--------*-----------------------------* -| 0x00 | 0x01 | nxdumptool version (major). | \ -*--------*--------*-----------------------------* \ -| 0x01 | 0x01 | nxdumptool version (minor). | --|---> Version number, which can be displayed by the host application as: "{major}.{minor}.{micro}". -*--------*--------*-----------------------------* / -| 0x02 | 0x01 | nxdumptool version (micro). | / -*--------*--------*-----------------------------* -| 0x03 | 0x01 | nxdumptool USB ABI version. | ------> Currently, always v1. -*--------*--------*-----------------------------* -| 0x04 | 0x08 | Git commit hash (string). | -*--------*--------*-----------------------------* -| 0x0C | 0x04 | Reserved. | -*--------*--------*-----------------------------* - -____________________________________________________________________________________________________________________________________ - - -SendFileProperties Command Block: 0x320 byte-long, set by nxdumptool as the value for the Command Block Size field in the command header (along with its matching command ID). - -Sent right before starting a file transfer. If it succeeds, file data will be transferred in bulk mode using 0x800000-sized chunks (the last one is truncated if needed). - -*--------*--------*-----------------------------* -| Offset | Size | Description | -*--------*--------*-----------------------------* -| 0x00 | 0x08 | File size (LE u64). | -*--------*--------*-----------------------------* -| 0x08 | 0x04 | Filename length (LE u32). | -*--------*--------*-----------------------------* -| 0x0C | 0x04 | NSP header size (LE u32). | -*--------*--------*-----------------------------* -| 0x10 | 0x301 | NULL terminated filename. | -> 0x301 is defined as FS_MAX_PATH in libnx. Always encoded using UTF-8. -*--------*--------*-----------------------------* -| 0x311 | 0x0F | Reserved. | -*--------*--------*-----------------------------* - -____________________________________________________________________________________________________________________________________ - - -CancelFileTransfer: holds no command block. Expects a USB status response, just like the rest of the commands. - -This command can only be issued during the file transfer stage from a SendFileProperties command. It is used to gracefully cancel an ongoing file transfer while also keeping the USB session alive. It's up to the host application to decide what to do with the incomplete data. - -The easiest way to detect this command during a file transfer is by checking the length of the last received block and then parse it to see if it matches a CancelFileTransfer command header. - -____________________________________________________________________________________________________________________________________ - - -SendNspHeader Command Block: variable length. The Command Block Size field in the command header holds the NSP header size. The NSP header data is placed after the command header. - -____________________________________________________________________________________________________________________________________ - - -EndSession: holds no command block. Expects a USB status response, just like the rest of the commands. - -This command is only issued while exiting nxdumptool, as long as the console is connected to a host device and a USB session has been successfully established. - -____________________________________________________________________________________________________________________________________ - - -USB status block (returned by the client application): - -*--------*--------*------------------------------------* -| Offset | Size | Description | -*--------*--------*------------------------------------* -| 0x00 | 0x04 | Magic word ("NXDT"). | -*--------*--------*------------------------------------* -| 0x04 | 0x04 | Status code (LE u32). | -*--------*--------*------------------------------------* -| 0x08 | 0x02 | Endpoint max packet size (LE u16). | -> Value used by the endpoint in the USB host device (I can't detect it using usb:ds calls on the Switch). -*--------*--------*------------------------------------* -| 0x0A | 0x06 | Reserved. | -*--------*--------*------------------------------------* - -USB status codes: - -*--------*-----------------------------------------* -| Value | Description | -*--------*-----------------------------------------* -| 0 | Success. | -*--------*-----------------------------------------* -| 1 | Reserved for internal nxdumptool usage. | -*--------*-----------------------------------------* -| 2 | Reserved for internal nxdumptool usage. | -*--------*-----------------------------------------* -| 3 | Reserved for internal nxdumptool usage. | -*--------*-----------------------------------------* -| 4 | Invalid magic word. | -*--------*-----------------------------------------* -| 5 | Unsupported command. | -*--------*-----------------------------------------* -| 6 | Unsupported USB ABI version. | -*--------*-----------------------------------------* -| 7 | Malformed command. | -*--------*-----------------------------------------* -| 8 | I/O error in USB host. | -*--------*-----------------------------------------* - -____________________________________________________________________________________________________________________________________ - - -Highlights: - -* Vendor ID: 0x057e. -* Product ID: 0x3000. -* Device descriptor strings will always match "nxdumptool" (product) and "DarkMatterCore" (manufacturer). -* Multiple device descriptors are provided to support USB 1.1, USB 2.0 and USB 3.0, each one with slightly different properties. -* A single configuration descriptor is provided per each possible device descriptor. -* A single Binary Object Store (BOS) descriptor is also provided. -* All transfers are performed in USB bulk mode, using 5-second timeouts. -* While handling USB commands, nxdumptool first issues the USB command header (so the USB host device can parse how much data to read next), then sends the USB command block (if required by the command being handled), and finally expects the host device to send a USB status response. - * This includes the SendFileProperties command, which expects a USB status response from the host right before starting the file data transfer. -* A USB status response is expected by nxdumptool right after the last file data chunk has been sent. -* If, while transferring file data, the last data chunk is aligned to the endpoint max packet size used by the USB host, a Zero Length Termination (ZLT) packet will be issued after transferring the last chunk, in order to comply with the USB bulk transfer specs. -* In the case of extracted RomFS transfers, the filename field from the SendFileProperties block may actually contain a filepath - this will always start with a '/'. The client application is free to decide how to handle this (e.g. create full directory tree, etc.). - - -NSP transfer mode: - -* If the NSP header size from a SendFileProperties block is greater than zero, the client should enter NSP transfer mode. The file size from this block represents, then, the full NSP size (including the NSP header). -* In this mode, the client should immediately create the output file, write "NSP header size" bytes of padding to it, reply with a USB status response and expect additional SendFileProperties commands. No data is transferred for this very first SendFileProperties block. -* Each further SendFileProperties block will hold the filename and size for a NSP file entry, and the NSP header size value will always be set to zero. The data received from each one of these file entries must be written to the output file created during the first SendFileProperties command. The sum of all file entry sizes should be equal to the full NSP size minus the NSP header size received during the first SendFileProperties command. -* Finally, the client will receive a SendNspHeader command with the NSP header data, which should be written at the start of the output file. The value from the Command Block Size in the command header from this block should match the NSP header size received in the first SendFileProperties command. -* If the NSP header size is aligned to the endpoint max packet size, the client should also expect a ZLT packet after receiving the NSP header data from the SendNspHeader command. diff --git a/include/core/keys.h b/include/core/keys.h index f3d7780..844acc5 100644 --- a/include/core/keys.h +++ b/include/core/keys.h @@ -30,13 +30,29 @@ extern "C" { #endif +/// Loads (and derives) NCA keydata from sysmodule program memory and the Lockpick_RCM keys file. +/// Must be called (and succeed) before calling any of the functions below. bool keysLoadNcaKeyset(void); +/// Returns a pointer to the AES-128-XTS NCA header key, or NULL if keydata hasn't been loaded. const u8 *keysGetNcaHeaderKey(void); -const u8 *keysGetKeyAreaEncryptionKeySource(u8 kaek_index); -const u8 *keysGetEticketRsaKek(bool personalized); -const u8 *keysGetTitlekek(u8 key_generation); -const u8 *keysGetKeyAreaEncryptionKey(u8 key_generation, u8 kaek_index); + +/// Decrypts 'src' into 'dst' using the provided key area encryption key index and key generation values. Runtime sealed keydata from the SMC AES engine is used to achieve this. +/// Both 'dst' and 'src' buffers must have a size of at least AES_128_KEY_SIZE. +/// Returns false if an error occurs or if keydata hasn't been loaded. +bool keysDecryptNcaKeyAreaEntry(u8 kaek_index, u8 key_generation, void *dst, const void *src); + +/// Returns a pointer to an AES-128-ECB NCA key area encryption key using the provided key area encryption key index and key generation values, or NULL if keydata hasn't been loaded. +/// This data is loaded from the Lockpick_RCM keys file. +const u8 *keysGetNcaKeyAreaEncryptionKey(u8 kaek_index, u8 key_generation); + +/// Decrypts a RSA-OAEP wrapped titlekey using console-specific keydata. +/// 'rsa_wrapped_titlekey' must have a size of at least 0x100 bytes. 'out_titlekey' must have a size of at least AES_128_KEY_SIZE. +/// Returns false if an error occurs or if keydata hasn't been loaded. +bool keysDecryptRsaOaepWrappedTitleKey(const void *rsa_wrapped_titlekey, void *out_titlekey); + +/// 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); #ifdef __cplusplus } diff --git a/include/core/nca.h b/include/core/nca.h index 03519f1..815b372 100644 --- a/include/core/nca.h +++ b/include/core/nca.h @@ -77,7 +77,8 @@ typedef enum { typedef enum { NcaKeyAreaEncryptionKeyIndex_Application = 0, NcaKeyAreaEncryptionKeyIndex_Ocean = 1, - NcaKeyAreaEncryptionKeyIndex_System = 2 + NcaKeyAreaEncryptionKeyIndex_System = 2, + NcaKeyAreaEncryptionKeyIndex_Count = 3 } NcaKeyAreaEncryptionKeyIndex; /// 'NcaKeyGeneration_Current' will always point to the last known key generation value. @@ -91,7 +92,8 @@ typedef enum { NcaKeyGeneration_810_811 = 9, NcaKeyGeneration_900_901 = 10, NcaKeyGeneration_910_1201 = 11, - NcaKeyGeneration_Current = NcaKeyGeneration_910_1201 + NcaKeyGeneration_Current = NcaKeyGeneration_910_1201, + NcaKeyGeneration_Max = 32 } NcaKeyGeneration; typedef struct { @@ -453,7 +455,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. -bool 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. diff --git a/include/core/services.h b/include/core/services.h index c37433d..7d058c7 100644 --- a/include/core/services.h +++ b/include/core/services.h @@ -40,14 +40,14 @@ bool servicesInitialize(); /// Closes services previously initialized by servicesInitialize(). void servicesClose(); +/// Check if a service has been initialized using its name. +bool servicesCheckInitializedServiceByName(const char *name); + /// Checks if a service is running using its name. /// Wrapper for the "AtmosphereHasService" SM API extension from Atmosphère and Atmosphère-based CFWs. /// Perfectly safe to use under development units. Not available in older Atmosphère releases. bool servicesCheckRunningServiceByName(const char *name); -/// Check if a service has been initialized using its name. -bool servicesCheckInitializedServiceByName(const char *name); - /// Changes CPU/MEM clock rates at runtime. void servicesChangeHardwareClockRates(u32 cpu_rate, u32 mem_rate); diff --git a/source/core/cnmt.c b/source/core/cnmt.c index 5f635a3..938bdfc 100644 --- a/source/core/cnmt.c +++ b/source/core/cnmt.c @@ -79,7 +79,7 @@ bool cnmtInitializeContext(ContentMetaContext *out, NcaContext *nca_ctx) for(i = 0; i < pfs_entry_count; i++) { if ((out->cnmt_filename = pfsGetEntryNameByIndex(&(out->pfs_ctx), i)) && (cnmt_filename_len = strlen(out->cnmt_filename)) >= CNMT_MINIMUM_FILENAME_LENGTH && \ - !strncasecmp(out->cnmt_filename + cnmt_filename_len - 5, ".cnmt", 5)) break; + !strcasecmp(out->cnmt_filename + cnmt_filename_len - 5, ".cnmt")) break; } if (i >= pfs_entry_count) diff --git a/source/core/keys.c b/source/core/keys.c index f8d2748..eb07e90 100644 --- a/source/core/keys.c +++ b/source/core/keys.c @@ -25,8 +25,11 @@ #include "keys.h" #include "mem.h" #include "nca.h" +#include "rsa.h" -#define KEYS_FILE_PATH "sdmc:/switch/prod.keys" /* Location used by Lockpick_RCM. */ +#define KEYS_FILE_PATH "sdmc:/switch/prod.keys" /* Location used by Lockpick_RCM. */ + +#define ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT 0x10001 /* Type definitions. */ @@ -44,32 +47,53 @@ typedef struct { } KeysMemoryInfo; typedef struct { - ///< Needed to decrypt the NCA header using AES-128-XTS. - u8 header_kek_source[0x10]; ///< Seed for header kek. Retrieved from the .rodata segment in the FS sysmodule. - u8 header_key_source[0x20]; ///< Seed for NCA header key. Retrieved from the .data segment in the FS sysmodule. - u8 header_kek[0x10]; ///< NCA header kek. Generated from header_kek_source. - u8 header_key[0x20]; ///< NCA header key. Generated from header_kek and header_key_source. + ///< AES-128-XTS key needed to handle NCA header crypto. + u8 nca_header_kek_source[AES_128_KEY_SIZE]; ///< Retrieved from the .rodata segment in the FS sysmodule. + u8 nca_header_key_source[AES_128_KEY_SIZE * 2]; ///< Retrieved from the .data segment in the FS sysmodule. + u8 nca_header_kek_sealed[AES_128_KEY_SIZE]; ///< Generated from nca_header_kek_source. Sealed by the SMC AES engine. + u8 nca_header_key[AES_128_KEY_SIZE * 2]; ///< Generated from nca_header_kek_sealed and nca_header_key_source. - ///< Needed to derive the KAEK used to decrypt the NCA key area. - u8 key_area_key_application_source[0x10]; ///< Seed for kaek 0. Retrieved from the .rodata segment in the FS sysmodule. - u8 key_area_key_ocean_source[0x10]; ///< Seed for kaek 1. Retrieved from the .rodata segment in the FS sysmodule. - u8 key_area_key_system_source[0x10]; ///< Seed for kaek 2. Retrieved from the .rodata segment in the FS sysmodule. + ///< AES-128-ECB keys needed to handle key area crypto from NCA headers. + u8 nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_Count][AES_128_KEY_SIZE]; ///< Retrieved from the .rodata segment in the FS sysmodule. + u8 nca_kaek_sealed[NcaKeyAreaEncryptionKeyIndex_Count][NcaKeyGeneration_Max][AES_128_KEY_SIZE]; ///< Generated from nca_kaek_sources. Sealed by the SMC AES engine. + u8 nca_kaek[NcaKeyAreaEncryptionKeyIndex_Count][NcaKeyGeneration_Max][AES_128_KEY_SIZE]; ///< Unsealed key area encryption keys. Retrieved from the Lockpick_RCM keys file. - ///< Needed to decrypt the titlekey block from a ticket. Retrieved from the Lockpick_RCM keys file. - u8 eticket_rsa_kek[0x10]; ///< eTicket RSA kek (generic). - u8 eticket_rsa_kek_personalized[0x10]; ///< eTicket RSA kek (console-specific). - u8 titlekeks[0x20][0x10]; ///< Titlekey encryption keys. + ///< AES-128-CTR key needed to decrypt the console-specific eTicket RSA device key stored in PRODINFO. + u8 eticket_rsa_kek[AES_128_KEY_SIZE]; ///< eTicket RSA key encryption key (generic). Retrieved from the Lockpick_RCM keys file. + u8 eticket_rsa_kek_personalized[AES_128_KEY_SIZE]; ///< eTicket RSA key encryption key (console-specific). Retrieved from the Lockpick_RCM keys file. - ///< Needed to reencrypt the key area from NCAs with titlekey crypto removed. Retrieved from the Lockpick_RCM keys file. - u8 key_area_keys[0x20][3][0x10]; ///< Key area encryption keys. -} keysNcaKeyset; + ///< AES-128-ECB keys needed to decrypt titlekeys. + u8 ticket_common_keys[NcaKeyGeneration_Max][AES_128_KEY_SIZE]; ///< Retrieved from the Lockpick_RCM keys file. +} KeysNcaKeyset; + +/// 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[0x10]; + u8 exponent[0x100]; + u8 modulus[0x100]; + u32 public_exponent; ///< Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT. Stored using big endian byte order. + u8 padding[0x14]; + u64 device_id; + u8 ghash[0x10]; +} EticketRsaDeviceKey; + +NXDT_ASSERT(EticketRsaDeviceKey, 0x240); /* Global variables. */ -static keysNcaKeyset g_ncaKeyset = {0}; +static KeysNcaKeyset g_ncaKeyset = {0}; static bool g_ncaKeysetLoaded = false; static Mutex g_ncaKeysetMutex = 0; +static SetCalRsa2048DeviceKey g_eTicketRsaDeviceKey = {0}; + +/// Used during the RSA-OAEP titlekey decryption steps. +static const u8 g_nullHash[0x20] = { + 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, + 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55 +}; + static KeysMemoryInfo g_fsRodataMemoryInfo = { .location = { .program_id = FS_SYSMODULE_TID, @@ -80,32 +104,40 @@ static KeysMemoryInfo g_fsRodataMemoryInfo = { .key_count = 4, .keys = { { - .name = "header_kek_source", - .hash = { 0x18, 0x88, 0xCA, 0xED, 0x55, 0x51, 0xB3, 0xED, 0xE0, 0x14, 0x99, 0xE8, 0x7C, 0xE0, 0xD8, 0x68, - 0x27, 0xF8, 0x08, 0x20, 0xEF, 0xB2, 0x75, 0x92, 0x10, 0x55, 0xAA, 0x4E, 0x2A, 0xBD, 0xFF, 0xC2 }, - .size = 0x10, - .dst = g_ncaKeyset.header_kek_source + .name = "nca_header_kek_source", + .hash = { + 0x18, 0x88, 0xCA, 0xED, 0x55, 0x51, 0xB3, 0xED, 0xE0, 0x14, 0x99, 0xE8, 0x7C, 0xE0, 0xD8, 0x68, + 0x27, 0xF8, 0x08, 0x20, 0xEF, 0xB2, 0x75, 0x92, 0x10, 0x55, 0xAA, 0x4E, 0x2A, 0xBD, 0xFF, 0xC2 + }, + .size = AES_128_KEY_SIZE, + .dst = g_ncaKeyset.nca_header_kek_source }, { - .name = "key_area_key_application_source", - .hash = { 0x04, 0xAD, 0x66, 0x14, 0x3C, 0x72, 0x6B, 0x2A, 0x13, 0x9F, 0xB6, 0xB2, 0x11, 0x28, 0xB4, 0x6F, - 0x56, 0xC5, 0x53, 0xB2, 0xB3, 0x88, 0x71, 0x10, 0x30, 0x42, 0x98, 0xD8, 0xD0, 0x09, 0x2D, 0x9E }, - .size = 0x10, - .dst = g_ncaKeyset.key_area_key_application_source + .name = "nca_kaek_application_source", + .hash = { + 0x04, 0xAD, 0x66, 0x14, 0x3C, 0x72, 0x6B, 0x2A, 0x13, 0x9F, 0xB6, 0xB2, 0x11, 0x28, 0xB4, 0x6F, + 0x56, 0xC5, 0x53, 0xB2, 0xB3, 0x88, 0x71, 0x10, 0x30, 0x42, 0x98, 0xD8, 0xD0, 0x09, 0x2D, 0x9E + }, + .size = AES_128_KEY_SIZE, + .dst = g_ncaKeyset.nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_Application] }, { - .name = "key_area_key_ocean_source", - .hash = { 0xFD, 0x43, 0x40, 0x00, 0xC8, 0xFF, 0x2B, 0x26, 0xF8, 0xE9, 0xA9, 0xD2, 0xD2, 0xC1, 0x2F, 0x6B, - 0xE5, 0x77, 0x3C, 0xBB, 0x9D, 0xC8, 0x63, 0x00, 0xE1, 0xBD, 0x99, 0xF8, 0xEA, 0x33, 0xA4, 0x17 }, - .size = 0x10, - .dst = g_ncaKeyset.key_area_key_ocean_source + .name = "nca_kaek_ocean_source", + .hash = { + 0xFD, 0x43, 0x40, 0x00, 0xC8, 0xFF, 0x2B, 0x26, 0xF8, 0xE9, 0xA9, 0xD2, 0xD2, 0xC1, 0x2F, 0x6B, + 0xE5, 0x77, 0x3C, 0xBB, 0x9D, 0xC8, 0x63, 0x00, 0xE1, 0xBD, 0x99, 0xF8, 0xEA, 0x33, 0xA4, 0x17 + }, + .size = AES_128_KEY_SIZE, + .dst = g_ncaKeyset.nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_Ocean] }, { - .name = "key_area_key_system_source", - .hash = { 0x1F, 0x17, 0xB1, 0xFD, 0x51, 0xAD, 0x1C, 0x23, 0x79, 0xB5, 0x8F, 0x15, 0x2C, 0xA4, 0x91, 0x2E, - 0xC2, 0x10, 0x64, 0x41, 0xE5, 0x17, 0x22, 0xF3, 0x87, 0x00, 0xD5, 0x93, 0x7A, 0x11, 0x62, 0xF7 }, - .size = 0x10, - .dst = g_ncaKeyset.key_area_key_system_source + .name = "nca_kaek_system_source", + .hash = { + 0x1F, 0x17, 0xB1, 0xFD, 0x51, 0xAD, 0x1C, 0x23, 0x79, 0xB5, 0x8F, 0x15, 0x2C, 0xA4, 0x91, 0x2E, + 0xC2, 0x10, 0x64, 0x41, 0xE5, 0x17, 0x22, 0xF3, 0x87, 0x00, 0xD5, 0x93, 0x7A, 0x11, 0x62, 0xF7 + }, + .size = AES_128_KEY_SIZE, + .dst = g_ncaKeyset.nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_System] } } }; @@ -120,11 +152,13 @@ static KeysMemoryInfo g_fsDataMemoryInfo = { .key_count = 1, .keys = { { - .name = "header_key_source", - .hash = { 0x8F, 0x78, 0x3E, 0x46, 0x85, 0x2D, 0xF6, 0xBE, 0x0B, 0xA4, 0xE1, 0x92, 0x73, 0xC4, 0xAD, 0xBA, - 0xEE, 0x16, 0x38, 0x00, 0x43, 0xE1, 0xB8, 0xC4, 0x18, 0xC4, 0x08, 0x9A, 0x8B, 0xD6, 0x4A, 0xA6 }, - .size = 0x20, - .dst = g_ncaKeyset.header_key_source + .name = "nca_header_key_source", + .hash = { + 0x8F, 0x78, 0x3E, 0x46, 0x85, 0x2D, 0xF6, 0xBE, 0x0B, 0xA4, 0xE1, 0x92, 0x73, 0xC4, 0xAD, 0xBA, + 0xEE, 0x16, 0x38, 0x00, 0x43, 0xE1, 0xB8, 0xC4, 0x18, 0xC4, 0x08, 0x9A, 0x8B, 0xD6, 0x4A, 0xA6 + }, + .size = (AES_128_KEY_SIZE * 2), + .dst = g_ncaKeyset.nca_header_key_source } } }; @@ -132,12 +166,18 @@ static KeysMemoryInfo g_fsDataMemoryInfo = { /* Function prototypes. */ static bool keysRetrieveKeysFromProgramMemory(KeysMemoryInfo *info); + static bool keysDeriveNcaHeaderKey(void); +static bool keysDeriveSealedNcaKeyAreaEncryptionKeys(void); + static int keysGetKeyAndValueFromFile(FILE *f, char **key, char **value); static char keysConvertHexCharToBinary(char c); static bool keysParseHexKey(u8 *out, const char *key, const char *value, u32 size); static bool keysReadKeysFromFile(void); +static bool keysGetDecryptedEticketRsaDeviceKey(void); +static bool keysTestEticketRsaDeviceKey(const void *e, const void *d, const void *n); + bool keysLoadNcaKeyset(void) { mutexLock(&g_ncaKeysetMutex); @@ -145,39 +185,49 @@ bool keysLoadNcaKeyset(void) bool ret = g_ncaKeysetLoaded; if (ret) goto end; - if (!(envIsSyscallHinted(0x60) && /* svcDebugActiveProcess. */ - envIsSyscallHinted(0x63) && /* svcGetDebugEvent. */ - envIsSyscallHinted(0x65) && /* svcGetProcessList. */ - envIsSyscallHinted(0x69) && /* svcQueryDebugProcessMemory. */ - envIsSyscallHinted(0x6A))) /* svcReadDebugProcessMemory. */ - { - LOG_MSG("Debug SVC permissions not available!"); - goto end; - } - + /* Retrieve FS .rodata keys. */ if (!keysRetrieveKeysFromProgramMemory(&g_fsRodataMemoryInfo)) { LOG_MSG("Unable to retrieve keys from FS .rodata segment!"); goto end; } + /* Retrieve FS .data keys. */ if (!keysRetrieveKeysFromProgramMemory(&g_fsDataMemoryInfo)) { LOG_MSG("Unable to retrieve keys from FS .data segment!"); goto end; } + /* Derive NCA header key. */ if (!keysDeriveNcaHeaderKey()) { LOG_MSG("Unable to derive NCA header key!"); goto end; } + /* Derive sealed NCA KAEKs. */ + if (!keysDeriveSealedNcaKeyAreaEncryptionKeys()) + { + LOG_MSG("Unable to derive sealed NCA KAEKs!"); + goto end; + } + + /* Read additional keys from the keys file. */ if (!keysReadKeysFromFile()) goto end; + /* Get decrypted eTicket RSA device key. */ + if (!keysGetDecryptedEticketRsaDeviceKey()) goto end; + ret = g_ncaKeysetLoaded = true; end: + /*if (ret) + { + LOG_DATA(&g_ncaKeyset, sizeof(KeysNcaKeyset), "NCA keyset dump:"); + LOG_DATA(&g_eTicketRsaDeviceKey, sizeof(SetCalRsa2048DeviceKey), "eTicket RSA device key dump:"); + }*/ + mutexUnlock(&g_ncaKeysetMutex); return ret; @@ -185,67 +235,133 @@ end: const u8 *keysGetNcaHeaderKey(void) { - return (const u8*)(g_ncaKeyset.header_key); -} - -const u8 *keysGetKeyAreaEncryptionKeySource(u8 kaek_index) -{ - const u8 *ptr = NULL; - - switch(kaek_index) - { - case NcaKeyAreaEncryptionKeyIndex_Application: - ptr = (const u8*)(g_ncaKeyset.key_area_key_application_source); - break; - case NcaKeyAreaEncryptionKeyIndex_Ocean: - ptr = (const u8*)(g_ncaKeyset.key_area_key_ocean_source); - break; - case NcaKeyAreaEncryptionKeyIndex_System: - ptr = (const u8*)(g_ncaKeyset.key_area_key_system_source); - break; - default: - LOG_MSG("Invalid KAEK index! (0x%02X).", kaek_index); - break; - } - + mutexLock(&g_ncaKeysetMutex); + const u8 *ptr = (g_ncaKeysetLoaded ? (const u8*)(g_ncaKeyset.nca_header_key) : NULL); + mutexUnlock(&g_ncaKeysetMutex); return ptr; } -const u8 *keysGetEticketRsaKek(bool personalized) +bool keysDecryptNcaKeyAreaEntry(u8 kaek_index, u8 key_generation, void *dst, const void *src) { - return (const u8*)(personalized ? g_ncaKeyset.eticket_rsa_kek_personalized : g_ncaKeyset.eticket_rsa_kek); -} - -const u8 *keysGetTitlekek(u8 key_generation) -{ - if (key_generation > 0x20) - { - LOG_MSG("Invalid key generation value! (0x%02X).", key_generation); - return NULL; - } - + Result rc = 0; + bool success = false; u8 key_gen_val = (key_generation ? (key_generation - 1) : key_generation); - return (const u8*)(g_ncaKeyset.titlekeks[key_gen_val]); -} - -const u8 *keysGetKeyAreaEncryptionKey(u8 key_generation, u8 kaek_index) -{ - if (key_generation > 0x20) - { - LOG_MSG("Invalid key generation value! (0x%02X).", key_generation); - return NULL; - } - - if (kaek_index > NcaKeyAreaEncryptionKeyIndex_System) + if (kaek_index >= NcaKeyAreaEncryptionKeyIndex_Count) { LOG_MSG("Invalid KAEK index! (0x%02X).", kaek_index); - return NULL; + goto end; } + if (key_gen_val >= NcaKeyGeneration_Max) + { + LOG_MSG("Invalid key generation value! (0x%02X).", key_gen_val); + goto end; + } + + if (!dst || !src) + { + LOG_MSG("Invalid destination/source pointer."); + goto end; + } + + mutexLock(&g_ncaKeysetMutex); + + if (g_ncaKeysetLoaded) + { + rc = splCryptoGenerateAesKey(g_ncaKeyset.nca_kaek_sealed[kaek_index][key_gen_val], src, dst); + if (!(success = R_SUCCEEDED(rc))) LOG_MSG("splCryptoGenerateAesKey failed! (0x%08X).", rc); + } + + mutexUnlock(&g_ncaKeysetMutex); + +end: + return success; +} + +const u8 *keysGetNcaKeyAreaEncryptionKey(u8 kaek_index, u8 key_generation) +{ + const u8 *ptr = NULL; u8 key_gen_val = (key_generation ? (key_generation - 1) : key_generation); - return (const u8*)(g_ncaKeyset.key_area_keys[key_gen_val][kaek_index]); + if (kaek_index >= NcaKeyAreaEncryptionKeyIndex_Count) + { + LOG_MSG("Invalid KAEK index! (0x%02X).", kaek_index); + goto end; + } + + if (key_gen_val >= NcaKeyGeneration_Max) + { + LOG_MSG("Invalid key generation value! (0x%02X).", key_gen_val); + goto end; + } + + mutexLock(&g_ncaKeysetMutex); + + if (g_ncaKeysetLoaded) ptr = (const u8*)(g_ncaKeyset.nca_kaek[kaek_index][key_gen_val]); + + mutexUnlock(&g_ncaKeysetMutex); + +end: + return ptr; +} + +bool keysDecryptRsaOaepWrappedTitleKey(const void *rsa_wrapped_titlekey, void *out_titlekey) +{ + if (!rsa_wrapped_titlekey || !out_titlekey) + { + LOG_MSG("Invalid parameters!"); + return false; + } + + size_t out_keydata_size = 0; + u8 out_keydata[0x100] = {0}; + EticketRsaDeviceKey *eticket_rsa_key = NULL; + bool success = false; + + mutexLock(&g_ncaKeysetMutex); + + if (g_ncaKeysetLoaded) + { + /* Get eTicket RSA device key. */ + eticket_rsa_key = (EticketRsaDeviceKey*)g_eTicketRsaDeviceKey.key; + + /* Perform a RSA-OAEP unwrap operation to get the encrypted titlekey. */ + success = (rsa2048OaepDecryptAndVerify(out_keydata, sizeof(out_keydata), rsa_wrapped_titlekey, eticket_rsa_key->modulus, eticket_rsa_key->exponent, sizeof(eticket_rsa_key->exponent), \ + g_nullHash, &out_keydata_size) && out_keydata_size >= AES_128_KEY_SIZE); + if (success) + { + /* Copy RSA-OAEP unwrapped titlekey. */ + memcpy(out_titlekey, out_keydata, AES_128_KEY_SIZE); + } else { + LOG_MSG("RSA-OAEP titlekey decryption failed!"); + } + } + + mutexUnlock(&g_ncaKeysetMutex); + + return success; +} + +const u8 *keysGetTicketCommonKey(u8 key_generation) +{ + const u8 *ptr = NULL; + u8 key_gen_val = (key_generation ? (key_generation - 1) : key_generation); + + if (key_gen_val >= NcaKeyGeneration_Max) + { + LOG_MSG("Invalid key generation value! (0x%02X).", key_gen_val); + goto end; + } + + mutexLock(&g_ncaKeysetMutex); + + if (g_ncaKeysetLoaded) ptr = (const u8*)(g_ncaKeyset.ticket_common_keys[key_gen_val]); + + mutexUnlock(&g_ncaKeysetMutex); + +end: + return ptr; } static bool keysRetrieveKeysFromProgramMemory(KeysMemoryInfo *info) @@ -309,30 +425,70 @@ static bool keysDeriveNcaHeaderKey(void) { Result rc = 0; - rc = splCryptoGenerateAesKek(g_ncaKeyset.header_kek_source, 0, 0, g_ncaKeyset.header_kek); + /* Derive nca_header_kek_sealed from nca_header_kek_source. */ + rc = splCryptoGenerateAesKek(g_ncaKeyset.nca_header_kek_source, 0, 0, g_ncaKeyset.nca_header_kek_sealed); if (R_FAILED(rc)) { - LOG_MSG("splCryptoGenerateAesKek(header_kek_source) failed! (0x%08X).", rc); + LOG_MSG("splCryptoGenerateAesKek failed! (0x%08X) (nca_header_kek_sealed).", rc); return false; } - rc = splCryptoGenerateAesKey(g_ncaKeyset.header_kek, g_ncaKeyset.header_key_source + 0x00, g_ncaKeyset.header_key + 0x00); + /* Derive nca_header_key from nca_header_kek_sealed and nca_header_key_source. */ + rc = splCryptoGenerateAesKey(g_ncaKeyset.nca_header_kek_sealed, g_ncaKeyset.nca_header_key_source, g_ncaKeyset.nca_header_key); if (R_FAILED(rc)) { - LOG_MSG("splCryptoGenerateAesKey(header_key_source + 0x00) failed! (0x%08X).", rc); + LOG_MSG("splCryptoGenerateAesKey failed! (0x%08X) (nca_header_key, part 1).", rc); return false; } - rc = splCryptoGenerateAesKey(g_ncaKeyset.header_kek, g_ncaKeyset.header_key_source + 0x10, g_ncaKeyset.header_key + 0x10); + rc = splCryptoGenerateAesKey(g_ncaKeyset.nca_header_kek_sealed, g_ncaKeyset.nca_header_key_source + AES_128_KEY_SIZE, g_ncaKeyset.nca_header_key + AES_128_KEY_SIZE); if (R_FAILED(rc)) { - LOG_MSG("splCryptoGenerateAesKey(header_key_source + 0x10) failed! (0x%08X).", rc); + LOG_MSG("splCryptoGenerateAesKey failed! (0x%08X) (nca_header_key, part 2).", rc); return false; } return true; } +static bool keysDeriveSealedNcaKeyAreaEncryptionKeys(void) +{ + Result rc = 0; + u32 key_cnt = 0; + u8 highest_key_gen = 0; + bool success = false; + + for(u8 i = 0; i < NcaKeyAreaEncryptionKeyIndex_Count; i++) + { + /* Get pointer to current KAEK source. */ + const u8 *nca_kaek_source = (const u8*)(g_ncaKeyset.nca_kaek_sources[i]); + + for(u8 j = 1; j <= NcaKeyGeneration_Max; j++) + { + /* Get pointer to current sealed KAEK. */ + u8 key_gen_val = (j - 1); + u8 *nca_kaek_sealed = g_ncaKeyset.nca_kaek_sealed[i][key_gen_val]; + + /* Derive sealed KAEK using the current KAEK source and key generation. */ + rc = splCryptoGenerateAesKek(nca_kaek_source, j, 0, nca_kaek_sealed); + if (R_FAILED(rc)) + { + //LOG_MSG("splCryptoGenerateAesKek failed for KAEK index %u and key generation %u! (0x%08X).", i, (j <= 1 ? 0 : j), rc); + break; + } + + /* Update derived key count and highest key generation value. */ + key_cnt++; + if (key_gen_val > highest_key_gen) highest_key_gen = key_gen_val; + } + } + + success = (key_cnt > 0); + if (success) LOG_MSG("Derived %u sealed NCA KAEK(s) (%u key generation[s]).", key_cnt, highest_key_gen + 1); + + return success; +} + /** * Reads a line from file f and parses out the key and value from it. * The format of a line must match /^ *[A-Za-z0-9_] *[,=] *.+$/. @@ -531,54 +687,57 @@ static bool keysReadKeysFromFile(void) while(true) { ret = keysGetKeyAndValueFromFile(keys_file, &key, &value); - if (ret == 1 || ret == -2) break; /* Break from the while loop if EOF is reached or if an I/O error occurs. */ + if (ret == 1 || ret == -2) break; /* Break from the while loop if EOF is reached or if an I/O error occurs. */ /* Ignore malformed lines. */ if (ret != 0 || !key || !value) continue; - if (strlen(key) == 15 && !strcasecmp(key, "eticket_rsa_kek")) + if (!strcasecmp(key, "eticket_rsa_kek")) { if ((parse_fail = !keysParseHexKey(g_ncaKeyset.eticket_rsa_kek, key, value, sizeof(g_ncaKeyset.eticket_rsa_kek)))) break; eticket_rsa_kek_available = true; key_count++; } else - if (strlen(key) == 28 && !strcasecmp(key, "eticket_rsa_kek_personalized")) + if (!strcasecmp(key, "eticket_rsa_kek_personalized")) { /* This only appears on consoles that use the new PRODINFO key generation scheme. */ if ((parse_fail = !keysParseHexKey(g_ncaKeyset.eticket_rsa_kek_personalized, key, value, sizeof(g_ncaKeyset.eticket_rsa_kek_personalized)))) break; eticket_rsa_kek_available = true; key_count++; } else { - for(u32 i = 0; i < 0x20; i++) + for(u32 i = 0; i < NcaKeyGeneration_Max; i++) { snprintf(test_name, sizeof(test_name), "titlekek_%02x", i); - if (strlen(key) == 11 && !strcasecmp(key, test_name)) + if (!strcasecmp(key, test_name)) { - if ((parse_fail = !keysParseHexKey(g_ncaKeyset.titlekeks[i], key, value, sizeof(g_ncaKeyset.titlekeks[i])))) break; + if ((parse_fail = !keysParseHexKey(g_ncaKeyset.ticket_common_keys[i], key, value, sizeof(g_ncaKeyset.ticket_common_keys[i])))) break; key_count++; break; } snprintf(test_name, sizeof(test_name), "key_area_key_application_%02x", i); - if (strlen(key) == 27 && !strcasecmp(key, test_name)) + if (!strcasecmp(key, test_name)) { - if ((parse_fail = !keysParseHexKey(g_ncaKeyset.key_area_keys[i][0], key, value, sizeof(g_ncaKeyset.key_area_keys[i][0])))) break; + if ((parse_fail = !keysParseHexKey(g_ncaKeyset.nca_kaek[NcaKeyAreaEncryptionKeyIndex_Application][i], key, value, \ + sizeof(g_ncaKeyset.nca_kaek[NcaKeyAreaEncryptionKeyIndex_Application][i])))) break; key_count++; break; } snprintf(test_name, sizeof(test_name), "key_area_key_ocean_%02x", i); - if (strlen(key) == 21 && !strcasecmp(key, test_name)) + if (!strcasecmp(key, test_name)) { - if ((parse_fail = !keysParseHexKey(g_ncaKeyset.key_area_keys[i][1], key, value, sizeof(g_ncaKeyset.key_area_keys[i][1])))) break; + if ((parse_fail = !keysParseHexKey(g_ncaKeyset.nca_kaek[NcaKeyAreaEncryptionKeyIndex_Ocean][i], key, value, \ + sizeof(g_ncaKeyset.nca_kaek[NcaKeyAreaEncryptionKeyIndex_Ocean][i])))) break; key_count++; break; } snprintf(test_name, sizeof(test_name), "key_area_key_system_%02x", i); - if (strlen(key) == 22 && !strcasecmp(key, test_name)) + if (!strcasecmp(key, test_name)) { - if ((parse_fail = !keysParseHexKey(g_ncaKeyset.key_area_keys[i][2], key, value, sizeof(g_ncaKeyset.key_area_keys[i][2])))) break; + if ((parse_fail = !keysParseHexKey(g_ncaKeyset.nca_kaek[NcaKeyAreaEncryptionKeyIndex_System][i], key, value, \ + sizeof(g_ncaKeyset.nca_kaek[NcaKeyAreaEncryptionKeyIndex_System][i])))) break; key_count++; break; } @@ -604,3 +763,86 @@ static bool keysReadKeysFromFile(void) return true; } + +static bool keysGetDecryptedEticketRsaDeviceKey(void) +{ + Result rc = 0; + u32 public_exponent = 0; + const u8 *eticket_rsa_kek = NULL; + EticketRsaDeviceKey *eticket_rsa_key = NULL; + Aes128CtrContext eticket_aes_ctx = {0}; + + /* Get eTicket RSA device key. */ + rc = setcalGetEticketDeviceKey(&g_eTicketRsaDeviceKey); + if (R_FAILED(rc)) + { + LOG_MSG("setcalGetEticketDeviceKey failed! (0x%08X).", rc); + return false; + } + + /* Get eTicket RSA device key encryption key. */ + eticket_rsa_kek = (const u8*)(g_eTicketRsaDeviceKey.generation > 0 ? g_ncaKeyset.eticket_rsa_kek_personalized : g_ncaKeyset.eticket_rsa_kek); + + /* Decrypt eTicket RSA device key. */ + eticket_rsa_key = (EticketRsaDeviceKey*)g_eTicketRsaDeviceKey.key; + aes128CtrContextCreate(&eticket_aes_ctx, eticket_rsa_kek, eticket_rsa_key->ctr); + aes128CtrCrypt(&eticket_aes_ctx, &(eticket_rsa_key->exponent), &(eticket_rsa_key->exponent), sizeof(EticketRsaDeviceKey) - sizeof(eticket_rsa_key->ctr)); + + /* Public exponent value must be 0x10001. */ + /* It is stored using big endian byte order. */ + public_exponent = __builtin_bswap32(eticket_rsa_key->public_exponent); + if (public_exponent != ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT) + { + LOG_MSG("Invalid public exponent for decrypted eTicket RSA device key! Wrong keys? (0x%08X).", public_exponent); + return false; + } + + /* Test RSA key pair. */ + if (!keysTestEticketRsaDeviceKey(&(eticket_rsa_key->public_exponent), eticket_rsa_key->exponent, eticket_rsa_key->modulus)) + { + LOG_MSG("eTicket RSA device key test failed! Wrong keys?"); + return false; + } + + return true; +} + +static bool keysTestEticketRsaDeviceKey(const void *e, const void *d, const void *n) +{ + if (!e || !d || !n) + { + LOG_MSG("Invalid parameters!"); + return false; + } + + Result rc = 0; + u8 x[0x100] = {0}, y[0x100] = {0}, z[0x100] = {0}; + + /* 0xCAFEBABE. */ + x[0xFC] = 0xCA; + x[0xFD] = 0xFE; + x[0xFE] = 0xBA; + x[0xFF] = 0xBE; + + rc = splUserExpMod(x, n, d, 0x100, y); + if (R_FAILED(rc)) + { + LOG_MSG("splUserExpMod failed! (#1) (0x%08X).", rc); + return false; + } + + rc = splUserExpMod(y, n, e, 4, z); + if (R_FAILED(rc)) + { + LOG_MSG("splUserExpMod failed! (#2) (0x%08X).", rc); + return false; + } + + if (memcmp(x, z, 0x100) != 0) + { + LOG_MSG("Invalid RSA key pair!"); + return false; + } + + return true; +} diff --git a/source/core/mem.c b/source/core/mem.c index 88c3028..746d9f1 100644 --- a/source/core/mem.c +++ b/source/core/mem.c @@ -81,6 +81,17 @@ static bool memRetrieveProgramMemory(MemoryLocation *location, bool is_segment) bool success = true; + /* Make sure we have access to debug SVC calls. */ + if (!(envIsSyscallHinted(0x60) && /* svcDebugActiveProcess. */ + envIsSyscallHinted(0x63) && /* svcGetDebugEvent. */ + envIsSyscallHinted(0x65) && /* svcGetProcessList. */ + envIsSyscallHinted(0x69) && /* svcQueryDebugProcessMemory. */ + envIsSyscallHinted(0x6A))) /* svcReadDebugProcessMemory. */ + { + LOG_MSG("Debug SVC permissions not available!"); + return false; + } + /* Clear output MemoryLocation element. */ memFreeMemoryLocation(location); diff --git a/source/core/nca.c b/source/core/nca.c index 2b60f32..bc0a673 100644 --- a/source/core/nca.c +++ b/source/core/nca.c @@ -361,7 +361,7 @@ void ncaSetDownloadDistributionType(NcaContext *ctx) LOG_MSG("Set download distribution type to %s NCA \"%s\".", titleGetNcmContentTypeName(ctx->content_type), ctx->content_id_str); } -bool 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) { @@ -419,6 +419,12 @@ bool ncaEncryptHeader(NcaContext *ctx) const u8 *header_key = keysGetNcaHeaderKey(); Aes128XtsContext hdr_aes_ctx = {0}, nca0_fs_header_ctx = {0}; + if (!header_key) + { + LOG_MSG("Failed to retrieve NCA header key!"); + 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); @@ -547,6 +553,12 @@ static bool ncaReadDecryptedHeader(NcaContext *ctx) const u8 *header_key = keysGetNcaHeaderKey(); Aes128XtsContext hdr_aes_ctx = {0}, nca0_fs_header_ctx = {0}; + if (!header_key) + { + LOG_MSG("Failed to retrieve NCA header key!"); + return false; + } + /* Read NCA header. */ if (!ncaReadContentFile(ctx, &(ctx->encrypted_header), sizeof(NcaHeader), 0)) { @@ -627,9 +639,8 @@ static bool ncaDecryptKeyArea(NcaContext *ctx) return false; } - Result rc = 0; - const u8 *kaek_src = NULL, null_key[AES_128_KEY_SIZE] = {0}; - u8 key_count = (ctx->format_version == NcaVersion_Nca0 ? 2 : 4), aes_kek[AES_128_KEY_SIZE] = {0}; + const u8 null_key[AES_128_KEY_SIZE] = {0}; + u8 key_count = (ctx->format_version == NcaVersion_Nca0 ? 2 : 4); /* Check if we're dealing with a NCA0 with a plaintext key area. */ if (ncaIsVersion0KeyAreaEncrypted(ctx)) @@ -638,22 +649,6 @@ static bool ncaDecryptKeyArea(NcaContext *ctx) return true; } - /* Get KAEK source for this KAEK index. */ - kaek_src = keysGetKeyAreaEncryptionKeySource(ctx->header.kaek_index); - if (!kaek_src) - { - LOG_MSG("Unable to retrieve KAEK source for index 0x%02X!", ctx->header.kaek_index); - return false; - } - - /* Generate AES key encryption key. */ - rc = splCryptoGenerateAesKek(kaek_src, ctx->key_generation, 0, aes_kek); - if (R_FAILED(rc)) - { - LOG_MSG("splCryptoGenerateAesKek failed! (0x%08X).", rc); - return false; - } - /* Clear decrypted key area. */ memset(&(ctx->decrypted_key_area), 0, NCA_USED_KEY_AREA_SIZE); @@ -667,10 +662,9 @@ static bool ncaDecryptKeyArea(NcaContext *ctx) if (!memcmp(src_key, null_key, AES_128_KEY_SIZE)) continue; /* Decrypt current key area entry. */ - rc = splCryptoGenerateAesKey(aes_kek, src_key, dst_key); - if (R_FAILED(rc)) + if (!keysDecryptNcaKeyAreaEntry(ctx->header.kaek_index, ctx->key_generation, dst_key, src_key)) { - LOG_MSG("splCryptoGenerateAesKey failed to decrypt NCA key area entry #%u! (0x%08X).", i, rc); + LOG_MSG("Failed to decrypt NCA key area entry #%u!", i); return false; } } @@ -698,10 +692,10 @@ static bool ncaEncryptKeyArea(NcaContext *ctx) } /* Get KAEK for these key generation and KAEK index values. */ - kaek = keysGetKeyAreaEncryptionKey(ctx->key_generation, ctx->header.kaek_index); + kaek = keysGetNcaKeyAreaEncryptionKey(ctx->header.kaek_index, ctx->key_generation); if (!kaek) { - LOG_MSG("Unable to retrieve KAEK for key generation 0x%02X and KAEK index 0x%02X!", ctx->key_generation, ctx->header.kaek_index); + LOG_MSG("Unable to retrieve KAEK for KAEK index 0x%02X and key generation 0x%02X!", ctx->header.kaek_index, ctx->key_generation); return false; } diff --git a/source/core/nxdt_utils.c b/source/core/nxdt_utils.c index 5178ead..b91af26 100644 --- a/source/core/nxdt_utils.c +++ b/source/core/nxdt_utils.c @@ -54,7 +54,8 @@ static int g_nxLinkSocketFd = -1; static const char *g_sizeSuffixes[] = { "B", "KiB", "MiB", "GiB" }; static const u32 g_sizeSuffixesCount = MAX_ELEMENTS(g_sizeSuffixes); -static const char *g_illegalFileSystemChars = "\\/:*?\"<>|^"; +static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|"; +static const size_t g_illegalFileSystemCharsLength = (MAX_ELEMENTS(g_illegalFileSystemChars) - 1); /* Function prototypes. */ @@ -103,10 +104,6 @@ bool utilsInitializeResources(void) u32 hos_version = hosversionGet(); LOG_MSG("Horizon OS version: %u.%u.%u.", HOSVER_MAJOR(hos_version), HOSVER_MINOR(hos_version), HOSVER_MICRO(hos_version)); - /* Retrieve custom firmware type. */ - _utilsGetCustomFirmwareType(); - LOG_MSG("Detected %s CFW.", (g_customFirmwareType == UtilsCustomFirmwareType_Atmosphere ? "Atmosphère" : (g_customFirmwareType == UtilsCustomFirmwareType_SXOS ? "SX OS" : "ReiNX"))); - /* Initialize needed services. */ if (!servicesInitialize()) { @@ -114,6 +111,10 @@ bool utilsInitializeResources(void) goto end; } + /* Retrieve custom firmware type. */ + _utilsGetCustomFirmwareType(); + LOG_MSG("Detected %s CFW.", (g_customFirmwareType == UtilsCustomFirmwareType_Atmosphere ? "Atmosphère" : (g_customFirmwareType == UtilsCustomFirmwareType_SXOS ? "SX OS" : "ReiNX"))); + /* Check if we're not running under a development unit. */ if (!_utilsIsDevelopmentUnit()) goto end; LOG_MSG("Running under %s unit.", g_isDevUnit ? "development" : "retail"); @@ -421,7 +422,7 @@ void utilsReplaceIllegalCharacters(char *str, bool ascii_only) for(size_t i = 0; i < strsize; i++) { - if (memchr(g_illegalFileSystemChars, str[i], sizeof(g_illegalFileSystemChars) - 1) || str[i] < 0x20 || (!ascii_only && str[i] == 0x7F) || (ascii_only && str[i] >= 0x7F)) str[i] = '_'; + if (memchr(g_illegalFileSystemChars, str[i], g_illegalFileSystemCharsLength) || str[i] < 0x20 || (!ascii_only && str[i] == 0x7F) || (ascii_only && str[i] >= 0x7F)) str[i] = '_'; } } @@ -646,9 +647,9 @@ FsStorage *utilsGetEmmcBisSystemPartitionStorage(void) void utilsOverclockSystem(bool overclock) { - u32 cpuClkRate = ((overclock ? CPU_CLKRT_OVERCLOCKED : CPU_CLKRT_NORMAL) * 1000000); - u32 memClkRate = ((overclock ? MEM_CLKRT_OVERCLOCKED : MEM_CLKRT_NORMAL) * 1000000); - servicesChangeHardwareClockRates(cpuClkRate, memClkRate); + u32 cpu_rate = ((overclock ? CPU_CLKRT_OVERCLOCKED : CPU_CLKRT_NORMAL) * 1000000); + u32 mem_rate = ((overclock ? MEM_CLKRT_OVERCLOCKED : MEM_CLKRT_NORMAL) * 1000000); + servicesChangeHardwareClockRates(cpu_rate, mem_rate); } static void _utilsGetCustomFirmwareType(void) @@ -727,7 +728,7 @@ static void utilsOverclockSystemAppletHook(AppletHookType hook, void *param) if (hook != AppletHookType_OnOperationMode && hook != AppletHookType_OnPerformanceMode) return; /* TO DO: read config here to actually know the value to use with utilsOverclockSystem. */ - utilsOverclockSystem(false); + utilsOverclockSystem(true); } static void utilsPrintConsoleError(void) diff --git a/source/core/services.c b/source/core/services.c index 9cafddd..daaec8c 100644 --- a/source/core/services.c +++ b/source/core/services.c @@ -39,8 +39,10 @@ typedef struct { /* Function prototypes. */ +static bool _servicesCheckInitializedServiceByName(const char *name, bool lock); + static Result servicesAtmosphereHasService(bool *out, SmServiceName name); -static bool servicesGetExosphereApiVersion(u32 *out); +static Result servicesGetExosphereApiVersion(u32 *out); static Result servicesNifmUserInitialize(void); static bool servicesClkGetServiceType(void *arg); @@ -71,6 +73,8 @@ static ClkrstSession g_clkrstCpuSession = {0}, g_clkrstMemSession = {0}; static Mutex g_servicesMutex = 0; +static u32 g_atmosphereVersion = 0; + /* Atmosphère-related constants. */ static const u32 g_smAtmosphereHasService = 65100; static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000; @@ -87,8 +91,8 @@ bool servicesInitialize(void) { ServiceInfo *service_info = &(g_serviceInfo[i]); - /* Check if this service has been already initialized or if it actually has a valid initialize function. */ - if (service_info->initialized || service_info->init_func == NULL) continue; + /* Check if this service has been already initialized. */ + if (service_info->initialized) continue; /* Check if this service depends on a condition function. */ if (service_info->cond_func != NULL) @@ -98,16 +102,19 @@ bool servicesInitialize(void) if (!service_info->cond_func(service_info)) continue; } + /* Check if this service actually has a valid initialization function. */ + if (service_info->init_func == NULL) continue; + /* Initialize service. */ rc = service_info->init_func(); if (R_FAILED(rc)) { - LOG_MSG("Failed to initialize %s service! (0x%08X).", service_info->name, rc); + LOG_MSG("Failed to initialize \"%s\" service! (0x%08X).", service_info->name, rc); ret = false; break; } - /* Update initialized flag. */ + /* Update flag. */ service_info->initialized = true; } @@ -130,37 +137,77 @@ void servicesClose(void) /* Close service. */ service_info->close_func(); - /* Update initialized flag. */ + /* Update flag. */ service_info->initialized = false; } mutexUnlock(&g_servicesMutex); } +bool servicesCheckInitializedServiceByName(const char *name) +{ + return _servicesCheckInitializedServiceByName(name, true); +} + bool servicesCheckRunningServiceByName(const char *name) { - if (!name || !*name) - { - LOG_MSG("Invalid parameters!"); - return false; - } - Result rc = 0; bool out = false; - SmServiceName service_name = smEncodeName(name); + SmServiceName service_name = {0}; + + mutexLock(&g_servicesMutex); + + if (!name || !*name || !_servicesCheckInitializedServiceByName("spl:", false)) + { + LOG_MSG("Invalid parameters!"); + goto end; + } + + service_name = smEncodeName(name); rc = servicesAtmosphereHasService(&out, service_name); if (R_FAILED(rc)) LOG_MSG("servicesAtmosphereHasService failed for \"%s\"! (0x%08X).", name, rc); +end: + mutexUnlock(&g_servicesMutex); + return out; } -bool servicesCheckInitializedServiceByName(const char *name) +void servicesChangeHardwareClockRates(u32 cpu_rate, u32 mem_rate) { + Result rc1 = 0, rc2 = 0; + mutexLock(&g_servicesMutex); + if ((g_clkSvcUsePcv && !_servicesCheckInitializedServiceByName("pcv", false)) || (!g_clkSvcUsePcv && !_servicesCheckInitializedServiceByName("clkrst", false))) + { + LOG_MSG("Error: clock service uninitialized."); + goto end; + } + + if (g_clkSvcUsePcv) + { + rc1 = pcvSetClockRate(PcvModule_CpuBus, cpu_rate); + rc2 = pcvSetClockRate(PcvModule_EMC, mem_rate); + } else { + rc1 = clkrstSetClockRate(&g_clkrstCpuSession, cpu_rate); + rc2 = clkrstSetClockRate(&g_clkrstMemSession, mem_rate); + } + + if (R_FAILED(rc1)) LOG_MSG("%sSetClockRate failed! (0x%08X) (CPU).", (g_clkSvcUsePcv ? "pcv" : "clkrst"), rc1); + if (R_FAILED(rc2)) LOG_MSG("%sSetClockRate failed! (0x%08X) (MEM).", (g_clkSvcUsePcv ? "pcv" : "clkrst"), rc2); + +end: + mutexUnlock(&g_servicesMutex); +} + +static bool _servicesCheckInitializedServiceByName(const char *name, bool lock) +{ bool ret = false; + if (lock) mutexLock(&g_servicesMutex); + if (!name || !*name) goto end; for(u32 i = 0; i < g_serviceInfoCount; i++) @@ -175,70 +222,57 @@ bool servicesCheckInitializedServiceByName(const char *name) } end: - mutexUnlock(&g_servicesMutex); + if (lock) mutexUnlock(&g_servicesMutex); return ret; } -void servicesChangeHardwareClockRates(u32 cpu_rate, u32 mem_rate) -{ - mutexLock(&g_servicesMutex); - - if (g_clkSvcUsePcv) - { - pcvSetClockRate(PcvModule_CpuBus, cpu_rate); - pcvSetClockRate(PcvModule_EMC, mem_rate); - } else { - clkrstSetClockRate(&g_clkrstCpuSession, cpu_rate); - clkrstSetClockRate(&g_clkrstMemSession, mem_rate); - } - - mutexUnlock(&g_servicesMutex); -} - /* SM API extension available in Atmosphère and Atmosphère-based CFWs. */ static Result servicesAtmosphereHasService(bool *out, SmServiceName name) { + if (!out || !name.name[0]) return MAKERESULT(Module_Libnx, LibnxError_BadInput); + u8 tmp = 0; Result rc = 0; - u32 version = 0; + + /* Get Exosphère API version. */ + if (!g_atmosphereVersion) + { + rc = servicesGetExosphereApiVersion(&g_atmosphereVersion); + if (R_FAILED(rc)) LOG_MSG("servicesGetExosphereApiVersion failed! (0x%08X).", rc); + } /* Check if service is running. */ /* Dispatch IPC request using CMIF or TIPC serialization depending on our current environment. */ - if (hosversionAtLeast(12, 0, 0) || (servicesGetExosphereApiVersion(&version) && version >= g_atmosphereTipcVersion)) + if (hosversionAtLeast(12, 0, 0) || g_atmosphereVersion >= g_atmosphereTipcVersion) { rc = tipcDispatchInOut(smGetServiceSessionTipc(), g_smAtmosphereHasService, name, tmp); } else { rc = serviceDispatchInOut(smGetServiceSession(), g_smAtmosphereHasService, name, tmp); } - if (R_SUCCEEDED(rc) && out) *out = tmp; + if (R_SUCCEEDED(rc)) *out = (tmp != 0); return rc; } -/* SMC API extension available in Atmosphère and Atmosphère-based CFWs. */ -static bool servicesGetExosphereApiVersion(u32 *out) +/* SMC config item available in Atmosphère and Atmosphère-based CFWs. */ +static Result servicesGetExosphereApiVersion(u32 *out) { - if (!out) - { - LOG_MSG("Invalid parameters!"); - return false; - } + if (!out) return MAKERESULT(Module_Libnx, LibnxError_BadInput); Result rc = 0; - u64 version = 0; + u64 cfg = 0; + u32 version = 0; - rc = splGetConfig(SplConfigItem_ExosphereApiVersion, &version); - if (R_FAILED(rc)) + rc = splGetConfig(SplConfigItem_ExosphereApiVersion, &cfg); + if (R_SUCCEEDED(rc)) { - LOG_MSG("splGetConfig failed! (0x%08X).", rc); - return false; + *out = version = (u32)((cfg >> 40) & 0xFFFFFF); + LOG_MSG("Exosphère API version: %u.%u.%u.", HOSVER_MAJOR(version), HOSVER_MINOR(version), HOSVER_MICRO(version)); } - *out = (u32)((version >> 40) & 0xFFFFFF); - - return true; + return rc; } static Result servicesNifmUserInitialize(void) @@ -252,7 +286,11 @@ static Result servicesClkrstInitialize(void) /* Open clkrst service handle. */ rc = clkrstInitialize(); - if (R_FAILED(rc)) return rc; + if (R_FAILED(rc)) + { + LOG_MSG("clkrstInitialize failed! (0x%08X).", rc); + return rc; + } /* Initialize CPU and MEM clkrst sessions. */ memset(&g_clkrstCpuSession, 0, sizeof(ClkrstSession)); @@ -261,6 +299,7 @@ static Result servicesClkrstInitialize(void) rc = clkrstOpenSession(&g_clkrstCpuSession, PcvModuleId_CpuBus, 3); if (R_FAILED(rc)) { + LOG_MSG("clkrstOpenSession failed! (0x%08X) (CPU).", rc); clkrstExit(); return rc; } @@ -268,6 +307,7 @@ static Result servicesClkrstInitialize(void) rc = clkrstOpenSession(&g_clkrstMemSession, PcvModuleId_EMC, 3); if (R_FAILED(rc)) { + LOG_MSG("clkrstOpenSession failed! (0x%08X) (MEM).", rc); clkrstCloseSession(&g_clkrstCpuSession); clkrstExit(); } @@ -298,6 +338,7 @@ static bool servicesClkGetServiceType(void *arg) /* Fill service info. */ sprintf(info->name, "%s", (g_clkSvcUsePcv ? "pcv" : "clkrst")); + info->cond_func = NULL; info->init_func = (g_clkSvcUsePcv ? &pcvInitialize : &servicesClkrstInitialize); info->close_func = (g_clkSvcUsePcv ? &pcvExit : &servicesClkrstExit); @@ -312,5 +353,5 @@ static bool servicesSplCryptoCheckAvailability(void *arg) if (strcmp(info->name, "spl:mig") != 0 || info->init_func == NULL || info->close_func == NULL) return false; /* Check if spl:mig is available (sysver equal to or greater than 4.0.0). */ - return !hosversionBefore(4, 0, 0); + return hosversionAtLeast(4, 0, 0); } diff --git a/source/core/tik.c b/source/core/tik.c index deaaab8..518f131 100644 --- a/source/core/tik.c +++ b/source/core/tik.c @@ -26,7 +26,6 @@ #include "save.h" #include "es.h" #include "keys.h" -#include "rsa.h" #include "gamecard.h" #include "mem.h" #include "aes.h" @@ -37,8 +36,6 @@ #define TIK_LIST_STORAGE_PATH "/ticket_list.bin" #define TIK_DB_STORAGE_PATH "/ticket.bin" -#define ETICKET_DEVKEY_PUBLIC_EXPONENT 0x10001 - #define ES_CTRKEY_ENTRY_ALIGNMENT 0x8 /* Type definitions. */ @@ -57,48 +54,24 @@ NXDT_ASSERT(TikListEntry, 0x20); /// 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. + u32 idx; ///< Entry index. + u8 key[AES_128_KEY_SIZE]; ///< AES-128-CTR key. + u8 ctr[AES_128_KEY_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out. } TikEsCtrKeyEntry9x; NXDT_ASSERT(TikEsCtrKeyEntry9x, 0x24); /// 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). + u32 idx1; ///< Always set to 0 (first entry). + u8 ctrdata[AES_128_KEY_SIZE * 2]; + u32 idx2; ///< Always set to 1 (second entry). } TikEsCtrKeyPattern9x; NXDT_ASSERT(TikEsCtrKeyPattern9x, 0x28); -/// Used to parse the eTicket device key retrieved from PRODINFO via setcalGetEticketDeviceKey(). -/// Everything after the AES CTR is encrypted. -typedef struct { - u8 ctr[0x10]; - u8 exponent[0x100]; - u8 modulus[0x100]; - u32 public_exponent; ///< Must match ETICKET_DEVKEY_PUBLIC_EXPONENT. Stored using big endian byte order. - u8 padding[0x14]; - u64 device_id; - u8 ghash[0x10]; -} TikEticketDeviceKeyData; - -NXDT_ASSERT(TikEticketDeviceKeyData, 0x240); - /* Global variables. */ -static SetCalRsa2048DeviceKey g_eTicketDeviceKey = {0}; -static bool g_eTicketDeviceKeyRetrieved = false; -static Mutex g_eTicketDeviceKeyMutex = 0; - -/// Used during the RSA-OAEP titlekey decryption stage. -static const u8 g_nullHash[0x20] = { - 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, - 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55 -}; - static const char *g_tikTitleKeyTypeStrings[] = { [TikTitleKeyType_Common] = "common", [TikTitleKeyType_Personalized] = "personalized" @@ -116,8 +89,8 @@ static MemoryLocation g_esMemoryLocation = { static bool tikRetrieveTicketFromGameCardByRightsId(Ticket *dst, const FsRightsId *id); static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRightsId *id); -static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik); -static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_generation); +static bool tikGetEncryptedTitleKeyFromTicket(Ticket *tik); +static bool tikGetDecryptedTitleKey(void *dst, const void *src, u8 key_generation); static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out); static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, bool personalized); @@ -127,9 +100,6 @@ static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size); -static bool tikRetrieveEticketDeviceKey(void); -static bool tikTestKeyPairFromEticketDeviceKey(const void *e, const void *d, const void *n); - bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gamecard) { if (!dst || !id) @@ -150,6 +120,7 @@ bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gam /* Clear output ticket. */ memset(dst, 0, sizeof(Ticket)); + /* Retrieve ticket data. */ bool tik_retrieved = (use_gamecard ? tikRetrieveTicketFromGameCardByRightsId(dst, id) : tikRetrieveTicketFromEsSaveDataByRightsId(dst, id)); if (!tik_retrieved) { @@ -157,21 +128,18 @@ bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gam return false; } - mutexLock(&g_eTicketDeviceKeyMutex); - bool titlekey_retrieved = tikGetTitleKekEncryptedTitleKeyFromTicket(dst); - mutexUnlock(&g_eTicketDeviceKeyMutex); - - if (!titlekey_retrieved) + /* Get encrypted titlekey from ticket. */ + if (!tikGetEncryptedTitleKeyFromTicket(dst)) { - LOG_MSG("Unable to retrieve titlekey from ticket!"); + LOG_MSG("Unable to retrieve encrypted titlekey from ticket!"); return false; } /* Even though tickets do have a proper key_generation field, we'll just retrieve it from the rights_id field. */ /* Old custom tools used to wipe the key_generation field or save its value to a different offset. */ - if (!tikGetTitleKekDecryptedTitleKey(dst->dec_titlekey, dst->enc_titlekey, id->c[0xF])) + if (!tikGetDecryptedTitleKey(dst->dec_titlekey, dst->enc_titlekey, id->c[0xF])) { - LOG_MSG("Unable to perform titlekek decryption!"); + LOG_MSG("Unable to decrypt titlekey!"); return false; } @@ -230,7 +198,7 @@ bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_c memset(tik_common_block->issuer, 0, sizeof(tik_common_block->issuer)); sprintf(tik_common_block->issuer, "%s", cert_chain_issuer); - /* Wipe the titlekey block and copy the titlekek-encrypted titlekey to it. */ + /* Wipe the titlekey block and copy the encrypted titlekey to it. */ memset(tik_common_block->titlekey_block, 0, sizeof(tik_common_block->titlekey_block)); memcpy(tik_common_block->titlekey_block, tik->enc_titlekey, 0x10); @@ -371,7 +339,7 @@ end: return success; } -static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik) +static bool tikGetEncryptedTitleKeyFromTicket(Ticket *tik) { TikCommonBlock *tik_common_block = NULL; @@ -381,38 +349,16 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik) return false; } - size_t out_keydata_size = 0; - u8 out_keydata[0x100] = {0}; - - TikEticketDeviceKeyData *eticket_devkey = NULL; - switch(tik_common_block->titlekey_type) { case TikTitleKeyType_Common: - /* No console-specific crypto used. Copy titlekek-encrypted titlekey right away. */ + /* No console-specific crypto used. Copy encrypted titlekey right away. */ memcpy(tik->enc_titlekey, tik_common_block->titlekey_block, 0x10); break; case TikTitleKeyType_Personalized: - /* Retrieve eTicket device key. */ - if (!tikRetrieveEticketDeviceKey()) - { - LOG_MSG("Unable to retrieve eTicket device key!"); - return false; - } - - eticket_devkey = (TikEticketDeviceKeyData*)g_eTicketDeviceKey.key; - - /* Perform a RSA-OAEP decrypt operation to get the titlekek-encrypted titlekey. */ - if (!rsa2048OaepDecryptAndVerify(out_keydata, 0x100, tik_common_block->titlekey_block, eticket_devkey->modulus, eticket_devkey->exponent, 0x100, g_nullHash, &out_keydata_size) || \ - out_keydata_size < 0x10) - { - LOG_MSG("RSA-OAEP titlekey decryption failed!"); - return false; - } - - /* Copy titlekek-encrypted titlekey. */ - memcpy(tik->enc_titlekey, out_keydata, 0x10); - + /* The titlekey block is encrypted using RSA-OAEP with a console-specific RSA key. */ + /* We have to perform a RSA-OAEP unwrap operation to get the encrypted titlekey. */ + if (!keysDecryptRsaOaepWrappedTitleKey(tik_common_block->titlekey_block, tik->enc_titlekey)) return false; break; default: LOG_MSG("Invalid titlekey type value! (0x%02X).", tik_common_block->titlekey_type); @@ -422,7 +368,7 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik) return true; } -static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_generation) +static bool tikGetDecryptedTitleKey(void *dst, const void *src, u8 key_generation) { if (!dst || !src) { @@ -430,17 +376,17 @@ static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_g return false; } - const u8 *titlekek = NULL; + const u8 *ticket_common_key = NULL; Aes128Context titlekey_aes_ctx = {0}; - titlekek = keysGetTitlekek(key_generation); - if (!titlekek) + ticket_common_key = keysGetTicketCommonKey(key_generation); + if (!ticket_common_key) { - LOG_MSG("Unable to retrieve titlekek for key generation 0x%02X!", key_generation); + LOG_MSG("Unable to retrieve ticket common key for key generation 0x%02X!", key_generation); return false; } - aes128ContextCreate(&titlekey_aes_ctx, titlekek, false); + aes128ContextCreate(&titlekey_aes_ctx, ticket_common_key, false); aes128DecryptBlock(&titlekey_aes_ctx, dst, src); return true; @@ -626,7 +572,7 @@ static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u 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}; + u8 null_ctr[AES_128_KEY_SIZE] = {0}, ctr[AES_128_KEY_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0}; bool is_volatile = false, success = false; @@ -765,85 +711,3 @@ static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 return true; } - -static bool tikRetrieveEticketDeviceKey(void) -{ - if (g_eTicketDeviceKeyRetrieved) return true; - - Result rc = 0; - u32 public_exponent = 0; - TikEticketDeviceKeyData *eticket_devkey = NULL; - Aes128CtrContext eticket_aes_ctx = {0}; - - rc = setcalGetEticketDeviceKey(&g_eTicketDeviceKey); - if (R_FAILED(rc)) - { - LOG_MSG("setcalGetEticketDeviceKey failed! (0x%08X).", rc); - return false; - } - - /* Decrypt eTicket RSA 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); - - /* Public exponent value must be 0x10001. */ - /* It is stored using big endian byte order. */ - public_exponent = __builtin_bswap32(eticket_devkey->public_exponent); - if (public_exponent != ETICKET_DEVKEY_PUBLIC_EXPONENT) - { - LOG_MSG("Invalid public RSA exponent for eTicket device key! Wrong keys? (0x%08X).", public_exponent); - return false; - } - - /* Test RSA key pair. */ - if (!tikTestKeyPairFromEticketDeviceKey(&(eticket_devkey->public_exponent), eticket_devkey->exponent, eticket_devkey->modulus)) - { - LOG_MSG("RSA key pair test failed! Wrong keys?"); - return false; - } - - g_eTicketDeviceKeyRetrieved = true; - - return true; -} - -static bool tikTestKeyPairFromEticketDeviceKey(const void *e, const void *d, const void *n) -{ - if (!e || !d || !n) - { - LOG_MSG("Invalid parameters!"); - return false; - } - - Result rc = 0; - u8 x[0x100] = {0}, y[0x100] = {0}, z[0x100] = {0}; - - /* 0xCAFEBABE. */ - x[0xFC] = 0xCA; - x[0xFD] = 0xFE; - x[0xFE] = 0xBA; - x[0xFF] = 0xBE; - - rc = splUserExpMod(x, n, d, 0x100, y); - if (R_FAILED(rc)) - { - LOG_MSG("splUserExpMod failed! (#1) (0x%08X).", rc); - return false; - } - - rc = splUserExpMod(y, n, e, 4, z); - if (R_FAILED(rc)) - { - LOG_MSG("splUserExpMod failed! (#2) (0x%08X).", rc); - return false; - } - - if (memcmp(x, z, 0x100) != 0) - { - LOG_MSG("Invalid RSA key pair!"); - return false; - } - - return true; -} diff --git a/source/core/usb.c b/source/core/usb.c index b028bfb..1db207c 100644 --- a/source/core/usb.c +++ b/source/core/usb.c @@ -45,8 +45,8 @@ #define USB_HS_EP_MAX_PACKET_SIZE 0x200 /* 512 bytes. */ #define USB_SS_BCD_REVISION 0x0300 /* USB 3.0. */ -#define USB_SS_EP_MAX_PACKET_SIZE 0x400 /* 1024 bytes. */ #define USB_SS_EP0_MAX_PACKET_SIZE 9 /* 512 bytes (1 << 9). */ +#define USB_SS_EP_MAX_PACKET_SIZE 0x400 /* 1024 bytes. */ #define USB_BOS_SIZE 0x16 /* usb_bos_descriptor + usb_2_0_extension_descriptor + usb_ss_usb_device_capability_descriptor. */ @@ -1329,6 +1329,7 @@ static bool usbTransferData(void *buf, u64 size, UsbDsEndpoint *endpoint) if (g_usbSessionStarted) ueventSignal(&g_usbTimeoutEvent); if (!thread_exit) LOG_MSG("eventWait failed! (0x%08X) (URB ID %u).", rc, urb_id); + return false; }