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.

Tuesday, April 28, 2015

C# Chat Client v3 - Challenge Response Authentification

I here want to build upon the Chat Client, which was presented in an earlier post, and remove a security issue mentioned there.
In the previous version the user chose a password when creating an account, the password then was sent to the server and saved as a hash in the database. On every login process the client sends the password of the user, the server then compares this with the saved one and thus can authenticate the user.
But now every one with access to the network traffic, for example a user in the same network or an attacker with access to internet nodes etc., can see and read the sent login message, and either read out the password or simply resend the same message to authenticate correctly. Because of this also encrypting the message would be of no use, we have to come up with something else.
The default choice would now be SSL /  TLS, an encryption protocol for secure data transfer. For this (but also depending on the choice of usage) via a public key system a key is chosen, with which the communication is encrypted and thus not readable anymore (also the key is always different, so the login messages are different too). But for TLS a certificate is needed, which authenticates the server. Probably not everybody has one and it will also probably be not free to get.
To ensure the easy and universal usability of the client, I thus set TLS aside and implemented a small custom, also based on public key cryptography, method: A so called challenge - response authentification. As a short introduction to asymmetric cryptography I can recommend the post about RSA.
A challenge - response authentification is, as the name already says, an authentication protocol, in which the server asks the client a challenge, which the client then has to solve (response). This can, for example, like here, be done with a public key algorithm.

The idea: When creating a new user account a new RSA key pair is created. The public key is then sent to the server, this saves it next the username in the database. The private key is saved on the computer of the user, but encrypted with AES and a chosen password. If the user wants to log in, he first loads his private key from the file by inputting the password. He then connects to the server, which encrypts a random string (here a combination of username, current time and 16 bit random number) with the public key of the user and sends it back. The user now can decrypt this with his private key and sends the decrypted string back. The server then compares both strings and logs the user in, if they match. Because: Under the assumption that RSA is secure, only the correct user can decrypt the string. Further, all messages can be send openly, reading them does not endanger the security of the system, since every time a new string is the correct login response and the public key anyway can be known (attack possibilites still exist,  but this method is already way more safe than the one used before - a total security can never be ensured anyway).
I have to highlight here though that this method alone is useless - without additional methods such a replay attack is still possible. Although the actual login process is now safe, the messages of a user logged in through a PHP session can simply be replayed - in these always session ID and / or cookie is contained, which can be read out or simply replayed. Then the server cannot distinguish the valid user and some attacker. As already mentioned, TLS solves this problem (but then already the simple login method would be enough), and in the next post about the client I mention a self made solution.

The code: As described in the post about RSA encryption in PHP, we first download the library phpseclib and then upload this into the folder of your PHP scripts.
The script register.php stayed the same, only the parameter password now is called pubkey:

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

$username = $_POST["username"];
$pubkey = $_POST["pubkey"];

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

$stmt->store_result();
$num_rows = $stmt->num_rows;

if ($num_rows > 0) {
     echo "Existing";
}
else {
     $stmt = $conn->prepare("INSERT INTO Users (username, pubkey) VALUES (?, ?);");
     $stmt->bind_param("ss", $username, $pubkey);
     $stmt->execute();
     echo "Success";
}
?>

Additionally there is a script checkuser.php for testing purposes whether the current user already exists, so that on the local computer not too early a private key file is created:

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

$username = $_POST["username"];
$password = $_POST["password"];

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

$stmt->store_result();
$num_rows = $stmt->num_rows;

if ($num_rows > 0) {
     echo "Yes";
}
else {
     echo "No";
}
?>

The login process consists of 2 parts, first he script prelogin.php is called:

<?php

include("connect.php");
include('Crypt/RSA.php');
session_start();

$username = $_POST["username"];

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

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


$rsa = new Crypt_RSA();
$rsa->loadKey($pubkey, CRYPT_RSA_PUBLIC_FORMAT_XML);
$challenge = $username.date("Y/m/d").date("h:i:sa").rand(0, 65536);

$_SESSION["username"] = $username;
$_SESSION["challenge"] = $challenge;

echo base64_encode($rsa->encrypt($challenge));
?>

First the public key of the submitted user, created during registration, is loaded from the database. With this we then encrypt the challenge and output it. But we now already set the username in the session and also save the challenge it it, to be able to later verify the answer.

The script login.php looks as follows:

<?php
session_start();

include("connect.php");

$username = $_POST["username"];
$challenge = $_POST["challenge"];

if($challenge == $_SESSION["challenge"] && isset($_SESSION["challenge"]) && ($_SESSION["username"] == $username)) {
    $_SESSION["LoggedIn"] = true;
    echo "LoginGood";
}
else {
    echo "LoginBad";
}
?>

The submitted decrypted challenge is compared with the saved one and on agreement the current user is logged in by setting the session variable LoggedIn.
Important is also the query whether the challenge is empty, since otherwise an attacker could possibly create a new session with an arbitrary username (and thus an empty challenge)!
The scripts send.php and receive.php are the same, except that in them the session variable LoggedIn is checked.

send.php:

<?php
session_start();

include("connect.php");

$Recipient = $_POST["Recipient"];
$Message = $_POST["Message"];
$Sender = $_SESSION['username'];

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

$stmt = $conn->prepare("INSERT INTO Messages () VALUES (?, ?, ?);");
$stmt->bind_param("sss", $Sender, $Recipient, $Message);
$stmt->execute();
    
?>

receive.php:

<?php

session_start();

include("connect.php");

if(!isset($_SESSION['LoggedIn']))
   {
   echo "Bitte erst login";
   exit;
   }
    
$Recipient = $_SESSION['username'];

$stmt = $conn->prepare("SELECT Sender, Message FROM Messages WHERE Recipient = ?");
$stmt->bind_param("s", $Recipient);
$stmt->execute();
    
$stmt->bind_result($sender, $message);
while($row = $stmt->fetch())
   {
          echo "$sender<br />";
          echo "$message<br />";
          $stmt->bind_result($sender, $message);
   }

$stmt = $conn->prepare("DELETE FROM Messages WHERE Recipient = ?");
$stmt->bind_param("s", $Recipient);
$stmt->execute();
?>

In the C# code I will only show the changed code parts, a majority is of course the same, especially the complete client user interface and a majority of the class SimpleChatClient. To this I added the class Crypto:

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)
            {
                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("users"))
                    Directory.CreateDirectory("users");

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

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

            static public RSACryptoServiceProvider GetRSA(string filename, string password)
            {
                byte[] EncryptedKey = File.ReadAllBytes("users/" + 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;
            }
        }

The functions AESEncode(), AESDecode(), RSAEncrypt() and RSADecrypt() are known from the previous posts about AES and RSA. The function CreateSymmetricKey() derives by the PBKDF2 algorithm out of the given password an encryption key and IV and returns these. The function CreateRSA() is called on every registration of a new user, with this a new key pair for RSA is created. With the passwort chosen by the user CreateSymmetricKey() is called and with the returned data the private key is AES encrypted and written into a local file. The function GetRSA() is called on login, it reads and decrypts the private key from the file.
The function Register() was changed in the obvious fashion:

public string Register(string username, string password)
{
    // register a new user
    if (HTTPPost(ServerUrl + "checkuser.php", "username=" + username) == "No")
    {
        string RSAPubKey = Crypto.CreateRSA(username, password);
        if (RSAPubKey == "Existing")
            return "Password file already existing on computer.";
        return HTTPPost(ServerUrl + "register.php", "username=" + username + "&pubkey=" + Uri.EscapeDataString(RSAPubKey));
    }
    else
        return "Already existing.";
}

Interesting is maybe the function Login():

public bool Login(string username, string password)
{
    // login
    Cookie = new CookieContainer();
    string Challenge = HTTPPost(ServerUrl + "prelogin.php", "username=" + username);
    RSA = Crypto.GetRSA(username, password);
    string ClearChallenge = Encoding.UTF8.GetString(Crypto.RSADecrypt(Convert.FromBase64String(Challenge), RSA.ExportParameters(true)));
    string Login = HTTPPost(ServerUrl + "login.php", "username=" + username + "&challenge=" + Uri.EscapeDataString(ClearChallenge));

    if (Login == "LoginGood")
    {
        CurrentUser = new User(username);
        return true;
    }
    else
    {
        Cookie = null;
        return false;
    }
}

First the script prelogin.php is called and the challenge outputted. Then the private RSA key is loaded and the challenge decrypted with it. Eventually the result is sent back as a Base64 string and the user by this authenticated.

The server url is: http://bloggeroliver.bplaced.net/Chat/V3/.

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