Skip to content

Commit

Permalink
Merge pull request #12 from PretendoNetwork/prudp-checksum
Browse files Browse the repository at this point in the history
PRUDP updates
  • Loading branch information
jonbarrow authored Dec 8, 2023
2 parents 8033e8c + 526fcdd commit 3416379
Showing 1 changed file with 72 additions and 49 deletions.
121 changes: 72 additions & 49 deletions docs/prudp.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ On the Nintendo Switch, NEX uses a WebSocket connection instead of UDP and the '
## V0 Format
This format is only used by the friends server and some 3DS games.

| Offset | Size | Description |
|--------|------|---------------------------------------|
| 0x0 | 1 | [Source](#virtual-ports) |
| 0x1 | 1 | [Destination](#virtual-ports) |
| 0x2 | 2 | [Type and flags](#type-and-flags) |
| 0x4 | 1 | [Session id](#session-id) |
| 0x5 | 4 | [Packet signature](#packet-signature) |
| 0x9 | 2 | [Sequence id](#sequence-id) |
| 0xB | | Packet-specific data |
| | | Payload |
| | 1 | [Checksum](#checksum) |
| Offset | Size | Description |
|--------|--------|---------------------------------------|
| 0x0 | 1 | [Source](#virtual-ports) |
| 0x1 | 1 | [Destination](#virtual-ports) |
| 0x2 | 2 | [Type and flags](#type-and-flags) |
| 0x4 | 1 | [Session id](#session-id) |
| 0x5 | 4 | [Packet signature](#packet-signature) |
| 0x9 | 2 | [Sequence id](#sequence-id) |
| 0xB | | Packet-specific data |
| | | Payload |
| | 1 or 4 | [Checksum](#checksum) |

Packet-specific data:

Expand Down Expand Up @@ -55,31 +55,54 @@ In DATA and DISCONNECT packets the packet signature is the first 4 bytes of the

In all other packets the signature is the connection signature that has been received while the connection was made.

**Quazal Rendez-Vous:**

In all Rendez-Vous packets the signature is the connection signature that has been received while the connection was made.

### Checksum
The checksum is calculated over the whole packet (both header and encrypted payload), and uses the following algorithm:
The checksum is calculated over the whole packet (both header and encrypted payload). A checksum can be either 1 byte or 4 bytes long. By default checksums are 1 byte long, but games have the option to enable the 4 byte checksum instead. All NEX titles use 1 byte checksums, though Rendez-Vous titles may be seen with either. The following algorithms are used, where `ACCESS_KEY` is the server [access key](#sandbox-access-key) bytes:

#### 1 Byte
```python
ACCESS_KEY = "12345678".encode() # Example dummy key

def calc_checksum(data):
words = struct.unpack_from("<%iI" %(len(data) // 4), data)
temp = sum(words) & 0xFFFFFFFF #32-bit

checksum = sum(ACCESS_KEY)
checksum += sum(data[len(data) & ~3:])
checksum += sum(struct.pack("I", temp))
return checksum & 0xFF #8-bit checksum
"""
Split the data into a list of little endian uint32's
If there is not enough data for a uint32, discard it
EX: b"abcdefghijk" (0x6162636465666768696A6B)
becomes (1684234849, 1751606885), aka (0x64636261, 0x68676665)
"""
words = struct.unpack_from("<%iI" %(len(data) // 4), data)
temp = sum(words) & 0xFFFFFFFF # Add the values and truncate to a uint32
main = struct.pack("I", temp) # Pack the sum of the main data back into bytes
remaining = data[len(data) & ~3:] # The data left over from the above

checksum = sum(ACCESS_KEY) # Checksum base
checksum += sum(remaining) # Add the remaining data first
checksum += sum(main) # Add the main data last
return checksum & 0xFF # Truncate to a uint8
```

<details><summary>The original Quazal Rendez-Vous library uses a different checksum algorithm.</summary><br>

#### 4 Byte
```python
ACCESS_KEY = "12345678".encode() # Example dummy key

def calc_checksum(checksum, data):
data += b"\0" * (4 - len(data) % 4)
words = struct.unpack("<%iI" %(len(data) // 4), data)
return ((sum(ACCESS_KEY) & 0xFF) + sum(words)) & 0xFFFFFFFF
data += b"\0" * (4 - len(data) % 4) # Pad data to nearest multiple of 4

"""
Split the data into a list of little endian uint32's
EX: b"abcdefgh" (0x6162636465666768)
becomes (1684234849, 1751606885), aka (0x64636261, 0x68676665)
"""
words = struct.unpack("<%iI" %(len(data) // 4), data)
checksum = sum(ACCESS_KEY) & 0xFF # Checksum base, truncated to uint8
checksum += sum(words) # Add the packet data sum

return checksum & 0xFFFFFFFF # Truncate to a uint32
```

This checksum takes up 4 bytes instead of 1.
</details>

## V1 Format
This format is used by all Wii U games and apps except for friends services, and some 3DS games.

Expand Down Expand Up @@ -165,48 +188,48 @@ The following techniques are used to achieve reliability:
* To keep the connection alive, both client and server may send PING packets to each other after a certain amount of time has passed.

### Encryption
**V0 and V1**: All payloads are encrypted using RC4, with separate streams for client-to-server packets and server-to-client packets. The connection to the authentication server is encrypted using a default key that's always the same: `CD&ML`. The connection to the secure server is encrypted using the session key from the [Kerberos ticket](/docs/nex/kerberos).
**V0 and V1**: All payloads are encrypted using RC4, with separate streams for client-to-server packets and server-to-client packets. The connection to the authentication server is encrypted using a default key that's always the same: `CD&ML`. The connection to the secure server is encrypted using the session key from the [Kerberos ticket](/docs/nex/kerberos). On Quazal Rendez-Vous, the streams are reset for each payload.

**Lite**: Since the underlying connection is SSL-encrypted anyway, no encryption is used by PRUDP.

<details><summary>Details on substreams and unreliable packets</summary><br>
#### Substreams and unreliable packets

It would be a bad idea to encrypt all reliable substreams with the same key, because that would make it easy to break the encryption. PRUDP encrypts the first reliable substream with the session key. A new key is generated for all other reliable substreams by modifying the key of the previous substream with the following algorithm:

```python
def modify_key(key):
# Only the first half of the key is modified
add = len(key) // 2 + 1
for i in range(len(key) // 2):
key[i] = (key[i] + add - i) & 0xFF
# Only the first half of the key is modified
add = len(key) // 2 + 1
for i in range(len(key) // 2):
key[i] = (key[i] + add - i) & 0xFF
```

Unreliable packets also have another issue: it's not possible to use a single RC4 stream to encrypt them, because the decryption would fail if the packets arrive in the wrong order. To solve this, a unique RC4 stream is used for each unreliable data packet. The key is generated as follows:

```python
def make_unreliable_key(packet, session_key):
# Generate a new key from the session key
part1 = combine_keys(session_key, bytes.fromhex("18d8233437e4e3fe"))
part2 = combine_keys(session_key, bytes.fromhex("233e600123cdab80"))
base_key = part1 + part2

# Modify the key such that no two packets use the same key
key = list(base_key)
key[0] = (key[0] + packet.sequence_id) & 0xFF
key[1] = (key[1] + (packet.sequence_id >> 8)) & 0xFF
key[31] = (key[31] + packet.session_id) & 0xFF
return bytes(key)
# Generate a new key from the session key
part1 = combine_keys(session_key, bytes.fromhex("18d8233437e4e3fe"))
part2 = combine_keys(session_key, bytes.fromhex("233e600123cdab80"))
base_key = part1 + part2

# Modify the key such that no two packets use the same key
key = list(base_key)
key[0] = (key[0] + packet.sequence_id) & 0xFF
key[1] = (key[1] + (packet.sequence_id >> 8)) & 0xFF
key[31] = (key[31] + packet.session_id) & 0xFF
return bytes(key)

def combine_keys(key1, key2):
return hashlib.md5(key1 + key2).digest()
return hashlib.md5(key1 + key2).digest()
```

</details>

### Sandbox access key
Every game server has a unique sandbox access key. This is used to calculate the [packet signature](#packet-signature). The only way to find the access key of a server is by disassembling a game that connects to this server.
Every game server has a unique sandbox access key. This is used to calculate the [packet signature](#packet-signature) and [packet checksum](#checksum). All NEX titles use access keys which are 8 lowercase hex characters, with the sole exception of the Friends server whose access key is `ridfebb9`. This limitation is only imposed by NEX, however. Rendez-Vous clients do not limit themselves to 8 lowercase hex characters, and may also use uppercase and non-hex characters. It seems that the access key may also be allowed to be up to 128 characters long, though no games are currently known to use anything larger than 8.

The only way to find the access key of a server is by checking the client. In most cases this involves disassembling the game, however some games have been known to store their access keys in external files. For NEX titles, tools such as [this](https://github.com/PretendoNetwork/access-key-extractor) exist to automate the extraction of these keys. A key may often times also be brute forced, as many valid keys exist for all servers due to their small size.

A list of game servers and their access keys can be found [here](/docs/game-servers).
A partial list of game servers and their access keys can be found [here](/docs/game-servers).

### Secure server connection
As explained on the [Game Server Overview](/docs/nex) page, every game server consists of an authentication server and a secure server. If a client wants to connect to the secure server it must first request a [ticket](/docs/nex/kerberos) from the authentication server. The ticket contains the session key that's used in the secure server connection, among other information.
Expand Down

0 comments on commit 3416379

Please sign in to comment.