Glengamoi (Forum) · AspHeute · .NET Heute (RSS-Suche) · AspxFiles (Wiki) · .NET Blogs
ASP German Homepage Homepage
 

Liste

.NET 2.0 (1)
.NET Allgemein (16)
.NET Fu (5)
ADO.NET (11)
Aprilscherz (3)
ASP Grundlagen (44)
ASP Tricks (83)
ASP.NET (44)
ASPIntranet.de (5)
C# (28)
Datenbank (44)
Dokumentation (4)
IIS 6.0 (1)
Komponenten (29)
Optimierung (10)
Server (21)
Sicherheit (34)
Tee Off (6)
VB.NET (6)
WAP (8)
Web Services (11)
XML (9)

RSS 2.0 - Die neuesten fünf Artikel auf AspHeute.com


 

Suchen





 

English Articles
Chinese Articles
Unsere Autoren
 
Link zu AspHeute
Impressum
Werben
Anfragen

Passwörter speichern - aber richtig!

Geschrieben von: Christoph Wille
Kategorie: Sicherheit

In sehr vielen - um nicht zu sagen fast allen - Webanwendungen werden Benutzerdaten verwaltet, vom Webforum bis hin zum Webshop. Diese Benutzerdaten umfassen auch die Logininformationen der User, welche neben dem Usernamen auch das Passwort enthalten - und das als Plain Text. Eine Sicherheitslücke par excellence.

Warum ist das eine Sicherheitslücke, wenn man den Usernamen und das Passwort als Plain Text speichert? Nun, stellen Sie sich vor, ein Cracker verschafft sich durch etwaige Betriebssystem- bzw. Serversoftwarefehler Zugang zum System, und kann die Benutzerdatenbank auslesen. Da er den Benutzernamen und das Passwort jedes beliebigen Users kennt, kann er jetzt als "echter" User einloggen und mit dessen Berechtigungen machen was er möchte - von der Bestellung im Webshop hin zu Rufmord im Forum. Und Sie sind der Betreiber...

Wie kann man dieses Sicherheitsrisiko eliminieren? Nun, warum weit schweifen wenn es eine seit Jahrzehnten bekannte und bewährte Methode zur sicheren Speicherung von Passwörtern gibt: unter UNIX werden Passwörter von Benutzern als sogenannter "gesalteter Hash" gespeichert.

Was ist ein gesalteter Hash?

Ein Hash ist ein numerischer Wert fixer Länge der Daten beliebiger Länge eindeutig identifiziert. Ein Beispiel für einen Hashalgorithmus ist SHA1, der bereits Thema eines ASP Artikels war. Der geneigte Leser könnte nun einwenden, daß das Speichern des Hashes anstatt des Passworts ausreichen würde - warum aber stimmt das nicht?

Der Grund hierfür ist, daß gegen gehashte Passwörter - ein gutes Beispiel sind die MD5-gehashten Passwörter von NT4 - üblicherweise eine sogenannte Dictionary Attacke gefahren wird. Dabei handelt es sich um einen Brute Force Angriff: alle Wörter in einem Wörterbuch wurden MD5 gehasht, und diese werden nun mit der Passwortdatenbank verglichen. Und raten Sie mal, wie schnell man damit einige Passwörter gefunden hat.

Der gesaltete Hash hat den Sinn, genau solche Attacken ins Leere laufen zu lassen, indem man jedem Passwort vor dem Hashen einen zufälligen Wert, den sogenannten Salt, anhängt - und erst dann den Hash über das Passwort und den Salt berechnet. Zwar muß man zum Vergleich des Passwortes den Salt neben gesalteten Hash mitspeichern, aber der einzige Angriffsvektor bleibt jetzt das Wörterbuch für jedes einzelne gespeicherte Passwort neu mit dem Salt zu codieren - und das dauert dann schon sehr lange.

Speichern des gesalteten Hashs

Wie bereits erwähnt, muß man jetzt anstatt Benutzername und Passwort drei Felder speichern: Benutzername, Salt und das mit dem Salt gehashte Passwort. Ebenfalls bereits erwähnt habe ich, daß wenn diese Daten einem Cracker in die Hände fallen, er mit Standardangriffen ein Problem bekommt, und sehr wahrscheinlich sich ein leichteres Opfer suchen wird.

Ein Punkt muß aber beachtet werden: eine "Passwort-Erinnerungsemail" kann man jetzt nicht mehr schicken - alles was man tun kann, ist dem User ein vollständig neues Passwort zu generieren und zuzusenden. Da auch in diesem Bereich viele Fehler passieren, beginnen wir mit dem .NET Code für das Generieren eines wirklich zufälligen Passwortes.

Passwörter generieren - aber richtig!

Die gesamte Klasse enstand im Zuge eines (C# ASP.NET) Community Projekts zusammen mit einem weiteren AspHeute-Autor, nämlich Alexander Zeitler. Auch dort stellte sich die Frage, wie man gute Passwörter generiert, und wie man diese korrekt in einer Datenbank ablegt.

Dafür haben wir die Klasse Password erstellt, die folgende Signatur hat:

namespace DotNetGermanUtils
{
  public class Password
  {
    public Password(string strPassword, int nSalt)

    public static string CreateRandomPassword(int PasswordLength)

    public static int CreateRandomSalt()

    public string ComputeSaltedHash()
  }
}

Die Methode zur Generierung eines neuen Passwortes ist statisch, und man kann bestimmen, wie lang das generierte Passwort sein soll:

    public static string CreateRandomPassword(int PasswordLength)
    {
      String _allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789";
      Byte[] randomBytes = new Byte[PasswordLength];
      RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
      rng.GetBytes(randomBytes);
      char[] chars = new char[PasswordLength];
      int allowedCharCount = _allowedChars.Length;

      for(int i = 0;i<PasswordLength;i++)
      {
        chars[i] = _allowedChars[(int)randomBytes[i] % allowedCharCount];
      }

      return new string(chars);
    }

Das Prinzip ist ähnlich zur ASP Lösung im Artikel Generieren eines sicheren Paßwortes, allerdings ist hier etwas spezielles eingebaut: wir verwenden kryptographisch sichere Zufallszahlen, um aus dem "Array" _allowedChars Buchstaben für das Passwort auszuwählen. Die Klasse RNGCryptoServiceProvider wurde auch schon im Artikel Unknackbare Verschlüsselung mit Onetime Pads besprochen.

Damit hat man ein wirklich zufälliges Passwort generiert, das man nun dem Benutzer als Initialpasswort setzen kann - nur braucht man dafür einen Salt.

Wir erzeugen einen Salt

Im Prinzip handelt es sich hier nur um eine Hilfsfunktion, die ebenfalls auf den RNGCryptoServiceProvider zurückgreift:

    public static int CreateRandomSalt()
    {
      Byte[] _saltBytes = new Byte[4];
      RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
      rng.GetBytes(_saltBytes);

      return ((((int)_saltBytes[0]) << 24) + (((int)_saltBytes[1]) << 16) + 
        (((int)_saltBytes[2]) << 8) + ((int)_saltBytes[3]));
    }

Erzeugt wird ein 4 Byte langer Salt (ein Integer, aus Einfachheitsgründen für die Speicherung in Datenbanktabellen). Dieser Salt sowie das generierte Passwort dienen als Grundlage, um den Salted Hash zu erzeugen.

Berechnen des Salted Hash

Das Berechnen des Salted Hash ist eine Instanzmethode, die auf zwei Membervariablen zugreift, die im Konstruktor gesetzt werden:

  public class Password
  {
    private string _password;
    private int _salt;

    public Password(string strPassword, int nSalt)
    {
      _password = strPassword;
      _salt = nSalt;
    }

Damit liefert die Methode ComputeSaltedHash nur noch den Hash selbst zurück, und nimmt keine weiteren Parameter an. Gehashed wird übrigens mit dem bekannten SHA1 Algorithmus:

    public string ComputeSaltedHash()
    {
      // Create Byte array of password string
      ASCIIEncoding encoder = new ASCIIEncoding();
      Byte[] _secretBytes = encoder.GetBytes(_password);
      
      // Create a new salt
      Byte[] _saltBytes = new Byte[4];
      _saltBytes[0] = (byte)(_salt >> 24);
      _saltBytes[1] = (byte)(_salt >> 16);
      _saltBytes[2] = (byte)(_salt >> 8);
      _saltBytes[3] = (byte)(_salt);

      // append the two arrays
      Byte[] toHash = new Byte[_secretBytes.Length + _saltBytes.Length];
      Array.Copy(_secretBytes, 0, toHash, 0, _secretBytes.Length);
      Array.Copy(_saltBytes, 0, toHash, _secretBytes.Length, _saltBytes.Length);

      SHA1 sha1 = SHA1.Create();
      Byte[] computedHash = sha1.ComputeHash(toHash);

      return encoder.GetString(computedHash);
    }

Damit haben wir die gesamten Funktionen beisammen, und können die Klasse zum Einsatz bringen.

Die Klasse Password im täglichen Einsatz

Ich habe ein kleines Beispiel zusammengestellt, das das Erstellen eines neuen Passwortes, eines neuen Salts, und dann das Generieren des Salted Hash zeigt:

using System;
using DotNetGermanUtils;

namespace HashPassword
{
  class TestApplication
  {
    [STAThread]
    static void Main(string[] args)
    {
      // Generate a new random password string
      string myPassword = Password.CreateRandomPassword(8);

      // Debug output
      Console.WriteLine(myPassword);

      // Generate a new random salt
      int mySalt = Password.CreateRandomSalt();

      // Initialize the Password class with the password and salt
      Password pwd = new Password(myPassword, mySalt);

      // Compute the salted hash
      // NOTE: you store the salt and the salted hash in the datbase
      string strHashedPassword = pwd.ComputeSaltedHash();

      // Debug output
      Console.WriteLine(strHashedPassword);
    }
  }
}

Folgender Punkt ist besonders wichtig: Generieren Sie für jeden Benutzer unbedingt einen neuen Salt. Sollten beide User zufällig das gleiche Passwort verwenden, so ist der Salted Hash dennoch verschieden für beide Benutzerkonten!

Der gezeigte Sourcecode zeigt das Erstellen eines neuen Passworts und eines neuen Salts, wie sieht es aus wenn der User einloggen möchte? Nun, nichts leichter als das:

// retrieve salted hash and salt from user database, based on username
...

Password pwd = new Password(txtPassword.Text, nSaltFromDatabase);

if (pwd.ComputeSaltedHash() == strStoredSaltedHash)
{
   // user is authenticated successfully
}
else
{
...

Im Prinzip nicht anders als bei bisherigen Benutzername/Passwort Implementierungen, aber die Daten sind bedeutend sicherer auch im Falle daß die (serverseitigen) Passwortdaten unbefugten Dritten in die Hände fallen.

Schlußbemerkung

Die Klasse, die im heutigen Artikel vorgestellt wurde, kann man in seine eigenen .NET Projekte einbinden - entweder direkt in C# Projekten oder als Assembly in anderen Programmiersprachen. Ausreden für unsichere Passwortspeicherung gibt es ab sofort nicht mehr!

Download des Codes

Klicken Sie hier, um den Download zu starten.

Verwandte Artikel

Aber bitte mit Rijndael
CAPICOM One
Generieren eines sicheren Paßwortes
Passwörter mit SHA1 absichern
PGP-Verschlüsselung bei Dateien
Unknackbare Verschlüsselung mit Onetime Pads
Ver- und entschlüsseln von Texten mit PGP

Wenn Sie jetzt Fragen haben...

Wenn Sie Fragen rund um die in diesem Artikel vorgestellte Technologie haben, dann schauen Sie einfach bei uns in den Community Foren der deutschen .NET Community vorbei. Die Teilnehmer helfen Ihnen gerne, wenn Sie sich zur im Artikel vorgestellten Technologie weiterbilden möchten.

Haben Sie Fragen die sich direkt auf den Inhalt des Artikels beziehen, dann schreiben Sie dem Autor! Unsere Autoren freuen sich über Feedback zu ihren Artikeln. Ein einfacher Klick auf die Autor kontaktieren Schaltfläche (weiter unten) und schon haben Sie ein für diesen Artikel personalisiertes Anfrageformular.

 

Und zu guter Letzt möchten wir Sie bitten, den Artikel zu bewerten. Damit helfen Sie uns, die Qualität der Artikel zu verbessern - und anderen Lesern bei der Auswahl der Artikel, die sie lesen sollten.

Bewerten Sie diesen Artikel
 Sehr gut   Nicht genügend  
   1  2  3  4  5  
 

  
   Für Ausdruck optimierte Seite

©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.