Advertisement

MMO realtime with ERLANG - What protocols?

Started by July 10, 2018 10:56 AM
4 comments, last by hplus0603 6 years, 4 months ago

Hello,

i'm currently working on a mobile MMO game. It's kinda RTS but not really, because at this point, there are no units/players you would see moving around. (Imagine Minecraft without players or animals, just seeing if a block is set, machine is working or it's current state)
But to provide nearly real-time updates to the players, I thought, using ERLANG with a headless protocol might be the best solution here.

My thoughts:
- Every transmission with sensitive data needs to be secure (especially the players personal data)
- To avoid overhead through TCP and handshaking every request, a new/existing player only does this time and resource expensive steps at registration or login. The given session is, lets say, 12h valid and allows the player to receive/send data.
- One ERLANG module is listening on port 80 (kind of an ERLANG webserver), to serve HTML pages (anyone can access)

Primitive concept sketch:
mmo_servermodel.png.b685b3e8ea61aa82e578830ce5827647.png

 

Because I'm lacking experience, especially which protocols to choose, I want to ask for your opinions on that.
I looked into several protocols like UDP, RUDP or DTLS, TCP or SRTP and tried to apply them that way, what made the most sense to me (colored in the image). But not really sure.

Thanks in advance!

You don't want to use port 80 unless you actually speak HTTP. Doing so will confuse "smart" routers that try to interpret the protocol.

For real-time-ish data, that needs to be encrypted, the best solution is to use HTTP/2.0 with a protocol upgrade. This will be bidirectional, support server push, and be encrypted. For example, the "gRPC" protobuf-based protocol from google uses this mechanism. In fact, I would recommend gRPC, except I don't know if there are any good libraries for Erlang for it.

Another option is to use the Websockets protocol, which you can run over HTTP/1.1. As long as you run it over HTTPS (on port 443) data will still be encrypted. Websockets lets you frame payloads in JSON objects, which is good, but is going to use more bandwidth than a binary based protocol like protobuf/gRPC.

The third option, and the most work, is to use HTTPS to establish a session, and exchange a shared secret (key) as part of that session. Then send data with UDP, and encrypt the payload with the key. Make sure to send a new, random, IV at the beginning of each packet, and you'd also want some session ID up front to know which key to apply for decryption. This is a lot more work to get going, but it allows you to use UDP (if low latency jitter really matters)

All of these require that you use HTTPS in some form. The easiest way to use HTTPS is to set up certificates with Let's Encrypt, which are free, and last three months; there are scripts to auto-renew these certificates, but again I don't know whether they exist specifically for Erlang or not.

enum Bool { True, False, FileNotFound };
Advertisement

EDIT: I don't think any of this helps for mobile dev, but it could be interesting to others so I'll leave it.

I'm doing the "third option" written above. If you like c++, Enet is good for the udp connections, cpp-httplib is good for the https server and client, and OpenSSL is good for the encryption, decryption of udp packets.
http://enet.bespin.org/
https://github.com/yhirose/cpp-httplib
https://www.openssl.org/source/

Here is a class I use in my game to encrypt and decrypt data that goes in my udp packet.
 


#pragma once
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <string>
#include <iostream>

//if debug isn't defined, turns into a no-op
template <typename ...T>
void debug(std::string file, int line, T&& ...args)
{
	#ifdef _DEBUG
		unsigned int index = file.find_last_of('\\');
		file[index] = '/';
		index = file.find_last_of('\\', --index);
		std::string finalfile = file.substr(index);
		finalfile[0] = '/';
		std::cout << finalfile << ":" << line << " ";
		using expander = int[];
		(void)expander {
			0, (void(std::cout << std::forward<T>(args)), 0)...
		};
		std::cout << std::endl;
	#endif
}

//use this, it outputs the file and line the output was made from!
#define debug(...) debug(__FILE__, __LINE__, __VA_ARGS__)

namespace ga {
	class cryptinfo {
		EVP_CIPHER_CTX* ectx;
		EVP_CIPHER_CTX* dctx;

		void fillectx()
		{
			debug("making new encryption ctx");
			if (!(ectx = EVP_CIPHER_CTX_new())) handleErrors();
			debug("setting ctx encryption engine to EVP_aes_256_cbc()");
			if (1 != EVP_EncryptInit_ex(ectx, EVP_aes_256_cbc(), NULL, NULL, NULL)) handleErrors();
			debug("initializing ctx with key and iv", key, " ", iv);
			if (1 != EVP_EncryptInit_ex(ectx, NULL, NULL, key, iv)) handleErrors();
			debug("making new encryption ctx good");
		}

		void filldctx()
		{
			debug("making new decryption ctx");
			if (!(dctx = EVP_CIPHER_CTX_new())) handleErrors();
			debug("setting ctx decryption engine to EVP_aes_256_cbc()");
			if (!EVP_DecryptInit_ex(dctx, EVP_aes_256_cbc(), NULL, NULL, NULL)) handleErrors();
			debug("initializing ctx with key and iv");
			if (!EVP_DecryptInit_ex(dctx, NULL, NULL, key, iv)) handleErrors();
			debug("making new decryption ctx good");
		}

		void filldectx()
		{
			debug("make both encryption and decryption ctx at the same time");
			fillectx();
			filldctx();
			debug("make both good");
		}

		void handleErrors(void)
		{
			unsigned long errCode;
			debug("An error has occured in encryption/decryption");
			while (errCode = ERR_get_error())
				debug(ERR_error_string(errCode, NULL));
			debug("should we really exit here?");
			system("pause");
			abort();
		}
	
		friend char* operator<<(char* os, cryptinfo  &e);
		friend char* operator>>(char* os, cryptinfo  &e);
		friend std::ostream& operator<<(std::ostream &os, const  cryptinfo  &e);
		friend std::istream& operator>>(std::istream &os, cryptinfo  &e);

	public:
		uint8_t key[33];
		uint8_t iv[AES_BLOCK_SIZE + 1];

		void fillkeyiv()
		{
			debug("creating new key and iv for a cryptinfo");
			debug("creating key");
			RAND_bytes(key, sizeof(key) -  1);
			key[32] = '\0'; //always null terminate our data so we can toString() (hope that works out lol)
			debug("creating iv");
			RAND_bytes(iv, sizeof(iv) - 1);
			iv[16] = '\0';
			debug("creating new key and iv good");
		}

		int encrypt(std::string& encrypt_in, std::string& encrypt_out, unsigned char* aad = 0, int aad_len = 0)
		{
			debug("encrypt called");
			fillectx();
			debug("encrypting string ", encrypt_in);
			int len;
			int ciphertextlen;
			unsigned char encrypt_in_buf[48];
			debug("writing encrypted bytes to ciphertext");
			if (1 != EVP_EncryptUpdate(ectx, encrypt_in_buf, &len, (const unsigned char*)encrypt_in.c_str(), encrypt_in.size())) handleErrors();
			ciphertextlen = len;
			debug("writing final bytes to ciphertext");
			if (1 != EVP_EncryptFinal_ex(ectx, encrypt_in_buf + ciphertextlen, &len)) handleErrors();
			ciphertextlen += len;
			encrypt_in_buf[ciphertextlen] = '\0';
			debug("free ctx obj");
			EVP_CIPHER_CTX_free(ectx);
			encrypt_out = (const char*)encrypt_in_buf;
			debug("encrypted string ", encrypt_out);
			debug("encrypt good");
			return ciphertextlen;
		}

		int decrypt(std::string& ciphertext, std::string& plainstr, unsigned char *aad = 0, int aad_len = 0)
		{
			debug("decrypt called");
			filldctx();
			debug("decrypting string ", ciphertext);
			unsigned char buf[48];
			debug("writing unencrypted bytes to plaintext");
			int len;
			if (!EVP_DecryptUpdate(dctx, buf, &len, (const unsigned char*)ciphertext.c_str(), ciphertext.size())) handleErrors();
			debug("writing final bytes to plaintext");
			int oldlen = len;
			int ret = EVP_DecryptFinal_ex(dctx, buf + oldlen, &len);
			oldlen += len;
			buf[oldlen] = '\0';
			debug("freeing ctx obj");
			EVP_CIPHER_CTX_free(dctx);
			plainstr = (const char*)buf;
			debug("the decrypted string ", plainstr);
			if (ret == 1)
			{
				debug("decryption good");
				return oldlen;
			}
			else
			{
				debug("EVP Decryption Error! check != -1");
				debug("decryption failed, but the program could be configured to work anyway, sometimes the decrypted data comes out fine but it throws errors anyway, probably because something is coming up as insecure");
				return -1;
			}
		}

		std::string keyToString()
		{
			return (const char*)key;
		}

		std::string ivToString()
		{
			return (const char*)iv;
		}

		void keyFromString(std::string key)
		{
			strcpy((char*)this->key, key.c_str());
		}

		void ivFromString(std::string iv)
		{
			strcpy((char*)this->iv, iv.c_str());
		}
	};

	std::ostream& operator<<(std::ostream &os, const  cryptinfo  &e)
	{
		debug("writing cryptinfo key and iv to stream");
		debug("writing key");
		for (int i = 0; i < 32; i++)
		{
			os << e.key[i];
		}
		os << e.key[32];
		debug("key written");
		debug("writing iv");
		for (int i = 0; i < AES_BLOCK_SIZE; i++)
		{
			os << e.iv[i];
		}
		os << e.iv[16];
		debug("iv written");
		debug("writing cryptinfo to stream good");
		return os;
	}

	char* operator<<(char* os, cryptinfo  &e)
	{
		debug("writing cryptinfo key and iv to buffer");
		debug("writing key");
		for (int i = 0; i < 33; i++)
		{
			os[i] = e.key[i];
		}
		debug("key written");
		debug("writing iv");
		for (int i = 33, j = 0; i < 33 + AES_BLOCK_SIZE + 1; ++i, ++j)
		{
			os[i] = e.iv[j];
		}
		debug("iv written");
		debug("writing cryptinfo to buffer good");
		return os;
	}

	std::istream& operator>>(std::istream &os, cryptinfo  &e)
	{
		debug("reading key and iv from stream into cryptinfo");
		debug("reading key");
		for (int i = 0; i < 33; i++)
		{
			os >> e.key[i];
		}
		debug("key read");
		debug("reading iv");
		for (int i = 0; i < AES_BLOCK_SIZE + 1; i++)
		{
			os >> e.iv[i];
		}
		debug("iv read");
		debug("reading key and iv from stream good");
		return os;
	}

	char* operator>>(char* os, cryptinfo  &e)
	{
		debug("reading key and iv from buffer into cryptinfo");
		debug("reading key");
		for (int i = 0; i < 33; i++)
		{
			e.key[i] = os[i];
		}
		debug("key read");
		debug("reading iv");
		for (int i = 33; i < 33 + AES_BLOCK_SIZE + 1; i++)
		{
			e.iv[i] = os[i];
		}
		debug("iv read");
		debug("reading key and iv from buffer good");
		return os;
	}
};

 

hello

On 7/10/2018 at 11:21 AM, hplus0603 said:

 Make sure to send a new, random, IV at the beginning of each packet

I'm doing this for my game, can you help me out? I don't understand why I need a new IV every packet. I use the same key and iv the whole session. Each client gets their own key and iv sent over https. Won't it throw errors if I change their IV lol? What is IV for lol. Ty for your help.

hello

I don't understand why I need a new IV every packet.

This goes to cryptographic protocol design.

The first parts of your encrypted payload, when decrypted, will likely be quite guessable, because it will contain things like sequence numbers, object IDs, packet type IDs, and so forth, that all take a fairly limited set of values.

This means that, if someone has a large number of packets encrypted with the same key, and know enough about the first 8-16 bytes of the payload, when you use a single IV for every packet they will be able to use differential cryptoanalysis to break the session, recover the key, and then decrypt the entire session.

However, when you use a different IV for each packet, the differential analysis doesn't work, and your key can stay secure. The problem is that the receiving end needs the IV to be able to decrypt each packet, hence the IV is generally prepended to the packet payload. As opposed to the key, the IV doesn't need to be secret; it just needs to be sufficiently different between packets.

An alternative, that "should" be as good, but has not had the seal of approval by crypto designers, is to use a good random number generator to generate 16 bytes of random data, and put that first in your encrypted payload. Because there is no similarity to look for, and assuming you don't use "ECB" mode for your cypher (you REALLY shouldn't!) then this will similarly confuse the differential attacks. The receiver would then decrypt the payload, throw away the first 16 random bytes, and recover the message you sent. This has the same overhead as sending the IV in the packet, and accomplishes the same thing, but is not the "recommended" way by cryptographic experts.

There exists a protocol called DTLS which is approximately TLS/SSL over UDP. If you don't want to implement your own, you can use that. It may have slightly higher overhead than a to-the-metal implementation, because it also provides additional features and flexibility, just like TLS has overhead on top of TCP, and provides additional features.

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement