Loginpasswort richtig hashen - password_hash() & password_verify()

  • Bin da gestern drüber gestolpert:
    bcrypt
    password_hash()
    password_verify()
    password_needs_rehash()


    Zum Thema "Salt":
    Falls sich jemand fragt,
    "warum denn den Salt mit speichern. Und was, wenn die db gehackt wird? Dann hat der Angreifer ja alle zugehörigen Salts?!"


    >>The only requirement for a salt is for it to be globally unique. It does not have to be kept secret. password_hash() takes care of it for you. Don't do anything fancy and stupid.
    Einfache Übersetzung:
    Das wichtigste ist, dass das Salt einzigartig ist. Es muss nicht geheim sein.
    password_hash() kümmert sich um das Salt. Es macht also keinen Sinn sich eine "Beste-Salt-function-ever" zu basteln.
    heißt: der Funktion password_hash() kein eigenen Salt mitgeben
    siehe auch: php.net: "Caution It is strongly recommended that you do not generate your own salt for this function."


    &


    >>Sehr gute Erklärung - also lesen!
    Hier mal eine kurze Zusammenfassung (ohne Übersetzung =)
    - The reason we use salts is to stop precomputation attacks, such as rainbow tables.
    - So, we use a salt. A salt is a random unique token stored with each password.
    - nobody has rainbow tables that include that hash.
    - The goal is to force the attacker to have to crack the hashes once he gets the database, instead of being able to just look them all up in a rainbow table.
    - One other idea to consider is a pepper. A pepper is a second salt which is constant between individual passwords, but not stored in the database. (KDF(password + pepper, salt))
    - So, how do we prevent brute-force attacks now?
    - A more solid approach is to use a key derivation function with a work factor. These functions take a password, a salt and a work factor. The work factor is a way to scale the speed of the algorithm against your hardware and security requirements:
    hash = KDF(password, salt, workFactor) (siehe password_hash() "cost"-Faktor)


    Hier ein einfaches Bsp:


    Zusammenfassung:
    Wir haben also erreicht, dass der Angreifer:
    - in die db eindringen muss (klar) um den Passwort-hash und Salt zu bekommen
    - aber auch zugriff auf das Pfeffer ($pepper) haben muss - sich somit also auch Zugriff auf die Files verschaffen muss
    - gezwungen ist den Passwort-hash zurück zu Rechnen, anstatt in "rainbow tables" danach zu suchen.


    Das Ganze nun noch abgerundet, indem man Prepared Statements nutzt (siehe php.net "PDO"), um eine SQL-injection zu verhindern (wenn richtig angewandt!).


    EDIT: 05.06.2016
    Es ist wichtig die Funktion password_verify zur Prüfung des Passwortes zu nutzen
    wenn sich User sich einloggt nicht if ($hash === $hashLogin), sondern wirklich password_verify() nutzen
    Warum: um Timing_attacks zu unterbinden.
    Siehe http://stackoverflow.com/a/29489833/3411766

    http://stackoverflow.com/a/29489833/3411766 schrieb:


    The answer is YES it uses length-constant time comparison.


    This is an excerpt of php's password_verify function


    Code
    1. /* We're using this method instead of == in order to provide
    2. * resistance towards timing attacks. This is a constant time
    3. * equality check that will always check every byte of both
    4. * values. */
    5. for (i = 0; i < hash_len; i++) {
    6. status |= (ret->val[i] ^ hash[i]);
    7. }


    You can have a look at the full source code at https://github.com/php/php-src…r/ext/standard/password.c


    "Man kanns auch übertreiben ..." ? - Nein. Man kann es auch gleich richtig machen =)

    Dieser Beitrag wurde bereits 5 Mal editiert, zuletzt von cottton () aus folgendem Grund: added quote - for dead link case ...

  • Hierzu noch ein paar Ergänzungen meinerseits:
    - Man sollte der DB-Zelle des PWs immer den Typ varchar(255) geben, sowie die Funktion

    PHP
    1. password_hash($pass,PASSWORD_DEFAULT)

    nutzen.
    Dies sorgt dafür, dass immer die aktuellsten und besten Verschlüsselungsmethoden angewandt werden, da PASSWORD_DEFAULT mit der Zeit aktualisiert wird. Weil dadurch auch die Hashes laenger werden koennen, wird empfohlen 255 Zeichen zuzulassen.


    - Um nun auch die Hashes auf dem neuesten Stand zu halten, empfiehlt sich diese Funktion:

    Da der genutzte Algorithmus neben dem automatisch generierten Salt direkt in den Hash gesetzt wird, funktioniert das einwandfrei, da password_verify() immer weiss, welcher ALgo genutzt wurde.


    Um einen guten Cost fuer die eigene Hardware zu finden, empfiehlt php.net (fast) folgende Funktion (10 ist der Standard)

    Dies kann man zB als Cronjob einmal im Monat machen. Da mit der Funktion oben auch die costs automatisch aktualisiert werden koennen, macht das im Zusammenspiel mit cottons Pepper jedem Scriptkiddie die Passbeschaffung zum Albtraum.


    Denn: Dadurch koennen Hashes in der DB liegen, die mit unterschiedlichen costs und Algos verschluesselt wurden. D.h. diese beiden Parameter muessen beim Brute Forcen fuer jedes Pass nochmal manuell eingelesen werden...

  • Korrekt :thumbsup:
    Man könnte die "cost"-Prüfung sogar mit vor die Passwortprüfung einbauen (oder direkt in die index.php), falls jemand keinen Cronjob einsetzen kann/will.
    Wenn wir die "cost" immer "aktuell" halten möchten, dann müssen wir ja den Wert irgentwo hinspeichern und beim hashen mitgeben.
    Also Speichern wir in einer kleinen Tabelle zB
    cost - 11
    last_chk - timestamp


    Wenn nun x Tage/Wochen vergangen sind, lassen wir die Prüfung laufen und aktualisieren die beiden Werte.

  • Müssen wir nicht, dass ist doch das Geniale am password_hash! Es werden alle benötigten Daten zur Überprüfung direkt mit in den Hash gesetzt! Wir brauchen nur eine kleine Variable(!),zB im File mit den SQL Logindaten oder so, diese lesen wir aus und schreiben sie als zusätzliche Cost Option beim Login Script in password_needs_rehash(). Et voilà: Man aktualisiert automatisch den Cost, ganz ohne Datenbank.


    Und das geht darum (Bild von php.net):2a34c7f2e658f6ae74f3869f2aa5886f-crypt-text-rendered.png
    Also wird alles, was zuer Ueberpruefung gebraucht wird, direkt in den Hash geschrieben, wodurch keine Costs, keine Salts, nichts irgendwo zusaetzlich gespeichert werden muss :thumbup:

  • PHP 5.5 wird aber noch nicht bei vielen laufen, ich bin zum Glück einer der Glücklichen bei denen es läuft; Seit heute früh 4 Uhr :thumbsup:
    #AllInkl.com #Webhosting


    [...]immer den Typ varchar(255) geben[...]


    Varchar sollte man nie nehmen, Dynamisch = langsam.
    CHAR(255), braucht zwar unter umständen etwas mehr Speicher, aber ist performanter. :thumbup:


    PS: Danke für den Gedankenanstoß, ich werde mir das mal genauer anschauen. Besonders, da mein aktuelles Projekt Sicherheit dringend benötigt.

  • Na, moment mal eben ...


    Was meinst Du denn immer mit automatisch?
    Die Variante, wo man die COST speichert ist ja Wurst- Aber man muss den Wert halt irgendwo hinlegen, FALLS man ihn mitgeben will.


    Und ob man ihn statisch oder dynamisch mitgeben will - man sollte auf jeden Fall, wie Du schon sagtest, den best möglichen Wert ermitteln.
    Da die Maschiene sich nicht dauernd ändern wird, könnte man nun aller paar Wochen/Monate den COST Wert erneut prüfen und aktualisieren.


    WENN man nun den COST Wert mitgeben und auch from time to time ändern will, dann sollte man die password_needs_rehash() function nutzen.
    Damit wird sichergestellt, dass nicht nur die neu registrierten User, sondern auch die schon vorhandenen User von der besseren Variante "profitieren".


    Was macht password_needs_rehash():
    - prüfen, ob algo sich geändert hat
    - prüfen, ob cost sich geändert hat

    Das Salt und PW sind btw nicht per . getrennt.
    Ich sehe auch keinen Grund auf VARCHAR zu verzichten. Selbst wenn CHAR schneller ist (müsste man prüfen) - VARCHAR belegt nur soviel Speicher, wie es benötigt.
    Und wenn ich die Wahl hätte zw. IMMER 255 belegt, oder paar micro(?) sec langsamer, dann ist wohl VARCHAR die bessere Wahl.
    Stellt euch auch mal vor ihr habt 1 mio user und CHAR belegt 255 anstatt der eigtl momentan benötigten 60. Da kann das schon was ausmachen.

    Ablauf wäre dann in etwa:


    PASSWORD_DEFAULT und PASSWORD_BCRYPT sind momentan übrigends genau das gleiche.
    Daher verwirrt die php.net docu ein bischen. Bei bcrypt kann man die cost mitgeben, bei default auch, da dort ja bcrypt genutzt wird.
    Fragts sich was passiert, wenn man per PASSWORD_DEFAULT die cost mitgibt, der default algo sich aber mal ändert und keine cost mehr akzeptiert wird
    da evtl vom neuen algo nicht unterstüzt. Aber das soll mal php.net´s Sorge sein ;D


  • Ich sehe auch keinen Grund auf VARCHAR zu verzichten. Selbst wenn CHAR schneller ist (müsste man prüfen) - VARCHAR belegt nur soviel Speicher, wie es benötigt.
    Und wenn ich die Wahl hätte zw. IMMER 255 belegt, oder paar micro(?) sec langsamer, dann ist wohl VARCHAR die bessere Wahl.
    Stellt euch auch mal vor ihr habt 1 mio user und CHAR belegt 255 anstatt der eigtl momentan benötigten 60.


    255 byte * 1.000.000 / 1024 / 1024 = 243 MegaByte


    Ergo => Immernoch Char verwenden. 243 MegaByte ist nichts.


    Solltest du es schaffen eine Seite zu bauen mit 1 Million registrierten Usern zieh ich meinen Hut, andernfalls ist dein Argument absolut nichtig.

  • Also Zusammenfassung:
    - einmal (oder alle paar Jubeljahre) einen Cost für die eigene Hardware generieren und irgendwo als Konstante oder in einer Tabelle speichern
    - grundsätzlich nur PASSWORD_DEFAULT als Algorithmus verwenden, da dieser regelmäßig erweitert wird.
    - Klartext Passwort pfeffern, Hash salzen LASSEN (von der Funktion), mit ein bisschen Cost abschmecken, in der DB anrichten...
    - Nach erfolgreichem LOGIN eines Users prüfen, ob das Pass noch den aktuellen Anforderungen gerecht wird und ggf. mit aktuellen Optionen rehashen
    - Dem Hash in der DB empfohlenerweise min. 255 Zeichen zugestehen (CHAR oder VARCHAR, CHAR schneller, VARCHAR weniger Speicher)


    Hab ich was vergessen?


  • Nö, passt alles mMn =)


    Thema CHAT und VARCHAR:
    bitte nicht falsch verstehen - ich will nicht klugscheißen. Ich will es halt immer wissen/selbst testen.
    Hat dann den Vorteil, dass man sich Sachen auch besser Einprägt =)

    Tests:
    2 Datenbanken :: `pw_char` und `pw_varchar`
    Jeweils befüllt mit 1 mio Usern (`uname` 1-5 Stellig und `pw` 60 Stellig)
    Einzige Unterschied:
    `pw_char`.`tbl` column `pw` = CHAR(255)
    und
    `pw_varchar`.`tbl` column `pw` = VARCHAR(255)



    Größenunterschiede: size.PNG


    Zeitunterschiede:
    Tests mit 100.000 SELECTs
    CHAR:


    VARCHAR:


    Also Bei der Geschwindigkeit sehe ich so keinen Unterschied. Es kann natürlich sein, dass es bei komplexeren Queries auffälliger wird.
    Und bei der Größe sehe ich einen enormen Unterschied. CHAR "reserviert" eben den gesammten Platz (es werden "freie"/ungenutzte Stellen mit Leerzeichen aufgefüllt).
    Also hat VARCHAR die Nase vorn.


    EDIT:
    So einen ähnlichen Test hatte ich auch vor kurzem gemacht zwecks INT(Anzeigebreite) oder INT.
    Die Frage war also: macht es bei TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT Sinn eine Anzeigebreite mitzugeben?
    Antwort: Nein
    Tests mit 2 Datenbanken
    Unterschiede


    db.tbl mit Angaben:
    TINYINT(1), SMALLINT(1), MEDIUMINT(1), INT(1), BIGINT(1)


    db.tbl ohne Angaben:
    TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT


    Bei 700 Einträgen mit $i++ gab es keine Größenunterschiede.
    Es gibt auch witzigerweise keine genauen Angaben zu dem Thema @dev.mysql.com.
    Auch beachtenswert: bei einem INT(1) Feld werden trotzdem "zu große Werte" korrekt gespeichert.
    Man könnte ja meinen INT(1) lässt nur 0-9 zu. Aber - nö. Wird mehr Platz gebraucht, wird er sich genommen (bis hoch zum jeweiligen Maximun des Types).


    Fazit: Numerische Typen (AUßER FLOAT! - anderes Thema) benötigen keine Anzeigebreite. Diese werden ignoriert und haben keinerlei Einfluss auf die Größe der db.


  • Und bei der Größe sehe ich einen enormen Unterschied. CHAR "reserviert" eben den gesammten Platz (es werden "freie"/ungenutzte Stellen mit Leerzeichen aufgefüllt).
    Also hat VARCHAR die Nase vorn.



    255 byte * 1.000.000 / 1024 / 1024 = 243 MegaByte
    Ergo => Immernoch Char verwenden. 243 MegaByte ist nichts.


    Dann sind es halt ein paar Megabyte mehr, aber du kannst beruhigt mit dem Gedanken schlafen, dass es performanter ist. :thumbup: Und bei einem großen Webprojekt mit einer Usaerbasis von Hunderttausenden sind die paar Megabyte mehr dein geringstes Problem und es kommt alles auf Performance an. Da ist Speichergröße nicht so relevant. Da kommt es darauf an den Server so kurz wie möglich zu belasten da viel Aufrufe gleichzeitig abgewickelt werden müssen.

    Und wenn wir hier schon im Strebermodus sind, bei 255 Zeichen ist VARCHAR sogar größer als CHAR .... also wenn du Zukunftsorientiert arbeiten möchtest ... :thumbsup:

  • Also Leute wer sich zum Thema Verschlüsselung informieren will .... http://www.kryptochef.net/ :D :D
    Ist zwar oftopic aber ja, das hat mir grad jemand gezeigt.


    Und wer sich WIRKLICH mit Verschluesselung beschaeftigen will, sollte einen entspr. Kurs an ner Uni oder so besuchen. Weil da naemlich noch so abnormal viel im Hintergrund steht, wovon ich noch nicht mal die Namen weiss :D
    Und weil grad mit Links um sich geworfen wird: CrypTool, zum Spielen...

  • hat jemand ne Idee wie man die password_needs_rehash() function in betimmten Intervallen ausführen kann?
    Ich möchte ja nicht bei jedem Login prüfen. Eher alles 3 Monate. Allerdings will ich den "last check" nicht irgentwo hinspeichern.
    Eher dachte ich an irgend was in Richtung date.


    Jemand ne Idee?

  • Ich bin mir nicht ganz sicher ob ich dich richtig verstehe


    nicht ganz =)
    beim Login stellen wir ja bei dem Modell hier fest, ob der Hash neu gehasht werden sollte:
    siehe Loginpasswort richtig hashen - password_hash() & password_verify()


    Der Gedanke ist jetzt password_needs_rehash() nicht bei jedem Login zu feuern. Denn das bedeutet ja doppeltes Hashen.
    Und mit einer hohen COST kann das anstatt 1 oder 2 sec schon 4 oder mehr sein.


    Also kurz; die Prüfung, ob der Hash neu gehasht werden muss, soll nur aller x Monate durchlaufen werden.


    Per cron wird das nicht gehen. Der könnte zwar die db durch rennen, hätte aber nur die Hashes. Zum Rehashen brauchen wir ja aber das PW in clear text (vom Login).
    Daher dachte ich an eine function die per Date irgendwie entscheidet, ob es mal wieder Zeit ist darauf Prüfen.


    Mal ein Blödes Bsp (pseudocode):

    PHP
    1. if( Tag == 1.April ){
    2. check if need to rehash
    3. }


    Weißt was ich meine`?