1
0
Fork 0
mirror of https://github.com/DarkMatterCore/nxdumptool.git synced 2024-11-26 04:02:11 +00:00

Small code refactor.

* Refactored keydata handling.

* Sealed NCA KAEKs are now generated at startup, and NCA key area entries are now decrypted by keysDecryptNcaKeyAreaEntry(), reducing the number of calls to spl functions.

* The eTicket RSA device key is now retrieved and decrypted at startup. RSA-OAEP wrapped titlekeys are now decrypted by keysDecryptRsaOaepWrappedTitleKey().

* Renamed titlekek -> ticket common key throughout the codebase.

* Added NcaKeyAreaEncryptionKeyIndex_Count and NcaKeyGeneration_Max enum values to nca.h.

* Proper usage of strcasecmp() in some functions.

* Moved syscall hint checks from keys.c to mem.c.

* Define illegal FS characters as an array rather than a char pointer.

* Services are now initialized before the CFW type checks.

* Fixed pcv/clkrst service initialization.

* Implemented additional thread safety and logfile output to service functions.

* Slightly tweaked running service checks.

* Added proper Markdown documentation for the USB ABI.
This commit is contained in:
Pablo Curiel 2021-05-11 02:00:33 -04:00
parent 455b86179b
commit 4c0c7d2c56
15 changed files with 773 additions and 545 deletions

View file

@ -279,7 +279,7 @@ static void nspDump(TitleInfo *title_info, u64 free_space)
// remove titlekey crypto // remove titlekey crypto
// has no effect if this nca doesn't use 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"); consolePrint("nca remove titlekey crypto failed\n");
goto end; goto end;

View file

@ -269,7 +269,7 @@ static void dump_thread_func(void *arg)
// remove titlekey crypto // remove titlekey crypto
// has no effect if this nca doesn't use 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"); consolePrint("nca remove titlekey crypto failed\n");
goto end; goto end;

218
host/README.md Normal file
View file

@ -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).

View file

@ -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.

View file

@ -30,13 +30,29 @@
extern "C" { extern "C" {
#endif #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); 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 *keysGetNcaHeaderKey(void);
const u8 *keysGetKeyAreaEncryptionKeySource(u8 kaek_index);
const u8 *keysGetEticketRsaKek(bool personalized); /// 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.
const u8 *keysGetTitlekek(u8 key_generation); /// Both 'dst' and 'src' buffers must have a size of at least AES_128_KEY_SIZE.
const u8 *keysGetKeyAreaEncryptionKey(u8 key_generation, u8 kaek_index); /// 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 #ifdef __cplusplus
} }

View file

@ -77,7 +77,8 @@ typedef enum {
typedef enum { typedef enum {
NcaKeyAreaEncryptionKeyIndex_Application = 0, NcaKeyAreaEncryptionKeyIndex_Application = 0,
NcaKeyAreaEncryptionKeyIndex_Ocean = 1, NcaKeyAreaEncryptionKeyIndex_Ocean = 1,
NcaKeyAreaEncryptionKeyIndex_System = 2 NcaKeyAreaEncryptionKeyIndex_System = 2,
NcaKeyAreaEncryptionKeyIndex_Count = 3
} NcaKeyAreaEncryptionKeyIndex; } NcaKeyAreaEncryptionKeyIndex;
/// 'NcaKeyGeneration_Current' will always point to the last known key generation value. /// 'NcaKeyGeneration_Current' will always point to the last known key generation value.
@ -91,7 +92,8 @@ typedef enum {
NcaKeyGeneration_810_811 = 9, NcaKeyGeneration_810_811 = 9,
NcaKeyGeneration_900_901 = 10, NcaKeyGeneration_900_901 = 10,
NcaKeyGeneration_910_1201 = 11, NcaKeyGeneration_910_1201 = 11,
NcaKeyGeneration_Current = NcaKeyGeneration_910_1201 NcaKeyGeneration_Current = NcaKeyGeneration_910_1201,
NcaKeyGeneration_Max = 32
} NcaKeyGeneration; } NcaKeyGeneration;
typedef struct { typedef struct {
@ -453,7 +455,7 @@ void ncaWriteHierarchicalIntegrityPatchToMemoryBuffer(NcaContext *ctx, NcaHierar
void ncaSetDownloadDistributionType(NcaContext *ctx); 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. /// 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. /// 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. /// The 'encrypted_header' member from the NCA context and its underlying NCA FS section contexts is updated by this function.

View file

@ -40,14 +40,14 @@ bool servicesInitialize();
/// Closes services previously initialized by servicesInitialize(). /// Closes services previously initialized by servicesInitialize().
void servicesClose(); 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. /// Checks if a service is running using its name.
/// Wrapper for the "AtmosphereHasService" SM API extension from Atmosphère and Atmosphère-based CFWs. /// 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. /// Perfectly safe to use under development units. Not available in older Atmosphère releases.
bool servicesCheckRunningServiceByName(const char *name); 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. /// Changes CPU/MEM clock rates at runtime.
void servicesChangeHardwareClockRates(u32 cpu_rate, u32 mem_rate); void servicesChangeHardwareClockRates(u32 cpu_rate, u32 mem_rate);

View file

@ -79,7 +79,7 @@ bool cnmtInitializeContext(ContentMetaContext *out, NcaContext *nca_ctx)
for(i = 0; i < pfs_entry_count; i++) 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 && \ 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) if (i >= pfs_entry_count)

View file

@ -25,8 +25,11 @@
#include "keys.h" #include "keys.h"
#include "mem.h" #include "mem.h"
#include "nca.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. */ /* Type definitions. */
@ -44,32 +47,53 @@ typedef struct {
} KeysMemoryInfo; } KeysMemoryInfo;
typedef struct { typedef struct {
///< Needed to decrypt the NCA header using AES-128-XTS. ///< AES-128-XTS key needed to handle NCA header crypto.
u8 header_kek_source[0x10]; ///< Seed for header kek. Retrieved from the .rodata segment in the FS sysmodule. u8 nca_header_kek_source[AES_128_KEY_SIZE]; ///< 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 nca_header_key_source[AES_128_KEY_SIZE * 2]; ///< Retrieved from the .data segment in the FS sysmodule.
u8 header_kek[0x10]; ///< NCA header kek. Generated from header_kek_source. u8 nca_header_kek_sealed[AES_128_KEY_SIZE]; ///< Generated from nca_header_kek_source. Sealed by the SMC AES engine.
u8 header_key[0x20]; ///< NCA header key. Generated from header_kek and header_key_source. 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. ///< AES-128-ECB keys needed to handle key area crypto from NCA headers.
u8 key_area_key_application_source[0x10]; ///< Seed for kaek 0. Retrieved from the .rodata segment in the FS sysmodule. u8 nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_Count][AES_128_KEY_SIZE]; ///< 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 nca_kaek_sealed[NcaKeyAreaEncryptionKeyIndex_Count][NcaKeyGeneration_Max][AES_128_KEY_SIZE]; ///< Generated from nca_kaek_sources. Sealed by the SMC AES engine.
u8 key_area_key_system_source[0x10]; ///< Seed for kaek 2. Retrieved from the .rodata segment in the FS sysmodule. 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. ///< AES-128-CTR key needed to decrypt the console-specific eTicket RSA device key stored in PRODINFO.
u8 eticket_rsa_kek[0x10]; ///< eTicket RSA kek (generic). 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[0x10]; ///< eTicket RSA kek (console-specific). u8 eticket_rsa_kek_personalized[AES_128_KEY_SIZE]; ///< eTicket RSA key encryption key (console-specific). Retrieved from the Lockpick_RCM keys file.
u8 titlekeks[0x20][0x10]; ///< Titlekey encryption keys.
///< Needed to reencrypt the key area from NCAs with titlekey crypto removed. Retrieved from the Lockpick_RCM keys file. ///< AES-128-ECB keys needed to decrypt titlekeys.
u8 key_area_keys[0x20][3][0x10]; ///< Key area encryption keys. u8 ticket_common_keys[NcaKeyGeneration_Max][AES_128_KEY_SIZE]; ///< Retrieved from the Lockpick_RCM keys file.
} keysNcaKeyset; } 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. */ /* Global variables. */
static keysNcaKeyset g_ncaKeyset = {0}; static KeysNcaKeyset g_ncaKeyset = {0};
static bool g_ncaKeysetLoaded = false; static bool g_ncaKeysetLoaded = false;
static Mutex g_ncaKeysetMutex = 0; 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 = { static KeysMemoryInfo g_fsRodataMemoryInfo = {
.location = { .location = {
.program_id = FS_SYSMODULE_TID, .program_id = FS_SYSMODULE_TID,
@ -80,32 +104,40 @@ static KeysMemoryInfo g_fsRodataMemoryInfo = {
.key_count = 4, .key_count = 4,
.keys = { .keys = {
{ {
.name = "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, .hash = {
0x27, 0xF8, 0x08, 0x20, 0xEF, 0xB2, 0x75, 0x92, 0x10, 0x55, 0xAA, 0x4E, 0x2A, 0xBD, 0xFF, 0xC2 }, 0x18, 0x88, 0xCA, 0xED, 0x55, 0x51, 0xB3, 0xED, 0xE0, 0x14, 0x99, 0xE8, 0x7C, 0xE0, 0xD8, 0x68,
.size = 0x10, 0x27, 0xF8, 0x08, 0x20, 0xEF, 0xB2, 0x75, 0x92, 0x10, 0x55, 0xAA, 0x4E, 0x2A, 0xBD, 0xFF, 0xC2
.dst = g_ncaKeyset.header_kek_source },
.size = AES_128_KEY_SIZE,
.dst = g_ncaKeyset.nca_header_kek_source
}, },
{ {
.name = "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, .hash = {
0x56, 0xC5, 0x53, 0xB2, 0xB3, 0x88, 0x71, 0x10, 0x30, 0x42, 0x98, 0xD8, 0xD0, 0x09, 0x2D, 0x9E }, 0x04, 0xAD, 0x66, 0x14, 0x3C, 0x72, 0x6B, 0x2A, 0x13, 0x9F, 0xB6, 0xB2, 0x11, 0x28, 0xB4, 0x6F,
.size = 0x10, 0x56, 0xC5, 0x53, 0xB2, 0xB3, 0x88, 0x71, 0x10, 0x30, 0x42, 0x98, 0xD8, 0xD0, 0x09, 0x2D, 0x9E
.dst = g_ncaKeyset.key_area_key_application_source },
.size = AES_128_KEY_SIZE,
.dst = g_ncaKeyset.nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_Application]
}, },
{ {
.name = "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, .hash = {
0xE5, 0x77, 0x3C, 0xBB, 0x9D, 0xC8, 0x63, 0x00, 0xE1, 0xBD, 0x99, 0xF8, 0xEA, 0x33, 0xA4, 0x17 }, 0xFD, 0x43, 0x40, 0x00, 0xC8, 0xFF, 0x2B, 0x26, 0xF8, 0xE9, 0xA9, 0xD2, 0xD2, 0xC1, 0x2F, 0x6B,
.size = 0x10, 0xE5, 0x77, 0x3C, 0xBB, 0x9D, 0xC8, 0x63, 0x00, 0xE1, 0xBD, 0x99, 0xF8, 0xEA, 0x33, 0xA4, 0x17
.dst = g_ncaKeyset.key_area_key_ocean_source },
.size = AES_128_KEY_SIZE,
.dst = g_ncaKeyset.nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_Ocean]
}, },
{ {
.name = "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, .hash = {
0xC2, 0x10, 0x64, 0x41, 0xE5, 0x17, 0x22, 0xF3, 0x87, 0x00, 0xD5, 0x93, 0x7A, 0x11, 0x62, 0xF7 }, 0x1F, 0x17, 0xB1, 0xFD, 0x51, 0xAD, 0x1C, 0x23, 0x79, 0xB5, 0x8F, 0x15, 0x2C, 0xA4, 0x91, 0x2E,
.size = 0x10, 0xC2, 0x10, 0x64, 0x41, 0xE5, 0x17, 0x22, 0xF3, 0x87, 0x00, 0xD5, 0x93, 0x7A, 0x11, 0x62, 0xF7
.dst = g_ncaKeyset.key_area_key_system_source },
.size = AES_128_KEY_SIZE,
.dst = g_ncaKeyset.nca_kaek_sources[NcaKeyAreaEncryptionKeyIndex_System]
} }
} }
}; };
@ -120,11 +152,13 @@ static KeysMemoryInfo g_fsDataMemoryInfo = {
.key_count = 1, .key_count = 1,
.keys = { .keys = {
{ {
.name = "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, .hash = {
0xEE, 0x16, 0x38, 0x00, 0x43, 0xE1, 0xB8, 0xC4, 0x18, 0xC4, 0x08, 0x9A, 0x8B, 0xD6, 0x4A, 0xA6 }, 0x8F, 0x78, 0x3E, 0x46, 0x85, 0x2D, 0xF6, 0xBE, 0x0B, 0xA4, 0xE1, 0x92, 0x73, 0xC4, 0xAD, 0xBA,
.size = 0x20, 0xEE, 0x16, 0x38, 0x00, 0x43, 0xE1, 0xB8, 0xC4, 0x18, 0xC4, 0x08, 0x9A, 0x8B, 0xD6, 0x4A, 0xA6
.dst = g_ncaKeyset.header_key_source },
.size = (AES_128_KEY_SIZE * 2),
.dst = g_ncaKeyset.nca_header_key_source
} }
} }
}; };
@ -132,12 +166,18 @@ static KeysMemoryInfo g_fsDataMemoryInfo = {
/* Function prototypes. */ /* Function prototypes. */
static bool keysRetrieveKeysFromProgramMemory(KeysMemoryInfo *info); static bool keysRetrieveKeysFromProgramMemory(KeysMemoryInfo *info);
static bool keysDeriveNcaHeaderKey(void); static bool keysDeriveNcaHeaderKey(void);
static bool keysDeriveSealedNcaKeyAreaEncryptionKeys(void);
static int keysGetKeyAndValueFromFile(FILE *f, char **key, char **value); static int keysGetKeyAndValueFromFile(FILE *f, char **key, char **value);
static char keysConvertHexCharToBinary(char c); static char keysConvertHexCharToBinary(char c);
static bool keysParseHexKey(u8 *out, const char *key, const char *value, u32 size); static bool keysParseHexKey(u8 *out, const char *key, const char *value, u32 size);
static bool keysReadKeysFromFile(void); static bool keysReadKeysFromFile(void);
static bool keysGetDecryptedEticketRsaDeviceKey(void);
static bool keysTestEticketRsaDeviceKey(const void *e, const void *d, const void *n);
bool keysLoadNcaKeyset(void) bool keysLoadNcaKeyset(void)
{ {
mutexLock(&g_ncaKeysetMutex); mutexLock(&g_ncaKeysetMutex);
@ -145,39 +185,49 @@ bool keysLoadNcaKeyset(void)
bool ret = g_ncaKeysetLoaded; bool ret = g_ncaKeysetLoaded;
if (ret) goto end; if (ret) goto end;
if (!(envIsSyscallHinted(0x60) && /* svcDebugActiveProcess. */ /* Retrieve FS .rodata keys. */
envIsSyscallHinted(0x63) && /* svcGetDebugEvent. */
envIsSyscallHinted(0x65) && /* svcGetProcessList. */
envIsSyscallHinted(0x69) && /* svcQueryDebugProcessMemory. */
envIsSyscallHinted(0x6A))) /* svcReadDebugProcessMemory. */
{
LOG_MSG("Debug SVC permissions not available!");
goto end;
}
if (!keysRetrieveKeysFromProgramMemory(&g_fsRodataMemoryInfo)) if (!keysRetrieveKeysFromProgramMemory(&g_fsRodataMemoryInfo))
{ {
LOG_MSG("Unable to retrieve keys from FS .rodata segment!"); LOG_MSG("Unable to retrieve keys from FS .rodata segment!");
goto end; goto end;
} }
/* Retrieve FS .data keys. */
if (!keysRetrieveKeysFromProgramMemory(&g_fsDataMemoryInfo)) if (!keysRetrieveKeysFromProgramMemory(&g_fsDataMemoryInfo))
{ {
LOG_MSG("Unable to retrieve keys from FS .data segment!"); LOG_MSG("Unable to retrieve keys from FS .data segment!");
goto end; goto end;
} }
/* Derive NCA header key. */
if (!keysDeriveNcaHeaderKey()) if (!keysDeriveNcaHeaderKey())
{ {
LOG_MSG("Unable to derive NCA header key!"); LOG_MSG("Unable to derive NCA header key!");
goto end; 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; if (!keysReadKeysFromFile()) goto end;
/* Get decrypted eTicket RSA device key. */
if (!keysGetDecryptedEticketRsaDeviceKey()) goto end;
ret = g_ncaKeysetLoaded = true; ret = g_ncaKeysetLoaded = true;
end: 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); mutexUnlock(&g_ncaKeysetMutex);
return ret; return ret;
@ -185,67 +235,133 @@ end:
const u8 *keysGetNcaHeaderKey(void) const u8 *keysGetNcaHeaderKey(void)
{ {
return (const u8*)(g_ncaKeyset.header_key); mutexLock(&g_ncaKeysetMutex);
} const u8 *ptr = (g_ncaKeysetLoaded ? (const u8*)(g_ncaKeyset.nca_header_key) : NULL);
mutexUnlock(&g_ncaKeysetMutex);
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;
}
return ptr; 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); Result rc = 0;
} bool success = false;
const u8 *keysGetTitlekek(u8 key_generation)
{
if (key_generation > 0x20)
{
LOG_MSG("Invalid key generation value! (0x%02X).", key_generation);
return NULL;
}
u8 key_gen_val = (key_generation ? (key_generation - 1) : key_generation); u8 key_gen_val = (key_generation ? (key_generation - 1) : key_generation);
return (const u8*)(g_ncaKeyset.titlekeks[key_gen_val]); if (kaek_index >= NcaKeyAreaEncryptionKeyIndex_Count)
}
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)
{ {
LOG_MSG("Invalid KAEK index! (0x%02X).", kaek_index); 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); 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) static bool keysRetrieveKeysFromProgramMemory(KeysMemoryInfo *info)
@ -309,30 +425,70 @@ static bool keysDeriveNcaHeaderKey(void)
{ {
Result rc = 0; 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)) 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; 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)) 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; 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)) 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 false;
} }
return true; 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. * 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_] *[,=] *.+$/. * The format of a line must match /^ *[A-Za-z0-9_] *[,=] *.+$/.
@ -531,54 +687,57 @@ static bool keysReadKeysFromFile(void)
while(true) while(true)
{ {
ret = keysGetKeyAndValueFromFile(keys_file, &key, &value); 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. */ /* Ignore malformed lines. */
if (ret != 0 || !key || !value) continue; 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; if ((parse_fail = !keysParseHexKey(g_ncaKeyset.eticket_rsa_kek, key, value, sizeof(g_ncaKeyset.eticket_rsa_kek)))) break;
eticket_rsa_kek_available = true; eticket_rsa_kek_available = true;
key_count++; key_count++;
} else } 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. */ /* 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; 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; eticket_rsa_kek_available = true;
key_count++; key_count++;
} else { } 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); 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++; key_count++;
break; break;
} }
snprintf(test_name, sizeof(test_name), "key_area_key_application_%02x", i); 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++; key_count++;
break; break;
} }
snprintf(test_name, sizeof(test_name), "key_area_key_ocean_%02x", i); 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++; key_count++;
break; break;
} }
snprintf(test_name, sizeof(test_name), "key_area_key_system_%02x", i); 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++; key_count++;
break; break;
} }
@ -604,3 +763,86 @@ static bool keysReadKeysFromFile(void)
return true; 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;
}

View file

@ -81,6 +81,17 @@ static bool memRetrieveProgramMemory(MemoryLocation *location, bool is_segment)
bool success = true; 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. */ /* Clear output MemoryLocation element. */
memFreeMemoryLocation(location); memFreeMemoryLocation(location);

View file

@ -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); 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) 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(); const u8 *header_key = keysGetNcaHeaderKey();
Aes128XtsContext hdr_aes_ctx = {0}, nca0_fs_header_ctx = {0}; 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. */ /* Prepare AES-128-XTS contexts. */
aes128XtsContextCreate(&hdr_aes_ctx, header_key, header_key + AES_128_KEY_SIZE, true); 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); 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(); const u8 *header_key = keysGetNcaHeaderKey();
Aes128XtsContext hdr_aes_ctx = {0}, nca0_fs_header_ctx = {0}; 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. */ /* Read NCA header. */
if (!ncaReadContentFile(ctx, &(ctx->encrypted_header), sizeof(NcaHeader), 0)) if (!ncaReadContentFile(ctx, &(ctx->encrypted_header), sizeof(NcaHeader), 0))
{ {
@ -627,9 +639,8 @@ static bool ncaDecryptKeyArea(NcaContext *ctx)
return false; return false;
} }
Result rc = 0; const u8 null_key[AES_128_KEY_SIZE] = {0};
const u8 *kaek_src = NULL, null_key[AES_128_KEY_SIZE] = {0}; u8 key_count = (ctx->format_version == NcaVersion_Nca0 ? 2 : 4);
u8 key_count = (ctx->format_version == NcaVersion_Nca0 ? 2 : 4), aes_kek[AES_128_KEY_SIZE] = {0};
/* Check if we're dealing with a NCA0 with a plaintext key area. */ /* Check if we're dealing with a NCA0 with a plaintext key area. */
if (ncaIsVersion0KeyAreaEncrypted(ctx)) if (ncaIsVersion0KeyAreaEncrypted(ctx))
@ -638,22 +649,6 @@ static bool ncaDecryptKeyArea(NcaContext *ctx)
return true; 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. */ /* Clear decrypted key area. */
memset(&(ctx->decrypted_key_area), 0, NCA_USED_KEY_AREA_SIZE); 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; if (!memcmp(src_key, null_key, AES_128_KEY_SIZE)) continue;
/* Decrypt current key area entry. */ /* Decrypt current key area entry. */
rc = splCryptoGenerateAesKey(aes_kek, src_key, dst_key); if (!keysDecryptNcaKeyAreaEntry(ctx->header.kaek_index, ctx->key_generation, dst_key, src_key))
if (R_FAILED(rc))
{ {
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; return false;
} }
} }
@ -698,10 +692,10 @@ static bool ncaEncryptKeyArea(NcaContext *ctx)
} }
/* Get KAEK for these key generation and KAEK index values. */ /* 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) 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; return false;
} }

View file

@ -54,7 +54,8 @@ static int g_nxLinkSocketFd = -1;
static const char *g_sizeSuffixes[] = { "B", "KiB", "MiB", "GiB" }; static const char *g_sizeSuffixes[] = { "B", "KiB", "MiB", "GiB" };
static const u32 g_sizeSuffixesCount = MAX_ELEMENTS(g_sizeSuffixes); 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. */ /* Function prototypes. */
@ -103,10 +104,6 @@ bool utilsInitializeResources(void)
u32 hos_version = hosversionGet(); u32 hos_version = hosversionGet();
LOG_MSG("Horizon OS version: %u.%u.%u.", HOSVER_MAJOR(hos_version), HOSVER_MINOR(hos_version), HOSVER_MICRO(hos_version)); 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. */ /* Initialize needed services. */
if (!servicesInitialize()) if (!servicesInitialize())
{ {
@ -114,6 +111,10 @@ bool utilsInitializeResources(void)
goto end; 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. */ /* Check if we're not running under a development unit. */
if (!_utilsIsDevelopmentUnit()) goto end; if (!_utilsIsDevelopmentUnit()) goto end;
LOG_MSG("Running under %s unit.", g_isDevUnit ? "development" : "retail"); 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++) 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) void utilsOverclockSystem(bool overclock)
{ {
u32 cpuClkRate = ((overclock ? CPU_CLKRT_OVERCLOCKED : CPU_CLKRT_NORMAL) * 1000000); u32 cpu_rate = ((overclock ? CPU_CLKRT_OVERCLOCKED : CPU_CLKRT_NORMAL) * 1000000);
u32 memClkRate = ((overclock ? MEM_CLKRT_OVERCLOCKED : MEM_CLKRT_NORMAL) * 1000000); u32 mem_rate = ((overclock ? MEM_CLKRT_OVERCLOCKED : MEM_CLKRT_NORMAL) * 1000000);
servicesChangeHardwareClockRates(cpuClkRate, memClkRate); servicesChangeHardwareClockRates(cpu_rate, mem_rate);
} }
static void _utilsGetCustomFirmwareType(void) static void _utilsGetCustomFirmwareType(void)
@ -727,7 +728,7 @@ static void utilsOverclockSystemAppletHook(AppletHookType hook, void *param)
if (hook != AppletHookType_OnOperationMode && hook != AppletHookType_OnPerformanceMode) return; if (hook != AppletHookType_OnOperationMode && hook != AppletHookType_OnPerformanceMode) return;
/* TO DO: read config here to actually know the value to use with utilsOverclockSystem. */ /* TO DO: read config here to actually know the value to use with utilsOverclockSystem. */
utilsOverclockSystem(false); utilsOverclockSystem(true);
} }
static void utilsPrintConsoleError(void) static void utilsPrintConsoleError(void)

View file

@ -39,8 +39,10 @@ typedef struct {
/* Function prototypes. */ /* Function prototypes. */
static bool _servicesCheckInitializedServiceByName(const char *name, bool lock);
static Result servicesAtmosphereHasService(bool *out, SmServiceName name); static Result servicesAtmosphereHasService(bool *out, SmServiceName name);
static bool servicesGetExosphereApiVersion(u32 *out); static Result servicesGetExosphereApiVersion(u32 *out);
static Result servicesNifmUserInitialize(void); static Result servicesNifmUserInitialize(void);
static bool servicesClkGetServiceType(void *arg); static bool servicesClkGetServiceType(void *arg);
@ -71,6 +73,8 @@ static ClkrstSession g_clkrstCpuSession = {0}, g_clkrstMemSession = {0};
static Mutex g_servicesMutex = 0; static Mutex g_servicesMutex = 0;
static u32 g_atmosphereVersion = 0;
/* Atmosphère-related constants. */ /* Atmosphère-related constants. */
static const u32 g_smAtmosphereHasService = 65100; static const u32 g_smAtmosphereHasService = 65100;
static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000; static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000;
@ -87,8 +91,8 @@ bool servicesInitialize(void)
{ {
ServiceInfo *service_info = &(g_serviceInfo[i]); ServiceInfo *service_info = &(g_serviceInfo[i]);
/* Check if this service has been already initialized or if it actually has a valid initialize function. */ /* Check if this service has been already initialized. */
if (service_info->initialized || service_info->init_func == NULL) continue; if (service_info->initialized) continue;
/* Check if this service depends on a condition function. */ /* Check if this service depends on a condition function. */
if (service_info->cond_func != NULL) if (service_info->cond_func != NULL)
@ -98,16 +102,19 @@ bool servicesInitialize(void)
if (!service_info->cond_func(service_info)) continue; 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. */ /* Initialize service. */
rc = service_info->init_func(); rc = service_info->init_func();
if (R_FAILED(rc)) 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; ret = false;
break; break;
} }
/* Update initialized flag. */ /* Update flag. */
service_info->initialized = true; service_info->initialized = true;
} }
@ -130,37 +137,77 @@ void servicesClose(void)
/* Close service. */ /* Close service. */
service_info->close_func(); service_info->close_func();
/* Update initialized flag. */ /* Update flag. */
service_info->initialized = false; service_info->initialized = false;
} }
mutexUnlock(&g_servicesMutex); mutexUnlock(&g_servicesMutex);
} }
bool servicesCheckInitializedServiceByName(const char *name)
{
return _servicesCheckInitializedServiceByName(name, true);
}
bool servicesCheckRunningServiceByName(const char *name) bool servicesCheckRunningServiceByName(const char *name)
{ {
if (!name || !*name)
{
LOG_MSG("Invalid parameters!");
return false;
}
Result rc = 0; Result rc = 0;
bool out = false; 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); rc = servicesAtmosphereHasService(&out, service_name);
if (R_FAILED(rc)) LOG_MSG("servicesAtmosphereHasService failed for \"%s\"! (0x%08X).", name, rc); if (R_FAILED(rc)) LOG_MSG("servicesAtmosphereHasService failed for \"%s\"! (0x%08X).", name, rc);
end:
mutexUnlock(&g_servicesMutex);
return out; return out;
} }
bool servicesCheckInitializedServiceByName(const char *name) void servicesChangeHardwareClockRates(u32 cpu_rate, u32 mem_rate)
{ {
Result rc1 = 0, rc2 = 0;
mutexLock(&g_servicesMutex); 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; bool ret = false;
if (lock) mutexLock(&g_servicesMutex);
if (!name || !*name) goto end; if (!name || !*name) goto end;
for(u32 i = 0; i < g_serviceInfoCount; i++) for(u32 i = 0; i < g_serviceInfoCount; i++)
@ -175,70 +222,57 @@ bool servicesCheckInitializedServiceByName(const char *name)
} }
end: end:
mutexUnlock(&g_servicesMutex); if (lock) mutexUnlock(&g_servicesMutex);
return ret; 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. */ /* SM API extension available in Atmosphère and Atmosphère-based CFWs. */
static Result servicesAtmosphereHasService(bool *out, SmServiceName name) static Result servicesAtmosphereHasService(bool *out, SmServiceName name)
{ {
if (!out || !name.name[0]) return MAKERESULT(Module_Libnx, LibnxError_BadInput);
u8 tmp = 0; u8 tmp = 0;
Result rc = 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. */ /* Check if service is running. */
/* Dispatch IPC request using CMIF or TIPC serialization depending on our current environment. */ /* 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); rc = tipcDispatchInOut(smGetServiceSessionTipc(), g_smAtmosphereHasService, name, tmp);
} else { } else {
rc = serviceDispatchInOut(smGetServiceSession(), g_smAtmosphereHasService, name, tmp); rc = serviceDispatchInOut(smGetServiceSession(), g_smAtmosphereHasService, name, tmp);
} }
if (R_SUCCEEDED(rc) && out) *out = tmp; if (R_SUCCEEDED(rc)) *out = (tmp != 0);
return rc; return rc;
} }
/* SMC API extension available in Atmosphère and Atmosphère-based CFWs. */ /* SMC config item available in Atmosphère and Atmosphère-based CFWs. */
static bool servicesGetExosphereApiVersion(u32 *out) static Result servicesGetExosphereApiVersion(u32 *out)
{ {
if (!out) if (!out) return MAKERESULT(Module_Libnx, LibnxError_BadInput);
{
LOG_MSG("Invalid parameters!");
return false;
}
Result rc = 0; Result rc = 0;
u64 version = 0; u64 cfg = 0;
u32 version = 0;
rc = splGetConfig(SplConfigItem_ExosphereApiVersion, &version); rc = splGetConfig(SplConfigItem_ExosphereApiVersion, &cfg);
if (R_FAILED(rc)) if (R_SUCCEEDED(rc))
{ {
LOG_MSG("splGetConfig failed! (0x%08X).", rc); *out = version = (u32)((cfg >> 40) & 0xFFFFFF);
return false; LOG_MSG("Exosphère API version: %u.%u.%u.", HOSVER_MAJOR(version), HOSVER_MINOR(version), HOSVER_MICRO(version));
} }
*out = (u32)((version >> 40) & 0xFFFFFF); return rc;
return true;
} }
static Result servicesNifmUserInitialize(void) static Result servicesNifmUserInitialize(void)
@ -252,7 +286,11 @@ static Result servicesClkrstInitialize(void)
/* Open clkrst service handle. */ /* Open clkrst service handle. */
rc = clkrstInitialize(); 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. */ /* Initialize CPU and MEM clkrst sessions. */
memset(&g_clkrstCpuSession, 0, sizeof(ClkrstSession)); memset(&g_clkrstCpuSession, 0, sizeof(ClkrstSession));
@ -261,6 +299,7 @@ static Result servicesClkrstInitialize(void)
rc = clkrstOpenSession(&g_clkrstCpuSession, PcvModuleId_CpuBus, 3); rc = clkrstOpenSession(&g_clkrstCpuSession, PcvModuleId_CpuBus, 3);
if (R_FAILED(rc)) if (R_FAILED(rc))
{ {
LOG_MSG("clkrstOpenSession failed! (0x%08X) (CPU).", rc);
clkrstExit(); clkrstExit();
return rc; return rc;
} }
@ -268,6 +307,7 @@ static Result servicesClkrstInitialize(void)
rc = clkrstOpenSession(&g_clkrstMemSession, PcvModuleId_EMC, 3); rc = clkrstOpenSession(&g_clkrstMemSession, PcvModuleId_EMC, 3);
if (R_FAILED(rc)) if (R_FAILED(rc))
{ {
LOG_MSG("clkrstOpenSession failed! (0x%08X) (MEM).", rc);
clkrstCloseSession(&g_clkrstCpuSession); clkrstCloseSession(&g_clkrstCpuSession);
clkrstExit(); clkrstExit();
} }
@ -298,6 +338,7 @@ static bool servicesClkGetServiceType(void *arg)
/* Fill service info. */ /* Fill service info. */
sprintf(info->name, "%s", (g_clkSvcUsePcv ? "pcv" : "clkrst")); sprintf(info->name, "%s", (g_clkSvcUsePcv ? "pcv" : "clkrst"));
info->cond_func = NULL;
info->init_func = (g_clkSvcUsePcv ? &pcvInitialize : &servicesClkrstInitialize); info->init_func = (g_clkSvcUsePcv ? &pcvInitialize : &servicesClkrstInitialize);
info->close_func = (g_clkSvcUsePcv ? &pcvExit : &servicesClkrstExit); 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; 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). */ /* 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);
} }

View file

@ -26,7 +26,6 @@
#include "save.h" #include "save.h"
#include "es.h" #include "es.h"
#include "keys.h" #include "keys.h"
#include "rsa.h"
#include "gamecard.h" #include "gamecard.h"
#include "mem.h" #include "mem.h"
#include "aes.h" #include "aes.h"
@ -37,8 +36,6 @@
#define TIK_LIST_STORAGE_PATH "/ticket_list.bin" #define TIK_LIST_STORAGE_PATH "/ticket_list.bin"
#define TIK_DB_STORAGE_PATH "/ticket.bin" #define TIK_DB_STORAGE_PATH "/ticket.bin"
#define ETICKET_DEVKEY_PUBLIC_EXPONENT 0x10001
#define ES_CTRKEY_ENTRY_ALIGNMENT 0x8 #define ES_CTRKEY_ENTRY_ALIGNMENT 0x8
/* Type definitions. */ /* 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. /// 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. /// First index in this list is always 0, and it's aligned to ES_CTRKEY_ENTRY_ALIGNMENT.
typedef struct { typedef struct {
u32 idx; ///< Entry index. u32 idx; ///< Entry index.
u8 key[AES_BLOCK_SIZE]; ///< AES-128-CTR key. u8 key[AES_128_KEY_SIZE]; ///< AES-128-CTR key.
u8 ctr[AES_BLOCK_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out. u8 ctr[AES_128_KEY_SIZE]; ///< AES-128-CTR counter/IV. Always zeroed out.
} TikEsCtrKeyEntry9x; } TikEsCtrKeyEntry9x;
NXDT_ASSERT(TikEsCtrKeyEntry9x, 0x24); NXDT_ASSERT(TikEsCtrKeyEntry9x, 0x24);
/// Lookup pattern for TikEsCtrKeyEntry9x. /// Lookup pattern for TikEsCtrKeyEntry9x.
typedef struct { typedef struct {
u32 idx1; ///< Always set to 0 (first entry). u32 idx1; ///< Always set to 0 (first entry).
u8 ctrdata[AES_BLOCK_SIZE * 2]; u8 ctrdata[AES_128_KEY_SIZE * 2];
u32 idx2; ///< Always set to 1 (second entry). u32 idx2; ///< Always set to 1 (second entry).
} TikEsCtrKeyPattern9x; } TikEsCtrKeyPattern9x;
NXDT_ASSERT(TikEsCtrKeyPattern9x, 0x28); 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. */ /* 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[] = { static const char *g_tikTitleKeyTypeStrings[] = {
[TikTitleKeyType_Common] = "common", [TikTitleKeyType_Common] = "common",
[TikTitleKeyType_Personalized] = "personalized" [TikTitleKeyType_Personalized] = "personalized"
@ -116,8 +89,8 @@ static MemoryLocation g_esMemoryLocation = {
static bool tikRetrieveTicketFromGameCardByRightsId(Ticket *dst, const FsRightsId *id); static bool tikRetrieveTicketFromGameCardByRightsId(Ticket *dst, const FsRightsId *id);
static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRightsId *id); static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRightsId *id);
static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik); static bool tikGetEncryptedTitleKeyFromTicket(Ticket *tik);
static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_generation); static bool tikGetDecryptedTitleKey(void *dst, const void *src, u8 key_generation);
static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out); static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out);
static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, bool personalized); 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 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) bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gamecard)
{ {
if (!dst || !id) if (!dst || !id)
@ -150,6 +120,7 @@ bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gam
/* Clear output ticket. */ /* Clear output ticket. */
memset(dst, 0, sizeof(Ticket)); memset(dst, 0, sizeof(Ticket));
/* Retrieve ticket data. */
bool tik_retrieved = (use_gamecard ? tikRetrieveTicketFromGameCardByRightsId(dst, id) : tikRetrieveTicketFromEsSaveDataByRightsId(dst, id)); bool tik_retrieved = (use_gamecard ? tikRetrieveTicketFromGameCardByRightsId(dst, id) : tikRetrieveTicketFromEsSaveDataByRightsId(dst, id));
if (!tik_retrieved) if (!tik_retrieved)
{ {
@ -157,21 +128,18 @@ bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gam
return false; return false;
} }
mutexLock(&g_eTicketDeviceKeyMutex); /* Get encrypted titlekey from ticket. */
bool titlekey_retrieved = tikGetTitleKekEncryptedTitleKeyFromTicket(dst); if (!tikGetEncryptedTitleKeyFromTicket(dst))
mutexUnlock(&g_eTicketDeviceKeyMutex);
if (!titlekey_retrieved)
{ {
LOG_MSG("Unable to retrieve titlekey from ticket!"); LOG_MSG("Unable to retrieve encrypted titlekey from ticket!");
return false; return false;
} }
/* Even though tickets do have a proper key_generation field, we'll just retrieve it from the rights_id field. */ /* 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. */ /* 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; 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)); memset(tik_common_block->issuer, 0, sizeof(tik_common_block->issuer));
sprintf(tik_common_block->issuer, "%s", cert_chain_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)); memset(tik_common_block->titlekey_block, 0, sizeof(tik_common_block->titlekey_block));
memcpy(tik_common_block->titlekey_block, tik->enc_titlekey, 0x10); memcpy(tik_common_block->titlekey_block, tik->enc_titlekey, 0x10);
@ -371,7 +339,7 @@ end:
return success; return success;
} }
static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik) static bool tikGetEncryptedTitleKeyFromTicket(Ticket *tik)
{ {
TikCommonBlock *tik_common_block = NULL; TikCommonBlock *tik_common_block = NULL;
@ -381,38 +349,16 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik)
return false; return false;
} }
size_t out_keydata_size = 0;
u8 out_keydata[0x100] = {0};
TikEticketDeviceKeyData *eticket_devkey = NULL;
switch(tik_common_block->titlekey_type) switch(tik_common_block->titlekey_type)
{ {
case TikTitleKeyType_Common: 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); memcpy(tik->enc_titlekey, tik_common_block->titlekey_block, 0x10);
break; break;
case TikTitleKeyType_Personalized: case TikTitleKeyType_Personalized:
/* Retrieve eTicket device key. */ /* The titlekey block is encrypted using RSA-OAEP with a console-specific RSA key. */
if (!tikRetrieveEticketDeviceKey()) /* 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;
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);
break; break;
default: default:
LOG_MSG("Invalid titlekey type value! (0x%02X).", tik_common_block->titlekey_type); LOG_MSG("Invalid titlekey type value! (0x%02X).", tik_common_block->titlekey_type);
@ -422,7 +368,7 @@ static bool tikGetTitleKekEncryptedTitleKeyFromTicket(Ticket *tik)
return true; 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) if (!dst || !src)
{ {
@ -430,17 +376,17 @@ static bool tikGetTitleKekDecryptedTitleKey(void *dst, const void *src, u8 key_g
return false; return false;
} }
const u8 *titlekek = NULL; const u8 *ticket_common_key = NULL;
Aes128Context titlekey_aes_ctx = {0}; Aes128Context titlekey_aes_ctx = {0};
titlekek = keysGetTitlekek(key_generation); ticket_common_key = keysGetTicketCommonKey(key_generation);
if (!titlekek) 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; return false;
} }
aes128ContextCreate(&titlekey_aes_ctx, titlekek, false); aes128ContextCreate(&titlekey_aes_ctx, ticket_common_key, false);
aes128DecryptBlock(&titlekey_aes_ctx, dst, src); aes128DecryptBlock(&titlekey_aes_ctx, dst, src);
return true; return true;
@ -626,7 +572,7 @@ static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u
TikCommonBlock *tik_common_block = NULL; TikCommonBlock *tik_common_block = NULL;
Aes128CtrContext ctr_ctx = {0}; Aes128CtrContext ctr_ctx = {0};
u8 null_ctr[AES_BLOCK_SIZE] = {0}, ctr[AES_BLOCK_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0}; u8 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; bool is_volatile = false, success = false;
@ -765,85 +711,3 @@ static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64
return true; 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;
}

View file

@ -45,8 +45,8 @@
#define USB_HS_EP_MAX_PACKET_SIZE 0x200 /* 512 bytes. */ #define USB_HS_EP_MAX_PACKET_SIZE 0x200 /* 512 bytes. */
#define USB_SS_BCD_REVISION 0x0300 /* USB 3.0. */ #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_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. */ #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 (g_usbSessionStarted) ueventSignal(&g_usbTimeoutEvent);
if (!thread_exit) LOG_MSG("eventWait failed! (0x%08X) (URB ID %u).", rc, urb_id); if (!thread_exit) LOG_MSG("eventWait failed! (0x%08X) (URB ID %u).", rc, urb_id);
return false; return false;
} }