Anzeige
Tutorialbeschreibung

Passwort-Klasse - Erzeugen und Validieren von Passwörtern, Salt erzeugen, Salted Hash berechnen

Passwort-Klasse - Erzeugen und Validieren von Passwörtern, Salt erzeugen, Salted Hash berechnen

PHP-Klasse zum Erzeugen von Passwörtern [wahlweise mnemonisch], Validieren von Passwörtern und Erzeugung entsprechender Passwort-Hashes mit oder ohne Salt inkl. Anwendungsbeispielen.

Die komplette Klasse liegt als Arbeitsdatei zum Download bereit.

Voraussetzung: PHP 5 oder höher


Einleitung


In diesem Tutorial möchte ich euch den möglichen Aufbau einer Passwort-Klasse zeigen, mit Hilfe derer man [wahlweise mnemonische] Passwörter erzeugen, vom Benutzer eingegebene Passwörter validieren und Hashwerte mit oder ohne Salt von generierten oder eingegebenen Passwörtern berechnen kann. Anschließend folgen ein paar Anwendungsbeispiele.

An dieser Stelle kommen für den ein oder anderen eventuell schon zwei Fragen auf:

1. Was bedeutet "mnemonisch"?
Mnemonische Passwörter sind besonders leicht zu merkende Passwörter, welche abwechselnd aus Konsonanten und Vokalen zusammengesetzt werden und am Ende meist noch eine Zahl enthalten. Beispiele hierfür wären "muhase437" oder "refakoni671".

2. Was ist ein Salt bzw. Salted Hash?
Diese Frage wird sehr gut auf wikipedia.de beantwortet, deshalb verweise ich einfach mal auf den entsprechenden Artikel: Salted Hash

Genug der Worte, auf zur Tat.


Treffen der Vorbereitungen

Festlegen der Variablen

Als Erstes definieren wir unsere benötigten Variablen. Wir beginnen mit der Festlegung der Zeichen, die zur Generierung der Passwörter verwendet werden sollen.

class Password {
    
    //-> Festlegen der Zeichen für die Passwortgenerierung
    private $uniqueLetters = 'abcdefghkmnpqrstuvwxyz';
    private $uniqueNumbers = '123456789';
    private $uniqueConsonants = 'bcdfghkmnpqrstvwxyz';
    private $uniqueVowels = 'aeu';
    
}
Dem aufmerksamen Beobachter wird hier nicht nur der komische Präfix "unique" der Variablen auffallen, sondern auch, dass da tatsächlich einige Zeichen des Alphabets fehlen. Der Grund hierfür ist ganz simpel: Angenommen, wir möchten einem Benutzer ermöglichen, sich ein neues Passwort an seine Emailadresse zusenden zu lassen. Dieses Passwort wird dann natürlich im Klartext in der Email stehen. Nun besteht durchaus die Möglichkeit, dass der Empfänger noch nie etwas von Copy&Paste gehört hat [ja, ich habe schon Pferde vor der Apotheke kotzen sehen]. Um beim Abtippen mögliche Verwechslungen wie zum Beispiel dem Großbuchstaben O und der Zahl 0 oder dem Großbuchstaben I [ihh] und dem Kleinbuchstaben l [ell] zu verhindern, entfernen wir diese Zeichen kurzerhand.

Danach teilen wir die Variable "uniqueLetters" noch einmal in Konsonanten und Vokale für die Generierung mnemonischer Passwörter auf.

Um jedoch die Flexibilität der Klasse zu gewährleisten, schieben wir die Variablen mit dem kompletten Alphabet gleich hinterher.

private $allLetters = 'abcdefghijklmnopqrstuvwxyz';
private $allNumbers = '1234567890';
private $allConsonants = 'bcdfghjklmnpqrstvwxyz';
private $allVowels = 'aeiou';
Nun legen wir noch einige Sonderzeichen fest.

private $specialChars = '!$%&=?/*-+#;:';
Jetzt steht fest, welche Zeichen wir verwenden. Definieren wir nun die Anzahl der Klein- und Großbuchstaben, Ziffern und Sonderzeichen.

private $lChars = 3; //-> Anzahl Kleinbuchstaben
private $uChars = 2; //-> Anzahl Großbuchstaben
private $nChars = 2; //-> Anzahl Ziffern
private $sChars = 1; //-> Anzahl Sonderzeichen
Hieraus ergibt sich logischerweise die Länge des generierten Passwortes. Nun legen wir das Standardverhalten für die Validierungsmethode fest.

private $minLength = 8;         //-> minimale Passwortlänge
private $lCharsRequired = true; //-> Kleinbuchstaben erforderlich?
private $uCharsRequired = true; //-> Großbuchstaben erforderlich?
private $nCharsRequired = true; //-> Ziffern erforderlich?
private $sCharsRequired = true; //-> Sonderzeichen erforderlich?
Demzufolge muss ein vom Benutzer eingegebenes Passwort aus mindestens acht Zeichen bestehen und mindestens einen Kleinbuchstaben, einen Großbuchstaben, eine Ziffer und ein Sonderzeichen enthalten.

Einige werden sich vielleicht fragen, weshalb hier nur ja/nein [true/false] festgelegt wird und nicht die Anzahl der Zeichen, wie es in einigen anderen Passwortklassen oder -funktionen der Fall ist. Ich halte eine solche Vorgehensweise nicht für nötig, wenn nicht sogar für sinnfrei.

Warum?

Gehen wir einmal von einer Brute-Force-Attacke aus. Erst einmal sollte sich jeder darüber im Klaren sein, dass ein Passwort immer geknackt werden kann. Die essentielle Frage, die man sich stellen muss, lautet: Wie viel Zeit wird dafür benötigt? Ein Passwort ist genau dann als sicher anzusehen, wenn der Aufwand den Nutzen übersteigt. Mit anderen Worten: hat ein Angreifer das Passwort sagen wir einmal beispielsweise nach sieben Tagen immer noch nicht herausgefunden, wird er den Versuch vermutlich als gescheitert ansehen und beenden.

Wir müssen also nur die maximale Zeit so hoch wie möglich ansetzen. Diese maximale Zeit berechnet sich aus der Anzahl der Kombinationsmöglichkeiten geteilt durch die Anzahl der Versuche pro Sekunde/Minute/etc. Die Anzahl der Kombinationsmöglichkeiten berechnet sich aus der Anzahl der möglichen Zeichen hoch die Länge des Passwortes.

Nehmen wir einmal an, ein Passwort besteht lediglich aus Klein- und Großbuchstaben und hat eine Länge von acht Zeichen. Unser Alphabet hat 26 Buchstaben, das ergibt bei Klein- und Großbuchstaben 52 mögliche Zeichen. Bei einer Passwortlänge von acht Zeichen ergibt das 52^8 = 53.459.728.531.456 mögliche Kombinationen. Wie man sieht, ist es völlig irrelevant, wie viele Klein- und Großbuchstaben vorhanden sind, die Anzahl möglicher Kombinationen bleibt gleich.

Anmerkung: Lasst euch von der Größe dieser Zahl nicht beirren. Ein solches Passwort kann mit heutigen Mittelklasse-Computern bereits in weniger als zwei Tagen geknackt werden. Es macht also durchaus Sinn, alle vier Variablen auf "true" und natürlich weiter oben bei der Festlegung der Anzahl der Zeichen alles auf >= 1 zu setzen.

Wenden wir uns wieder der Klasse zu und setzen schnell noch ein paar Verhaltensvariablen und initialisieren die Ein- und Ausgabevariablen.

private $isMnemonic = false;    //-> mnemonische Passwörter erzeugen: ja/nein [true/false]
private $useUniqueChars = true; //-> nur eindeutige Zeichen erlauben: ja/nein [true/false]
private $useSalt = true;        //-> Salted Hash generieren: ja/nein [true/false]

private $salt = '';
private $password = '';
private $hash = '';
Die Festlegung der Variablen ist damit abgeschlossen.

Erstellen der Setter-, Getter- und Konfigurationsmethoden

public function isMnemonic($value) {
    
    $this->isMnemonic = $value;
}

public function useUniqueChars($value) {
    
    $this->useUniqueChars = $value;
}

public function useSalt($value) {
        
    $this->useSalt = $value;
}

public function setSalt($salt) {
    
    $this->salt = $salt;
}

public function getSalt() {
    
    return $this->salt;
}

public function setPassword($password) {
    
    $this->password = $password;
}

public function getPassword() {
    
    return $this->password;
}

public function setHash($hash) {
    
    $this->hash = $hash;
}

public function getHash() {
    
    return $this->hash;
}



Das Herzstück - der Passwortgenerator


Wir erstellen uns eine Methode namens "generatePassword", prüfen, ob ähnliche Zeichen zugelassen sind und definieren anhand dessen die Variablen für die verwendeten Zeichen.

public function generatePassword($lChars = null, $uChars = null, $nChars = null, $sChars = null) {
    
    if($this->useUniqueChars == true):
    
        $letters = $this->uniqueLetters;
        $numbers = $this->uniqueNumbers;
        $consonants = $this->uniqueConsonants;
        $vowels = $this->uniqueVowels;
        
    else:
    
        $letters = $this->allLetters;
        $numbers = $this->allNumbers;
        $consonants = $this->allConsonants;
        $vowels = $this->allVowels;
        
    endif;
    
    $specialChars = $this->specialChars;
}
Wie ihr sehen könnt, können vier Parameter übergeben werden, die euch bekannt vorkommen sollten und mit Hilfe derer man die Anzahl der jeweils verwendeten Zeichen abweichend von der Grundkonfiguration festlegen kann. Diese Parameter fragen wir im Folgenden ab. Wurde nichts übergeben, lesen wir die Grundkonfiguration ein.

if($lChars === null) $lChars = $this->lChars;
if($uChars === null) $uChars = $this->uChars;
if($nChars === null) $nChars = $this->nChars;
if($sChars === null) $sChars = $this->sChars;
Als Nächstes prüfen wir, ob ein mnemonisches Passwort [oder eben auch nicht] generiert werden soll und leeren die Variable "password" für den Fall, dass diese vorher schon einmal gefüllt wurde.

$this->password = '';
        
if($this->isMnemonic == true):
    
else:
    
endif;

Mnemonisches Passwort generieren

Wie eingangs bereits erwähnt, wechseln sich Konsonanten und Vokale ab. Dieser Aufbau erfordert, dass diese immer paarweise in einer einzigen for-Schleife generiert werden müssen. Die Anzahl der Durchläufe bestimmen wir, indem wir die Anzahl der Groß- und Kleinbuchstaben und Sonderzeichen addieren, durch zwei teilen und anschließend auf die nächsthöhere ganze Zahl aufrunden. Anschließend hängen wir noch die Anzahl der übergebenen oder vorkonfigurierten Ziffern an. Für die zufällige Auswahl der Konsonanten, Vokale und Ziffern dient uns die Funktion mt_rand().
if($this->isMnemonic == true):
    
    $maxConsonants = strlen($consonants) - 1;
    $maxVowels = strlen($vowels) - 1;
    
    for($i = 1, $j = ceil(($lChars + $uChars + $sChars) / 2); $i <= $j; $i++):
        $this->password .= $consonants[mt_rand(0, $maxConsonants)];
        $this->password .= $vowels[mt_rand(0, $maxVowels)];
    endfor;
    
    $maxNumbers = strlen($numbers) - 1;
    
    for($i = 1; $i <= $nChars; $i++) $this->password .= $numbers[mt_rand(0, $maxNumbers)];
    
else:
Hierbei wird das Passwort unter Umständen ein Zeichen länger als die Vorgabe, was aber in keinem Fall negativ zu bewerten ist, da sich Lesbarkeit und Sicherheit allenfalls erhöhen können.

Beispiel:
generatePassword(3, 3, 1, 1); //-> sollte ein Passwort der Länge 8 ergeben
//-> $lChars + $uChars + $sChars = 3 + 3 + 1 = 7
//-> 7/2 = 3.5
//-> ceil(3.5) = 4
//-> vier Durchläufe mit je zwei Zeichen ergibt 8 Zeichen plus 1 Ziffern = Passwort der Länge 9
//-> Mit den hier verwendeten Vorgaben $lChars = 3, $uChars = 2, $nChars = 2 und $sChars = 1 stimmt die Passwortlänge jedoch.


Standardpasswort generieren

Das ist recht schnell erledigt. Anhand von vier for-Schleifen reihen wir wieder mit Hilfe der Funktion mt_rand() zufällig ausgewählte Zeichen aneinander und schütteln die so entstandene Zeichenkette am Ende ein Mal kräftig durch.

$maxLetters = strlen($letters) - 1;
$maxNumbers = strlen($numbers) - 1;
$maxSpecialChars = strlen($specialChars) - 1;

for($i = 1; $i <= $lChars; $i++) $this->password .= $letters[mt_rand(0, $maxLetters)];
for($i = 1; $i <= $uChars; $i++) $this->password .= strtoupper($letters[mt_rand(0, $maxLetters)]);
for($i = 1; $i <= $nChars; $i++) $this->password .= $numbers[mt_rand(0, $maxNumbers)];
for($i = 1; $i <= $sChars; $i++) $this->password .= $specialChars[mt_rand(0, $maxSpecialChars)];

str_shuffle($this->password);

Passwörter validieren

Ausnahmsweise mal als Erstes die vollständige Methode

public function validatePassword($returnDetails = false, $minLength = null, $lCharsRequired = null,
                                 $uCharsRequired = null, $nCharsRequired = null, $sCharsRequired = null) {
    
    if($minLength === null) $minLength = $this->minLength;
    if($lCharsRequired === null) $lCharsRequired = $this->lCharsRequired;
    if($uCharsRequired === null) $uCharsRequired = $this->uCharsRequired;
    if($nCharsRequired === null) $nCharsRequired = $this->nCharsRequired;
    if($sCharsRequired === null) $sCharsRequired = $this->sCharsRequired;
    
    $aValidation = array('length' => true, 'lChars' => true, 'uChars' => true, 'nChars' => true, 'sChars' => true);
    
    if($minLength > 0 && strlen($this->password) < $this->minLength) $aValidation['length'] = false;
    if($lCharsRequired == true && !preg_match('/[a-z]/', $this->password)) $aValidation['lChars'] = false;
    if($uCharsRequired == true && !preg_match('/[A-Z]/', $this->password)) $aValidation['uChars'] = false;
    if($nCharsRequired == true && !preg_match('/[0-9]/', $this->password)) $aValidation['nChars'] = false;
    if($sCharsRequired == true && !preg_match('/[^A-Za-z0-9]/', $this->password)) $aValidation['sChars'] = false;
    
    if($returnDetails == true) return $aValidation;
    
    return array_search(false, $aValidation, true) !== false ? false : true;
}
Wie schon bei der Methode "generatePassword" können auch hier per Parameter Werte abweichend von der Grundkonfiguration übergeben werden. Zusätzlich gibt es einen Parameter namens "returnDetails". Da diese Methode vorrangig dazu dienen wird, vom Benutzer eingegebene Passwörter, beispielsweise bei einem Registrierungsformular, zu überprüfen, wäre es sinnvoll, diesem Benutzer bei einem negativen Ergebnis mitzuteilen, was genau euch an dem gewählten Passwort nicht gefällt. Wird dieser Parameter auf "true" gesetzt, wird ein Array zurückgeliefert. Dabei geben die Schlüssel an, was geprüft wurde und bekommen entweder den Wert "false" [fehlgeschlagen] oder "true" [erfolgreich] zugewiesen. Anhand dieser Angaben können detailierte Fehlermeldungen generiert werden [zum Beispiel, dass das Passwort ein Sonderzeichen enthalten muss]. Bleibt der Paramater "returnDetails" auf "false", wird lediglich ein einfaches "true" oder "false" zurückgeliefert.

Die Methode geht zuerst einmal davon aus, dass alle Voraussetzungen erfüllt sind [siehe Array "aValidation"], prüft danach alle Vorgaben und ändert bei einem Fehlschlag den Wert des entsprechenden Schlüssels auf "false".

Hashwert erzeugen

Apropos Registrierungsformular...
In der Regel werden Benutzerdaten, wie auch das Passwort, in einer Datenbank gespeichert. Jedes noch so tolle Passwort verliert jedoch seinen Sinn und Zweck, wenn diese Datenbank in die Hände Dritter gerät und die Passwörter darin im Klartext hinterlegt sind. Deshalb sollte man immer nur entsprechende Hashwerte speichern. Einfache Hashwerte sind aber längst nicht mehr sicher. Durch den Einsatz von Rainbow Tables können Passwörter in kürzester Zeit rekonstruiert werden. Abhilfe soll hier ein Salted Hash schaffen. Wir wenden diese Methode zweimal an und generieren einen sogenannten Double Salted Hash. Als Algorithmus kommt SHA-1 zum Einsatz.

Zunächst erstellen wir uns eine Methode, um einen Salt zu generieren. Wie der Salt aussieht, ist eigentlich völlig egal. Er sollte nur möglichst zu jeder Zeit eindeutig sein, und da kommt uns die Funktion "uniqid" doch sehr gelegen.

public function createSalt() {
   
    $this->salt = sha1(uniqid(mt_rand(), true));
}
Passwort und Salt sind generiert, nun können wir den Double Salted Hash berechnen.

public function makeHash() {
    
    if($this->useSalt == false): $this->hash = sha1($this->password);
    else: $this->hash = sha1($this->salt.sha1($this->salt.$this->password));
    endif;
}
Der Flexibilität halber können durch das Setzen der Variable "useSalt" auf "false" auch einfache Hashwerte erzeugt werden.


Anwendungsbeispiele

Passwörter generieren

//-> mnemonisches Passwort generieren und ausgeben
require_once 'Pfad/zur/Passwort-Klasse';
$pw = new Password();

$pw->isMnemonic(true);
$pw->generatePassword();

print $pw->getPassword(); //-> ergibt zum Beispiel ratefu39

//-> Standardpasswort generieren und ausgeben
require_once 'Pfad/zur/Passwort-Klasse';
$pw = new Password();

$pw->generatePassword();

print $pw->getPassword(); //-> ergibt zum Beispiel fG#1Agh3

Passwort validieren [Registrierungsformular]

$_POST['password'] = 'abc123def';

require_once 'Pfad/zur/Passwort-Klasse';
$pw = new Password();

$pw->setPassword($_POST['password']);

$result = $pw->validatePassword();
//-> $result = false;

$result = $pw->validatePassword(true);
//-> $result = array('length' => true, 'lChars' => true, 'uChars' => false, 'nChars' => true, 'sChars' => false);
//-> 'uChars' == false [enthält keinen Großbuchstaben]
//-> 'sChars' == false [enthält kein Sonderzeichen]

//-> wenn $result = true
$pw->createSalt();
$pw->makeHash();

$salt = $pw->getSalt();
$passwordHash = $pw->getHash();

//-> neuen Benutzer mit $passwordHash und $salt in der Datenbank anlegen

Anmeldung verifizieren [Loginsystem]

//-> Benutzerdaten aus der Datenbank auslesen
$userdata = array('passwordHash' => 'passwordHash', 'salt' => 'salt');

require_once 'Pfad/zur/Passwort-Klasse';
$pw = new Password();

$pw->setSalt($userdata['salt']);
$pw->setPassword($_POST['password']);

$pw->makeHash();
$passwordHash = $pw->getHash();

if($passwordHash == $userdata['passwordHash']): //-> Login erfolgreich
else: //-> Login fehlgeschlagen
endif;

Kommentare
Achtung: Du kannst den Inhalt erst nach dem Login kommentieren.
Portrait von bual
  • 09.07.2010 - 10:46

Sehr gut beschreiben. Top Arbeit.

Portrait von Kurtilein
  • 08.03.2010 - 12:49

Sehr gutes Tutorial und wirklich nützlich. Hervoragend beschrieben. Besten Dank

Alternative Portrait
-versteckt-(Autor hat Seite verlassen)
  • 07.03.2010 - 14:19

Klasse Tutorial! Sehr nützlich!

Portrait von Necros
  • 06.03.2010 - 12:22

Sehr gutes Tutorial! Alles ausführlich beschrieben und man hat sich an dieser Stelle viel Recherche erspart. Vielen Dank dafür!

Portrait von stoevchen
  • 06.03.2010 - 02:55

wow, super! vielen dank für die beispiele & erklärungen! TOP!

Portrait von lintschi
  • 05.03.2010 - 14:06

toll!
ich bin wirklich ein technischer nobody und kann damit aber was anfangen!
super! danke!

Portrait von hergy
  • 05.03.2010 - 09:03

Super Arbeit!!! Sehr nützlich, vielen Dank!!!

Portrait von Jafix
  • 02.03.2010 - 20:37

Wow! Echt gut! Dafür bekommst du von mir nur ein Wort: TOP! ;-) *thumbsup*

Portrait von mfgleo
  • 02.03.2010 - 15:43

Danke für die ausführliche Erklärung und die Beispiele. Sehr Gut!!!

Portrait von XxKlenerxX
  • 01.03.2010 - 21:09

Gut erklärt, auch oft/immer gebrauch bar, top!

Portrait von tetsalo
  • 01.03.2010 - 16:52

Vielen Dank für eure Kommentare.

@ChrisvA: Auf die Gestaltung der PDF habe ich leider keinen Einfluss, liegt also nicht an mir.

Portrait von ChrisvA
  • 01.03.2010 - 14:47

Sehr schon aufgeschrieben, was so eine Klasse alles machen muss.
Schade nur, dass im PDF das Syntaxhighlighting nicht drinnen ist, liegt aber wohl an PSD-Tutorials

Portrait von K-Dawg
  • 01.03.2010 - 10:40

Guter Tutorial. Vielen Dank :)

x
×
×