Passwort-Verschlüsselung mit .NET Core (Tutorial #2)

Schon wieder über einen Monat her, seit ich das Thema .NET Core angeschnitten habe und ich habe doch bereits ganz viele weitere Projekte und Ideen, aber natürlich möchte ich trotzdem eine kleine, aber nutzbare App schreiben, eine Passwort-Verschlüsselung mit .NET Core. Lauffähig als App unter Windows, Mac und Linux, dabei gehen wir dann auch direkt darauf ein, wie euer Source Code zu einer App kompiliert wird.

Wo stehen wir?

Auch, wenn der erste Teil der Reihe bereits etwas zurück liegt, bisher haben wir .NET Core auf den Systemen zum Laufen bekommen und mit einem simplen „Hello World“ getestet, ob die Installation auch wirklich lauffähig ist:

  • … .NET auf Mac und Linux installieren
  • … eine erste Konsolen-Anwendung bauen
  • … eine erste kleine „real-world“-Applikation erstellen
  • … die Applikationen für das jeweilige System zur Verfügung stellen
  • … und eine native GUI auf dem jeweiligen System anzeigen

Dann ist es nun Zeit, die erste richtige Anwendung zu schreiben. Der Code bleibt weiterhin relativ simpel, nutzt Features von .NET Core, könnte aber Anwendungsbereiche finden.

Passwort-Verschlüsselung mit .NET Core

cd ~/Desktop
dotnet new console -o PasswordEncryptor
cd PasswordEncryptor
dotnet restore

Wie im vorigen Teil finden wir eine Program.cs-Datei, die ein vorgefertigtes Hello World enthält, welches via dotnet run ausgegeben werden könnte. Dann kann es jetzt ans coden gehen.

Zu allererst, was soll das Programm erreichen?! Ich möchte ein Programm erstellen, dass ein Passwort entgegennimmt, es hashed und mit einem Salt versieht, sodass man es speichern könnte – wir geben es nur in der Konsole aus. Danach soll ein weiteres Passwort eingegeben werden und wir prüfen dann, ob die beiden Passwörter übereinstimmen.

Öffnet Program.cs im Editor eurer Wahl und editiert die Main-Methode.

static void Main(string[] args)
{
    Console.WriteLine("Bitte Passwort eingeben:");
    string password = Console.ReadLine();
    
    Console.WriteLine($"Dein Passwort ist {password}");
}

In Zeile 3 werdet ihr nach dem Passwort gefragt, danach wird eure Eingabe in der Variable password gespeichert und letztendlich wieder ausgegeben. Fehlt nur noch das Hashen zwischendrin und hier kommt uns .NET zur Hilfe, denn im .NET Namespace System.Security.Cryptography finden wir allerhand Hilfsmethoden, die wir bereits nutzen können. Unter anderem die Klasse SHA256, die den SHA256-Hash einer Eingabe berechnet.
Wir erstellen also eine neue Methode, direkt unter Main

private static string EncryptPassword(string password)
{
    using(var sha256 = SHA256.Create())
    {
        var hashedBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(password));
        var hash = BitConverter.ToString(hashedBytes).Replace("-", "").ToLower();
        
        return hash;
    }
}

Okay, was geht hier vor?! Zuerst einmal haben wir eine private Methode erstellt, also eine Methode, die nur aus der Klasse Program heraus aufgerufen werden kann und die einen String, namens password entgegennimmt und einen weiteren String (den Passwort-Hash) zurückgibt.
Das using nutzen wir, damit die SHA256-Ressource nach Durchlauf des im Block definierten Codes wieder vom Speichermanagement freigegeben wird. Eine super Ressource zu dem Thema findet ihr von Paul Ballard im Pluralsight Blog.
Innerhalb des using-Blocks geschieht nun aber, was uns genauer interessiert – das Hashing! ComputeHash der SHA256-Instanz nimmt ein ByteArray entgegen, hashed dieses und gibt das ByteArray der verschlüsselten Zeichen zurück. Somit müssen wir das Passwort über weitere Helfer-Methoden aus dem Encoding-Namespace vom String zum ByteArray bringen.
Haben wir in hashedBytes die Zeichenkette des gehashten Passwortes, bringen wir dies wieder über BitConverter.ToString() in den Datentyp string und entfernen dann alle Bindestriche, damit es eine reine Zeichenkette wird (die BitConverter-Klasse konvertiert ByteArrays in Basisdatentypen, wie Strings und auch zurück).
die Variable hash enthält dann den Hash-Wert als String und diesen geben wir zurück.
Eine letzte Sache noch in der Main-Methode – die Verschlüsselung muss aufgerufen, der Rückgabewert gespeichert und dann in der Konsole ausgegeben werden, damit wir unsere Arbeit betrachten können:

static void Main(string[] args)
{
    Console.WriteLine("Bitte Passwort eingeben:");
    string password = Console.ReadLine();

    string hashedPassword = Program.EncryptPassword(password);
    
    Console.WriteLine($"Dein Passwort-Hash ist {hashedPassword}");
}

Speichern, ab an die Konsole und…

dotnet run

Keine Panik! Wir kriegen das hin.

error CS0103: The name 'SHA256' does not exist in the current context [/...../PasswordEncryptor/PasswordEncryptor.csproj] – Wie im Post beschrieben, befindet sich SHA256 nicht im System-Namespace, sondern in System.Security.Cryptography. Diesen müssen wir also importieren. In Zeile 2 der Datei – direkt unter using System; nehmen wir also folgendes auf:

using System.Security.Cryptography;

Wieder speichern, dann

dotnet run

Encoding befindet sich scheinbar ebenfalls in einem anderen Namespace – und laut Microsoft Doku ist dies auch so: Namespace Text. Also in Zeile 3 kommt nun

using using System.Text;

Aller guten Dinge sind Drei, also speichern, dann noch ein letztes Mal:

dotnet run

Und wir sehen den lang ersehnten Passwort-Hash!

Ein wenig Salz dazu…

Dass Passwörter als Hash gespeichert werden, wissen nicht nur wir, sondern auch die Jungs, die an eure Passwörter wollen. Mittlerweile ist es Standard, jedem Passwort-Hash einen Salt-Wert dazu zu geben. Salt ist eine zufällige Zahlenkette, die dem Hash zugegeben wird. Nur wir, als Entwickler wissen, an welcher Stelle im Hash das Salt anfängt und nur wir kennen den Salt-Wert. Somit ist es dann im besten Fall auch nur uns möglich, das Salt wieder zu entfernen, so den korrekten Hash-Wert zu erlangen und zu prüfen, ob Passwort-Eingaben übereinstimmen. Aber eins nach dem Anderen. Erst einmal geben wir nun Salz dazu – und auch hier hilft Microsoft uns mit .NET enorm. Erneut legen wir eine weitere Methode an, diesmal mit dem Namen GetSalt()

private static string GetSalt()
{
    byte[] bytes = new byte[16];
    using(var keyGenerator = RandomNumberGenerator.Create())
    {
        keyGenerator.GetBytes(bytes);
	return BitConverter.ToString(bytes).Replace("-", "").ToLower();
    }
}

Wir initialisieren ein ByteArray mit maximaler Länge von 16, nutzen .NET’s RandomNumberGenerator aus dem System.Security.Cryptography-Namespace und erstellen so eine zufällige Zeichenkette, die dann – via BitConverter – konvertiert als String aus der Methode zurückgegeben wird. In Main erzeugen wir dann einen Salt-Wert und fügen diesen zur Einfachheit einfach an das Passwort an.

static void Main(string[] args)
{
    Console.WriteLine("Bitte Passwort eingeben:");
    string password = Console.ReadLine();
    string salt = Program.GetSalt();
    string hashedPassword = Program.EncryptPassword(password) + salt;
    
    Console.WriteLine($"Dein Passwort-Hash ist {hashedPassword}");
}

Und alle Jahre wieder: Speichern und

dotnet run

Vergleichen wir nun den reinen Passwort Hash mit dem Hash inklusive Salt, dann erkennen wir, dass eine Zeichenkette an den gleich gebliebenen Hash angefügt wurde.
532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25
532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e2540b991bd7b5cba8f231c5b88a0785a06
Und das Salt ist ein jeweils zufällig generierter Wert, während dasselbe Wort immer denselben hash besitzen wird. Somit können wir die Passwort-Hashes etwas unstatischer machen und Rainbow-Tables das Leben schwerer machen.
Wichtig ist, dass natürlich neben dem gesamten Hash-Wert auch das Salt gespeichert wird, da dieser so nicht mehr generiert wird. Die Logik, wie das Salt in den Hash integriert wird, bleibt euch überlassen.

Darf ich rein? Die Passwort-Prüfung

Verschlüsseln ist schön und gut, ein fiktiver User kann so nun einen Account erstellen, aber wir prüfen wir, ob sein Passwort korrekt ist, wenn er es eingeben hat? Da kommt uns zu Gute, dass der Hash eines Wortes immer denselben Wert haben wird. Test hat immer den SHA256-Wert von 532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25 – was sicher auch Rainbow-Tables wissen, also nutzt das nicht als euer Passwort. Auch nicht mit 123 angefügt. Ernsthaft.
Aber gehen wir an die Entschlüsselung. Unsere Annahme ist, wir speichern den gesamten Hash-Wert und das Salt in einer Datenbank und haben somit Zugriff auf diese beiden Wert und der User gibt sein Passwort im Klartext ein. Wir wollen dies prüfen. Los geht’s wieder in Main. Den vorigen Teil lassen wir stehen und es geht darunter weiter.

static void Main(string[] args)
{
    //...voriger Code steht hier oben
    // ==============================================
    string databaseSalt = salt;
    string databaseHash = hashedPassword;

    Console.WriteLine("Bitte zu prüfendes Passwort eingeben");
    string passwordToVerify = Console.ReadLine();
}

In Zeile 5 und 6 nehmen wir die beiden Werte vom alten Code und speichern diese in neuen Variablen – wir simulieren hier nur, dass wir diese beiden Werte aus der eben angesprochenen Datenbank ausgelesen haben. Danach bitten wir um Passwort-Eingabe, wir bereits zu Anfang. Danach brauchen wir wieder eine Methode, die die Passwörter vergleicht. Wir nennen sie IsPasswordCorrect():

private static bool IsPasswordCorrect(string password, string salt, string hash)
{
    string correctPassHash = hash.Replace(salt, "");
    string newPassHash = Program.EncryptPassword(password);
		
    if (correctPassHash.Equals(newPassHash))
    {
        return true;
    }
    else
    {
        return false;
    }
}

Diese Methode nimmt das neu eingegebene Passwort entgegen, den Salt-Wert aus der Datenbank und den gesamten Hash-Wert aus der Datenbank. Im ersten Schritt (Zeile 3) entfernen wir das Salt aus dem Hash, eine Zeile darunter nutzen wir unsere zuvor erstellte Methode EncryptPassword(), um den Hash-Wert des neu eingegebene Passwortes zu erstellen. In Zeile 6 werden beide Werte verglichen und sind sie identisch, dann geben wir true zurück, ansonsten false. Das war’s schon! Nun können wir die Methode aufrufen, also zurück zu Main():

static void Main(string[] args)
{
    //...voriger Code steht hier oben
    // ==============================================
    string databaseSalt = salt;
    string databaseHash = hashedPassword;

    Console.WriteLine("Bitte zu prüfendes Passwort eingeben");
    string passwordToVerify = Console.ReadLine();

    bool correct = Program.IsPasswordCorrect(passwordToVerify, databaseSalt, databaseHash);
    if (correct)
    {
        Console.WriteLine("Passwörter sind identisch!");
    }
    else
    {
        Console.WriteLine("Falsches Passwort eingegeben!");
    }
}

Der Code ist ziemlich eindeutig. Die eben geschriebene Methode wird mit allen drei Parametern aufgerufen und wir prüfen, ob wir true oder false zurück bekommen und geben eine entsprechende Ausgabe aus.
Speichern… und wieder in ab die Konsole:

dotnet run

Testet es mit identischen und verschiedenen Passwörtern.

Wir können nun also Passwörter verschlüsselt speichern und trotzdem prüfen, ob der User eine korrekte Eingabe beim Login vorgenommen hat. Wir haben .NET-Methoden zur Vereinfachung unseres Lebens sinnvoll genutzt und können das Programm sowohl auf Windows, Mac und Linux laufen lassen.

Next Steps

Wo sind wir auf unserer Agenda? Letztendlich haben wir „nur“ die Applikation erstellt, was allerdings schon ein ziemlicher Weg war. Aktuell müssten wir den User immer das Terminal öffnen lassen, um das Programm via dotnet run zu starten. Das macht ja niemand, deshalb ist unser nächstes Ziel, die Applikation erst einmal simpel ausführbar für alle Systeme bereit zu stellen und danach möchte ich eine kleine GUI bauen, die ein Input „Registrieren“ und ein Input „Einloggen“ besitzt.  Beim Einloggen soll dann geprüft werden, ob das Passwort vom „Registrieren“ genutzt wurde.

  • … .NET auf Mac und Linux installieren
  • … eine erste Konsolen-Anwendung bauen
  • … eine erste kleine „real-world“-Applikation erstellen
  • … die Applikationen für das jeweilige System zur Verfügung stellen
  • … und eine native GUI auf dem jeweiligen System anzeigen

Ich gebe mir alle Mühe, den nächsten Teil schneller zu bringen, als diesen hier 😉 Bis dahin, viel Spaß beim Coden! Den kompletten Code für das File findet ihr als Github Gist hier.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.