Donnerstag, 30. April 2015

C# Chat Client v4 - Verschlüsselung der Kommunikation

In diesem Post möchte ich den Chat Client noch etwas weiter ausbauen und nun, nach Benutzung von RSA zur Sicherung der Authentifizierung, auch die Kommunikation mit RSA verschlüsseln. Jede Nachricht wird also verschlüsselt und kann nur noch von dem richtigen Empfänger entschlüsselt werden.
Normalerweise, z.B. bei PGP, wird ein symmetrischer Schlüssel gewählt und dieser mit einem asymmetrischen Verfahren verschlüsselt, die komplette Nachricht dann mit dem symmetrischen. Dieses wird gemacht, da symmetrische Verfahren deutlich schneller sind. Hier benutzen wir allerdings nur RSA, also ein asymmetrisches Verfahren, da Chatnachrichten eh nicht so lang sind, eine symmetrische Verschlüsselung könnte man vielleicht später einmal einbauen.

Funktionsweise: Beim Erstellen eines neuen Benutzers wird nun, neben dem für die Authentifizierung benötigtem Schlüsselpaar, ein weiteres Schlüsselpaar zur Verschlüsselung der Kommunikation angelegt. Der private Schlüssel wird ebenfalls AES verschlüsselt (mit dem Passwort für die Authentifizierung) auf dem lokalen Computer gespeichert, der öffentliche an die Datenbank geschickt und dort neben dem Benutzer gespeichert. Möchte man nun eine Nachricht an einen anderen Benutzer schicken, erfragt der Client den öffentlichen Schlüssel dieses mittels eines neuen PHP Skripts. Dann wird die Nachricht verschlüsselt und an den Server geschickt. Holt sich der Empfänger diese ab, entschlüsselt er diese mit seinem privaten Schlüssel, welchen er durch Eingabe seines Login Passworts freischaltet. 

Für den PHP Server ist die Verschlüsselung also unsichtbar, in den Skripten haben sich lediglich die Namen einiger Variablen geändert, und in der Datenbank ist ein neues Feld für den Schlüssel zum Senden dazugekommen. Allerdings ist doch etwas Aufmerksamkeit geboten, insbesondere bei der Kodierung der Daten, deshalb findet ihr ein paar generelle Erklärungen zur Verwendung von RSA in C# und PHP gemischt in dem verlinkten Post. Neu ist das Skript getsendkey.php, mit welchem der passende öffentliche Schlüssel zum Senden an einen Benutzer abgefragt werden kann:
<?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;
?>

Der komplette C# Code sieht so aus:

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);
        }
    }
}

Die Klasse SimpleChatClient verwaltet nun eine Liste ActiveChats vom Typ User. Wird in der Benutzeroberfläche per Klick auf Senden ein neuer Chat gestartet oder eine neue Nachricht von einem neuen Benutzer empfangen, wird der Benutzer zu ActiveChats hinzugefügt, dessen öffentlicher Schlüssel auf dem Server nachgeschlagen und gespeichert. Beim Senden wird die Nachricht dann mit dem gespeicherten Schlüssel verschlüsselt, beim Empfangen mit dem gespeicherten privaten entschlüsselt. Beim Registrieren wird der private Schlüssel für das Empfangen, wie der Schlüssel für die Authentifizierung, lokal auf dem Computer gespeichert, nur in dem Unterordner "send".

Die Server URL ist http://bloggeroliver.bplaced.net/Chat/V4/.

Das ganze Projekt, inklusive der Skripte, kann hier heruntergeladen werden.

Ich habe hier auch eine Seite zum Projekt aufgemacht, von wo u.A. eine lauffähige Version installiert werden kann.
Auch hier heiße ich wieder bloggeroliver und freue mich auf eure Nachrichten.

Auf diese Client Version sehe ich momentan noch die folgenden Angriffe:

1.) Wie bei der ersten Version der Authentifizierung kann ein Angreifer auch einfach Nachrichten eines eingeloggten Benutzers, wie zum Beispiel hole meine Nachrichten oder sende Nachricht, wiederholen, also eine Replay Attack fahren. Grund ist, dass bei diesen immer Session ID und / oder Cookie mitgeschickt wird und diese Daten immer gleich sind.
Lösung: Verwendung einer Verschlüsselung, wie zum Beispiel TLS. Aus den erwähnten Gründen möchte ich darauf aber verzichten, deshalb: Alle Anfragen obiger Art werden vom Client mit seinem privaten Schlüssel signiert und vom Server geprüft.

2.) Der Server ist für den Client nicht authentifiziert, dies ist insbesondere problematisch, wenn der Client vom Server Public Keys abfragt. Denn ein Man in the Middle könnte diese abfangen und ändern, sodass nun sein öffentlicher Schlüssel benutzt wird und er die Nachricht entziffern kann. Lösung: Der Server verschlüsselt die Antwort mit dem privaten Schlüssel des Empfängers (oder besser: Der Server kriegt auch ein Schlüsselpaar und signiert die Anfragen).

3.) Die Empfänger- und Sendernamen werden im Klartext übertragen und können somit mitgelesen werden. Lösung: Auch diese verschlüsseln.

Ich werde diese Lösungen umsetzen und die neue Version auf obiger Projektseite veröffentlichen, aber keine Posts mehr darüber schreiben, da das Prinzip eines solchen Programms nun klar sein sollte.
Außer diesen sehe ich allerdings momentan keine weiteren Lücken, und würde den Client dann als sicher einstufen.
Freue mich auf Kommentare und Diskussion, insbesondere ob ihr noch andere findet.

Dienstag, 28. April 2015

C# Chat Client v3 - Challenge Response Authentifizierung

Ich möchte hier auf den in einem vorigen Post vorgestellten Chat Client aufbauen und eine dort erwähnte Sicherheitslücke schließen.
In der vorigen Version wählte der Benutzer beim Erstellen seines Accounts ein Passwort, dieses wurde dann an den Server gesendet und gehasht in der Datenbank gespeichert. Bei jedem Login Vorgang sendete der Client das Passwort des Benutzers wieder mit, der Server verglich dieses mit dem gespeicherten und konnte so den Benutzer authentifizieren.
Nun kann jedoch jeder mit Zugriff auf den Netzwerkverkehr, wie zum Beispiel ein Benutzer im gleichen Netzwerk oder ein Angreifer mit Zugriff auf Internetknoten u.ä., die gesendete Login Nachricht einfach mitlesen, und entweder das Passwort auslesen oder einfach die gleiche Nachricht noch einmal senden, um sich korrekt zu authentifizieren. Deshalb würde zum Beispiel auch das Verschlüsseln der Nachricht bzw. des Passworts nichts bringen, wir müssen uns etwas anders überlegen.
Die Standardwahl wäre die Verwendung von SSL / TLS, ein Verschlüsselungsprotokoll zur sicheren Datenübertragung. Hierbei wird (u.A., und je nach Verwendungsart) mittels Public - Key Verfahren ein Schlüssel ausgehandelt, mit welchem die Kommunikation verschlüsselt wird und somit nicht mehr mitlesbar ist (auch ist der Schlüssel immer anders, die Login Nachrichten sind also immer verschieden). Für TLS wird jedoch ein Zertifikat benötigt, welches den Server authentifiziert. Dieses hat wohl nicht jeder und es wird nicht unbedingt kostenlos zu erhalten sein.
Um die einfache und allgemeine Verwendbarkeit des Clients zu gewährleisten, verzichte ich daher auf TLS und implementiere selber ein kleines, ebenfalls auf Public - Key Kryptographie basierendes, Verfahren: Eine sogenannte Challenge - Response Authentifizierung. Als kurze Einführung in die asymmetrische Kryptographie kann ich den Post über RSA empfehlen.
Eine Challenge - Response Authentifizierung ist, wie der Name schon sagt, ein Authentifizierungsverfahren, bei welchem der Server dem Client eine Aufgabe (Challenge) stellt, die der Client lösen muss (Response). Dies kann zum Beispiel, wie hier, mit einem Public - Key Verfahren umgesetzt werden.
Die Idee: Bei der Erzeugung eines neuen Benutzeraccounts wird ein neues RSA Schlüsselpaar erzeugt. Der öffentliche Schlüssel wird an den Server geschickt, dieser speichert es neben dem Benutzer in der Datenbank. Der private Schlüssel wird auf dem PC des Benutzers gespeichert, allerdings AES verschlüsselt mit einem selber gewählten Passwort. Möchte sich der Benutzer nun einloggen, lädt er zuerst seinen privaten Schlüssel durch Eingabe seines Passworts aus der Datei. Er meldet sich dann beim Server, welcher einen Zufallsstring (hier eine Kombination aus Benutzernamen, aktueller Uhrzahl und 16 Bit Zufallszahl) mit dem öffentlichen Schlüssel des Benutzers verschlüsselt und diesen an ihn schickt. Der Benutzer kann diesen nun mit seinem privaten Schlüssel entschlüsseln und schickt den String zurück. Der Server vergleicht die beiden Strings und loggt den Benutzer bei Gleichheit ein. Denn: Unter der Annahme, dass RSA sicher ist, kann nur der richtige Benutzer den gesendeten String entschlüsseln. Auch können alle Nachrichten nun beliebig mitgehört werden ohne die Sicherheit zu gewährleisten, da jedes Mal ein anderer String die richtige Login Antwort ist und der öffentliche Schlüssel bekannt sein darf (Es gibt immer noch Angriffsmöglichkeiten, allerdings ist dieses Verfahren schon deutlich sicherer als das vorher verwendete - eine 100% Sicherheit kann eh nicht garantiert werden.).
Insbesondere muss ich hier darauf hinweisen, dass dieses Verfahren allein erstmal gar nichts bringt - ohne zusätzliche Methoden kann eine solche Replay Attacke einfach wiederholt werden. Zwar ist der eigentliche Login Vorgang jetzt sicher, allerdings können die Nachrichten eines per PHP Session eingeloggten Benutzers einfach wiederholt werden - in diesen ist immer Session ID und / oder Cookie enthalten, welche ausgelesen werden kann oder die Nachricht einfach wiederholt werden kann. Dann kann der Server den tatsächlich eingeloggten Benutzer und den Angreifer nicht unterscheiden. Wie schon erwähnt, TLS löst dieses Problem (dann würde allerdings auch schon die einfache Login Methode reichen), und im nächsten Post über den Client erwähne ich eine selbstgemachte Lösung.

Der Code: Wie im Post zur RSA Verschlüsselung mit PHP beschrieben, laden wir zuerst die Bibliothek phpseclib herunter und laden diese dann in den Ordner unserer PHP Skripte hoch.
Das Skript register.php ist gleich geblieben, nur dass ein Parameter jetzt pubkey statt password heißt:

<?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";
}
?>

Zusätzlich gibt es noch ein Skript checkuser.php zum Testen, ob der Benutzer schon existiert, damit auf dem lokalen PC nicht zu früh ein Private Key File angelegt wird:

<?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";
}
?>

Der Login besteht nun aus 2 Teilen, zuerst wird das Skript prelogin.php aufgerufen:

<?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));
?>

Als erstes wird der bei der Registrierung gespeicherte öffentliche Schlüssel des angegebenen Benutzers aus der Datenbank geladen. Mit diesem verschlüsseln wir dann die Challenge und geben diese aus. Wir setzen nun aber auch schon den Benutzernamen der Session und speichern zusätzlich die Challenge in der Session, um später die korrekte Antwort überprüfen zu können.

Die Login.php sieht so aus:

<?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";
}
?>

Die übergebene entschlüsselte Challenge wird mit der gespeicherten verglichen und bei Übereinstimmung wird der Benutzer durch Setzen der Session Variable LoggedIn eingeloggt. Wichtig ist hier auch die Abfrage auf leer, da sonst ein Angreifer eventuell eine neue Session mit einem beliebigen Benutzernamen (und damit leerer Challenge) aufmachen könnte!
Die Skripte send.php und receive.php sind die gleichen, nur dass in diesen die Session Variable LoggedIn geprüft wird.

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();
?>

Beim C# Code werde ich nur die geänderten Stellen zeigen. Der Großteil ist natürlich gleich, insbesondere die komplette Client Oberfläche und ein Großteil der Klasse SimpleChatClient.
Zu dieser ist die Klasse Crypto hinzugekommen:

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;
            }
        }

Die Funktionen AESEncode(), AESDecode(), RSAEncrypt() and RSADecrypt() sind aus den vorigen Posts zu AES und RSA bekannt. Die Funktion CreateSymmetricKey() leitet nach dem PBKDF2 Verfahren aus dem übergebenen Passwort einen Verschlüsselungskey und IV ab und gibt diese zurück. Die Funktion CreateRSA() wird bei der Registrierung eines neuen Benutzers aufgerufen, hierbei wird ein Schlüsselpaar für RSA erzeugt. Mit dem vom Benutzer gewählten Passwort wird CreateSymmetricKey() aufgerufen und mit den zurückgegebenen Daten der private Schlüssel AES verschlüsselt in eine lokale Datei geschrieben. Die Funktion GetRSA() wird beim Login aufgerufen, sie liest und entschlüssel den privaten Key aus dieser Datei.
Die Funktion Register() wurde in der einleuchtenden Weise angepasst:

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.";
}

Interessant ist vielleicht noch die Funktion 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;
    }
}

Zuerst wird das Skript prelogin.php aufgerufen und die Challenge ausgelesen. Dann wird der private RSA Schlüssel geladen und die Challenge mit diesem entschlüsselt. Schließlich wird das Ergebnis als Base64 String zurückgeschickt und der Benutzer dadurch authentifiziert.

Die Server URL ist http://bloggeroliver.bplaced.net/Chat/V3/.

Das ganze Projekt, inklusive der Skripte, kann hier heruntergeladen werden.