Як використовувати PHP's password_hash для хешування та перевірки паролів


94

Нещодавно я намагався реалізувати власну безпеку на скрипті входу, який мені трапився в Інтернеті. Намагаючись навчитися створювати власний сценарій для створення солі для кожного користувача, я натрапив password_hash.

З того, що я розумію (виходячи з показань на цій сторінці ), сіль уже утворюється в рядку, коли ви використовуєте password_hash. Це правда?

Ще одне запитання у мене було, чи не було б розумно мати 2 солі? Один безпосередньо у файлі та один у БД? Таким чином, якщо хтось компрометує вашу сіль у БД, у вас все ще є та, яка знаходиться безпосередньо у файлі? Я читав тут, що зберігання солі ніколи не є розумною ідеєю, але мене завжди бентежило, що люди мають на увазі під цим.


8
Ні. Нехай функція піклується про сіль. Подвійне засолювання завдасть вам неприємностей, і в цьому немає необхідності.
Funk Forty Niner

Відповіді:


182

Використання password_hash- це рекомендований спосіб зберігання паролів. Не розділяйте їх на БД та файли.

Скажімо, ми маємо такий вхід:

$password = $_POST['password'];

Спочатку хеш пароля, роблячи це:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Потім перегляньте результати:

var_dump($hashed_password);

Як бачите, він хешований. (Припускаю, ви зробили ці кроки).

Тепер ви зберігаєте цей хешований пароль у своїй базі даних, переконавшись, що стовпець пароля достатньо великий, щоб містити хешоване значення (принаймні 60 символів або довше) . Коли користувач запитує їх увійти, ви перевіряєте введення пароля за допомогою цього хеш-значення в базі даних, виконавши наступне:

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

Офіційна довідка


2
Добре, я просто спробував це, і це спрацювало. Я сумнівався у функції, оскільки вона здавалася майже надто простою. Як довго ви рекомендуєте робити довжину мого varchar? 225?
Джош Поттер,

4
Це вже є в посібниках php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php, які ОП, ймовірно, не читав і не розумів. Це питання задавали частіше, ніж взагалі.
Funk Forty Niner

Це інша сторінка.
Джош Поттер,

@JoshPotter відрізняється від чого? плюс, помітив, що вони не відповіли на ваше друге запитання. вони, мабуть, очікують, що ти дізнаєшся сам, або вони цього не знають.
Funk Forty Niner

8
@FunkFortyNiner, б / к Джош задав питання, я знайшов його через 2 роки, і це мені допомогло. Це суть SO. Цей посібник приблизно такий же чіткий, як і грязь.
toddmo

23

Так, ви правильно зрозуміли, функція password_hash () генерує сіль самостійно і включає її в отримане хеш-значення. Зберігання солі в базі даних абсолютно правильне, воно робить свою справу, навіть якщо відомо.

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

Друга сіль, про яку ви згадали (та, що зберігається у файлі), насправді - це перець або ключ на стороні сервера. Якщо ви додаєте його перед перемішуванням (як сіль), то додаєте перець. Хоча є кращий спосіб, ви можете спочатку обчислити хеш, а потім зашифрувати (двосторонній) хеш за допомогою ключа на стороні сервера. Це дає вам можливість змінити ключ, коли це необхідно.

На відміну від солі, цей ключ слід тримати в таємниці. Люди часто змішують це і намагаються приховати сіль, але краще дати солі зробити свою справу і додати секрет ключем.


8

Так, це правда. Чому ви сумніваєтесь у найпоширеніших запитаннях PHP щодо функції? :)

Результат бігу password_hash()складається з чотирьох частин:

  1. використаний алгоритм
  2. параметри
  3. сіль
  4. фактичний хеш пароля

Отже, як бачите, хеш є його частиною.

Звичайно, ви могли б отримати додаткову сіль для додаткового рівня безпеки, але я щиро думаю, що це надмірно в звичайному додатку php. За замовчуванням алгоритм bcrypt хороший, а необов’язковий Blow-Fish, можливо, ще кращий.


2
BCrypt - це функція хешування , тоді як Blowfish - це алгоритм шифрування . BCrypt походить від алгоритму Blowfish.
martinstoeckli

7

Ніколи не використовуйте md5 () для захисту вашого пароля, навіть із сіллю, це завжди небезпечно !!

Захистіть свій пароль за допомогою найновіших алгоритмів хешування, як показано нижче.

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

Для узгодження із зашифрованим паролем бази даних та введеним користувачем паролем використовуйте наведену нижче функцію.

<?php 

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

Якщо ви хочете використовувати свою власну сіль, використовуйте власну згенеровану функцію для цього ж, просто виконайте наведені нижче дії, але я не рекомендую це, оскільки воно виявляється застарілим в останніх версіях PHP.

Прочитайте про password_hash () перед використанням коду нижче.

<?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
Варіант солі застарілий з поважних причин, оскільки функція робить все можливе, щоб генерувати криптографічно безпечну сіль, і майже неможливо зробити це краще.
martinstoeckli

@martinstoeckli, так, ти маєш рацію, я щойно оновив свою відповідь, дякую!
Махеш Ядав,

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'])); Це код, який використовується в login.php .. я хочу обійтися без використання escape та md5. я хочу використовувати хеш пароля ..
rashmi sm

PASSWORD_DEFAULT - Використовуйте алгоритм bcrypt (Вимагає PHP 5.5.0). Зверніть увагу, що ця константа призначена для зміни з плином часу, коли нові та більш потужні алгоритми додаються до PHP. З цієї причини тривалість результату від використання цього ідентифікатора може змінюватися з часом.
Адріан П.

5

Існує очевидна відсутність дискусій щодо зворотної та вперед сумісності, яка вбудована в функції паролів PHP. Важливо:

  1. Зворотна сумісність: Функції паролів - це, по суті, добре написана оболонка crypt()і, за своєю суттю, зворотно сумісні з crypt()хешами -format, навіть якщо вони використовують застарілі та / або небезпечні алгоритми хешування.
  2. Форвардна сумісність: вставка password_needs_rehash()та трохи логіки у ваш робочий процес автентифікації може тримати ваші хеші в курсі поточних та майбутніх алгоритмів з потенційно нульовими майбутніми змінами робочого процесу. Примітка: Будь-який рядок, який не відповідає зазначеному алгоритму, буде позначений для необхідності перепрофілювання, включаючи несумісні з христом хеші.

Наприклад:

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

Вихід:

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)

Як останнє зауваження, враховуючи те, що ви можете повторно хешувати пароль користувача під час входу, ви повинні розглянути "припинення дії" небезпечних застарілих хешів для захисту своїх користувачів. Під цим я маю на увазі, що після певного пільгового періоду ви видаляєте всі незахищені [напр.: Голі MD5 / SHA / інакше слабкі] хеші, і ваші користувачі покладаються на механізми скидання пароля вашої програми.


0

Повний код класу пароля:

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

Я створив функцію, яку весь час використовую для перевірки пароля та для створення паролів, наприклад, для їх зберігання в базі даних MySQL. Він використовує випадково створену сіль, яка є набагато безпечнішою, ніж статична сіль.

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
Краще опустити saltпараметр, він автоматично генерується функцією password_hash () , дотримуючись найкращих практик. Замість PASSWORD_BCRYPTодного можна використовувати PASSWORD_DEFAULTдля написання майбутнього коду перевірки.
martinstoeckli

Дякую за пораду. Я, мабуть, це наглядав у документації. Це були довгі ночі.
Геррит Фріс,

1
Відповідно до secure.php.net/manual/en/function.password-hash.php "Опція солі застаріла станом на PHP 7.0.0. Зараз бажано просто використовувати сіль, яка генерується за замовчуванням".
jmng
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.