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. –