Secure Connections

Protect your game from hackers
Once your online game reaches a certain popularity people will try to cheat. You will need to account for this both at the game layer and at the network layer. RakNet handles the network layer by providing secure connections if you wish to use them. Secure connections:

- Uses AES encryption with randomized, chained blocks, preventing unauthorized reads and blocking replay attacks.
- Adds CRCs so that data tampering can be detected.
- Uses randomized, encrypted SYNCookies to prevent unauthorized logins.
- Uses RSA encryption to protect the AES key.

Most games will want to use secure connections. However, they add up to 15 bytes per packet and take time to compute so you may wish to limit usage to release mode.
The relevant header is as follows:
void InitializeSecurity(const char *pubKeyE, const char *pubKeyN, const char *privKeyP, const char *privKeyQ )
pubKeyE and pubKeyN are the private keys corresponding to the well-known variables of the same name. The same holds true with privKeyP and privKeyQ.
In all cases you can pass 0 to all the parameters and RakNet will generate a new key. However, it takes a few moments to do this which you may wish to avoid by generating the RSA keys in advance and passing them to the function.
While it isn't necessary for the client to have the public key in advance, if you don't do this you will be vulnerable to a man in the middle attack. This attack consists of someone sitting between you and the server, modifying the transmitted public key to something with known results, decrypting the AES key, and sending that to the server encrypted with the transmitted public key. The AES key can then be used to read data transmissions between the client and server.
See the sample at Samples\Encryption to see how to save and load keys.

How secure connections work

The system that requires that connections be secure calls RakPeer::InitializeSecurity().
InitializeSecurity in turn calls
rsacrypt.generatePrivateKey(RAKNET_RSA_FACTOR_LIMBS);
rsacrypt is a member variable of RakPeer. The same private key is used for all remote systems.
You can also call InitializeSecurity passing the public keys. If you do so, these public keys are checked when you connect to another system with the corresponding private keys. This is important to know who you are talking to - e.g. prevent a man in the middle attack.
Every 10 seconds, a 20 byte random number is generated. This random number is used on the system receiving connections (the server).
See GenerateSYNCookieRandomNumber()
When a connection request arrives to the server, it calls RakPeer::SecuredConnectionResponse()
SecuredConnectionResponse() will hash the SYN cookie, the source port, and the source IP address
// Hash the SYN-Cookie
// s2c syn-cookie = SHA1_HASH(source ip address + source port + random number)
sha1.Reset();
sha1.Update( ( unsigned char* ) & systemAddress.binaryAddress, sizeof( systemAddress.binaryAddress ) );
sha1.Update( ( unsigned char* ) & systemAddress.port, sizeof( systemAddress.port ) );
sha1.Update( ( unsigned char* ) & ( newRandomNumber ), sizeof(newRandomNumber) );
sha1.Final();
The hashed SYN-Cookie, and the public keys e and modulus are sent back to the system that wants to connect.
The system that wants to connect gets the packet and calls RakPeer::SecuredConnectionConfirmation()
It copies out e and the modulus, and compares that to the known public key, if one was set. If it doesn't match, it aborts the connection process.
The system that wants to connect creates a 20 byte random number. It writes this number to the message, then encrypts it with the public key
memcpy( message, randomNumber, sizeof( randomNumber ) );
privKeyPncrypt.setPublicKey( n, RAKNET_RSA_FACTOR_LIMBS, e );
privKeyPncrypt.encrypt( encryptedMessage, message );
The other systems AESKey is XORED with our random number
// Take the remote system's AESKey (SynCookie) and XOR with our random number.
for ( j = 0; j < 16; j++ )
remoteSystem->AESKey[ j ] = data[ 1 + j ] ^ randomNumber[ j ];
Lastly, it replies to the other system
// c2s RSA(random number), same syn-cookie
Now on the server, when it gets that packet, it hashes the SYN-Cookie
// Hash the SYN-Cookie
// s2c syn-cookie = SHA1_HASH(source ip address + source port + random number)
sha1.Reset();
sha1.Update( ( unsigned char* ) & systemAddress.binaryAddress, sizeof( systemAddress.binaryAddress ) );
sha1.Update( ( unsigned char* ) & systemAddress.port, sizeof( systemAddress.port ) );
sha1.Update( ( unsigned char* ) & ( newRandomNumber ), 20 );
sha1.Final();
This is checked against the packet, to make sure this packet is from the same system we have been talking to all along
// Confirm if
//syn-cookie ?= HASH(source ip address + source port + last random number)
//syn-cookie ?= HASH(source ip address + source port + current random number)
If the hash (using SHA1) passes, then the connection is accepted.
// On connection accept, AES key is c2s RSA_Decrypt(random number) XOR s2c syn-cookie
// Get the random number from the other system first
memcpy( encryptedMessage, data + 1 + 20, sizeof( encryptedMessage ) );
// Use rsaCrypt to decrypt this random number
rsacrypt.decrypt( message, encryptedMessage );
The 16 byte AES KEY is read out, XORED with the decrypted random number
// Save the AES key
for ( i = 0; i < 16; i++ )
AESKey[ i ] = data[ 1 + i ] ^ ( ( unsigned char* ) ( message ) ) [ i ];

See Also

Index