Counter-Strike: Global Offensive Network Channel Encryption

From Valve Developer Community
Jump to: navigation, search
English (en)Español (es)Translate (Translate)
Underlinked - Logo.png
This article needs more Wikipedia icon links to other articles to help Wikipedia icon integrate it into the encyclopedia. Please help improve this article by adding links Wikipedia icon that are relevant to the context within the existing text.
January 2024

This page outlines the basics of how CS:GO network channel encryption works, and how 3rd parties can provide client software and server plugins to ensure that communications between game client and game server use network channel encryption keys. CS:GO update version 1.35.6.0 or above is required to use the 3rd party network channel encryption keys protocol.

Counter-Strike: Global Offensive

CS:GO network channel communication can be encrypted with a 16-byte binary key. Initial "connectionless" packets starting with four bytes 0xFF 0xFF 0xFF 0xFF and followed by either A2S_GETCHALLENGE 'q' byte or C2S_CONNECT 'k' byte and are always sent unencrypted in the clear. After the client connection is accepted by the game server, the network channel is established and must be used. If the network channel must use the encryption key then the key configured on the client side and on the server side must match for communication to succeed.

Exchanging Encryption Keys

3rd parties can implement encryption keys management for their players in many different ways, and can control which clients require encryption keys and which clients can connect without an encryption key. If the client requires an encryption key then typically 3rd party backend will have a secure connection with client software to inform the 3rd party client software in a secure way of the required encryption key. Secure implementation of backend to client protocol is required because this channel is the most likely target for man-in-the-middle attack by malicious clients with the goal of extracting the encryption key. 3rd party client software will forward the encryption key to the CS:GO game client using machine-local communication. At the same time 3rd party backend will communicate the client's encryption key to the 3rd party game server that will be hosting the match for this client. By the time the client connects to the game server they can both use the same established encryption key and benefit from encrypted network channel communication.

Setting Client Encryption Key

The following code shows an example of setting the CS:GO game client encryption key from 3rd party client software. The format of the payload is the remote server address for which the client should use the key, the angle symbol '>', the 32-bit key index to use which must contain non-zero in the upper word ( ( idx & 0xFFFF0000 ) != 0 ) to avoid conflicting with built-in encryption keys, the 16-byte plaintext key that the client will use to encrypt messages, and non-zero amount up to 256 bytes of 3rd party defined payload that client will send to the server over the wire unencrypted so shouldn't contain the encryption key because man-in-the-middle attacker will be able to extract it.

void TestSettingEngineCryptKey()
{
                const HWND hwndEngine = FindWindow( "Valve001", NULL );
                if ( !hwndEngine )
                {
                                ::MessageBox( NULL, "Failed to find CS:GO HWND", "Encryption Key", MB_OK | MB_ICONEXCLAMATION );
                                return;
                }

                //
                // Data description for encrypted channel
                //
                char const * szServerAddressPrefix = "172.16.10.197:27016>";
                int32 nClientKeyIndex = 0x00010102;
                byte arrPlainTextClientEncryptionKey[16] = {
                                0x01, 0x02, 0x03, 0x04, 0x15, 0x16, 0x17, 0x00,
                                0x10, 0x20, 0x30, 0x40, 0xA8, 0xB8, 0xC8, 0x00
                };
                byte arrEncryptedCookieToSendToServerOverWire[64] = {
                                0xDD, 0xEE, 0xFF, 0x00, 0x15, 0x16, 0x17, 0x00,
                                0xAA, 0xBB, 0xCC, 0x00, 0xA8, 0xB8, 0xC8, 0x00,
                                0xDD, 0xEE, 0xFF, 0x00, 0x15, 0x16, 0x17, 0x00,
                                0xAA, 0xBB, 0xCC, 0x00, 0xA8, 0xB8, 0xC8, 0x00,
                                0xDD, 0xEE, 0xFF, 0x00, 0x15, 0x16, 0x17, 0x00,
                                0xAA, 0xBB, 0xCC, 0x00, 0xA8, 0xB8, 0xC8, 0x00,
                                0xDD, 0xEE, 0xFF, 0x00, 0x15, 0x16, 0x17, 0x00,
                                0x00, 0xBB, 0xCC, 0x00, 0xA8, 0xB8, 0xC8, 0x00
                };

                //
                // Fill out the data structure to send to the engine.
                //
                COPYDATASTRUCT copyData;
                copyData.dwData = 0x43525950; // CRYP
                copyData.cbData = strlen( szServerAddressPrefix )
                                + sizeof( nClientKeyIndex )
                                + sizeof( arrPlainTextClientEncryptionKey )
                                + sizeof( arrEncryptedCookieToSendToServerOverWire );
                byte *pbBufferForEngine = ( byte * ) stackalloc( copyData.cbData );
                copyData.lpData = pbBufferForEngine;

                // Put all the data into the buffer
                V_memcpy( pbBufferForEngine,
                                szServerAddressPrefix, strlen( szServerAddressPrefix ) );
                V_memcpy( pbBufferForEngine + strlen( szServerAddressPrefix ),
                                &nClientKeyIndex, sizeof( nClientKeyIndex ) );
                V_memcpy( pbBufferForEngine + strlen( szServerAddressPrefix ) + sizeof( nClientKeyIndex ),
                                arrPlainTextClientEncryptionKey, sizeof( arrPlainTextClientEncryptionKey ) );
                V_memcpy( pbBufferForEngine + strlen( szServerAddressPrefix ) + sizeof( nClientKeyIndex ) + sizeof( arrPlainTextClientEncryptionKey ),
                                arrEncryptedCookieToSendToServerOverWire, sizeof( arrEncryptedCookieToSendToServerOverWire ) );

                // Send the message to the engine
                if ( !SendMessageA( hwndEngine, WM_COPYDATA, 0, ( LPARAM ) &copyData ) )
                {
                                ::MessageBox( NULL, "CS:GO Engine Denied Request", "Encryption Key", MB_OK | MB_ICONEXCLAMATION );
                                return;
                }

                ::MessageBox( NULL, "CS:GO Engine Accepted Request", "Encryption Key", MB_OK | MB_ICONEXCLAMATION );
}

Setting Server Encryption Key

A 3rd party server plugin is required to set the game server encryption key, and game server must run with -externalnetworkcryptkey command line parameter to activate the feature. Plugin must implement IServerPluginCallbacks interface v4 "ISERVERPLUGINCALLBACKS004". The v4 interface has been extended with two additional methods compared to v3: BNetworkCryptKeyCheckRequired and BNetworkCryptKeyValidate. Full declaration of C++ interface is included here.

#define INTERFACEVERSION_ISERVERPLUGINCALLBACKS				"ISERVERPLUGINCALLBACKS004"
abstract_class IServerPluginCallbacks
{
public:
	// Initialize the plugin to run
	// Return false if there is an error during startup.
	virtual bool			Load(	CreateInterfaceFn interfaceFactory, CreateInterfaceFn gameServerFactory  ) = 0;

	// Called when the plugin should be shutdown
	virtual void			Unload( void ) = 0;

	// called when a plugins execution is stopped but the plugin is not unloaded
	virtual void			Pause( void ) = 0;

	// called when a plugin should start executing again (sometime after a Pause() call)
	virtual void			UnPause( void ) = 0;

	// Returns string describing current plugin.  e.g., Admin-Mod.  
	virtual const char     *GetPluginDescription( void ) = 0;

	// Called any time a new level is started (after GameInit() also on level transitions within a game)
	virtual void			LevelInit( char const *pMapName ) = 0;

	// The server is about to activate
	virtual void			ServerActivate( edict_t *pEdictList, int edictCount, int clientMax ) = 0;

	// The server should run physics/think on all edicts
	virtual void			GameFrame( bool simulating ) = 0;

	// Called when a level is shutdown (including changing levels)
	virtual void			LevelShutdown( void ) = 0;

	// Client is going active
	virtual void			ClientActive( edict_t *pEntity ) = 0;

	// Client is fully connected ( has received initial baseline of entities )
	virtual void			ClientFullyConnect( edict_t *pEntity ) = 0;

	// Client is disconnecting from server
	virtual void			ClientDisconnect( edict_t *pEntity ) = 0;
	
	// Client is connected and should be put in the game
	virtual void			ClientPutInServer( edict_t *pEntity, char const *playername ) = 0;

	// Sets the client index for the client who typed the command into their console
	virtual void			SetCommandClient( int index ) = 0;

	// A player changed one/several replicated cvars (name etc)
	virtual void			ClientSettingsChanged( edict_t *pEdict ) = 0;

	// Client is connecting to server ( set retVal to false to reject the connection )
	//	You can specify a rejection message by writing it into reject
	virtual PLUGIN_RESULT	ClientConnect( bool *bAllowConnect, edict_t *pEntity, const char *pszName, const char *pszAddress, char *reject, int maxrejectlen ) = 0;

	// The client has typed a command at the console
	virtual PLUGIN_RESULT	ClientCommand( edict_t *pEntity, const CCommand &args ) = 0;

	// A user has had their network id setup and validated 
	virtual PLUGIN_RESULT	NetworkIDValidated( const char *pszUserName, const char *pszNetworkID ) = 0;

	// This is called when a query from IServerPluginHelpers::StartQueryCvarValue is finished.
	// iCookie is the value returned by IServerPluginHelpers::StartQueryCvarValue.
	// Added with version 2 of the interface.
	virtual void			OnQueryCvarValueFinished( QueryCvarCookie_t iCookie, edict_t *pPlayerEntity, EQueryCvarValueStatus eStatus, const char *pCvarName, const char *pCvarValue ) = 0;

	// added with version 3 of the interface.
	virtual void			OnEdictAllocated( edict_t *edict ) = 0;
	virtual void			OnEdictFreed( const edict_t *edict  ) = 0;	

	//
	// Allow plugins to validate and configure network encryption keys (added in Version 4 of the interface)
	// Game server must run with -externalnetworkcryptkey flag, and 3rd party client software must set the
	// matching encryption key in the client game process.
	//

	// BNetworkCryptKeyCheckRequired allows the server to allow connections from clients or relays that don't have
	// an encryption key. The function must return true if the client encryption key is required, and false if the client
	// is allowed to connect without an encryption key. It is recommended that if client wants to use encryption key
	// this function should return true to require it on the server side as well.
	// Any plugin in the chain that returns true will flag connection to require encryption key for the engine and check
	// with other plugins will not be continued.
	// If no plugin returns true to require encryption key then the default implementation will require encryption key
	// if the client wants to use it.
	virtual bool			BNetworkCryptKeyCheckRequired( uint32 unFromIP, uint16 usFromPort, uint32 unAccountIdProvidedByClient,
		bool bClientWantsToUseCryptKey ) = 0;

	// BNetworkCryptKeyValidate allows the server to validate client's over the wire encrypted payload cookie and return
	// false if the client cookie is malformed to prevent connection to the server. If this function returns true then
	// the plugin allows the client to connect with the encryption key, and upon return the engine expects the plugin
	// to have copied 16-bytes of client encryption key into the buffer pointed at by pbPlainTextKeyForNetChan. That key
	// must match the plaintext key supplied by 3rd party client software to the client game process, not the client cookie
	// transmitted unencrypted over the wire as part of the connection packet.
	// Any plugin in the chain that returns true will stop evaluation of other plugins and the 16-bytes encryption key
	// copied into pbPlainTextKeyForNetchan will be used. If a plugin returns false then evaluation of other plugins will
	// continue and buffer data in pbPlainTextKeyForNetchan will be preserved from previous calls.
	// If no plugin returns true and the encryption key is required then the client connection will be rejected with
	// an invalid certificate error.
	virtual bool			BNetworkCryptKeyValidate( uint32 unFromIP, uint16 usFromPort, uint32 unAccountIdProvidedByClient,
		int nEncryptionKeyIndexFromClient, int numEncryptedBytesFromClient, byte *pbEncryptedBufferFromClient,
		byte *pbPlainTextKeyForNetchan ) = 0;
};

An example implementation of the server plugin is provided here.

bool CServerPluginExample::BNetworkCryptKeyCheckRequired( uint32 unFromIP, uint16 usFromPort, uint32 unAccountIdProvidedByClient,
		bool bClientWantsToUseCryptKey )
{
                // Example server plugin implementation always requires encryption
                return true;
}
bool CServerPluginExample::BNetworkCryptKeyValidate( uint32 unFromIP, uint16 usFromPort, uint32 unAccountIdProvidedByClient,
                int nEncryptionKeyIndexFromClient, int numEncryptedBytesFromClient, byte *pbEncryptedBufferFromClient,
                byte *pbPlainTextKeyForNetchan )
{
                //
                // Example server plugin implementation:
                //
                byte arrPlainTextClientEncryptionKey[ 16 ] = {
                                0x01, 0x02, 0x03, 0x04, 0x15, 0x16, 0x17, 0x00,
                                0x10, 0x20, 0x30, 0x40, 0xA8, 0xB8, 0xC8, 0x00
                };
                V_memcpy( pbPlainTextKeyForNetchan, arrPlainTextClientEncryptionKey, 16 );
                return true;
}

External links

Wikipedia - Letter.png
This article has not been added to any content Wikipedia icon categories. Please help out by Wikipedia icon adding categories.
January 2024