Verwendung von password_hash von PHP zum Hashing und Überprüfen von Passwörtern

89

Kürzlich habe ich versucht, meine eigene Sicherheit in einem Anmeldeskript zu implementieren, auf das ich im Internet gestoßen bin. Nachdem ich versucht hatte zu lernen, wie ich mein eigenes Skript erstellen kann, um für jeden Benutzer ein Salt zu generieren, bin ich darauf gestoßen password_hash.

Soweit ich weiß (basierend auf den Angaben auf dieser Seite ), wird bei der Verwendung bereits Salz in der Zeile generiert password_hash. Ist das wahr?

Eine andere Frage, die ich hatte, war, wäre es nicht klug, zwei Salze zu haben? Eine direkt in der Datei und eine in der DB? Auf diese Weise haben Sie immer noch das Salz direkt in der Datei, wenn jemand Ihr Salz in der Datenbank kompromittiert? Ich habe hier weiter gelesen, dass das Lagern von Salzen niemals eine kluge Idee ist, aber es hat mich immer verwirrt, was die Leute damit gemeint haben.

Josh Potter
quelle
8
Lassen Sie die Funktion sich um das Salz kümmern. Doppeltes Salzen wird Ihnen Probleme bereiten und es besteht keine Notwendigkeit dafür.
Funk Forty Niner

Antworten:

175

Die Verwendung password_hashist die empfohlene Methode zum Speichern von Kennwörtern. Trennen Sie sie nicht in DB und Dateien.

Nehmen wir an, wir haben folgenden Input:

$password = $_POST['password'];

Sie hashen zuerst das Passwort, indem Sie dies tun:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Dann sehen Sie die Ausgabe:

var_dump($hashed_password);

Wie Sie sehen können, ist es gehasht. (Ich nehme an, Sie haben diese Schritte ausgeführt).

Jetzt speichern Sie dieses Hash-Passwort in Ihrer Datenbank und stellen sicher , dass Ihre Passwortspalte groß genug ist, um den Hash-Wert zu speichern (mindestens 60 Zeichen oder länger) . Wenn ein Benutzer nach einer Anmeldung fragt, überprüfen Sie die Kennworteingabe mit diesem Hashwert in der Datenbank folgendermaßen:

// Query the database for username and password
// ...

if(password_verify($password, $hashed_password)) {
    // If the password inputs matched the hashed password in the database
    // Do something, you know... log them in.
} 

// Else, Redirect them back to the login page.

Offizielle Referenz

Akar
quelle
2
Ok, ich habe es gerade versucht und es hat funktioniert. Ich bezweifelte die Funktion, weil es fast zu einfach schien. Wie lange empfehlen Sie mir, die Länge meines Varchars zu machen? 225?
Josh Potter
4
Dies ist bereits in den Handbüchern php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php enthalten , die das OP wahrscheinlich nicht gelesen oder verstanden hat. Diese Frage wurde öfter gestellt als keine.
Funk Forty Niner
Das ist eine andere Seite.
Josh Potter
@JoshPotter anders als was? Außerdem habe ich festgestellt, dass sie auf Ihre zweite Frage nicht geantwortet haben. Sie erwarten wahrscheinlich, dass Sie es selbst herausfinden, oder sie wissen es nicht.
Funk Forty Niner
7
@FunkFortyNiner, b / c Josh stellte die Frage, ich fand sie 2 Jahre später und sie half mir. Das ist der Punkt von SO. Dieses Handbuch ist ungefähr so ​​klar wie Schlamm.
Toddmo
23

Ja, Sie haben es richtig verstanden. Die Funktion password_hash () generiert selbst ein Salt und nimmt es in den resultierenden Hash-Wert auf. Das Speichern des Salzes in der Datenbank ist absolut korrekt, es macht seine Arbeit, auch wenn es bekannt ist.

// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($_POST['password'], PASSWORD_DEFAULT);

// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($_POST['password'], $existingHashFromDb);

Das zweite Salz, das Sie erwähnt haben (das in einer Datei gespeichert ist), ist tatsächlich ein Pfeffer oder ein serverseitiger Schlüssel. Wenn Sie es vor dem Hashing hinzufügen (wie das Salz), fügen Sie einen Pfeffer hinzu. Es gibt jedoch einen besseren Weg, Sie könnten zuerst den Hash berechnen und anschließend den Hash mit einem serverseitigen Schlüssel verschlüsseln (in beide Richtungen). Dies gibt Ihnen die Möglichkeit, den Schlüssel bei Bedarf zu ändern.

Im Gegensatz zum Salz sollte dieser Schlüssel geheim gehalten werden. Die Leute mischen es oft durch und versuchen, das Salz zu verstecken, aber es ist besser, das Salz seinen Job machen zu lassen und das Geheimnis mit einem Schlüssel hinzuzufügen.

martinstoeckli
quelle
8

Ja, es ist wahr. Warum bezweifeln Sie die PHP-FAQ zur Funktion? :) :)

Das Ergebnis des Laufens password_hash()besteht aus vier Teilen:

  1. der verwendete Algorithmus
  2. Parameter
  3. Salz-
  4. tatsächlicher Passwort-Hash

Wie Sie sehen können, ist der Hash ein Teil davon.

Sicher, Sie könnten ein zusätzliches Salz für eine zusätzliche Sicherheitsebene haben, aber ich denke ehrlich, dass dies in einer regulären PHP-Anwendung übertrieben ist. Der Standard-bcrypt-Algorithmus ist gut, und der optionale Blowfish-Algorithmus ist wohl noch besser.

Joel Hinz
quelle
2
BCrypt ist eine Hashing- Funktion, während Blowfish ein Algorithmus zur Verschlüsselung ist . BCrypt stammt jedoch aus dem Blowfish-Algorithmus.
Martinstoeckli
6

Verwenden Sie niemals md5 (), um Ihr Passwort zu sichern, auch mit Salz, es ist immer gefährlich !!

Stellen Sie sicher, dass Ihr Passwort mit den neuesten Hashing-Algorithmen wie folgt gesichert ist.

<?php

// Your original Password
$password = '121@121';

//PASSWORD_BCRYPT or PASSWORD_DEFAULT use any in the 2nd parameter
/*
PASSWORD_BCRYPT always results 60 characters long string.
PASSWORD_DEFAULT capacity is beyond 60 characters
*/
$password_encrypted = password_hash($password, PASSWORD_BCRYPT);

Verwenden Sie die folgende Funktion, um das verschlüsselte Kennwort der Datenbank und das vom Benutzer eingegebene Kennwort abzugleichen.

<?php 

if (password_verify($password_inputted_by_user, $password_encrypted)) {
    // Success!
    echo 'Password Matches';
}else {
    // Invalid credentials
    echo 'Password Mismatch';
}

Wenn Sie Ihr eigenes Salz verwenden möchten, verwenden Sie Ihre benutzerdefinierte Funktion für dasselbe. Befolgen Sie einfach die nachstehenden Anweisungen. Ich empfehle dies jedoch nicht, da es in den neuesten Versionen von PHP als veraltet eingestuft wird.

Lesen Sie mehr über password_hash (), bevor Sie den folgenden Code verwenden.

<?php

$options = [
    'salt' => your_custom_function_for_salt(), 
    //write your own code to generate a suitable & secured salt
    'cost' => 12 // the default cost is 10
];

$hash = password_hash($your_password, PASSWORD_DEFAULT, $options);
Mahesh Yadav
quelle
4
Die Salzoption ist aus guten Gründen veraltet, da die Funktion ihr Bestes tut, um ein kryptografisch sicheres Salz zu erzeugen, und es fast unmöglich ist, es besser zu machen.
Martinstoeckli
@martinstoeckli, ja du hast recht, ich habe gerade meine Antwort aktualisiert, Danke!
Mahesh Yadav
if (isset ($ _ POST ['btn-signup'])) {$ uname = mysql_real_escape_string ($ _ POST ['uname']); $ email = mysql_real_escape_string ($ _ POST ['email']); $ upass = md5 (mysql_real_escape_string ($ _ POST ['pass'])); Dies ist Code, der in login.php verwendet wird. Ich möchte auf die Verwendung von Escape und MD5 verzichten. Ich möchte Passwort Hash verwenden ..
Rashmi Sm
PASSWORD_DEFAULT - Verwenden Sie den bcrypt-Algorithmus (PHP 5.5.0 erforderlich). Beachten Sie, dass sich diese Konstante im Laufe der Zeit ändern soll, wenn PHP neue und stärkere Algorithmen hinzugefügt werden. Aus diesem Grund kann sich die Länge des Ergebnisses aus der Verwendung dieses Bezeichners im Laufe der Zeit ändern.
Adrian P.
4

Es gibt einen deutlichen Mangel an Diskussionen über die Abwärts- und Vorwärtskompatibilität, die in die Passwortfunktionen von PHP integriert ist. Vor allem:

  1. Abwärtskompatibilität: Die Kennwortfunktionen sind im Wesentlichen ein gut geschriebener Wrapper crypt()und von Natur aus abwärtskompatibel mit crypt()-format-Hashes, selbst wenn sie veraltete und / oder unsichere Hash-Algorithmen verwenden.
  2. Vorwärtskompatibilität: Durch Einfügen password_needs_rehash()und ein wenig Logik in Ihren Authentifizierungsworkflow können Sie Ihre Hashes mit aktuellen und zukünftigen Algorithmen auf dem neuesten Stand halten, ohne dass zukünftige Änderungen am Workflow erforderlich sind. Hinweis: Alle Zeichenfolgen, die nicht mit dem angegebenen Algorithmus übereinstimmen, werden für eine erneute Aufbereitung gekennzeichnet, einschließlich nicht kryptakompatibler Hashes.

Z.B:

class FakeDB {
    public function __call($name, $args) {
        printf("%s::%s(%s)\n", __CLASS__, $name, json_encode($args));
        return $this;
    }
}

class MyAuth {
    protected $dbh;
    protected $fakeUsers = [
        // old crypt-md5 format
        1 => ['password' => '$1$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
        // old salted md5 format
        2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
        // current bcrypt format
        3 => ['password' => '$2y$10$3eUn9Rnf04DR.aj8R3WbHuBO9EdoceH9uKf6vMiD7tz766rMNOyTO']
    ];

    public function __construct($dbh) {
        $this->dbh = $dbh;
    }

    protected function getuser($id) {
        // just pretend these are coming from the DB
        return $this->fakeUsers[$id];
    }

    public function authUser($id, $password) {
        $userInfo = $this->getUser($id);

        // Do you have old, turbo-legacy, non-crypt hashes?
        if( strpos( $userInfo['password'], '$' ) !== 0 ) {
            printf("%s::legacy_hash\n", __METHOD__);
            $res = $userInfo['password'] === md5($password . $userInfo['salt']);
        } else {
            printf("%s::password_verify\n", __METHOD__);
            $res = password_verify($password, $userInfo['password']);
        }

        // once we've passed validation we can check if the hash needs updating.
        if( $res && password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
            printf("%s::rehash\n", __METHOD__);
            $stmt = $this->dbh->prepare('UPDATE users SET pass = ? WHERE user_id = ?');
            $stmt->execute([password_hash($password, PASSWORD_DEFAULT), $id]);
        }

        return $res;
    }
}

$auth = new MyAuth(new FakeDB());

for( $i=1; $i<=3; $i++) {
    var_dump($auth->authuser($i, 'foo'));
    echo PHP_EOL;
}

Ausgabe:

MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)

MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)

MyAuth::authUser::password_verify
bool(true)

Als letzte Anmerkung, da Sie das Kennwort eines Benutzers nur beim Anmelden erneut hashen können, sollten Sie unsichere Legacy-Hashes als "Sonnenuntergang" betrachten, um Ihre Benutzer zu schützen. Damit meine ich, dass Sie nach einer bestimmten Nachfrist alle unsicheren [z. B. nackten MD5 / SHA / ansonsten schwachen] Hashes entfernen und Ihre Benutzer sich auf die Kennwortrücksetzmechanismen Ihrer Anwendung verlassen.

Sammitch
quelle
0

Klassenkennwort vollständiger Code:

Class Password {

    public function __construct() {}


    /**
     * 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_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;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
                $cost = 10;
                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);
                break;
            default :
                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
                return null;
        }
        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 (strlen($salt) < $required_salt_len) {
                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
                return null;
            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
                $salt = str_replace('+', '.', base64_encode($salt));
            }
        } else {
            $salt = str_replace('+', '.', base64_encode($this->generate_entropy($required_salt_len)));
        }
        $salt = substr($salt, 0, $required_salt_len);

        $hash = $hash_format . $salt;

        $ret = crypt($password, $hash);

        if (!is_string($ret) || strlen($ret) <= 13) {
            return false;
        }

        return $ret;
    }


    /**
     * Generates Entropy using the safest available method, falling back to less preferred methods depending on support
     *
     * @param int $bytes
     *
     * @return string Returns raw bytes
     */
    function generate_entropy($bytes){
        $buffer = '';
        $buffer_valid = false;
        if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
            $buffer = openssl_random_pseudo_bytes($bytes);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && is_readable('/dev/urandom')) {
            $f = fopen('/dev/urandom', 'r');
            $read = strlen($buffer);
            while ($read < $bytes) {
                $buffer .= fread($f, $bytes - $read);
                $read = strlen($buffer);
            }
            fclose($f);
            if ($read >= $bytes) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid || strlen($buffer) < $bytes) {
            $bl = strlen($buffer);
            for ($i = 0; $i < $bytes; $i++) {
                if ($i < $bl) {
                    $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
                } else {
                    $buffer .= chr(mt_rand(0, 255));
                }
            }
        }
        return $buffer;
    }

    /**
     * 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' => 10,
     *    ),
     * )
     *
     * @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 (substr($hash, 0, 4) == '$2y$' && 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'] : 10;
                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
     */
    public 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) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
            return false;
        }

        $status = 0;
        for ($i = 0; $i < strlen($ret); $i++) {
            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
        }

        return $status === 0;
    }

}
Dimitris Maniatis
quelle
-1

Ich habe eine Funktion erstellt, die ich ständig zur Kennwortüberprüfung und zum Erstellen von Kennwörtern verwende, z. B. um sie in einer MySQL-Datenbank zu speichern. Es wird ein zufällig erzeugtes Salz verwendet, das viel sicherer ist als die Verwendung eines statischen Salzes.

function secure_password($user_pwd, $multi) {

/*
    secure_password ( string $user_pwd, boolean/string $multi ) 

    *** Description: 
        This function verifies a password against a (database-) stored password's hash or
        returns $hash for a given password if $multi is set to either true or false

    *** Examples:
        // To check a password against its hash
        if(secure_password($user_password, $row['user_password'])) {
            login_function();
        } 
        // To create a password-hash
        $my_password = 'uber_sEcUrE_pass';
        $hash = secure_password($my_password, true);
        echo $hash;
*/

// Set options for encryption and build unique random hash
$crypt_options = ['cost' => 11, 'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)];
$hash = password_hash($user_pwd, PASSWORD_BCRYPT, $crypt_options);

// If $multi is not boolean check password and return validation state true/false
if($multi!==true && $multi!==false) {
    if (password_verify($user_pwd, $table_pwd = $multi)) {
        return true; // valid password
    } else {
        return false; // invalid password
    }
// If $multi is boolean return $hash
} else return $hash;

}
Gerrit Fries
quelle
6
Es ist am besten, den saltParameter wegzulassen . Er wird automatisch von der Funktion password_hash () gemäß den Best Practices generiert . Stattdessen PASSWORD_BCRYPTkann PASSWORD_DEFAULTman zukunftssicheren Code schreiben.
Martinstoeckli
Danke für diesen Rat. Ich muss es in der Dokumentation überwacht haben. Es waren einige lange Nächte.
Gerrit Fries
1
Laut Secure.php.net/manual/en/function.password-hash.php "Die Salt-Option ist ab PHP 7.0.0 veraltet. Es wird jetzt bevorzugt, einfach das standardmäßig generierte Salt zu verwenden."
25.