Um die Grundlagen in PHP und MySQL zu vertiefen, eignen sich eigene kleine Skripte, die sich nicht auf WordPress, Typo3 etc. beziehen. In diesem Tutorial möchte ich die alte Methode des Loginsystem auf PHP-Einfach ablösen und euch auf den neusten Standard bringen.
Um diese Methode benutzen zu können ist die PHP Version 5.3.7 vorausgesetzt, denn es werden Funktionen benutzt die erst ab der PHP Version 5.5 verfügbar sind. Dank der password_compat von ircmaxell aus GitHub ist es uns aber schon möglich die benötigten Funktionen schon ab der PHP Version 5.3.7 zu verwenden.
Schritt 1: Installation und erstes Testen
Um die neue Password_* Funktionen ab der PHP Version 5.3.7 benutzen zu können, benötigen wir die Datei von ircmaxell, welche man hier downloaden kann. Nach dem Download legt ihr euch so wie ich, einen Unterordner mit dem Namen inc an und legt dort die heruntergeladene password_compat.php ab.
Nun erstellen wir uns im Hauptverzeichnis eine index.html und eine check.php an. In der check.php schreiben wir folgenden Code, direkt zu Anfang der Datei hinein:
Um zu testen, ob die benötigten Funktionen nun auch enthalten sind, schreiben wir nach der If-Anweisung folgenden Code hinein:
echo password_hash('test', PASSWORD_BCRYPT);
Das oben gezeigte Beispiel erzeugt eine ähnliche Ausgabe: $2y$10$ndh.9cKGK4XSU4syTyU.vemimYKxnax8VKLWD6Ucyt4q6fKP3JeqW
Wenn alles funktioniert hat, dann sind die Funktionen erfolgreich installiert. Im nächsten Schritt widmen wir uns der optimalen Konfiguration der Passwort Verschlüsselung.
Schritt 2: Optimale Konfiguration
Nach der Installation der Funktionen widmen wir uns der Konfiguration des SALT und des COST. Der SALT ist eine zufällig gewählte Zeichenfolge die im Klartext an das verschlüsselte Passwort angehängt wird, welche die Entropie der Eingabe erhöht. Der COST hingegen definiert wie oft das Passwort verschlüsselt werden soll. Um einen guten COST zu finden, empfiehlt es sich eine weitere Datei im Hauptverzeichnis zu erstellen, welche danach wieder gelöscht werden kann.
<?php
/**
* This code will benchmark your server to determine how high of a cost you can
* afford. You want to set the highest cost that you can without slowing down
* you server too much. 8-10 is a good baseline, and more is good if your servers
* are fast enough. The code below aims for > 500 milliseconds stretching time,
* which is a good baseline for systems handling interactive logins.
*/
$timeTarget = 0.5; // 500 milliseconds
Das oben gezeigte Beispiel erzeugt eine ähnliche Ausgabe: $2y$09$vRtUHQJ2dldbCN0Y5iC1jOabVJcvMV243rwNagOPFT14LVxqAOtDa
Schritt 3: Das HTML Formular
Nachdem wir die benötigten Funktionen installiert und konfiguriert haben, widmen wir uns dem HTML Formular, welches später unseren Login abfragen soll. Ich verwende in diesem Beispiel ein ganz einfaches Formular, welches den Benutzernamen und das Passwort erfordert.
Schritt 4: Verifizieren mit password_verify()
In diesem Schritt schreiben wir das PHP Script um uns später authentifizieren zu können. Ich beziehe mich hierbei auf das aus Schritt 3 geschriebene HTML Formular. Als erstes stellen wir eine Abfrage, welche überprüft ob unser Formular abgesendet worden ist. Dazu gehen wir in unsere check.php und schreiben an den Anfang der Datei, noch vor dem einbinden der password_compat.php
Mit dieser Abfrage überprüfen wir ob das Formular abgesendet worden ist. Für den Produktiveinsatz wird diese Methode zur Überprüfung ob das Formular abgesendet worden ist, nicht EMPFOHLEN!!! , da wir uns mit den neuen password_* Funktionen beschäftigen. Um das Passwort nun dem Usernamen zu verifizieren benutzen wir die password_verify() Funktion und fragen im nächsten Schritt ob auch das Passwort aktualisiert werden muss, weil wir zum Beispiel vor geraumer Zeit den SALT oder den COST aktualisiert haben.
PHP
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
<?php
if(password_verify($_POST['password'], password_hash($_POST['password'], PASSWORD_BCRYPT, $options))) {
if(password_needs_rehash(password_hash($_POST['password'], PASSWORD_BCRYPT, $options), PASSWORD_BCRYPT, $options)) {
.. // Neues Passwort speichern
}
echo 'Das Passwort war korrekt. Hier geht es weiter!';
} else {
echo 'Das Passwort war nicht korrekt. Versuche es nochmal erneut!';
}
Schritt 5: Integration von MySQL und Einrichtung
Im letzten Schritt richten wir unsere Datenbank ein und verknüpfen die Datenbank mit unserem PHP Skript, denn aktuell ist es so, dass sich jeder immer noch einloggen kann und das wollen wir verhindern.
Wir gehen also in unser PHPmyAdmin und erstellen uns eine Datenbank, die ich in meinem Beispiel passwordhash nenne. Wenn wir die Datenbank erstellt haben, führen wir folgende MySQL Abfrage durch:
Zitat:
DROP TABLE IF EXISTS `accounts`;
CREATE TABLE IF NOT EXISTS `accounts` (
`id` int(10) NOT NULL,
`user` varchar(64) NOT NULL,
`passwordhash` varchar(72) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
ALTER TABLE `accounts`
ADD PRIMARY KEY (`id`),
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
Nachdem wir unsere Tabelle in unsere Datenbank eingefügt haben, verbinden wir uns mit der Datenbank und lesen den abgefragten User aus.
PHP
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
<?php
$mysql = new MySQLi('localhost', 'BENUTZERNAME', 'PASSWORD', 'passwordhash');
$user = $mysql->query('SELECT `passwordhash` FROM `accounts` WHERE `user` = \''. $_POST['username'] .'\' LIMIT 0,1');
$result = $user->fetch_assoc();
Nun haben wir in der Variable $result unsere Ergebnisse und können diese in unserer Password Überprüfung ersetzen. Danach sieht unser Code folgender maßen aus:
echo 'Das Passwort war korrekt. Hier geht es weiter!';
} else {
echo 'Das Passwort war nicht korrekt. Versuche es nochmal erneut!';
}
Versuchen wir es doch mal. Wir erstellen uns einen Testaccount mit dem Namen test und dem Passwort test, wenn alles funktioniert habt ihr bis jetzt alles richtig gemacht.
Damit nun auch bei einer Aktualisierung unserer COST und unserer SALT der neue Wert in der Datenbank gespeichert wird, schreiben wir noch einen SQL Befehl, der unser Passwort aktualisieren soll, welches in die if-Anweisung von password_needs_rehash() gehört.
Fertig ist unsere Login funktion und wir können uns nun Fehlerfrei anmelden.
Schritt 6: Ende, kompletter Code im Überblick
In diesem Schritt erkläre ich gar nichts, sondern gebe euch nochmal den kompletten Code in der Übersicht.
<?php
/**
* A Compatibility library with PHP 5.5's simplified password hashing API.
*
* @author Anthony Ferrara <ircmaxell@php.net>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @copyright 2012 The Authors
*/
namespace {
if (!defined('PASSWORD_BCRYPT')) {
/**
* PHPUnit Process isolation caches constants, but not function declarations.
* So we need to check if the constants are defined separately from
* the functions to enable supporting process isolation in userland
* code.
*/
define('PASSWORD_BCRYPT', 1);
define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
}
if (!function_exists('password_hash')) {
/**
* Hash the password using the specified algorithm
*
* @param string $password The password to hash
* @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
* @param array $options The options for the algorithm to use
*
* @return string|false The hashed password, or false on error.
*/
function password_hash($password, $algo, array $options = array()) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
return null;
}
if (is_null($password) || is_int($password)) {
$password = (string) $password;
}
if (!is_string($password)) {
trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
return null;
}
if (!is_int($algo)) {
trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
return null;
}
$resultLength = 0;
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = PASSWORD_BCRYPT_DEFAULT_COST;
if (isset($options['cost'])) {
$cost = $options['cost'];
if ($cost < 4 || $cost > 31) {
trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
return null;
}
}
// The length of salt to generate
$raw_salt_len = 16;
// The length required in the final serialization
$required_salt_len = 22;
$hash_format = sprintf("$2y$%02d$", $cost);
// The expected length of the final crypt() output
$resultLength = 60;
break;
default:
trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
return null;
}
$salt_req_encoding = false;
if (isset($options['salt'])) {
switch (gettype($options['salt'])) {
case 'NULL':
case 'boolean':
case 'integer':
case 'double':
case 'string':
$salt = (string) $options['salt'];
break;
case 'object':
if (method_exists($options['salt'], '__tostring')) {
$salt = (string) $options['salt'];
break;
}
case 'array':
case 'resource':
default:
trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
return null;
}
if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
return null;
} elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
$salt_req_encoding = true;
}
} else {
$buffer = '';
$buffer_valid = false;
if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
$buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($raw_salt_len);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && @is_readable('/dev/urandom')) {
$file = fopen('/dev/urandom', 'r');
$read = PasswordCompat\binary\_strlen($buffer);
while ($read < $raw_salt_len) {
$buffer .= fread($file, $raw_salt_len - $read);
$read = PasswordCompat\binary\_strlen($buffer);
}
fclose($file);
if ($read >= $raw_salt_len) {
$buffer_valid = true;
}
}
if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
$buffer_length = PasswordCompat\binary\_strlen($buffer);
for ($i = 0; $i < $raw_salt_len; $i++) {
if ($i < $buffer_length) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
} else {
$buffer .= chr(mt_rand(0, 255));
}
}
}
$salt = $buffer;
$salt_req_encoding = true;
}
if ($salt_req_encoding) {
// encode string with the Base64 variant used by crypt
$base64_digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
$bcrypt64_digits =
'./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$base64_string = base64_encode($salt);
$salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
}
$salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
$hash = $hash_format . $salt;
$ret = crypt($password, $hash);
if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
return false;
}
return $ret;
}
/**
* Get information about the password hash. Returns an array of the information
* that was used to generate the password hash.
*
* array(
* 'algo' => 1,
* 'algoName' => 'bcrypt',
* 'options' => array(
* 'cost' => PASSWORD_BCRYPT_DEFAULT_COST,
* ),
* )
*
* @param string $hash The password hash to extract info from
*
* @return array The array of information about the hash.
*/
function password_get_info($hash) {
$return = array(
'algo' => 0,
'algoName' => 'unknown',
'options' => array(),
);
if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
$return['algo'] = PASSWORD_BCRYPT;
$return['algoName'] = 'bcrypt';
list($cost) = sscanf($hash, "$2y$%d$");
$return['options']['cost'] = $cost;
}
return $return;
}
/**
* Determine if the password hash needs to be rehashed according to the options provided
*
* If the answer is true, after validating the password using password_verify, rehash it.
*
* @param string $hash The hash to test
* @param int $algo The algorithm used for new password hashes
* @param array $options The options array passed to password_hash
*
* @return boolean True if the password needs to be rehashed.
*/
function password_needs_rehash($hash, $algo, array $options = array()) {
$info = password_get_info($hash);
if ($info['algo'] != $algo) {
return true;
}
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = isset($options['cost']) ? $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST;
if ($cost != $info['options']['cost']) {
return true;
}
break;
}
return false;
}
/**
* Verify a password against a hash using a timing attack resistant approach
*
* @param string $password The password to verify
* @param string $hash The hash to verify against
*
* @return boolean If the password matches the hash
*/
function password_verify($password, $hash) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
return false;
}
$ret = crypt($password, $hash);
if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
return false;
}
$status = 0;
for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
$status |= (ord($ret[$i]) ^ ord($hash[$i]));
}
return $status === 0;
}
}
}
namespace PasswordCompat\binary {
if (!function_exists('PasswordCompat\\binary\\_strlen')) {
/**
* Count the number of bytes in a string
*
* We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
* In this case, strlen() will count the number of *characters* based on the internal encoding. A
* sequence of bytes might be regarded as a single multibyte character.
*
* @param string $binary_string The input string
*
* @internal
* @return int The number of bytes
*/
function _strlen($binary_string) {
if (function_exists('mb_strlen')) {
return mb_strlen($binary_string, '8bit');
}
return strlen($binary_string);
}
/**
* Get a substring based on byte limits
*
* @see _strlen()
*
* @param string $binary_string The input string
* @param int $start
* @param int $length
*
* @internal
* @return string The substring
*/
function _substr($binary_string, $start, $length) {
if (function_exists('mb_substr')) {
return mb_substr($binary_string, $start, $length, '8bit');
}
return substr($binary_string, $start, $length);
}
/**
* Check if current PHP version is compatible with the library
*
* @return boolean the check result
*/
function check() {
static $pass = NULL;
if (is_null($pass)) {
if (function_exists('crypt')) {
$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
$test = crypt("password", $hash);
$pass = $test == $hash;
} else {
$pass = false;
}
}
return $pass;
}
}
}
<?php
/**
* This code will benchmark your server to determine how high of a cost you can
* afford. You want to set the highest cost that you can without slowing down
* you server too much. 8-10 is a good baseline, and more is good if your servers
* are fast enough. The code below aims for > 500 milliseconds stretching time,
* which is a good baseline for systems handling interactive logins.
*/
$timeTarget = 0.5; // 500 milliseconds
Du solltest vielleicht prüfen, bevor du die password_compat.php einbindest, ob der jenige eventuell bereits PHP 5.5 verwendet. Denn dann benötigt man die entsprechende Datei nicht und verhindert auch so Fehlermeldungen.
Orginal von Karamba
Wird nicht bei der Funktion password_hash das Salz automatisch hinzugefügt?
Karamba
Ja, aber den Salt der in der $options ist. Und den Salt sollte man wie es Htaccess auch macht mit der mcrypt Funktion generieren.
Nein, den Salt sollte man allgemein NICHT selbst generieren, wenn man die password_hash Funktion verwendet.
Zitat:
Caution:
It is strongly recommended that you do not generate your own salt for this function. It will create a secure salt automatically for you if you do not specify one.
Orginal von FalkenaugeMihawk
Du solltest vielleicht prüfen, bevor du die password_compat.php einbindest, ob der jenige eventuell bereits PHP 5.5 verwendet. Denn dann benötigt man die entsprechende Datei nicht und verhindert auch so Fehlermeldungen.
Diesen Beitrag habe ich erst jetzt gesehen, weil ich mir letztens nicht den kompletten Thread durchgelesen habe, deswegen erst jetzt meine Antwort darauf ^^
Wieso muss man vorher prüfen, ob PHP 5.5+ vorhanden ist, bevor man das Skript hinzufügt?
Wenn man sich das Skript mal wirklich anschaut, erkennt man, dass die entsprechenden Konstanten und Funktionen nur dann definiert werden, wenn PASSWORD_BCRYPT und password_hash() NICHT existieren. Dieses Skript ist extra darauf ausgelegt, dass es sich "selbst abschaltet", sobald man es nicht mehr braucht, damit es eben nicht zu gewissen Fehlermeldungen kommt.
Dieses Update der Prüfung auf die aktuelle Version ist somit eigentlich unnötig und verstehe nicht, welche Fehlermeldungen da kommen sollten, wenn man dann später mal PHP 5.5+ verwenden sollte, während man dieses Skript weiterhin einbindet.
ich habe mal eine Frage bezüglich des "salt" ab PHP7.0.
Wenn ich dies verwende gibt er mir die Meldung aus, das dieses veraltet sei.
Zitat:
WARNING password_hash(): Use of the 'salt' option to password_hash is deprecated on line number 9
Soll man den "salt" dann einfach weg lassen?
Ja, man sollte auch vorher den Salt nicht selbst setzen, weil PHP schon automatisch selbst einen möglich sicheren Salt generiert, wenn man keinen Salt angibt. Entsprechend konnte man den Salt höchstens genau so sicher, aber eher unsicherer machen, wenn man ihn selbst geniert hat, weswegen diese Option auch veraltet ist und bald sicher komplett entfernt wird.
Edit:
Zitat:
Caution
It is strongly recommended that you do not generate your own salt for this function. It will create a secure salt automatically for you if you do not specify one.
As noted above, providing the salt option in PHP 7.0 will generate a deprecation warning. Support for providing a salt manually may be removed in a future PHP release.