Jak używać password_hash PHP do mieszania i weryfikowania haseł


98

Ostatnio próbowałem zaimplementować własne zabezpieczenia w skrypcie logowania, na który natknąłem się w Internecie. Po trudnościach z nauczeniem się, jak stworzyć własny skrypt, aby wygenerować sól dla każdego użytkownika, natknąłem się na password_hash.

Z tego, co rozumiem (na podstawie czytania na tej stronie ), sól jest już generowana w wierszu, gdy używasz password_hash. Czy to prawda?

Kolejne pytanie, które zadałem, brzmiało: czy nie byłoby mądrze mieć 2 sole? Jeden bezpośrednio w pliku, a drugi w bazie danych? W ten sposób, jeśli ktoś naruszy twoją sól w bazie danych, nadal masz tę bezpośrednio w pliku? Czytałem tutaj, że przechowywanie soli nigdy nie jest mądrym pomysłem, ale zawsze myliłem się, co ludzie przez to rozumieją.


8
Nie. Niech funkcja zajmie się solą. Podwójne solenie sprawi Ci kłopoty i nie ma takiej potrzeby.
Funk Forty Niner

Odpowiedzi:


184

Używanie password_hashjest zalecanym sposobem przechowywania haseł. Nie oddzielaj ich od DB i plików.

Powiedzmy, że mamy następujące dane wejściowe:

$password = $_POST['password'];

Najpierw hashujesz hasło, wykonując następujące czynności:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Następnie zobacz wynik:

var_dump($hashed_password);

Jak widać, jest to zaszyfrowane. (Zakładam, że wykonałeś te kroki).

Teraz przechowujesz to zaszyfrowane hasło w swojej bazie danych, upewniając się, że kolumna hasła jest wystarczająco duża, aby pomieścić zaszyfrowaną wartość (co najmniej 60 znaków lub dłużej) . Kiedy użytkownik prosi o zalogowanie się, sprawdzasz wprowadzone hasło za pomocą tej wartości skrótu w bazie danych, wykonując następujące czynności:

// 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.

Oficjalne odniesienie


2
Ok, właśnie spróbowałem i zadziałało. Wątpiłem w tę funkcję, ponieważ wydawała się prawie zbyt łatwa. Jak długo polecacie zrobić długość mojego varchar? 225?
Josh Potter

4
Jest to już w podręcznikach php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php , których OP prawdopodobnie nie przeczytał lub nie zrozumiał. To pytanie było zadawane częściej niż żadne.
Funk Forty Niner

To inna strona.
Josh Potter

@JoshPotter różni się od tego? dodatkowo zauważyłem, że nie odpowiedzieli na Twoje drugie pytanie. prawdopodobnie oczekują, że sam się dowiesz, lub nie wiedzą.
Funk Forty Niner

8
@FunkFortyNiner, b / c Josh zadał pytanie, znalazłem je 2 lata później i pomogło mi. O to chodzi w SO. Ta instrukcja jest tak przejrzysta jak błoto.
toddmo,

25

Tak, zrozumiałeś to poprawnie, funkcja password_hash () sama wygeneruje sól i włączy ją do wynikowej wartości skrótu. Przechowywanie soli w bazie danych jest absolutnie poprawne, spełnia swoje zadanie, nawet jeśli jest znane.

// 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);

Druga sól, o której wspomniałeś (ta przechowywana w pliku), to tak naprawdę klucz pieprzowy lub serwer. Jeśli dodasz go przed haszowaniem (jak sól), to dodajesz pieprz. Jest jednak lepszy sposób, możesz najpierw obliczyć hash, a następnie zaszyfrować (dwukierunkowo) hash za pomocą klucza po stronie serwera. Daje to możliwość zmiany klucza w razie potrzeby.

W przeciwieństwie do soli, ten klucz powinien być trzymany w tajemnicy. Ludzie często mieszają to i próbują ukryć sól, ale lepiej pozwolić soli wykonać swoje zadanie i dodać sekret kluczem.


8

Tak, to prawda. Dlaczego wątpisz w FAQ PHP na temat tej funkcji? :)

Wynik biegania password_hash()składa się z czterech części:

  1. zastosowany algorytm
  2. parametry
  3. Sól
  4. aktualny skrót hasła

Jak więc widzisz, hash jest tego częścią.

Jasne, możesz mieć dodatkową sól dla dodatkowej warstwy bezpieczeństwa, ale szczerze myślę, że to przesada w zwykłej aplikacji php. Domyślny algorytm bcrypt jest dobry, a opcjonalny algorytm blowfish jest prawdopodobnie jeszcze lepszy.


2
BCrypt to funkcja haszująca , a Blowfish to algorytm szyfrowania . BCrypt wywodzi się jednak z algorytmu Blowfish.
martinstoeckli

7

Nigdy nie używaj md5 () do zabezpieczenia hasła, nawet jeśli używasz soli, jest to zawsze niebezpieczne !!

Zabezpiecz swoje hasło najnowszymi algorytmami haszowania, jak poniżej.

<?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);

W celu dopasowania do zaszyfrowanego hasła bazy danych i hasła wprowadzonego przez użytkownika użyj poniższej funkcji.

<?php 

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

Jeśli chcesz użyć własnej soli, użyj niestandardowej funkcji do tego samego, po prostu postępuj zgodnie z poniższymi instrukcjami, ale nie polecam tego, ponieważ w najnowszych wersjach PHP jest to przestarzałe.

Przeczytaj o password_hash () przed użyciem poniższego kodu.

<?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);

4
Opcja soli jest przestarzała z ważnych powodów, ponieważ funkcja robi wszystko, co w jej mocy, aby wygenerować kryptograficznie bezpieczną sól i jest prawie niemożliwe, aby zrobić to lepiej.
martinstoeckli

@martinstoeckli, tak, masz rację, właśnie zaktualizowałem moją odpowiedź, dzięki!
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'])); To jest kod używany w login.php .. Chcę obejść się bez używania escape i md5. chcę użyć skrótu hasła ..
rashmi sm

PASSWORD_DEFAULT - Użyj algorytmu bcrypt (wymaga PHP 5.5.0). Zauważ, że ta stała ma się zmieniać w czasie, gdy do PHP dodawane są nowe i silniejsze algorytmy. Z tego powodu długość wyniku przy użyciu tego identyfikatora może się zmieniać w czasie.
Adrian P.

5

Brakuje dyskusji na temat kompatybilności wstecz i do przodu, która jest wbudowana w funkcje haseł PHP. Szczególnie:

  1. Kompatybilność wsteczna: Funkcje haseł są zasadniczo dobrze napisanym opakowaniem crypt()i są z natury zgodne wstecz z crypt()hashami -format, nawet jeśli używają przestarzałych i / lub niezabezpieczonych algorytmów mieszania.
  2. Kompatybilność do przodu: wstawienie password_needs_rehash()i odrobina logiki do przepływu pracy uwierzytelniania może zapewnić aktualność skrótów z obecnymi i przyszłymi algorytmami z potencjalnie zerowymi przyszłymi zmianami w przepływie pracy. Uwaga: każdy ciąg, który nie pasuje do określonego algorytmu, zostanie oznaczony jako wymagający ponownego skrótu, w tym skróty niezgodne z szyfrowaniem.

Na przykład:

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;
}

Wynik:

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)

Na koniec, biorąc pod uwagę, że hasło użytkownika można ponownie zaszyfrować tylko przy logowaniu, należy rozważyć „wygaśnięcie” niezabezpieczonych, starszych skrótów, aby chronić użytkowników. Rozumiem przez to, że po pewnym okresie karencji usuwasz wszystkie niezabezpieczone [np. Gołe MD5 / SHA / inaczej słabe] skróty i sprawiasz, że użytkownicy polegają na mechanizmach resetowania haseł aplikacji.


0

Pełny kod hasła klasy:

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;
    }

}

0

Zbudowałem funkcję, której używam cały czas do sprawdzania poprawności haseł i tworzenia haseł, np. Do przechowywania ich w bazie danych MySQL. Używa losowo generowanej soli, która jest o wiele bezpieczniejsza niż użycie soli statycznej.

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;

}

6
Najlepiej pominąć saltparametr, zostanie on automatycznie wygenerowany przez password_hash () , zgodnie z najlepszymi praktykami. Zamiast tego PASSWORD_BCRYPTmożna użyć PASSWORD_DEFAULTdo napisania przyszłego kodu.
martinstoeckli

Dzięki za radę. Musiałem to nadzorować w dokumentacji. To były długie noce.
Gerrit Fries

1
Według secure.php.net/manual/en/function.password-hash.php „Opcja soli jest przestarzała w PHP 7.0.0. Obecnie preferuje się po prostu użycie domyślnie generowanej soli.”
jmng
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.