Cryptography - am I doing it right?

Started by
26 comments, last by Washu 9 years, 12 months ago

Thanks, that was a very thoughtful response! :)

I will spend the weekend trying to fork out some code that can implement this protocol. I think I already found relevant example code for C# here.

Couple of questions though:

You say the server's message is encrypted using the server's private key. Does this mean the client can decrypt it using the server's public key? o_O

So far I've been using automatic account generation for signups (because I didn't want to use my server as a web server as well), but that probably isn't a good idea.

Anyway, can I hash the hashes currently stored in the DB and generate public keys from them?

Advertisement

Then would it not be safer to encrypt everything in the first packet BUT the accountname with AES, so that if the accountname's corresponding hash in the DB can descramble the packet, that hash can be used to encrypt communication onwards? Why/why not?


So, are you saying that you're storing hash(password) in the database, and compute hash(password) on the client, and then encrypt the first packet, except for the account name, using hash(password) as key?

That's better, but it's not particularly good, because now, someone can just take a dump of your database (which contains hash(password) mapped to customer-name) and impostor any customer name, without knowing the password -- they don't need to know the password; they just need to know hash(password)!

As suggested above, if you use public/private keys, you're much better off. TLS/SSL does this for you.
enum Bool { True, False, FileNotFound };


I will spend the weekend trying to fork out some code that can implement this protocol.

I would still be concerned that it doesn't actually solve your security problems.

You are looking for an encrypted communications channel. That is analogous to hiring an armored vehicle delivery service. Very nice, you have protected one thing. On your server side you have a nice environment that you control. The armored car will be delivering the package to BadGuysRUs, and will be returning a package using the same armored vehicle service.

Let's assume you do implement a rock-solid TLS implementation. Communication between the two endpoints is moderately secure; TLS interception proxies are very common in corporate, academic, and other computing environments. Your connection with the corporate firewall is secure, they re-encode it with the company-mandated certificate, and it works just fine. Using your own encryption system is much more effort and is extremely difficult to get right, but it can get through a TLS proxy securely. That is just one item on a long list of security.
I have also heard security described as a fence. Communications encryption is a single fencepost. It is high and very strong, but still just a single post that can be easily walked around.
That kind of encryption does have many valid uses. If you are talking with a bank to check your account balance and do transfers, it makes it difficult to eavesdrop on the session to learn your bank balance or to add some extra money to a money transfer. The bank assumes that you are in charge of your end of the connection; they don't have protection against keyloggers or compromised web browsers or someone with a gun to your head. It only protects in-transit communication, but for banking that is the critical part.
Before trying to secure your application, you need to figure out exactly what attacks you are trying to prevent. Encrypting the communication reduces the risk of a "robbing the stagecoach" type of attack. While it is more common for financial institutions, that type of attack is extremely rare in games. The most common attack is aimed against the client itself; why attack the stagecoach when you have complete ownership of one endpoint? The bad guys can just attach a debugger or run inside a sandbox environment and have perfect access to everything.
You say the server's message is encrypted using the server's private key. Does this mean the client can decrypt it using the server's public key?

Yes, that is how public key cryptography works. It takes the public key to read what the private key has encrypted, and vice versa. There are different sytems, some use exponentiation modulo a prime (with huge numbers) and others use multiplication on an elliptic curve or Edwards curve or ... something else. The trick behind all these is that some operation is easy to perform and commutative, but hard to invert.

I will explain it using multiplication, the way it works with ECDH. Since both "public" and "private" start with "p" I will refer to the private key as the "secret" key, so it will have the identifier S. The public key will of course have the identifier P.

Person 1 and person 2 want to talk to each other. Both have agreed on some curve parameters and a deliberately chosen base point B. These are conventions that are necessary so it will work (everybody must do the same calculations, after all!). It does not matter a lot what they choose, as long as they agree.

Person 1 has the secret private key S1 and calculates his private key P1 = B*S1. Person 2 does the same, calculating P2=B*S2.

Now, the public keys are, well... public. That means you can tell the other person that key, or you could even put it on your website where everybody can look it up. So, person 1 knows person 2's public key (but not the private one) and vice versa.

Now person 1 can calculate S1*P2, which is really S1*(B*S2), and person 2 can calculate S2*P1, which is really S2*(B*S1). Thus, they both know B*S1*S2, which they can use as key for a symmetric cipher. None of them knows the other's P.

The trouble is making sure that when someone tells you "this is my public key" it's really the correct one. Someone who intercepts your traffic might exchange your partner's key with his own and do the same on the other end. This is called the Man-in-the-middle attack. One way to thwart that is by signing keys (works that way in TLS), but of course simply embedding the key into the game executable works just fine, too. The obvious disadvantage is that it isn't nearly as flexible or maintainable, but on the other hand it is dead simple. Since you already know the correct key, there is no way someone who is sitting on the network between you and the server could tell you the wrong one (well, unless they infect your computer with malware that patches the executable, bleh).


I will spend the weekend trying to fork out some code that can implement this protocol.

I would still be concerned that it doesn't actually solve your security problems.

You are looking for an encrypted communications channel. That is analogous to hiring an armored vehicle delivery service. Very nice, you have protected one thing. On your server side you have a nice environment that you control. The armored car will be delivering the package to BadGuysRUs, and will be returning a package using the same armored vehicle service.

Let's assume you do implement a rock-solid TLS implementation. Communication between the two endpoints is moderately secure; TLS interception proxies are very common in corporate, academic, and other computing environments. Your connection with the corporate firewall is secure, they re-encode it with the company-mandated certificate, and it works just fine. Using your own encryption system is much more effort and is extremely difficult to get right, but it can get through a TLS proxy securely. That is just one item on a long list of security.
I have also heard security described as a fence. Communications encryption is a single fencepost. It is high and very strong, but still just a single post that can be easily walked around.
That kind of encryption does have many valid uses. If you are talking with a bank to check your account balance and do transfers, it makes it difficult to eavesdrop on the session to learn your bank balance or to add some extra money to a money transfer. The bank assumes that you are in charge of your end of the connection; they don't have protection against keyloggers or compromised web browsers or someone with a gun to your head. It only protects in-transit communication, but for banking that is the critical part.
Before trying to secure your application, you need to figure out exactly what attacks you are trying to prevent. Encrypting the communication reduces the risk of a "robbing the stagecoach" type of attack. While it is more common for financial institutions, that type of attack is extremely rare in games. The most common attack is aimed against the client itself; why attack the stagecoach when you have complete ownership of one endpoint? The bad guys can just attach a debugger or run inside a sandbox environment and have perfect access to everything.

Again, this game is open source, so attackers already have an open playing field. My main concern is to try to prevent attacks of the type where "robbing the stagecoach" will allow men in the middle to get away with users' passwords. Now, I know that social engineering is a much more effective way to get users' passwords, but I can't do anything about that. I can however protect against MiM attacks.

I'm also not using TLS, I'm taking samoth's advice and trying to aim for Elliptic Curve Diffie Hellman. I'll probably post the code here for peer review, and if it doesn't look good, I'll just revert to hplus' advice and use AES.

hplus0603: It would actually be physically impossible for anyone to take a dump of my DB! :) At least as of yet, because I'm running the login server and city (game) server on the same box, and thus the DB isn't connected to the intarwebs.

Again, this game is open source, so attackers already have an open playing field.


That's a common misconception, and it's very dangerous. Open, auditable implementations makes software *more* secure. OpenSSL is a very secure TLS implementation, and has been open source since day 1.
enum Bool { True, False, FileNotFound };

That's better, but it's not particularly good, because now, someone can just take a dump of your database (which contains hash(password) mapped to customer-name) and impostor any customer name, without knowing the password -- they don't need to know the password; they just need to know hash(password)!

if they can already get a dump of your entire database, i think there are more problems to worry about then the person being able to simply act as an imposter of a customer.

Check out https://www.facebook.com/LiquidGames for some great games made by me on the Playstation Mobile market.

if they can already get a dump of your entire database, i think there are more problems to worry about then the person being able to simply act as an imposter of a customer


This is also a common, and dangerous, misconception about system security.
enum Bool { True, False, FileNotFound };

Ok, time for peer review!

Here's my code:


using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using GonzoNet;

namespace CryptoSample
{
    public class PacketHandlers
    {
        //Not sure why, but both keys must derive from the same master for decryption to work.
        private static CngKey MasterKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP256);
        public static ECDiffieHellmanCng ClientKey = new ECDiffieHellmanCng(MasterKey), 
            ServerKey = new ECDiffieHellmanCng(MasterKey);

        private static byte[] SessionKey, IV;
        private static Guid ChallengeResponse = Guid.NewGuid();
        public static Guid ClientNOnce = Guid.NewGuid();

        //This will be generated when the client sends the first packet.
        public static byte[] ClientIV;

        /// <summary>
        /// Helper method to generate an Initialization Vector for the client.
        /// </summary>
        public static void GenerateClientIV()
        {
            AesCryptoServiceProvider AES = new AesCryptoServiceProvider();
            AES.GenerateIV();
            ClientIV = AES.IV;
        }

        /// <summary>
        /// A client requested login.
        /// </summary>
        /// <param name="Client">NetworkClient instance.</param>
        /// <param name="Packet">ProcessedPacket instance.</param>
        public static void InitialClientConnect(NetworkClient Client, ProcessedPacket Packet)
        {
            Console.WriteLine("Server receives data - test 1");

            //AES is used to encrypt all further communication between client and server.
            AesCryptoServiceProvider AesCrypto = new AesCryptoServiceProvider();
            AesCrypto.GenerateKey();
            AesCrypto.GenerateIV();
            AES AesEncryptor = new AES(AesCrypto.Key, AesCrypto.IV);
            SessionKey = AesCrypto.Key;
            IV = AesCrypto.IV;

            Guid Nonce = new Guid(Packet.ReadPascalString());

            //Username would normally be used to lookup client's public key in DB (only time such a use is valid).
            string Username = Packet.ReadPascalString();
            ECDiffieHellmanPublicKey ClientPub = StaticStaticDiffieHellman.ImportKey("ClientPublic.dat");

            PacketStream EncryptedPacket = new PacketStream(0x02, 0);
            EncryptedPacket.WriteHeader();

            MemoryStream StreamToEncrypt = new MemoryStream();
            BinaryWriter Writer = new BinaryWriter(StreamToEncrypt);
            Writer.Write((byte)ChallengeResponse.ToByteArray().Length);
            Writer.Write(ChallengeResponse.ToByteArray(), 0, ChallengeResponse.ToByteArray().Length);
            Writer.Write((byte)SessionKey.Length);
            Writer.Write(SessionKey, 0, SessionKey.Length);
            Writer.Write((byte)IV.Length);
            Writer.Write(IV, 0, IV.Length);
            Writer.Flush();

            byte[] EncryptedData = StaticStaticDiffieHellman.Encrypt(ServerKey, ClientPub, Nonce.ToByteArray(), 
                StreamToEncrypt.ToArray());

            EncryptedPacket.WriteUInt16((ushort)(PacketHeaders.UNENCRYPTED + 1 + EncryptedData.Length));

            EncryptedPacket.WriteByte((byte)EncryptedData.Length);
            EncryptedPacket.Write(EncryptedData, 0, EncryptedData.Length);

            Client.Send(EncryptedPacket.ToArray());

            Console.WriteLine("Test 1: passed!");
        }

        /// <summary>
        /// Initial response from server to client.
        /// </summary>
        /// <param name="Client">A NetworkClient instance.</param>
        /// <param name="Packet">A ProcessedPacket instance.</param>
        public static void HandleServerChallenge(NetworkClient Client, ProcessedPacket Packet)
        {
            Console.WriteLine("Client receives encrypted data - test 2");

            byte[] PacketBuf = new byte[Packet.ReadByte()];
            Packet.Read(PacketBuf, 0, (int)PacketBuf.Length);

            ECDiffieHellmanPublicKey ServerPub = StaticStaticDiffieHellman.ImportKey("ServerPublic.dat");

            MemoryStream DecryptedStream = new MemoryStream(StaticStaticDiffieHellman.Decrypt(ClientKey, ServerPub, 
                ClientNOnce.ToByteArray(), PacketBuf));
            BinaryReader Reader = new BinaryReader(DecryptedStream);

            Guid ChallengeResponse = new Guid(Reader.ReadBytes(Reader.ReadByte()));
            SessionKey = Reader.ReadBytes(Reader.ReadByte());
            IV = Reader.ReadBytes(Reader.ReadByte());

            //Yay, we have key and IV, we can now start encryption with AES!
            AES AesEncryptor = new AES(SessionKey, IV);

            PacketStream EncryptedPacket = new PacketStream(0x03, 0);
            EncryptedPacket.WriteHeader();

            MemoryStream StreamToEncrypt = new MemoryStream();
            BinaryWriter Writer = new BinaryWriter(StreamToEncrypt);
            Writer.Write((byte)ChallengeResponse.ToByteArray().Length);
            Writer.Write(ChallengeResponse.ToByteArray(), 0, ChallengeResponse.ToByteArray().Length);

            //Encrypt data using key and IV from server, hoping that it'll be decrypted correctly at the other end...
            byte[] EncryptedData = AesEncryptor.Encrypt(StreamToEncrypt.ToArray());

            EncryptedPacket.WriteUInt16((ushort)(PacketHeaders.UNENCRYPTED + EncryptedData.Length + 1));
            EncryptedPacket.WriteByte((byte)EncryptedData.Length);
            EncryptedPacket.Write(EncryptedData, 0, EncryptedData.Length);

            Client.Send(EncryptedPacket.ToArray());

            Console.WriteLine("Test 2: passed!");
        }

        public static void HandleChallengeResponse(NetworkClient Client, ProcessedPacket Packet)
        {
            Console.WriteLine("Server receives challenge response - test 3");

            byte[] PacketBuf = new byte[Packet.ReadByte()];
            Packet.Read(PacketBuf, 0, (int)PacketBuf.Length);

            AES AesEncryptor = new AES(SessionKey, IV);
            MemoryStream DecryptedStream = new MemoryStream(AesEncryptor.Decrypt(PacketBuf));
            BinaryReader Reader = new BinaryReader(DecryptedStream);

            byte[] CResponseBuf = Reader.ReadBytes(Reader.ReadByte());
            Guid CResponse = new Guid(CResponseBuf);

            if (CResponse.CompareTo(ChallengeResponse) == 0)
                Console.WriteLine("Received correct challenge response, client was authenticated!");

            Console.WriteLine("Test 3: passed!");
        }
    }
}

ECDH:


using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.IO;

namespace CryptoSample
{
    /// <summary>
    /// Contains methods for en/decryption and ex/importing keys.
    /// From: http://stackoverflow.com/questions/3196297/minimal-message-size-public-key-encryption-in-net
    /// </summary>
    public static class StaticStaticDiffieHellman
    {
        private static Aes DeriveKeyAndIv(ECDiffieHellmanCng privateKey, ECDiffieHellmanPublicKey publicKey, byte[] nonce)
        {
            privateKey.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
            privateKey.HashAlgorithm = CngAlgorithm.Sha256;
            privateKey.SecretAppend = nonce;
            byte[] keyAndIv = privateKey.DeriveKeyMaterial(publicKey);
            byte[] key = new byte[16];
            Array.Copy(keyAndIv, 0, key, 0, 16);
            byte[] iv = new byte[16];
            Array.Copy(keyAndIv, 16, iv, 0, 16);

            Aes aes = new AesManaged();
            aes.Key = key;
            aes.IV = iv;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;

            return aes;
        }

        public static byte[] Encrypt(ECDiffieHellmanCng privateKey, ECDiffieHellmanPublicKey publicKey, byte[] nonce, byte[] data)
        {
            Aes aes = DeriveKeyAndIv(privateKey, publicKey, nonce);
            return aes.CreateEncryptor().TransformFinalBlock(data, 0, data.Length);
        }

        public static byte[] Decrypt(ECDiffieHellmanCng privateKey, ECDiffieHellmanPublicKey publicKey, byte[] nonce, byte[] encryptedData)
        {
            Aes aes = DeriveKeyAndIv(privateKey, publicKey, nonce);
            return aes.CreateDecryptor().TransformFinalBlock(encryptedData, 0, encryptedData.Length);
        }

        public static void ExportKey(string Path, ECDiffieHellmanPublicKey Key)
        {
            using (BinaryWriter Writer = new BinaryWriter(File.Create(Path)))
            {
                Writer.Write((byte)Key.ToByteArray().Length);
                Writer.Write(Key.ToByteArray());
            }
        }

        public static ECDiffieHellmanPublicKey ImportKey(string Path)
        {
            ECDiffieHellmanPublicKey Key;
            using (BinaryReader Reader = new BinaryReader(File.Open(Path, FileMode.Open)))
            {
                Key = ECDiffieHellmanCngPublicKey.FromByteArray(Reader.ReadBytes(Reader.ReadByte()), CngKeyBlobFormat.EccPublicBlob);
                return Key;
            }
        }
    }
}

AES:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CryptoSample
{
    using System;
    using System.Text;
    using System.Security.Cryptography;

    /// <summary>
    /// AES class is derived from the MSDN .NET CreateEncryptor() example
    /// http://msdn.microsoft.com/en-us/library/09d0kyb3.aspx
    /// </summary>
    class AES
    {
        // Symmetric algorithm interface is used to store the AES service provider
        private SymmetricAlgorithm AESProvider;

        // Crytographic transformers are used to encrypt and decrypt byte arrays
        private ICryptoTransform encryptor;
        private ICryptoTransform decryptor;

        /// <summary>
        /// Constructor for AES class that takes byte arrays for the key and IV
        /// </summary>
        /// <param name="key">Cryptographic key</param>
        /// <param name="IV">Cryptographic initialization vector</param>
        public AES(byte[] key, byte[] IV)
        {
            // Initialize AESProvider with AES service provider
            AESProvider = new AesCryptoServiceProvider();

            // Set the key and IV for AESProvider
            AESProvider.Key = key;
            AESProvider.IV = IV;

            // Initialize cryptographic transformers from AESProvider
            encryptor = AESProvider.CreateEncryptor();
            decryptor = AESProvider.CreateDecryptor();
        }

        /// <summary>
        /// Constructor for AES class that generates the key and IV from salted passwords
        /// </summary>
        /// <param name="keyPassword">Password used to generate the key</param>
        /// <param name="IVPassword">Password used to generate the IV</param>
        /// <param name="salt">Salt used to secure the passwords</param>
        public AES(string keyPassword, string IVPassword, string salt)
        {
            // Initialize AESProvider with AES service provider
            AESProvider = new AesCryptoServiceProvider();

            // Initialize a hasher with the default MD5 algorithm
            MD5 md5 = System.Security.Cryptography.MD5.Create();

            // Generate the key and IV for AESProvider from hashed, salted passwords
            AESProvider.Key = md5.ComputeHash(UnicodeEncoding.Unicode.GetBytes(keyPassword + salt));
            AESProvider.IV = md5.ComputeHash(UnicodeEncoding.Unicode.GetBytes(IVPassword + salt));

            // Initialize cryptographic transformers from AESProvider
            encryptor = AESProvider.CreateEncryptor();
            decryptor = AESProvider.CreateDecryptor();
        }

        /// <summary>
        /// Encrypts a string with AES
        /// </summary>
        /// <param name="plainText">String to encrypt</param>
        /// <returns>Encrypted string</returns>
        public string Encrypt(string plainText)
        {
            // Convert string to bytes
            byte[] plainBytes = UnicodeEncoding.Unicode.GetBytes(plainText);

            // Encrypt bytes
            byte[] secureBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);

            // Return encrypted bytes as a string
            return UnicodeEncoding.Unicode.GetString(secureBytes);
        }

        /// <summary>
        /// Encrypts a byte array with AES
        /// </summary>
        /// <param name="plainBytes">Data to encrypt</param>
        /// <returns>Encrypted byte array</returns>
        public byte[] Encrypt(byte[] plainBytes)
        {
            // Encrypt bytes
            return encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
        }

        /// <summary>
        /// Decrypts a string with AES
        /// </summary>
        /// <param name="secureText">Encrypted string to decrypt</param>
        /// <returns>Decrypted string</returns>
        public string Decrypt(string secureText)
        {
            // Convert encrypted string to bytes
            byte[] secureBytes = UnicodeEncoding.Unicode.GetBytes(secureText);

            // Decrypt bytes
            byte[] plainBytes = decryptor.TransformFinalBlock(secureBytes, 0, secureBytes.Length);

            // Return decrypted bytes as a string
            return UnicodeEncoding.Unicode.GetString(plainBytes);
        }

        /// <summary>
        /// Decrypts data with AES
        /// </summary>
        /// <param name="secureBytes">Encrypted data to decrypt</param>
        /// <returns>Decrypted data</returns>
        public byte[] Decrypt(byte[] secureBytes)
        {
            // Decrypt bytes
            return decryptor.TransformFinalBlock(secureBytes, 0, secureBytes.Length);
        }
    }
}

Comments, please! Is this safe?

Safe against what?

Also, quoting myself:

OpenSSL is a very secure TLS implementation


That sounds quite ironic :-) But the truth is that OpenSSL is still the least bad of the public implementations available. Kind of like how democracy is the worst form of government, except for all the alternatives.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement