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.

Kommentare:

  1. Nettes Projekt ☺

    Leider scheinen im Client noch viele Fehler drin zu sein, vielleicht wäre u.a. ein try-catch hilfreich da sonst das Programm sich bei Fehlern gerne beenden möchte. Wie ist die Syntax für connect.php zwecks MySQL und wie muss die Datenbank bzw. Tabellen aussehen?

    An sich wäre ein Projekt wie dieses schon schön da man nicht unbedingt auf andere Messenger-Dienste angewiesen wäre. Zum einfachen schreiben würde dieser Chat schon ausreichen, vielleicht könnte man noch Kleinigkeiten wie Dateien senden einbauen oder so. Nett wäre außerdem, wenn zumindest für Android was geben täte 😇

    AntwortenLöschen
  2. Wie soll die Datenbank aussehen? Bei mir kommt immer ein Fehler "The item SimpleChatClient_TemporaryKey.pfx does not exist in the project directory... - wo finde ich diese Datei? (Deswegen lässt es sich mit Visual Studio nicht kompilieren) Wo kann ich die mysql(i) Zugangsdaten eintragen? In der Datei "connect.php" sind keine Variablen hierfür vorhanden.

    AntwortenLöschen
    Antworten
    1. Hallo, die .pfx - Datei hat nichts mit der Datenbank zu tun, sondern wird beim digitalen Signierens des Projekts erstellt. Falls der Fehler bei dir auftritt, kannst du das Projekt selber signieren (Projekteigenschaften - Signieren).
      Bzgl. der Zugangsdaten schaust du dir zum allgemeinen Verständnis am besten die vorigen Posts in dieser Reihe an, angefangen mit http://csharp-tricks.blogspot.de/2015/03/c-chat-client-v1.html :-) Dort wird die Datei connect.php mit den Zugangsdaten erstellt und über die Datenbank gesprochen. Viele Grüße und viel Erfolg

      Löschen