Sonntag, 26. April 2015

RSA in C# und PHP (mit Zusatzbibliotheken)

Hinweis: Der nächste Post ist eine einfachere Version dieses Posts, die externen Klassen zur Schlüsselkonvertierung werden dort nicht mehr benötigt, ich empfehle die Verwendung des nächsten Posts.

Nachdem ich in vorigen Posts getrennt beschrieben habe, wie man RSA in C# und RSA in PHP benutzt, möchte ich heute zeigen, wie man dies sprachübergreifend tut.
Ich tue dies, da die Umsetzung doch ein paar Tücken bereithält, insbesondere bezüglich des verwendeten Public / Private Key Formats und der Kodierung der Daten bei der Übertragung.
Außerdem habe ich im Internet nur stückweise Erklärungen gefunden, deswegen wollte ich hier ein komplett funktionsfähiges Beispiel posten, bei welchem PHP Server und C# Client beide Keys erzeugen, austauschen und verschlüsselte Daten hin und her senden.

Die .Net RSA Implementierung verwaltet Schlüssel in einem XML Format, die im vorigen Post beschriebene PHP Implementierung phpseclib verwendet das PEM Format. Obwohl die Konvertierung zwischen beiden natürlich auch per Hand möglich wäre, habe ich für beide Richtungen externe Klassen benutzt. Um in PHP das XML Format einlesen zu können, habe ich diese Erweiterung der RSA Klasse aus der phpseclib verwendet.
Um das PEM Format mit C# einzulesen, habe ich die Klasse OpenSSLKeys.cs leicht abgeändert und benutzt.

XML Format in PHP einlesen: Auf dem oben verlinkten Codeblog wurde das Prinzip beschrieben und eine Erweiterung der RSA Klasse gepostet. Zuerst ist das Skript herunterzuladen (da der Downloadlink auf der Seite nicht funktioniert, hier ein Reupload), welche dann in das gleiche Verzeichnis wie die Datei RSA.php aus der phpseclib hochgeladen werden muss. Anschließend binden wir im Code das Skript RSA_XML.php mittels include ein und verwenden statt der Klasse Crypt_RSA die Klasse Crypt_RSA_XML. Diese ist eine Erweitung der Klasse Crypt_RSA und stellt neben der herkömmlichen Funktion loadKey() auch die Funktion loadKeyfromXML() zur Verfügung, mit welcher ein Key eingelesen werden kann, welcher in C# mittels der Funktion ToXmlString() der Klasse RSACryptoServiceProvider exportiert wurde.

PEM Format in C# einlesen: Hierfür habe ich die oben verlinkte Klasse OpenSSLKeys.cs verwendet, welche von JavaScience entwickelt wurde. Diese ist zum Projekt hinzuzufügen, dann können wir mittels JavaScience.opensslkey.DecodePEMKey() auf die benötigte Funktion zugreifen. Ich habe die Datei leicht verändert, da sie in ihrer ursprünglichen Form als Standalone Programm gedacht war, mit welchem per Konsolen Ein- und Ausgabe mit dem Benutzer kommuniziert wird. Im Wesentlichen habe ich nur die Main Funktion entfernt und der Funktion eine Rückgabe gegeben (nur getestet für das Konvertieren von Public und Private Keys, die Datei kann noch mehr). Die geänderte Datei kann hier heruntergeladen werden.

Wie schon erwähnt, muss man nun noch ein bisschen bei der Datenübertragung mit HTTP Post bezüglich des Formats aufpassen. Ich verwende die HTTP Post Methode zur Übertragung von Daten an den Server. Zuerst muss man die verwendete Kodierung des Servers herausfinden, bei mir ist es UTF8. Deswegen werden die Daten im Post in das Format konvertiert. Wenn man nun den öffentlichen Schlüssel im XML Format übertragen möchte, enthält dieser u.a. Symbole wie <, >. Diese werden aber vom Server als Steuerzeichen betrachtet, deswegen müssen wir diese vorher escapen, was wir mittels Uri.EscapeDataString() tun. Beim Verschlüsseln eines Strings mit RSA ist die Ausgabe sehr kryptisch, es entstehen viele Sonderzeichen. Diese können nicht alle mit UTF8 dargestellt werden, deshalb konvertieren wir die Ausgabe mittels Convert.ToBase64String() in einen Base64 String. Der Server muss diesen dann zurückkonvertieren. Auch diesen String escapen wir, um eventuelle Steuerzeichen korrekt zu übertragen. Wenn wir auf dem Server mittels echo den öffentlichen Schlüssel ausgeben, rufen wir darauf die Funktion nl2br() auf, welche für eine korrekte Darstellung der Zeichenumbrüche sorgt.

Nun zum Code, ich möchte ein kleines Beispielprojekt vorstellen. Das C# Programm und das PHP Skript erstellen beide ein Public / Private Schlüssel Paar und teilen dem anderen ihren öffentlichen Schlüssel mit. Dann verschlüsseln sie jeweils eine Nachricht mit diesem, schicken diese und der Empfänger entschlüsselt sie mit seinem privaten Schlüssel.
Das PHP Skript sieht so aus (http://bloggeroliver.bplaced.net/PHPExamples/ClientServerRSA.php):

<?php
include('Crypt/RSA_XML.php');

session_start();

if (!isset($_SESSION["username"])) {
     $_SESSION["username"] = $_POST["username"];
     $_SESSION["ClientPubKey"] = $_POST["ClientPubKey"];
    
     $rsa = new Crypt_RSA_XML();
     extract($rsa->createKey());
     $_SESSION["ServerPrivateKey"] = $privatekey;
     echo nl2br($publickey);
}
else {
     $rsaclient = new Crypt_RSA_XML();
     $rsaclient->loadKeyfromXML($_SESSION["ClientPubKey"]);
     echo base64_encode($rsaclient->encrypt("Hello client"))."<br />";
    
     $rsaserver = new Crypt_RSA_XML();
     $rsaserver->loadKey($_SESSION["ServerPrivateKey"]);
     echo "Client sent: ".$rsaserver->decrypt(base64_decode($_POST["SecretMessage"]));
}
?>

Wir arbeiten mit Sessions (beachtet hierzu den Post über Sessions mit C#). Das Skript wird vom C# Programm 2mal aufgerufen. Ist noch keine Session angelegt, erzeugt der Server ein Schlüsselpaar und speichert seinen privaten Schlüssel. Ebenfalls speichert er den gesendeten öffentlichen Schlüssel des Clients und gibt seinen öffentlichen aus. Beim nächsten Aufruf dann wird mittels loadKeyFromXML() der gespeicherte öffentliche Schlüssel des Clients eingelesen und der Text "Hello client" verschlüsselt. Dieser wird zwecks Datenübertragung in einen Base64 String konvertiert und ausgegeben. Außerdem lädt der Server seinen privaten Schlüssel und entschlüsselt damit die Nachricht vom Client, welche ebenfalls als Base64 String übergeben wurde.

Der C# Code sieht wie folgt 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 ClientServerRSA
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        CookieContainer Cookie = null;

        private void Form1_Load(object sender, EventArgs e)
        {
            Cookie = new CookieContainer();
            RSACryptoServiceProvider ClientRSA = new RSACryptoServiceProvider();
            string ServerPubKeyOutput = HTTPPost("http://bloggeroliver.bplaced.net/PHPExamples/ClientServerRSA.php", "username=MyName&ClientPubKey=" + Uri.EscapeDataString(ClientRSA.ToXmlString(false)));
            File.WriteAllText("serverpubkey.txt", ServerPubKeyOutput.Replace("<br />", ""));
            StreamReader sr = System.IO.File.OpenText("serverpubkey.txt");
            string pemkey = sr.ReadToEnd();
            string ServerPubKey = JavaScience.opensslkey.DecodePEMKey(pemkey);

            RSACryptoServiceProvider ServerRSA = new RSACryptoServiceProvider();
            ServerRSA.FromXmlString(ServerPubKey);
            string SecretMessage = Uri.EscapeDataString(Convert.ToBase64String(RSAEncrypt(Encoding.UTF8.GetBytes("Hello server"), ServerRSA.ExportParameters(false))));

            string ServerAnswer = HTTPPost("http://bloggeroliver.bplaced.net/PHPExamples/ClientServerRSA.php", "username=MyName&SecretMessage=" + SecretMessage);
            string[] SplitAnswer = ServerAnswer.Split(new string[] { "<br />" }, StringSplitOptions.None);
            string ServerMessage = Encoding.UTF8.GetString(RSADecrypt(Convert.FromBase64String(SplitAnswer[0]), ClientRSA.ExportParameters(true)));
            MessageBox.Show("Server sent: " + ServerMessage + Environment.NewLine + SplitAnswer[1]);
        }




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

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

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

In den Funktion RSADecrypt() und RSAEncrypt() ist die Übergabe von true als 2. Parameter wichtig, welcher die Benutzung von OAEP Padding einschaltet. (Dies ist die Standardeinstellung von phpseclib, könnte aber auch dort geändert werden).
Ansonsten habe ich die Funktionen HTTPPost(), RSADecrypt() und RSAEncrypt() in vorigen Posts erläutert, weswegen ich hier nicht mehr darauf eingehen werde.
In der 3. Zeile der Form_Load() escapen wir den erzeugten Public Key und senden diesen an den Server. Dabei lesen wir dessen Antwort, den Public Key des Servers, aus und schreiben diesen in eine Datei. Diese PEM Datei wandeln wir dann mittels JavaScience.opensslkey.DecodePEMKey() in einen XML Key ein und importieren diesen. Mit diesem verschlüsseln wir die Nachricht "Hello server" und schicken diese als Base64 String und escaped an den Server. Schließlich entschlüsseln wir die Nachricht vom Server.

Keine Kommentare:

Kommentar veröffentlichen