Schöner hashen mit bcrypt
Gastartikel von Oliver Sperke.
Ich bin 34 Jahre alt und seit 10 Jahren selbständiger Webentwickler. Mein Fokus liegt dabei auf der Erstellung, Beratung und Optimierung in den Bereichen High Performance, Usability und Sicherheit in den gängisten Internetsprachen: PHP, HTML, Javascript und CSS.
Bei meinem vorherigem Gastbeitrag wurde ich direkt im ersten Kommentar aus meiner heilen Welt geworfen. Dort stand nämlich folgender „erschütternder Kommentar“ zu lesen:
Das du Salting und Mehrfachhashing predigst, während der Rest der Welt schon einen Schritt weiter zu bcrypt geht… Traurig.
Nun ja, dazu möchte ich drei Dinge sagen.
- Ich predige nicht (Ausnahme: „Es heißt Standard, verdammt, nicht Standart!“).
- Ach, wenn die Welt schon mal auf dem Stand des einfachen md5 wäre …
- Bcrypt verdient einen eigenen Beitrag.
Natürlich hatte der Autor völlig recht. Über Hashfunktionen im Web zu schreiben und bcrypt nicht zu erwähnen ist fast schon schändlich. Also bcrypt ist eine Hashfunktion, die auf Langsamkeit optimiert wurde. Um genauer zu sein, es ist nicht mal ein richtiger Hashalgorithmus, sondern eine Blowfish Verschlüsselung, bei der am Ende „die Schlüssel weggeworfen werden“, daher lässt sich das Ergebnis nicht mehr entschlüsseln. bcrypt ist eine Weiterentwicklung der „Traditional DES Scheme“ Funktion aus der Unixwelt. Obwohl dieses Verfahren 30 Jahre lang (!) gute Dienste geleistet hat, stellen sich so langsam „Alterserscheinungen“ ein. Der Zahn der Zeit nagt auch hier in Form von gestiegener Rechenleistung.
Kurze Rückschau
Hashalgorithmen wie md5 und die shaX sind auf Schnelligkeit optimiert. Das ist gut, wenn man prüfen will, ob der heruntergeladen Film auch wirklich korrekt übertragen wurde. Das ist auch gut, wenn man testen möchte, ob das SSL Zertifikat einer Webseite nicht verfälscht wurde. Das ist aber eher suboptimal, wenn man damit Passwörter speichern will. Wie schon im letzten Artikel erklärt und ausgiebig diskutiert, ist die gestiegene Rechenleistung auch ein Problem für die klassischen Hashfunktionen in Webanwendungen. Zwar kann man mit Salts, mehr Salts und Mehrfachhashes das Schlimmste verhindern, aber irgendwie ist das alles nicht so nachvollziehbar für jeden. Kurz gesagt: „Das geht besser“.
Was bcrypt kann
Selbst wenn wir dafür sorgen, dass unsere Passwörter gut geschützt sind, können wir natürlich kaum verhindern, dass ein Benutzer trotzdem ein schwaches Passwort verwendet. Wenn es der Angreifer gezielt auf eine Person, z. B. den Admin abgesehen hat, wird die Situation noch schlimmer, denn bis 10 Zeichen kann man auch schon mal eine Brute Force Attacke für einen einzelnen Hash probieren. Deshalb sollten wir unserem Cracker das Leben grundsätzlich so schwer wie möglich machen.
Bcrypt bringt gewisse Vorkehrungen für genau diesen Fall mit. Der Algorithmus ist auch optimiert noch sehr langsam, und das ist gut so. Der Hash beinhaltet einen Wert für die Rundenanzahl, den sog. „Kostenfaktor“. Dies ist der Aufwand der bei der Berechnung betrieben werden muss. Jedem Hash kann zusätzlich ein individueller Salt zugeordnet werden. In PHP ist bcrypt über die crypt() Funktion seit Version 5.3 fest implementiert. Davor hing der Einsatz vom Betriebsystem ab.
Was bcrypt nicht kann
Bcrypt bringt einige schöne Fähigkeiten mit, die wir in der Webwelt wunderbar nutzen können. Allerdings gibt es einige Dinge, die designbedingt nicht vorgesehen sind. Dazu zählt ein geheimer Salt, den wir in unserer Anwendung hinterlegen können. Der Sinn resultiert aus der Überlegung, dass wenn ein Angreifer aus welchen Gründen auch immer auf die Datenbank zugreifen kann, er auch noch auf den Quelltext der Webanwendung zugreifen können muss. Im Zweifel entscheidet sich dort, ob unsere Passwörter weiter geschützt sind oder nicht.
Die zweite fehlende Funktion, ist die Möglichkeit Hashes von zusätzlichen Faktoren, wie der E-Mail Adresse (ersatzweise dem Benutzernamen oder die UserID) abhängig zu machen. Der Hintergrund ist etwas speziell. Nehmen wir mal an, ein Angreifer findet in unserer Webanwendung eine XSS Lücke, mit denen er die Session eines Benutzers übernehmen kann. Was wäre das Schlimmste, was er tun kann? Richtig, er ändert das Passwort oder die E-Mail Adresse. Wie kann ich das am effektivsten verhindern? Ganz klar, ich muss ihn zwingen zur Überprüfung das alte Passwort einzugeben. Durch die Kopplung der Hashes an die E-Mail kann ich das gar nicht vergessen. Man könnte natürlich den Salt abhängig von der E-Mail machen, aber was ist wenn ein Benutzer mehrfach angemeldet ist? Brute Force Attacken werden mit jedem Ziel lohnenswerter.
Vom Rein und Raus
Ein ganz einfacher Hash ensteht so:
crypt ( 'Passwort', '$2a$04$EinSaltFuerDasPasswort' );
Als Ausgabe ergibt sich:
$2a$04$EinSaltFuerDasPasswore.oNHNUzZrs1V5tpdv/WJ64.DIyBV1kC
Auf den ersten Blick ist das im Vergleich zu md5(‘Passwort’) natürlich etwas verwirrend, aber dafür schreibe ich das ja hier. Im ersten Argument steht der zu hashende String. Das zweite Argument besteht aus drei Teilen, die je mit einem $ eingeleitet werden. Der erste Block bestimmt die verwendete Funktion. Die möglichen Werte könnt Ihr auf php.net nachschauen. Wir nutzen hier nur $2a für bcrypt.
Der zweite Block beschreibt die Anzahl der Runden, mit dem der Hash erstellt wird. Der Wert darf zwischen 04 und 31 liegen. Mit jeder Runde verdoppelt sich die Zeit zur Erstellung, das System ist also exponentiell. Wenn eine Runde etwa 1 ms dauert, dann dauern 31 Runden ca. 74 Minuten. Genug Luft nach oben also. Brauchbare Werte liegen derzeit bei etwa 08 bis 12, je nach eingesetzter Hardware und Geduld. Gibt man Zahlen ausserhalb des Bereichs an, wird *0 zurück gegeben.
Der dritte Block ist der individuelle Salt. Dieser darf aus Groß- und Kleinbuchstaben, Zahlen, sowie ./ bestehen. Tauchen andere Zeichen auf, wird ebenfalls *0 zurück gegeben. Die Eingabewerte müssen also gut gewählt sein. Weiterhin darf der Salt aus 128 Bits, also 21 1/2 Zeichen bestehen. Wen das verwundert, 21 Zeichen werden komplett dargestellt, beim letzten Zeichen werden die Hälfte der Bits verworfen. Deshalb wird aus ‘EinSaltFuerDasPasswort’ im hash ‘EinSaltFuerDasPasswore’.
Die Ausgabe entspricht der Eingabe, gefolgt vom eigentlichem Hash. Jetzt kann man natürlich berechtigt fragen, was daran sicher sein soll, wenn da ja alles steht. Stimmt, aber genau dieses Verfahren ist gleichzeitig ein Vorteil.
Einmal bcrypt …
Ich erstelle der Einfachheit halber eine Funktion, mit der man die Eigenschaften von bcrypt richtig nutzen kann. Auch hier gilt wieder – nichts ist in Stein gemeisselt. Wenn Ihr Vorschläge habt, her damit. Die Funktionen nenne ich (besonders kreativ) bcrypt_encode und bcrypt_check.
function bcrypt_encode ( $password )
{
return crypt ( $password, '$2a$04$EinSaltFuerDasPasswort' );
}Diese Funktion gibt uns einen ersten Anfang. Ein Aufruf von bcrypt(‘Passwort’) gibt uns den o. g. Hash zurück. Die Saltfunktion nutzt natürlich überhaupt nichts, wenn man überall den gleichen Salt verwendet. Da der Salt in der Datenbank steht und daher nicht geheim ist, kann dieser pseudozufällig sein kann. Folgendes Konstrukt ist also ausreichend.
$salt = substr ( str_shuffle ( './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) , 0, 22 );
Die Anzahl der Runden sollte variabel sein. Der Hintergrund ist ganz einfach. Manche Accounts sind wichtiger als andere. Wenn ich als Admin 3 Sekunden zum Login warten muss, dann stört mich das nicht. Einem Besucher diese Wartezeit zu erklären, könnte sich aber schwierig gestalten oder als technische Schwäche fehlinterpretiert werden. Als Bonbon kann man dem Besucher auch anbieten, die sichere Variante zu wählen. Ein Normalwert sollte festgelegt werden, aber mit der Möglichkeit zu abweichenden Werten. Übertragen auf unsere Funktion ergibt sich.
function bcrypt_encode( $password, $rounds='08' )
{
$salt = substr ( str_shuffle ( './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) , 0, 22 );
return crypt ( $password, '$2a$' . $rounds . '$' . $salt );
}Zur Passwortspeicherung habe ich jetzt eigentlich alle Möglichkeiten von bcrypt ausgeschöpft. Mit jedem Aufruf wird ein neuer Hash mit einem anderem Salt erzeugt. Nur sicherer fühl ich mich jetzt nicht, denn genau wie oben schon erwähnt gebe ich dem Angreifer freiwillig alle Daten. Es ist natürlich besser als ein purer md5 hash, aber eher noch gut gemeint als gut gemacht.
Daher würde ich diese Funktion gerne erweitern. Wie bei unserem md5 Beispiel bringe ich zusätzlich einen Salt ein, der nur im Quelltext hinterlegt ist. Dieser muss vor dem ersten Aufruf der Funktion mit define(‘SALT’, ‘beliebigerWert’) definiert werden. Ausserdem möchte ich, dass man bei einer Änderung der E-Mail Adresse das alte Passwort eingeben werden muss. In den Kommentaren zum letzten Beitrag hat ein Besucher erwähnt, dass das Einbringen eines Salt mit hash_hmac() sicherer wäre als einfaches voranstellen oder anhängen. Auch wenn ich die Bedenken in diesem speziellem Fall nur bedingt teile, da sich der individuelle Salt in jeder Zeile ändert und daher ein Angriff auf den systemweiten Salt sinnlos wäre, schaden kann es auch nicht und wenn wir schon einmal dran sind, klotzen wir mal richtig. ![]()
Dazu erweitern wir zunächst einmal mit str_pad() unseren String auf die viefachefache Länge des Passwortes, indem wir ihn mit dem sha1 hash der E-Mail Adresse davor und dahinter auffüllen. Ich nehme hier sha1, weil ich möchte, dass sich bei jeder E-Mail der Salt komplett ändert. Diesen String jagen wir dann durch hash_hmac() mit unserem systemweiten Salt in Whirlpool als Binärausgabe. Den entstandenen Zeichensalat verpacken wir mit bcrypt.
function bcrypt_encode ( $email, $password, $rounds='08' )
{
$string = hash_hmac ( "whirlpool", str_pad ( $password, strlen ( $password ) * 4, sha1 ( $email ), STR_PAD_BOTH ), SALT, true );
$salt = substr ( str_shuffle ( './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) , 0, 22 );
return crypt ( $string, '$2a$' . $rounds . '$' . $salt );
}Ein Aufruf von
bcrypt_encode( 'oliver@anonsphere.com', 'Test-Null8Fünfzehn' );
führt also zu diesen Ergebnissen:
// Passwort mit E-Mail auf vierfache Länge aufgefüllt 4e707693b984367edadf5a022867Test-Null8Fünfzehn4e707693b984367edadf5a022867e // hash_hmac mit Whirlpool und systemweitem Salt (im Original binär) b0492febbd81ef10387e2e7127295e09c197ff0cadf8ae5dc98182178f9e0891cf86b91abb1c723e0de510361cb67e1149460a687271672d77de439d7c572b57 // Verpackt mit bcrypt $2a$08$jb8v67zmCNMO9dlX1tkVqOxGlhQkJNL45AvfpbEWvqXnGC8YcO7Hm
Die Sicherheit dürfte nur schwer anzuzweifeln sein. ![]()
… und zurück
Jetzt fehlt noch die Möglichkeit den gespeicherten Passworthash mit unserem Passwort zu vergleichen. Jetzt kommen wir zu dem Teil, wo bcrypt von nett, auf cool wechselt.
$2a$08$jb8v67zmCNMO9dlX1tkVqOxGlhQkJNL45AvfpbEWvqXnGC8YcO7Hm
Schauen wir uns doch mal diesen Hash genauer an. Wir haben oben gesehen, der Hash besteht aus den Einstellungen und dem Ergebnis. Anders gesagt, der Hash liefert uns alles, was wir zum Berechnen brauchen. Dazu schreibe ich ihn mal anders, damit es klarer wird.
Einstellungen: $2a$08$jb8v67zmCNMO9dlX1tkVqOx
Hash: GlhQkJNL45AvfpbEWvqXnGC8YcO7Hm
Der erste Teil entspricht genau dem Code, den wir zur Erzeugung genutzt haben, daher muss auch bei korrekter E-Mail und Passwort und diesen Einstellungen unser Hash heraus kommen. Dass heißt, wenn der hinterlegte hash die Variable $stored hat, dann muss folgende Bedingung wahr sein.
crypt ( $string, substr ( $stored, 0, 30 ) ) == $stored;
Oder übertragen in eine eigene Funktion.
function bcrypt_check ( $email, $password, $stored )
{
$string = hash_hmac ( "whirlpool", str_pad ( $password, strlen ( $password ) * 4, sha1 ( $email ), STR_PAD_BOTH ), SALT, true );
return crypt ( $string, substr ( $stored, 0, 30 ) ) == $stored;
}Der Vorteil erschliesst sich erst auf den zweiten Blick. Während man bei der Umstellung von md5 auf sha1 oder von sha1 auf sha256 Probleme mit bestehenden Benutzerlogins bekommt, weil die alten Passwörter ungültig werden, nimmt uns bcrypt alle nötigen Workarounds ab. Bei der Prüfung ist es nämlich vollkommen egal, was als Ursprungswert an Runden und Salt hinterlegt wurde. Wenn ich irgendwann mal die Sicherheit erhöhen will oder auf einen schnelleren/langsameren Server umziehe, ändere ich den $rounds Wert und trotzdem funktionieren alte Logins weiter. Sobald aber die E-Mail oder das Passwort geändert wird, wird der neue Wert übernommen.
Was noch zu sagen wäre
Bcrypt ist eine schöne Sache, entbindet Euch aber keineswegs von zusätzlichen Sicherheitsüberlegungen. Wer zukunftsorientiert an ein neues Projekt geht oder wer schon beim letzten Artikel überlegt hat, ob seine Passwortspeicherung wirklich so sicher ist, wie er dachte, sollte überlegen, ob der Einsatz lohnt. Ein sicheres Verfahren deshalb abzulösen, ist aber auf jeden Fall unnötig.
Bleibt mir abschliessend nur noch eins zu sagen: „ES HEISST STANDARD!!11“.
Keine ähnlichen Artikel.



… dass die Welt noch nicht mal bei Hashing angekommen ist kann man ja auch schön an den Webseiten erkennen, die einem auf Anfrage gern das aktuelle Passwort zusenden…
Dominik
18 Jul 11 at 09:40
@Dominik
Oder alternetiv das alte Passwort im value-Attribut stehen lassen…
Quu
18 Jul 11 at 10:05
Bezüglich der Generierung eines Salts:
Natürlich generierst du dir mit dem str_shuffle() von einem Alphabet einen zufälligen String. Da kein Zeichen hier jedoch doppelt vorkommen kann, nimmst du dir einiges an möglichen Kombinationen aus deinem endgültigen String.
Mit deinem Beispiel hast du also folgende Möglichkeiten (Unabhängig, dass du davon nur 22 Zeichen herausschneidest):
64 mögliche Zeichen für den ersten Wert
* 63 mögliche Zeichen für den zweiten Wert
* 62 mögliche Zeichen für den dritten Wert
* 61 mögliche Zeichen für den vierten Wert
* 60 mögliche Zeichen für den fünften Wert
* 59 mögliche Zeichen für den sechsten Wert
…
* 2 mögliche Zeichen für den 63 Wert
* 1 mögliches Zeichen für den 64 Wert
Damit ergeben sich dann 1.2688693218588E+89 verschiedene Möglichkeiten.
Solltest du dich jetzt entscheiden, dass du einen 64 Zeichen String zusammenbaust, bei dem Zeichen auch doppelt vorkommen dürfen, würdest du mit 64*64*64…*64 auf 3.9402006196394E+115 verschiedene Möglichkeiten kommen.
Gut, letzten Endes wird der Unterschied in diesen Dimensionen wahrscheinlich keinen Unterschied machen, allerdings denke ich mir halt, warum sollte man diese Möglichkeit verschenken, wenn sie nur 2 Minuten länger dauert, zu implementieren?
P.s. Ich hoffe, die Rechnung war soweit korrekt, sonst blamier ich mich jetzt aber definitiv
Christian
18 Jul 11 at 10:46
„Weiterhin darf der Salt aus 128 Bits, also 21 1/2 Zeichen bestehen. Wen das verwundert, 21 Zeichen werden komplett dargestellt, beim letzten Zeichen werden die Hälfte der Bits verworfen.“
Das verwirrt mich. 128 Bits sind bei 8 Bit pro Zeichen genau 16 Zeichen. 128 Bits geteilt durch 21.5 macht bei mir 5.95…, eine völlig krumme Zahl.
Die Idee einer einzigen crypt()-Funktion, die die Parameter, die für die einzelnen Hash-Funktionen benötigt werden, in einen String-Parameter zusammenmanscht, halte ich übrigens für eine ganz, ganz blöde Idee. Wer kommt auf sowas?
GodsBoss
18 Jul 11 at 12:29
Noch eine Frage zur check-Funktion:
crypt($string, substr($stored, 0, 30)) == $stored;
Wenn die zusätzlichen Bits hinter dem Salt verworfen werden, sollte das substr doch eigentlich unnötig sein, oder? Die 30 Zeichen scheinen ja ein Spezifikum von bcrypt zu sein. Wenn ich jetzt nicht nur die Parameter, sondern den ganzen Algorithmus wechseln möchte, der möglicherweise mehr oder weniger als 30 Zeichen erwartet, würde die schöne Eigenschaft der rückwärtskompatiblen Hashes kaputtgehen.
Jetzt die Frage: Würde in deinem Fall
crypt($string, $stored) == $stored;
als Prüfung ausreichen und würde das auch für andere Crypt-Algorithmen funktionieren?
Marcel
18 Jul 11 at 12:36
@GodsBoss:
Das Salt wird auf dem base64-Alphabet angegeben, da steht 1 Zeichen für 6 Bits. 6*21 = 126, genaugenommen sind es also 21 1/3 Zeichen á 6 Bit.
Die Idee mit dem “zusammengemanschten” Parameter halte ich für gar nicht so doof, weil man (wenn ich das richtig verstehe) den Output der crypt-Funktion direkt wieder zur Prüfung verwenden kann, explizit speichern zu müssen, welchen Algorithmus man mit welchen Parametern verwendet hat.
Marcel
18 Jul 11 at 12:41
Ups, da hab ich ein Wort vergessen. Ich meinte natürlich “…, ohne explizit speichern zu müssen, …”
Marcel
18 Jul 11 at 12:44
@Marcel:
Achso! Gut, dass das nicht im Artikel steht, sonst könnte das noch jemand verstehen.
Aus Entwicklersicht ist es einfach nur hässlich, mehrere Parameter einer Funktion in einen zu packen. Wahrscheinlich wäre das Erste, was ich machen würde, das Schreiben eines Wrappers. Zusammenpfriemeln kann ich jederzeit, zerlegen ist viel schwieriger.
GodsBoss
18 Jul 11 at 12:48
@Quu
Schlüsselerlebnis neulich beim ICQ Support
Ich: „Mein Passwort geht nicht“
Support: „Hm bei xy kann es ändern“
Ich: „Ja, hab ich grade“
Support: „Lassen Sie mal beim Passwort das letzte Zeichen weg. Das funktioniert nur bis 20 Zeichen“
Ich: „Was zum …?“
@Christian
Mach Dir nix draus, ich verdreh die Zahlen auch immer, nicht wahr Michael?
Die Rechnung stimmt nicht ganz … 32^22 + 64^21 … 64^1 – macht aber, wie Du schon sagst, auch nicht den Megaunterschied, weil 64^21 übersteigt locker die Weltbevölkerung. Wenn Dir das trotzde zu wenig ist, kannst Du den String beliebig verlängern.
@GodsBoss
Das base64 Alphabet kommt mit 6 Bit aus, also eigentlich sind es 21 2/3 Zeichen, weil von den letzten 6 Bit werden 2 verworfen. Mit den Parametern, soweit ich weiß machen das alle Umsetzungen so, denn Du ersparst Dir damit das auseinander ziehen der Parameter.
Oliver
18 Jul 11 at 13:02
Zerlegen sollte ja eigentlich unnötig sein, und beim Zusammenpfriemeln musst du über die erwarteten Parameter Bescheid wissen, und bist damit auf die crypt-Algorithmen beschränkt, die du vorgesehen hast. Dabei ist das gar nicht nötig. Die Ausgabe crypt-Funktion enthält alle Informationen die nötig sind, du brauchst also nur den String abspeichern, und kannst direkt gegen diesen prüfen, ohne dass du was über den Algorithmus oder dessen Parameter wissen musst.
So kannst du zum Beispiel auch direkt Hashes aus einer anderen Nutzerbasis verwenden, die möglicherweise mit einem Algorithmus erzeugt wurde, der von dir gar nicht vorgesehen war.
Marcel
18 Jul 11 at 13:06
@GodsBoss
21 1/3, wie auch Marcel geschrieben hat. Es sind 126 Bits + 2 = 128
@Marcel
Ohne das substr sollte es auch gehen, ja. Hab ich aber selbst nicht probiert.
Oliver
18 Jul 11 at 13:06
@Oliver:
20 Zeichen? Bei meinem letzten ICQ-Passwortwechsel waren es noch 8 (habe ich aber selbst herausgefunden, Holladiewaldfee!).
Base64 kenne ich, ich habe nur die erlaubten Zeichen nicht genau im Kopf. Meine Verwirrung lässt sich aber leicht erklären – so wie es im Artikel steht, ist es einfach irreführend. Erstens wird Base64 gar nicht erwähnt, zweitens ist es folgendermaßen:
Der Salt selbst ist 128 Bit lang. 128 Bit kriege ich nicht in 21 Base64-Zeichen rein (die haben nur 126 Bit), also werden 22 gebraucht. Die haben dann 132 (22*6) Bit, also 4 zuviel, das sind die, die verworfen werden. Im Artikel steht nun, dass die Hälfe des _Zeichens_ verworfen wird – wenn aber 4 Bit verworfen werden, heißt das, dass ein Zeichen aus 8 Bit besteht. Das ist aber natürlich nur insoweit korrekt, dass der String tatsächlich aus 22 Bytes besteht. Von jedem Byte sind aber 2 Bit ungenutzt (da Base64) und vom letzten Byte sogar 6 Bit (die 2, die sowieso ungenutzt sind plus die 4, die zum Auffüllen dienten).
GodsBoss
18 Jul 11 at 13:29
@GodsBoss
Das Lustige ist ja, dass der Support
a) mein Passwort kannte (und zwar alle Zeichen)
b) aber von meinem Passwort mit 21 Zeichen nur 20 Zeichen genutzt wurden
Mit den Bits hast Du natürlich Recht. Evtl. müsste Michael das anpassen.
Oliver
18 Jul 11 at 13:34
@Oliver:
Klingt so, als hätte ein ausreichend engagierter ICQ-Mitarbeiter gute Chancen, einen Artikel auf TheDailyWTF.com platzieren zu können.
GodsBoss
18 Jul 11 at 13:36
[...] den vorhergehenden Beiträgen habe ich ja schon locker über Passwörter und wie man mit Ihnen umgehen sollte erzählt. Dabei [...]
TLS/SSL für Heimwerker | PHP Gangsta - Der PHP Blog
25 Jul 11 at 09:42
Stehe ich da jetzt auf dem Schlauch oder lässt sich mit der Funktion nicht überprüfen, ob der Benutzer in einer Datenbank existiert? Für die Variable $stored brauche ich ja erstmal das vorhandene Passwort, an welchem ich prüfen kann…
Sicher wird das grad ein Eigentor. ^^
Monti
28 Jul 11 at 00:32
Nö!
$email ist der Benutzername, der eingegeben wurde
$password ist das Passwort, was eingegeben wurde
$stored ist der Hash aus der Datenbank
SELECT `password` AS `stored` FROM `user` WHERE `email`=’$email’
$valid_user = bcrypt_check ( $email, $password, $stored )
Klar?
Oliver
28 Jul 11 at 01:42
Oje, ich hab die Möglichkeit, die Einträge per Email einzugrenzen komplett übersehen…
Danke nochmal.
Monti
28 Jul 11 at 08:47
Ich persönlich bin eh kein Fan von diesen Konstrukten, wo E-Mail und Passwort aus der DB abgefragt werden, also …
SELECT * FROM `user` WHERE `email`=’$email’ AND `password`=’$password’
Die Gefahr von SQL Injections ist mir zu hoch. Allerdings muss man dann drauf achten, dass die E-Mail oder auch der Benutzername einmalig sind, was aber meistens der Fall sein sollte.
Oliver
28 Jul 11 at 12:17
@Oliver:
Prepared Statements nutzen -> Keine SQL Injection.
GodsBoss
28 Jul 11 at 12:22
Der Gedanke ist noch aus der Zeit vor MySQL 5.0 hängen geblieben.
Ich selbst benutze eh fast nur die Active Records von Yii, sofern möglich.
http://www.yiiframework.com/doc/guide/1.1/de/database.ar
Oliver
28 Jul 11 at 12:29
Das Active Record Antipattern? o_O
GodsBoss
28 Jul 11 at 12:33
Ja, die Kritik ist bei Yii weitgehend unberechtigt, weil auch komplexe Relationen möglich sind und der Rest geht über Parameter Binding. Mir fällt spontan auch kaum etwas ein, dass sich nicht über AR abbilden lässt. Ich habe auch bisher kaum Abfragen gesehen, wo ich sagen würde, dass ist umständlich oder unnötig – die SQL Befehle kann man sich ja ausgeben lassen. Sofern da doch mal was drin steht, sitzt wie so oft das Problem vorm Monitor.
Auch die Validierung ist in meinen Augen kein großes Problem, denn Form und DB Controller sind bei mir getrennt. Falls also ein Feld nicht korrekt ist, kommt es erst gar nicht zur SQL – die Validierung in den AR selbst übernimmt Yii mit den Eigenschaften, die ich auch in den Tabellenfeldern hinterlege. Durch die Trennung wird die Applikation auch nicht aufgeblasen.
Füge ich z. B. ein Feld dazu, lasse ich Gii laufen und kopiere die neuen Eigenschaften in meinen DB Controller.
Oliver
28 Jul 11 at 13:13
Aus meiner Sicht ist die Hauptkritik an ActiveRecord die Verletzung des SRP. Die ist aber prinzipbedingt und kann durch keine Implementierung verhindert werden.
Das von dir verlinkte Parameter Binding bezieht sich übrigens auf DAOs, die, wenn ich die Informationen bezüglich Yii richtig interpretiere, mit ActiveRecord gar nichts zu tun haben. Aber da liege ich eventuell falsch.
GodsBoss
28 Jul 11 at 13:45
Ja, das Parameterbinding kann man dann benutzen, wenn man mit AR nicht weiter kommt. Das ist dann die „Softwareversion“ von Prepared Statements, die aber bei unterstützten Datenbanken auch die Technik von Prepared Statements nutzt.
SRP halte ich bei Datenbanken, aber nicht nur da für ein sehr schwaches Argument, denn es würde ja bedeuten, dass ich nicht einen DB Controller sondern mindestens mal 4 bräuchte (=> CRUD plus Abhängigkeiten plus evtl. Validierung plus plus), was die Wartung garantiert nicht einfacher macht und auch die Performance nicht verbessert.
Oliver
28 Jul 11 at 14:01
Wie ist der Passwort-Code zu erzeugen, wenn nur die eMail-Adresse geändert wurde?
Christian
5 Sep 11 at 17:30
@Christian
In der Variante muss das Passwort bei Änderung der E-Mail Adresse neu eingegeben werden.
Oliver
5 Sep 11 at 17:33
Sehe ich es richtig, dass die eMail-Adresse nur unter Angabe des aktuellen Kennworts geändert werden kann?
Christian
5 Sep 11 at 17:37
Sorry, hatte nicht eine solch schnelle Antwort erwartet und die Seite in der Zeit nicht aktualisiert. Dieser und mein vorheriger Eintrag können gerne gelöscht werden. Danke.
Christian
5 Sep 11 at 17:40
“Ach, wenn die Welt schon mal auf dem Stand des einfachen md5 wäre …”
Dazu kann ich nur sagen: Die Welt ist dir weit voraus, nur weil einige wenige noch weniger wissen, musst du nicht gleich überheblich werden.
1. Nutze Standards, denke dir keine eigenen Verfahren aus, wenn du sie nicht ganz verstehst.
2. “In den Kommentaren zum letzten Beitrag hat ein Besucher erwähnt, dass das Einbringen eines Salt mit hash_hmac() sicherer wäre als einfaches voranstellen oder anhängen. Auch wenn ich die Bedenken in diesem speziellem Fall nur bedingt teile, da sich der individuelle Salt in jeder Zeile ändert und daher ein Angriff auf den systemweiten Salt sinnlos wäre, schaden kann es auch nicht und wenn wir schon einmal dran sind, klotzen wir mal richtig.”
So läuft IT-Sicherheit nicht, du weißt nicht, ob es unsicher ist, du nimmst Dinge an, aber du verstehst die mathematischen Zusammenhänge dahinter nicht. Wenn man die Mathematik hinter einem kryptographischer Hash nicht versteht, weil man sie wahrscheinlich gar nicht kennt, KANN MAN KEINE Schlussfolgerungen darüber treffen, wann er wie angewendet sicher ist, sondern sich nur auf Standards verlassen.
3. Ein Salt MUSS zufällig sein, dein str_replace verwendet 1. eine unsichere Zufallsfunktion, 2. können dadurch einige Bitfolgen nicht vorkommen, da zweimal das gleiche Zeichen nicht vorkommen kann. Verstehst du die Mathematik hinter dem Hash? Nein, woher willst du dann wissen, dass aufgrund dessen ein Angreifer keinen Vorteil hat, vielleicht hat er ihn nicht, aber du kannst es nicht wissen, deshalb, mache es richtig. Am besten verwendet man /dev/urandom, wenn man auf Linux Servern arbeitet, um einen Salt zu generieren, denn die Jungs, die /dev/random programmiert haben, wissen was sie tun.
Auch E-Mail-Adressen gehören nicht ein einen Salt, sondern NUR Zufallswerte! Wenn du einen zufälligen Salt generiert hast, kannst du da gerne noch eine E-Mail-Addresse reinpacken, dass bringt dir aber 0 an Sicherheit und erhöht nur die Rechenleistung. Wie kommst du überhaupt darauf das zu tun? Verstehst du die Mathematik dahinter und hast bewiesen, dass es sichere ist? Annahmen aus Halbwissen heraus zu schlussfolgern ist in der IT-Sicherheit mehr als falsch, es ist einfach unsicher.
Beispiel:
sha256(salt, passwort) ist sehr viel unsicherer als hmac(Salt, Passwort) mit sha256, warum? Ich weiß es nicht, es hat mit der Mathematik zu tun und das ist mir zu hoch, aber Leute die es verstehen, geben diese Tipps, also nutze sie.
Max
24 Sep 11 at 13:41
Hallo,
ich habe das Problem, dass die Passwörter ab und zu nicht mehr als gültig erkannt werden. Erst wenn ein Passwort neu erzeugt wird funktioniert es dann wieder…
Kann mir jemand erklären wieso das passiert (alles laut Beispiel implementiert)?
thx!
Kim
12 Jan 12 at 11:27
@Kim: Was hast Du denn vorher benutzt?
Oliver
12 Jan 12 at 11:42
@Oliver: Ich meinte, dass die Passwörter welche über das hier vorgestellte Verfahren “bcrypt_encode” erstellt wurden nicht immer über die Funktion “bcrypt_check” beim Verifizierungsverfahren als richtig erkannt werden.
Kim
12 Jan 12 at 13:09
@Kim: Kannst Du Deinen Code posten? Also wie Du das aufrufst? Und warum nicht immer? Wie sind die anderen Daten erstellt?
Oliver
12 Jan 12 at 13:16
Also der Code sieht folgender maßen aus. Das Einzige was hinzugefügt wurde ist dass das Passwort zuvor noch per md5 zusätzlich kodiert wird.
function bcrypt_encode ( $email, $password, $rounds=’12′ ){
$password = md5($password);
$string = hash_hmac ( “whirlpool”, str_pad ( $password, strlen ( $password ) * 4, sha1 ( $email ), STR_PAD_BOTH ), BLOWFISCH_SALT, true );
$salt = substr ( str_shuffle ( ‘./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ’ ) , 0, 22 );
return crypt ( $string, ‘$2a$’ . $rounds . ‘$’ . $salt );
}
Kim
12 Jan 12 at 13:24
Und die Passwörter waren vorher in md5 und Du hast die nach bcrypt konvertiert?
Oliver
12 Jan 12 at 13:36
Nein, das sind neu erstellte Passwörter mit dem hier vorgestelltem Verfahren. Deshalb ist ja alles so komisch! Kann es damit zusammenhängen, dass ich das Passwort zuvor mit md5 kodiere?
Kim
12 Jan 12 at 13:41
@Kim:
Nö, eigentlich kann das damit nix zu tun haben, denn ein md5 hash ist ja auch immer gleich. Ich verstehe aber immer noch nicht so ganz das Problem. Also wenn Deine Passwörter mit der Funktion kodiert sind, kannst Du sie auch vergleichen?
@Max:
Deinen Kommentar hab ich gar nicht gesehen. Ich bin nicht überheblich. Ich weiß aber, welche Funktionen in der weiten Welt unterwegs sind.
1. Ja
2. Du hast das Beispiel nicht verstanden. hash_hmac ist dann wichtig, wenn der systemweite Salt geschützt werden muss. Dieser systemweite Salt ist hier nur bedingt sicherheitsrelevant. Er stellt nur eine Hürde dar. Wenn das System kompromittiert ist, muss man davon ausgehen, dass er bekannt ist. Ist er nicht bekannt, kann hash_hmac davor schützen diesen zu berechnen. Den Aufwand wird zwar im normalen Leben kaum jemand betreiben, aber er könnte es. Ich habe den Unterschied verstanden. Deshalb kann man das mit hash_hmac machen, muss es aber nicht.
3. Du hast offensichtlich das Prinzip nicht verstanden. /dev/random ist auch nur pseudozufällig und /dev/urandom ist in diesem Fall Unsinn. Ein Angreifer hat keinen Vorteil dadurch, auch wenn der Salt aus unterschiedlichen Zeichen besteht, das hab ich oben aber auch schon erklärt. Ein Angriff auf Basis von bestimmten Salts in bcrypt ist derzeit nicht bekannt. Das mit den E-Mail Adressen hast Du übrigens auch nicht verstanden. Warum ich das mache, obwohl ich es doch ausführlich erklärt habe.
Das hier ist keine endgültige Sicherheitsempfehlung, denn die gibt es nicht. Es ist eine Möglichkeit der Implementierung, die ICH persönlich für sehr sicher halte. Beweise, dass es angreifbar und ich werde den Vorschlag nachbessern. Bis dahin ist er als sicher zu behandeln.
Oliver
12 Jan 12 at 14:09
Kleiner Zwischenruf zu einer Aussage/Frage die ich mir auch gestellt habe
> Sehe ich es richtig, dass die eMail-Adresse nur unter
> Angabe des aktuellen Kennworts geändert werden kann?
Ich würde sagen nein. Um den neuen Hash zu generieren brauche ich ja nur 1. die eMailadresse und 2. irgendein Passwort. Die Prüfung darauf, ob es das alte Passwort ist, also mit der alten eMailadresse zusammen den alten Hash ergibt, erfolgt ja nicht automatisch. bcrypt_check() muss man in die Applikationslogik übernehmen, das hat aber mit dem ganzen Verfahren imho nichts zu tun, man sollte schon alle relevanten Zugriffe durch eine zusätzliche Passwortabfrage sichern.
chorn
13 Jan 12 at 12:15
@chorn:
In dem Beispiel schon. Wenn Du die E-Mail änderst, ändert sich der Wert vor und nach dem Passwort und damit dann auch der bcrypt hash. Du benötigst also zur Prüfung und zum Ändern immer eine E-Mail (alt oder neu) und das Passwort (alt oder neu). Das ist wirklich nur, damit ich nicht vergessen kann, bei einer Änderung der Mail das Passwort abzufragen. Wer das nicht will, kann das einfach entfernen. Das würde es auch tun:
function bcrypt_encode ( $password, $rounds='08' ) { $string = hash_hmac ( "whirlpool", $password, SALT, true ); $salt = substr ( str_shuffle ( './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) , 0, 22 ); return crypt ( $string, '$2a$' . $rounds . '$' . $salt ); } function bcrypt_check ( $password, $stored ) { $string = hash_hmac ( "whirlpool", $password, SALT, true ); return crypt ( $string, substr ( $stored, 0, 30 ) ) == $stored; }Oliver
13 Jan 12 at 13:11
Hi,
wenn man den Salt festeinstellt, kommen bei mir auf verschiedenen Systemen auch unterschiedliche Werte raus, ab dem 30. Zeichen.
Man kann also nicht auf System A crypten und diese Werte dann auf System B einsetzen weil bcrypt_check dann nicht funktioniert.
Das ganze liegt an:
hash_hmac ( “whirlpool”, $password, SALT, true );
Per:
hash_hmac ( “whirlpool”, $password, SALT, false );
kommen auf beiden Systeme identische Werte raus. Liegt also an der Rückgabe der Binärdaten.
Weiß jemand wie das zusammenhängt?
Mario
13 Jan 12 at 19:31
noch als Ergänzung:
Wenn ich die Binärausgabe nutze und über das Ergebnis noch einmal sha1 laufen lasse, kommen auch identische Werte raus:
sha1(hash_hmac ( “whirlpool”, $password, SALT, true ));
Mario
13 Jan 12 at 19:38
@Mario:
Schräg! Ne, kann ich Dir nicht sagen, warum das unterschiedlich ist. Was sind denn die Systeme?
Oliver
13 Jan 12 at 21:33
@Oliver:
mein Offlinesystem: Mac mit MAMP, PHP 5.3.6. Und online mein Hoster, PHP 5.3.8
Mario
14 Jan 12 at 16:01
@Mario
Kann das nur bestätigen.
Habe mir stundenlang darüber den Kopf zerbrochen!!!
Testsystem XAMPP auf Windows und Live Linux Server
Kim
31 Jan 12 at 12:24
Vielen Dank für den wunderbaren Beitrag.
Um die Sicherheit einer Webseite zu erhöhen sollte das Passwort in der Formulareingabe jedoch bereits vor dem POST-Versand verschlüsselt werden. Daher ist die Frage, wie die Javascript-Funktionen zu bcrypt aussehen würden
Meier
8 Feb 12 at 09:09
Das wird nicht funktionieren, weil der Salt ja serverseitig gespeichert ist. Du kannst aber TLS/SSL verwenden oder http://www.phpgangsta.de/tlsssl-fur-heimwerker
Oliver
8 Feb 12 at 09:45
Sollte $2a eigentlich noch verwendet werden und nicht $2y ?
siehe hier: http://lists.opensuse.org/opensuse-security-announce/2011-08/msg00015.html
und hier: http://en.wikipedia.org/wiki/Crypt_%28Unix%29
Ausserdem wäre noch ein Hinweis auf phpass nicht schlecht, das automatisch ein Fallback auf das nächst sichere Verfahren verwendet, falls bcrypt im System nicht verfügbar ist: http://www.openwall.com/phpass/
(siehe auch c’t 13/2011)
Holger
25 Mrz 12 at 04:19
Hm, müsste ich nachher noch mal in Ruhe durchlesen, aber so wie ich das sehe, betrifft das nur die PAM Module bzw. die glibc des Linuxsystems. Da aber PHP 5.3 nicht mehr auf das darunter liegende System angewiesen ist, dürfte es auch nicht betroffen sein. Die 2x und 2y Werte sind ja dementsprechend auch nicht in PHP verfügbar. Auch ein Problem mit Umlauten wäre mir nicht bekannt.
PHP Pass kann man sicher erwähnen, allerdings braucht man das wirklich nur, wenn man Versionen vor 5.3 einsetzt. Da ja der Support lange eingestellt wurde, ist der Einsatz von 5.2.x sowieso ein Sicherheitsrisiko. Wer sich mit bcrypt beschäftigt, wird keine alte Version mehr einsetzen.
Oliver
25 Mrz 12 at 15:39