Hallo Jens,
Also ich habe hier Passworte, die allesamt deutlich länger als 8 Zeichen sind, z.B folgendes:
$1$2C6Q0Qkx$BHWpm2Yrv/qQId405EcpZ.
Wie kann ich daraus jetzt das "Salz" erkennen bzw. wie kann ich es auf Gleichheit mit Klartext-String xy prüfen?
1. Ändere das Passwort bitte mal, jetzt wo's hier veröffentlicht wird. Man kann es sicher nicht SOFORT knacken, aber wenn es nicht sicher genug ist: Brute Force ist bei hinreichend kurzen Passwörtern bei der heutigen Rechenleistung _verdammt_ schnell. ;-)
2. crypt() ist eine Standard-POSIX-Funktion zum hashen von Passwörtern. crypt() arbeitet immer mit "Salts", d.h. einer zufälligen Zeichenkette. Dies wird gemacht, um zu verhindern, dass Leute einfach für alle möglichen Passwörter Hashes vorherberechnen können. Wenn man z.B. nur eine normale Hash-Funktion wie MD5() nimmt und dann gehashtes_passwort = MD5(passwort) macht, dann ist das nicht sehr sicher, weil viele Leute sich inzwischen sogenannte "Rainbow-Tabellen" angelegt haben, die MD5-hashes für eine MENGE Passwörter vorberechnet erhalten. Man muss dann nur sehen, ob der Hash in der eigenen Tabelle enthalten ist und wenn ja, kennt man das Passwort.
Salts arbeiten so:
gehashtes_password = salt + HASH(salt + passwort)
Das heißt: Vor dem Verwenden der Hashfunktion (hier HASH genannt), wird vor das Passwort der Inhalt von salt gehängt, salt muss zufällig gewählt sein (für jedes Passwort neu). Um das ganze wiederherstellen zu können, wird das salt zusätzlich noch VOR das gehashte Passwort gehängt.
Es gibt nun 2 mögliche Implementierungen von crypt() in aktuellen UNIX-artigen Betriebsystemen:
1. Triple DES
Das ist der klassische crypt()-Algorithmus, der i.A. nur unter UNIX-
artigen Betriebssystemen zur Verfügung steht. Er benutzt einen Salt,
der 2 Zeichen lang ist und das Passwort darf maximal 8 Zeichen lang
sein (der Rest ist abgeschnitten). Dafür kann jedes Passwort einein-
deutig auf einen Hash abgebildet werden (bei gegebenem Salt), d.h.
zwei unterschiedliche Passwörter bei gleichem Salt verursachen zwei
unterschiedliche Hashes (das KANN bei MD5 schon gar nicht garantiert
werden weil man in MD5 ja beliebig lange Daten reinstopfen kann,
hinterher aber nur Daten einer festen Länge wieder herauskommen).
Es gibt auch ANSI-C-Implementierungen des Algorithmus und ich hatte
vor Ewigkeiten irgendwann sogar mal ein kleines Windows-Programm
geschrieben gehabt, womit man auch unter Windows das traditionelle
crypt() verwenden kann. Großer Nachteil: Die ganze andere Software
(Apache, PHP, ...) baut den Algorithmus unter Windows nicht nach und
kann den dagegen nicht verwenden (unter POSIX verwenden sie halt die
Betriebssystemfunktion).
Der Salt ist idR. 2 Zeichen lang, es gibt auch eine Variation, bei der
der Salt 9 Zeichen lang ist.
2. MD5-basiert
Hier wird als Hash-Algorithmus MD5 verwendet und der Salt ist 8
Zeichen lang. In dem Fall (zur Unterscheidung) fängt der Hash dann
mit $1$ an, gefolgt von dem Salt, gefolgt von einem weiteren $, gefolgt
von dem eigentlichen Hash. Dies ist der Fall des obigen Passworts.
[diverse andere Software hat das noch irgendwie erweitert; PHP bietet auf einigen Systemen noch $2a$ an für Blowfish als Hash-Algo, was aber m.W. nicht standardisiert ist, Apache verwendet nicht $1$ sondern $apr1$ für MD5, $apr2$ für SHA1]
Da Dein obiges Passwort ein MD5-basierter crypt-Hash ist, kannst Du in PHP einfach folgendes machen:
if ($gehashtes_passwort == crypt ($eingegebenes_passwort, $gehashtes_passwort)) {
# Zugang gewährt
} else {
# Zugang verweigert
}
Wie funktioniert das? Man muss crypt() ja sagen können, welchen Salt man beim vorherigen Passwort verwendet hat, damit man den Hash rekonstruieren kann. Daher müsste man strenggenommen '$1$altersalt$' als 2. Parameter an crypt übergeben (oder halt die 2 Zeichen für 3DES). Da das aber unpraktisch wäre, akzeptiert crypt() einfach das gesamte alte Passwort und verwendet automatisch nur den Salt-Teil wieder.
Wenn Du dieses Verfahren weiter verwenden willst (MD5 + Salt ist nicht schlecht, SHA1 oder etwas anderes wäre natürlich besser, das geht aber mit crypt() nicht [1]), dann musst Du bei _neuen_ (!) Passwörtern dafür sorgen, dass immer MD5 als Hash-Algorithmus genommen wird, d.h.
$neues_gehashtes_passwort = crypt ($eingegebenes_neues_passwort, $salt);
Wobei $salt gerade sowas wäre wie
$salt = '$1$'.$acht_zufaellige_zeichen.'$';
Wobei die acht zufälligen Zeichen aus [a-zA-Z0-9./] sein sollten und vor allem zufällig sein müssen, aber ruhig berechenbar sein dürfen. Wenn Du also einen brauchbaren Salt mit PHP erzeugen willst, kannst Du z.B. nutzen:
function generate_salt ($len = 8) {
static $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./';
$salt = '';
for ($i = 0; $i < $len; $i++) {
$salt .= $chars[mt_rand (0, 63)];
}
return $salt;
}
(Alternativ auch rand() statt mt_rand() - aber da die Vorhersagbarkeit des Zufallszahlgenerators für diese Problemstellung keine Rolle spielt, ist das eigentlich relativ egal, was man nimmt (MT ist kryptographisch nicht sicher, rand ist aber auf einigen älteren Systemen noch sehr viel schlechter als MT, dafür auf neueren Systemen _deutlich_ besser))
Zusammengefasst nochmal:
Altes Passwort überprüfen:
if ($gehashtes_passwort == crypt ($eingegebenes_passwort, $gehashtes_passwort)) {
# Zugang gewährt
} else {
# Zugang verweigert
}
Neues Passwort hashen:
$neues_gehashtes_passwort = crypt ($eingegebenes_neues_passwort, '$1$' . generate_salt () . '$');
Übrigens, Du kannst Dir evtl. auch mal http://www.openwall.com/phpass/ anschauen, wenn Du Dich für Passwort Hashing in PHP interessierst.
Viele Grüße,
Christian
[1] Gut, Blowfish ginge noch, wenn das auf Deinem System verfügbar wäre. Wenn es das ist, würde ich das MD5 vorziehen, allerdings ist das Verfahren etwas komplizierter...