How To Implement Encryption?

So, I’ve spent a little time trying to take my game across the multiplayer threshold, and web development has always thrown a wrench in my gears. Between the slew of services available, the new mindset needed to work in that framework, the multiple programming languages needed - it’s always seemed like a nightmare.

With that said, I may be doing this all wrong, might be reinventing the wheel, maybe I’m spot on, who knows. However, I intend on making a competitive game, and cheating is not cool, so I decided I would encrypt all transmissions between the client and server. Problem is, I’m not sure how.

I made this script from what I could glean around the web, a little GitHub copilot help, and some best coding practices picked up from CodeMonkey (Hugo), Sebastian, and Brackeys. Without further ado, here it is –

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

namespace Benevolence.Utilities
{
    /// <summary>
    /// A static class to implement different kinds of encryption to a project
    /// </summary>
    public static class Cryptography
    {
        // RSA ENCRYPTION
        public static class RSA{
            /// <summary>
            /// Generates a new RSA key pair.
            /// </summary>
            /// <param name="publicKey">The generated public key.</param>
            /// <param name="privateKey">The generated private key.</param>
            public static void GenerateKeys(out string publicKey, out string privateKey)
            {
                using (var rsa = new RSACryptoServiceProvider(2048))
                {
                    publicKey = rsa.ToXmlString(false);
                    privateKey = rsa.ToXmlString(true);
                }
            }

            /// <summary>
            /// Encrypts the specified data using the specified public key.
            /// </summary>
            /// <param name="publicKey">The public key to use for encryption.</param>
            /// <param name="data">The data to encrypt.</param>
            /// <returns>The encrypted data.</returns>
            public static byte[] Encrypt(string publicKey, string data)
            {
                byte[] bytesToEncrypt = Encoding.UTF8.GetBytes(data);

                using (var rsa = new RSACryptoServiceProvider(2048))
                {
                    rsa.FromXmlString(publicKey);
                    return rsa.Encrypt(bytesToEncrypt, false);
                }
            }

            /// <summary>
            /// Decrypts the specified data using the specified private key.
            /// </summary>
            /// <param name="privateKey">The private key to use for decryption.</param>
            /// <param name="encryptedData">The data to decrypt.</param>
            /// <returns>The decrypted data.</returns>
            public static string Decrypt(string privateKey, byte[] encryptedData)
            {
                using (var rsa = new RSACryptoServiceProvider(2048))
                {
                    rsa.FromXmlString(privateKey);
                    byte[] bytesDecrypted = rsa.Decrypt(encryptedData, false);
                    return Encoding.UTF8.GetString(bytesDecrypted);
                }
            }
        }

        // AES ENCRYPTION
        public static class AES{
            /// <summary>
            /// Taking in a generic type T, this method outputs a byte array
            /// of that data encrypted with AES encryption
            /// </summary>
            /// <typeparam name="T">Any Type</typeparam>
            /// <param name="input">The data to be encrypted</param>
            /// <param name="key">The AES Key needed for encryption</param>
            /// <param name="iv">The AES IV needed for encryption</param>
            /// <returns>byte[] - The encrypted input</returns>
            public static byte[] Encrypt<T>(T input, byte[] key, byte[] iv)
            {
                try
                {
                    byte[] rawData;
                    BinaryFormatter binaryFormatter = new BinaryFormatter();
                    using (MemoryStream ms = new MemoryStream())
                    {
                        binaryFormatter.Serialize(ms, input);
                        rawData = ms.ToArray();
                    }
                    using (Aes aesAlg = Aes.Create())
                    {
                        aesAlg.Key = key;
                        aesAlg.IV = iv;

                        ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

                        using (MemoryStream msEncrypt = new MemoryStream())
                        {
                            using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                            {
                                csEncrypt.Write(rawData, 0, rawData.Length);
                                csEncrypt.FlushFinalBlock();
                                return msEncrypt.ToArray();
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogWarning("Encryption failed: " + ex.Message);
                    return null;
                }
            }

            /// <summary>
            /// Taking in a byte array of encrypted data, this returns
            /// a generic type T of the decrypted data
            /// </summary>
            /// <typeparam name="T">Any Type</typeparam>
            /// <param name="encryptedData">The data to be decrypted</param>
            /// <param name="key">The AES Key that was used to encrypt the data</param>
            /// <param name="iv">The IV that was used to encrypt the data</param>
            /// <returns><typeparamref name="T"/> - The decrypted data</returns>
            public static T Decrypt<T>(byte[] encryptedData, byte[] key, byte[] iv)
            {
                try
                {
                    using (Aes aesAlg = Aes.Create())
                    {
                        aesAlg.Key = key;
                        aesAlg.IV = iv;

                        ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                        using (MemoryStream msDecrypt = new MemoryStream(encryptedData))
                        {
                            using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                            {
                                BinaryFormatter binaryFormatter = new BinaryFormatter();
                                return (T)binaryFormatter.Deserialize(csDecrypt);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogWarning("Decryption failed: " + ex.Message);
                    return default(T);
                }
            }

            /// <summary>
            /// Uses the AES Library to create a new AES ALG and
            /// generate an AES Key and IV
            /// </summary>
            /// <param name="key">The byte array that represents the Key</param>
            /// <param name="iv">The byte array that represents the IV</param>

            public static void GenerateKeyAndIV(out byte[] key, out byte[] iv){
                using (Aes aesAlg = Aes.Create()){
                    aesAlg.GenerateKey();
                    aesAlg.GenerateIV();
                    key = aesAlg.Key;
                    iv = aesAlg.IV;
                }
            }
        }
    }
}

So, am I on the right track here? If so, what should I do next? Where do I store the keys so that the clients aren’t shipped with the network key in the build? How do I even do that, ship two different builds that are dependent on each other.

This is a broad spectrum, I wasn’t sure where to put it as it has a lot to do with multiplayer, but not the services offered by Unity, and more to do with how to script it then how to use Unity’s services.

– I am using Netcode For Gameobjects and I intend to use each of Unity’s Gaming Services and Cloud products as needed. –

You don’t need to. You can spare yourself the trouble. The way you approach it with AES encryption of byte arrays in a web client is likely going to be too costly in terms of CPU anyway - you only have this single thread for the time being. It’s too precious a resource to waste it on byte-mangling traffic where, for all we know, no alterations are currently occuring.

For cheating to happen in your game you first have to have a game that’s mildly popular. Get that done first. :wink:

And when cheating happens in your game, analyze what they are doing, fix that, push an update. Rinse and repeat. It’s way more likely players are going to exploit bugs or loopholes in your game first before they turn to realtime modification of transport-level data streams.

And when that DOES become an issue you can simply enable SSL secured communication in the transport. This would encrypt all your traffic, not just select pieces.

3 Likes

Thank you! You have saved me such a hassle. Now I’ll need to go open a thread over on general to discuss how to actually separate game logic between builds so the client builds don’t get sensitive parts of the application.

We’ve discussed that before. :wink:

Easiest with Unity 6 - use Multiplayer Roles. This allows for script and asset stripping.

1 Like

Um, you missed the point of what CodeSmile said.

Make the game first. Seriously.

If you’re concerned about the user ‘hacking your save files,’ or ‘cheating in your game,’ which is playing on their computer, just don’t be.

There’s nothing you can do about it. Nothing is secure, it is not your computer, it is the user’s computer.

If it must be secure, store it on your own server and run 100% of the game-critical logic ON YOUR SERVER, and have the user connect to download it.

Anything else is a waste of your time and the only person you’re going to inconvenience is yourself when you’re debugging the game and you have savegame errors. Work on your game instead.

Remember, it only takes one 12-year-old in Finland to write a script to read/write your game files and everybody else can now use that script. Read about Cheat Engine to see more ways you cannot possibly control this.

The bulk of the game is finished. I am just concerned, having never dealt with deployment and multiplayer before, with the “Am I dong this right?” aspect of things. I have a completed game loop, AI, UI, win/lose factors, inventory systems, animations, 3D models, cut scene renders… I mean it’s pretty much ready to go, just needing the all important other people to play with factor. Thus far, I’ve been using Agents in place of players, but now I’m trying to refactor for multiplayer. Rather than refactoring once for Netcode, then again because I missed something important with security, then again to handle server compliance… (Refactoring is inevitable, it’s a part of coding, but minimizing the need for it is a good thing I think).

Regardless, I am exploring options before I commit. Gathering data, looking for best practices, trying to commit to a framework so the game can be completed.

Identify your threat model:

  • what are the actors
  • what are their motivations
  • what are the attack surfaces
  • what are their attack vectors to those surfaces

Until you do that, all your attempts at “fixing it” are just security theater, much like the security line at the airport, which misses weapons all the time.

Keep in mind that while you may be attacked by amateur actors, they are likely to be using tools developed using inside information and code and methods developed by actual security professionals, so you are ALWAYS going to be at a massive disadvantage.

Security is an entirely different thing than Unity3D and game development in general: if you really care, hire a professional. Anything else is almost certainly a complete waste of time and a massive ongoing cost for you.

1 Like

LMAO! True words right there!

So the standard approach is basically release a game, rely on the fact that the average person can’t do anything anyways and those who can can’t be stopped?

I may not be the most experienced developer multiplayer wise, but from what I’ve seen, developers don’t use encryption on client, at least not in your case.
“Client is in the hands of the enemy”, it means that no matter what you do, no matter what encryption you use, it will always be hackable at some point. Game can always be disassembled and things will be hacked on client. If you have endpoint like “AddCoins”, no matter what you do, people will find way to send exact data to give themselves more free coins.
The best way to deal with that is to validate everything on server and design your server api around that. For example, you have endpoint “CompleteQuest(questId)”, then server will validate if player in fact completed the quest and give him reward.
On top of that I guess you can use some anti-cheat software, but server-side validation is the primary way to deal with cheaters.

2 Likes

tl;dr: Don’t trust the client.

3 Likes

Correct, and it’s because you don’t need to decrypt anything. You can simply modify the data before it reaches the encryption stage either by: (a) decompiling, modifying, and recompiling the code to modify the data the way you want, or (b) pulling the data out of memory, modifying it, and then pushing it back into memory.

Unity is very susceptible to (a) because the compilers (including IL2CPP) aren’t able to heavily optimize the code therefore it doesn’t get scrambled as thoroughly as it would with C++. An obfuscator can assist to a limited degree but only if the people trying to defeat it aren’t very competent.

Everything is susceptible to (b). Some companies try to limit it by scanning for the common tools (eg Cheat Engine) but that’s trivial to defeat too.

2 Likes

Also this actually becomes a privacy issue. When games start to spy on your system, something is wrong :slight_smile: Most anti cheat software does only check the own process memory space. Scanning the whole system is usually unnecessary and as you said, usually not worth it. Apart from that, depending on the game, it’s technically possible to run the game in a VM and have the memory manipulating on the host system. This makes it much harder to actually manipulate the game, but anti cheat software have no chance to reliably detect it as it’s essentially the virtual hardware that is “cheating”.

For competitive games where it’s important to prevent / avoid cheating, the majority of the game mechanics should be done on the server and not on the client. Many games actually use a somewhat hybrid approach. Example: Minecraft. In minecraft the movement of the player is local to each client and the clients simply tell the server where they are. That’s why there are fly hacks and stuff like that. Many servers implement some sort of serverside check and kick a player when something unusual is detected. Though they need to use generous thresholds to not get any false positives. However almost all other parts of the game are server-side mechanics. The actual world representation is on the server and all clients only have a copy. Manipulating the local representation has no effect on the other clients. Likewise inventories and NPCs are also controlled by the server.

There has been games which are essentially 100% server based. One example is my favourite FPS game: Quake3 / Quakelive. Here even the client movement is actually done on the server. The client only sends the input state alongside a timestamp. The server will actually calculate the round trip time to account for the ping delay. So when an input packet of a client arrives at the server, the server would essentially “backtrack” and do the movement change in the “past”. Though we talk about at max 100ms. Since the movement is not instant but you always get a ramp when you start moving, the server can simply calculate what the speed should be now, given the time passed since the input change. Such a setup is quite tricky and hard to get right. The actual visuals are updated by snapshots of the server and the client needs to interpolate between snapshots and may extrapolate a bit when a packet was dropped.

Even in a setup like Q3 people have managed to “cheat”. Since the server calculates your ping time based on the response delay, a client that actually has a good connection (say 1-3ms) can actively delay his responses and fake a bad connection like 70ms. This can give him an advantage as he actually sees the server in almost realtime but he’s able to “backdate” certain events by his fake ping time. Q3 is actually quite good in only sending data to the client that is actually necessary. So even a cheater with wallhack can’t see the other players unless they get into the “PVS” (potentially visible set). So such cheats are less effective.

2 Likes

Old thread bumped unnecessariily.