Thursday, April 30, 2015

C# Chat Client v4 - Encryption of the Communication

In this post I want to improve the Chat Client a bit further and now, after using RSA to secure the authentication, also encrypt the communication with RSA. That means that every message is encrypted with RSA and can only be decrypted and read by the correct receiver.
Normally, e.g. in PHP, a symmetric key is chosen and encrypted by an asymmetric cipher, and the complete message than by the symmetric one. This is done because symmetric encryptions are notably faster. Here we just use RSA, so an asymmetric encryption, since chat messages are not that long anyway, maybe one could implement the symmetric encryption later.

Idea: When creating a new user now, additionally to the key pair required for the authentication, a new key pair is created for encrypting the communication. The private key is then also AES encrypted (with the password of the authenticaion) and saved on the local computer, the public one send to the database and there stored next the user. If one now wants to send a message to a different user, the client asks the server for the public key via a PHP script. Then the message is encrypted and send to the server. If the receiver fetches this, he decrypts it with his private key, which he made available by entering his login password.

So for the PHP server the encryption is invisible, in the scripts just some variable names changed, and in the database an extra field for the public key for message sending was added. But still one needs to pay a bit attention, especially to the coding of the data, therefore you find some information about the general usage of RSA with C# and PHP combined in the linked post. New is the script getsendkey.php, with which the correspoding public key for sending to a user can be queried:

<?php
include("connect.php");

session_start();

$User = $_POST["user"];

if(!isset($_SESSION['LoggedIn'])) {
     echo "Login first.";
     exit;
}

$stmt = $conn->prepare("SELECT sendkey FROM Users WHERE username = ?");
$stmt->bind_param("s", $User);
$stmt->execute();

$stmt->bind_result($sendkey);
$row = $stmt->fetch();

echo $sendkey;
?>

The complete C# code looks as follows:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Net;
using System.IO;
using System.Security.Cryptography;

namespace SimpleChatClient
{


    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        SimpleChatClient Client;
        bool LoggedIn = false;
        Dictionary<string, Chat> Chats = new Dictionary<string, Chat>(); // stores all active chats

        private void button1_Click(object sender, EventArgs e)
        {
            if (!LoggedIn)
            {
                Client = new SimpleChatClient();

                bool Login = (Client.Login(textBox1.Text, textBox2.Text));
                if (Login)
                {
                    // Login successful, enable chatting etc.
                    textBox3.Enabled = true;
                    button3.Enabled = true;
                    this.Height = 570;
                    tabControl1.Visible = true;
                    LoggedIn = true;
                    button1.Text = "Logout";
                    button2.Enabled = false;
                }
                else
                    MessageBox.Show("Login failed.");
            }
            else
            {
                // logout, disable chatting etc.
                Client = new SimpleChatClient();
                Chats = new Dictionary<string, Chat>();
                tabControl1.TabPages.Clear();
                textBox1.Text = "";
                textBox2.Text = "";
                button2.Enabled = true;
                textBox3.Enabled = false;
                button3.Enabled = false;
                this.Height = 176;
                tabControl1.Visible = false;
                LoggedIn = false;
                button1.Text = "Login";
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Client = new SimpleChatClient();
            string Register = Client.Register(textBox1.Text, textBox2.Text);
            MessageBox.Show(Register);
        }

        private void button3_Click_1(object sender, EventArgs e)
        {
            ProvideChat(textBox3.Text);
        }

        private void ProvideChat(string name)
        {
            Chat Dummy;
            bool ChatExisting = Chats.TryGetValue(name, out Dummy);
            if (ChatExisting)
            {   // if chat already exists, make its tabpage active
                tabControl1.SelectedTab = Dummy.ChatPage;
                return;
            }
            else
            {
                // create new tabpage for the conversation
                TabPage NewPage = new TabPage(name);

                TextBox ChatWindow = new TextBox();
                ChatWindow.Left = 10;
                ChatWindow.Top = 10;
                ChatWindow.Width = 532;
                ChatWindow.Height = 180;
                ChatWindow.Multiline = true;
                ChatWindow.ScrollBars = ScrollBars.Vertical;
                ChatWindow.ReadOnly = false;
                NewPage.Controls.Add(ChatWindow);

                TextBox SendBox = new TextBox();
                SendBox.Left = 10;
                SendBox.Top = 200;
                SendBox.Width = 450;
                NewPage.Controls.Add(SendBox);
                SendBox.Name = "snd" + name;
                SendBox.Click += new EventHandler(SendBox_Click);
                SendBox.TextChanged += new EventHandler(SendBox_TextChanged);

                Button SendButton = new Button();
                SendButton.Left = 470;
                SendButton.Top = 200;
                SendButton.Text = "Send";
                SendButton.Name = "btn" + name;
                SendButton.Click += new EventHandler(SendButton_Click);
                NewPage.Controls.Add(SendButton);

                Chat NewChat = new Chat();
                NewChat.ChatWindow = ChatWindow;
                NewChat.SendBox = SendBox;
                NewChat.ChatPage = NewPage;
                NewChat.Partner = name;
                NewChat.SendButton = SendButton;

                NewPage.Name = "tpg" + name;
                tabControl1.SelectedIndexChanged += new EventHandler(tabControl1_SelectedIndexChanged);

                Chats.Add(name, NewChat);

                Client.Add(name);

                this.AcceptButton = NewChat.SendButton;

                if (tabControl1.InvokeRequired)
                {
                    tabControl1.Invoke(new Action(() =>
                    {
                        tabControl1.TabPages.Add(NewPage);
                        tabControl1.SelectedTab = NewPage;
                    }));
                }
                else
                {
                    tabControl1.TabPages.Add(NewPage);
                    tabControl1.SelectedTab = NewPage;
                }

                this.ActiveControl = NewChat.SendBox;
            }
        }

        private void tabControl1_SelectedIndexChanged(Object sender, EventArgs e)
        {
            // change tabpages / chats
            if (((TabControl)sender).SelectedIndex != -1)
            {
                string Receiver = ((TabControl)sender).TabPages[((TabControl)sender).SelectedIndex].Name.ToString().Substring(3, ((TabControl)sender).TabPages[((TabControl)sender).SelectedIndex].Name.ToString().Length - 3);
                this.AcceptButton = Chats[Receiver].SendButton;
                this.ActiveControl = Chats[Receiver].SendBox;
            }
        }

        private void SendBox_Click(Object sender, EventArgs e)
        {
            // click in textbox for sending
            string Receiver = ((TextBox)sender).Name.Substring(3, ((TextBox)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            CurrentChat.NewMessage = false;
            CurrentChat.ChatPage.Text = CurrentChat.Partner;
        }

        private void SendBox_TextChanged(Object sender, EventArgs e)
        {
            string Receiver = ((TextBox)sender).Name.Substring(3, ((TextBox)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            CurrentChat.NewMessage = false;
            CurrentChat.ChatPage.Text = CurrentChat.Partner;
        }

        private void SendButton_Click(Object sender, EventArgs e)
        {
            // send message
            string Receiver = ((Button)sender).Name.Substring(3, ((Button)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            Client.Send(Receiver, CurrentChat.SendBox.Text);
            CurrentChat.ChatWindow.Text += Client.GetUsername() + ": " + CurrentChat.SendBox.Text + Environment.NewLine;
            CurrentChat.SendBox.Text = "";
            Chats[Receiver].ChatWindow.SelectionStart = Chats[Receiver].ChatWindow.TextLength;
            Chats[Receiver].ChatWindow.ScrollToCaret();
        }

        private void IncomingMessage(string NameSender, string message)
        {
            // rceive an incoming message and display it in the correct chat window
            ProvideChat(NameSender);

            Chats[NameSender].NewMessage = true;
            Chats[NameSender].ChatWindow.Invoke(new Action(() =>
            {
                Chats[NameSender].ChatWindow.Text += NameSender + ": " + message + Environment.NewLine;
                Chats[NameSender].ChatWindow.SelectionStart = Chats[NameSender].ChatWindow.TextLength;
                Chats[NameSender].ChatWindow.ScrollToCaret();
            }));
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            // periodically poll messages from server
            if (Client != null)
            {
                List<SimpleChatClient.Message> Messages = Client.Receive();
                foreach (SimpleChatClient.Message m in Messages)
                {
                    IncomingMessage(m.Sender, m.Msg);
                }
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Height = 176;
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            // timer used for the blinking effect for new messages
            foreach (Chat c in Chats.Values)
            {
                if (c.NewMessage)
                {
                    string BlankName = "";
                    BlankName = BlankName.PadLeft(c.Partner.Length, ' ');
                    if (c.ChatPage.Text == BlankName)
                        c.ChatPage.Text = c.Partner;
                    else
                        c.ChatPage.Text = BlankName;
                }
            }
        }

        private void button4_Click(object sender, EventArgs e)
        {
            if (tabControl1.SelectedIndex != -1)
            {
                string Current = tabControl1.TabPages[tabControl1.SelectedIndex].Name.ToString().Substring(3, tabControl1.TabPages[tabControl1.SelectedIndex].Name.ToString().Length - 3);
                tabControl1.TabPages.RemoveAt(tabControl1.SelectedIndex);
                Chats.Remove(Current);
            }
        }
    }

    public class Chat
    {
        public TextBox ChatWindow;
        public TextBox SendBox;
        public bool NewMessage = false;
        public TabPage ChatPage;
        public string Partner;
        public Button SendButton;
    }

    public class SimpleChatClient
    {
        public static class Crypto
        {
            static public byte[] RSAEncrypt(byte[] DataToEncrypt, RSAParameters RSAKeyInfo)
            {
                byte[] encryptedData;

                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                RSA.ImportParameters(RSAKeyInfo);

                encryptedData = RSA.Encrypt(DataToEncrypt, true);

                return encryptedData;
            }

            static public byte[] RSADecrypt(byte[] DataToDecrypt, RSAParameters RSAKeyInfo)
            {
                byte[] decryptedData;

                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                RSA.ImportParameters(RSAKeyInfo);

                decryptedData = RSA.Decrypt(DataToDecrypt, true);

                return decryptedData;
            }

            static public void CreateSymmetricKey(string password, string salt, out byte[] Key, out byte[] IV)
            {
                if (salt.Length < 8)
                    salt = salt.PadRight(8);

                Rfc2898DeriveBytes Generator = new Rfc2898DeriveBytes(password, System.Text.Encoding.UTF8.GetBytes(salt), 10000);
                Key = Generator.GetBytes(16);
                IV = Generator.GetBytes(16);
            }

            static public string  CreateRSA(string filename, string password, string folder)
            {
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                byte[] Key;
                byte[] IV;
                CreateSymmetricKey(password, filename, out Key, out IV);
                string RSAKey = RSA.ToXmlString(true);
                byte[] EncryptedKey = AESEncode(RSAKey, Key, IV);

                if (!Directory.Exists(folder))
                    Directory.CreateDirectory(folder);

                if (File.Exists(folder + "/" + filename))
                    return "Existing";

                File.WriteAllBytes(folder + "/" + filename, EncryptedKey);
                return RSA.ToXmlString(false);
            }

            static public RSACryptoServiceProvider GetRSA(string filename, string password, string folder)
            {
                byte[] EncryptedKey = File.ReadAllBytes(folder + "/" + filename);
                byte[] Key;
                byte[] IV;
                CreateSymmetricKey(password, filename, out Key, out IV);
                string RSAKey = AESDecode(EncryptedKey, Key, IV);
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                RSA.FromXmlString(RSAKey);
                return RSA;
            }

            static public string AESDecode(byte[] encryptedBytes, byte[] key, byte[] IV)
            {
                Rijndael AESCrypto = Rijndael.Create();

                AESCrypto.Key = key;
                AESCrypto.IV = IV;

                MemoryStream ms = new MemoryStream();
                CryptoStream cs = new CryptoStream(ms, AESCrypto.CreateDecryptor(), CryptoStreamMode.Write);

                cs.Write(encryptedBytes, 0, encryptedBytes.Length);
                cs.Close();

                byte[] DecryptedBytes = ms.ToArray();
                return System.Text.Encoding.UTF8.GetString(DecryptedBytes);
            }

            static public byte[] AESEncode(string plaintext, byte[] key, byte[] IV)
            {
                Rijndael AESCrypto = Rijndael.Create();
                AESCrypto.Key = key;
                AESCrypto.IV = IV;

                MemoryStream ms = new MemoryStream();
                CryptoStream cs = new CryptoStream(ms, AESCrypto.CreateEncryptor(), CryptoStreamMode.Write);

                byte[] PlainBytes = System.Text.Encoding.UTF8.GetBytes(plaintext);
                cs.Write(PlainBytes, 0, PlainBytes.Length);
                cs.Close();

                byte[] EncryptedBytes = ms.ToArray();
                return EncryptedBytes;
            }
        }

        public class User
        {
            public string Username;
            public RSACryptoServiceProvider RSASend;

            public User(string username)
            {
                Username = username;
            }
        }

        public class Message
        {
            public string Sender;
            public string Msg;

            public Message(string sender, string message)
            {
                Sender = sender;
                Msg = message;
            }
        }

        User CurrentUser = null;
        CookieContainer Cookie = null;
        string ServerUrl = "http://bloggeroliver.bplaced.net/Chat/V4/";

        List<User> ActiveChats = new List<User>();

        public string GetUsername()
        {
            return CurrentUser.Username;
        }

        private string HTTPPost(string url, string postparams)
        {
            string responseString = "";

            try
            {
                // performs the desired http post request for the url and parameters
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.CookieContainer = Cookie; // explicitely use the cookiecontainer to save the session

                string postData = postparams;
                byte[] data = Encoding.UTF8.GetBytes(postData);

                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
                request.ContentLength = data.Length;

                using (var stream = request.GetRequestStream())
                {
                    stream.Write(data, 0, data.Length);
                }

                var response = (HttpWebResponse)request.GetResponse();

                responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

                return responseString;
            }
            catch
            {
                // MessageBox.Show("error" + url + postparams + responseString);
                return null;
            }
        }

        public string Register(string username, string password)
        {
            // register a new user
            if (HTTPPost(ServerUrl + "checkuser.php", "username=" + username) == "No")
            {
                string LoginPubKey = Crypto.CreateRSA(username, password, "users");
                if (LoginPubKey == "Existing")
                    return "Password file already existing on computer.";
                string SendPubKey = Crypto.CreateRSA(username, password, "send");

                return HTTPPost(ServerUrl + "register.php", "username=" + username + "&loginkey=" + Uri.EscapeDataString(LoginPubKey) + "&sendkey=" + Uri.EscapeDataString(SendPubKey));
            }
            else
                return "Already existing.";
        }

        public bool Login(string username, string password)
        {
            // login
            Cookie = new CookieContainer();
            string Challenge = HTTPPost(ServerUrl + "prelogin.php", "username=" + username);
            RSACryptoServiceProvider RSALogin = Crypto.GetRSA(username, password, "users");
            string ClearChallenge = Encoding.UTF8.GetString(Crypto.RSADecrypt(Convert.FromBase64String(Challenge), RSALogin.ExportParameters(true)));
            string Login = HTTPPost(ServerUrl + "login.php", "username=" + username + "&challenge=" + Uri.EscapeDataString(ClearChallenge));
      
            if (Login == "LoginGood")
            {
                CurrentUser = new User(username);
                CurrentUser.RSASend = Crypto.GetRSA(username, password, "send");
                return true;
            }
            else
            {
                Cookie = null;
                return false;
            }
        }

        public void Logout()
        {
            // logout
            CurrentUser = null;
            Cookie = null;
        }

        public string Send(string recipient, string message)
        {
            string EncryptedMessage = "";
            foreach (User u in ActiveChats)
            {
                if (u.Username == recipient)
                {
                    EncryptedMessage = Uri.EscapeDataString(Convert.ToBase64String(Crypto.RSAEncrypt(Encoding.UTF8.GetBytes(message), u.RSASend.ExportParameters(false))));
                }
            }
            return (HTTPPost(ServerUrl + "send.php", "Recipient=" + recipient + "&Message=" + (EncryptedMessage)));
        }

        public string GetSendKey(string user)
        {
            return (HTTPPost(ServerUrl + "getsendkey.php", "user=" + user));
        }

        public List<Message> Receive()
        {
            // receive messages
            if (CurrentUser == null)
                return new List<Message>();
            string Messages = HTTPPost(ServerUrl + "receive.php", "");
            if (Messages == null)
                return new List<Message>();

            // message format is: sender<br />message<br />, split regarding to this in single messages
            string[] Splits = Messages.Split(new string[] { "<br />" }, StringSplitOptions.None);
            List<Message> Received = new List<Message>();
            for (int i = 0; i < Splits.Length - 1; i += 2)
            {
                Received.Add(new Message(Splits[i], Encoding.UTF8.GetString(Crypto.RSADecrypt(Convert.FromBase64String(Splits[i + 1]), CurrentUser.RSASend.ExportParameters(true)))));
            }
            return Received;
        }

        public void Add(string name)
        {
            User NewUser = new User(name);
            RSACryptoServiceProvider NewRSA = new RSACryptoServiceProvider();
            NewRSA.FromXmlString(GetSendKey(name));
            NewUser.RSASend = NewRSA;
            ActiveChats.Add(NewUser);
        }
    }
}

The class SimpleChatClient now manages a list ActiveChat of the type User. If by the user interface via a click on Send a new chat is started or a message of a new user is received, the user is added to ActiveChats, his public key is queried from the server and saved. When sending the message then this  is encrypted with the saved key, when receiving one it is decrypted with the private key. When registering the private key for receiving, similar to the private key for the authentication, is saved locally on the computer, only in the subfolder "send".

The server URL is http://bloggeroliver.bplaced.net/Chat/V4/.

The complete project, including the scripts, can be downloaded here.

I here also created a page for this project, from where, for example, a runnable version can be installed.
Also here I am called bloggeroliver and am looking forward to your messages.

For this client version I currently see the following attacks:

1.) As in the first version of the authentication an attacker now can simply replay messages of a logged in user, like for example fetch my messages oder send a message. The reason for this is that in these always session ID and / or cookie is send with them and this data is always the same.
Solution: Usage of encryption, for example TLS. Because of the mentioned reasons I do not want to use this, therefor: All requests are signed by the client and the signature is checked by the server.

2.) The server is not authenticated to the client, which is especially problematic when the client queries public keys from the server. A man in the middle could intercept these and change them, so that now his public key is sent and he can decrypt the messages. Solution: The sever encrypts its answer with the private key of the recipient (or better: The server also gets a keypair and signs the answers).

3.) Sender and receiver name are sent as plaintext and thus can also be read. Solution: Also encrypt these.

I will implement these solutions and publish the new version on the project page shown above, but write no more posts about it, since the idea of such a program now should be clear.
Except these I currently do not see any other vulnerabilities, and thus would declare the client as safe then.
I am looking forward to comments and discussion, especially if you can find different ones.

No comments:

Post a Comment