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.

No comments:

Post a Comment