Как использовать PHP password_hash для хэширования и проверки паролей

#php #mysqli

Вопрос:

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

Из того, что я понимаю (исходя из чтения на этой странице), соль уже генерируется в строке при использовании password_hash . Это правда?

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

Комментарии:

1. Нет. Пусть функция позаботится о соли. Двойная соль вызовет у вас проблемы, и в этом нет необходимости.

2. Как упоминает @martinstoeckli в своем ответе, то, что здесь описывается, известно как «перец» и часто рекомендуется в наши дни. Ты был первопроходцем, Джош! : D

Ответ №1:

Использование 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.
 

Официальная ссылка

Комментарии:

1. Хорошо, я только что попробовал это, и это сработало. Я сомневался в этой функции, потому что она казалась слишком простой. Как долго вы рекомендуете мне делать длину моего varchar? 225?

2. Это уже есть в руководствах php.net/manual/en/function.password-hash.php php.net/manual/en/function.password-verify.php что ОП, вероятно, не прочитал или не понял. Этот вопрос задавали чаще, чем ничего.

3. @FunkFortyNiner, b / c Джош задал вопрос, я нашел его 2 года спустя, и это помогло мне. В этом смысл SO. Это руководство примерно так же ясно, как грязь.

4. Что касается длины, из руководства PHP по password_hash в примере есть комментарий: «Имейте в виду, что значение по умолчанию может меняться со временем, поэтому вы хотели бы подготовиться, разрешив вашему хранилищу расширяться до 60 символов (255 было бы хорошо)»

5. @toddmo: Чтобы поддержать ваш комментарий, я только что подошел к этому вопросу в июне 2020 года, и обсуждение спасло меня от нескольких часов разочарования. Я тоже большую часть времени нахожу руководство по PHP таким же ясным, как грязь.

Ответ №2:

Да, вы правильно поняли, функция 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);
 

Вторая соль, которую вы упомянули (та, которая хранится в файле), на самом деле является ключом pepper или ключом на стороне сервера. Если вы добавляете его перед хешированием (как соль), то вы добавляете перец. Однако есть лучший способ, вы могли бы сначала вычислить хэш, а затем зашифровать (двусторонний) хэш с помощью ключа на стороне сервера. Это дает вам возможность изменить ключ, когда это необходимо.

В отличие от соли, этот ключ должен храниться в секрете. Люди часто путают и пытаются скрыть соль, но лучше позволить соли выполнять свою работу и добавить секрет с ключом.

Ответ №3:

Да, это правда. Почему вы сомневаетесь в php faq по функции? 🙂

Результат выполнения password_hash() состоит из четырех частей:

  1. используемый алгоритм
  2. параметры
  3. соль
  4. фактический хэш пароля

Итак, как вы можете видеть, хэш является его частью.

Конечно, у вас может быть дополнительная соль для дополнительного уровня безопасности, но я, честно говоря, думаю, что это перебор в обычном приложении php. Алгоритм bcrypt по умолчанию хорош, а дополнительный алгоритм blowfish, возможно, даже лучше.

Комментарии:

1. BCrypt — это функция хеширования , в то время как Blowfish — это алгоритм шифрования . Однако BCrypt происходит от алгоритма Blowfish.

Ответ №4:

Существует явное отсутствие обсуждения обратной и прямой совместимости, которая встроена в функции паролей PHP. Примечательно:

  1. Обратная совместимость: функции паролей по сути представляют собой хорошо написанную оболочку crypt() и по своей сути обратно совместимы с crypt() хэшами формата, даже если они используют устаревшие и / или небезопасные алгоритмы хэширования.
  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'], '

Вывод:

 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 / в противном случае слабые] хэши и заставляете своих пользователей полагаться на механизмы сброса пароля вашего приложения.

Комментарии:

1. Ага. Когда я менял нашу защиту паролей на use password_hash , я намеренно использовал низкое cost значение, чтобы позже увеличить его и проверить, что password_needs_rehash() все работает так, как задумано. (Версия с низким cost значением так и не была запущена в производство.)

Ответ №5:

Класс Пароль полный код:

 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$d$", $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 . $sa<

        $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') amp;amp; !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid amp;amp; function_exists('openssl_random_pseudo_bytes')) {
            $buffer = openssl_random_pseudo_bytes($bytes);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid amp;amp; 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


Ответ №6:

Я создал функцию, которую я постоянно использую для проверки пароля и для создания паролей, например, для их хранения в базе данных 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 amp;amp; $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;

}
 

Комментарии:

1. Лучше всего опустить salt параметр, он будет автоматически сгенерирован функцией password_hash() , следуя рекомендациям. Вместо PASSWORD_BCRYPT этого можно использовать PASSWORD_DEFAULT для написания кода будущего доказательства.

2. Согласно secure.php.net/manual/en/function.password-hash.php "Опция salt устарела с версии PHP 7.0.0. Теперь предпочтительнее просто использовать соль, которая генерируется по умолчанию. "

) !== 0 ) {
printf("%s::legacy_hashn", __METHOD__);
$res = $userInfo['password'] === md5($password . $userInfo['salt']);
} else {
printf("%s::password_verifyn", __METHOD__);
$res = password_verify($password, $userInfo['password']);
}

// once we've passed validation we can check if the hash needs updating.
if( $res amp;amp; password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
printf("%s::rehashn", __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;
}
Вывод:


В качестве последнего замечания, учитывая, что вы можете повторно хэшировать пароль пользователя только при входе в систему, вам следует рассмотреть возможность "закатывания" небезопасных устаревших хэшей для защиты ваших пользователей. Под этим я подразумеваю, что после определенного льготного периода вы удаляете все небезопасные [например: голые MD5 / SHA / в противном случае слабые] хэши и заставляете своих пользователей полагаться на механизмы сброса пароля вашего приложения.

Комментарии:

1. Ага. Когда я менял нашу защиту паролей на use password_hash , я намеренно использовал низкое cost значение, чтобы позже увеличить его и проверить, что password_needs_rehash() все работает так, как задумано. (Версия с низким cost значением так и не была запущена в производство.)

Ответ №5:

Класс Пароль полный код:


Ответ №6:

Я создал функцию, которую я постоянно использую для проверки пароля и для создания паролей, например, для их хранения в базе данных MySQL. Он использует случайно сгенерированную соль, которая намного безопаснее, чем использование статической соли.


Комментарии:

1. Лучше всего опустить salt параметр, он будет автоматически сгенерирован функцией password_hash() , следуя рекомендациям. Вместо PASSWORD_BCRYPT этого можно использовать PASSWORD_DEFAULT для написания кода будущего доказательства.

2. Согласно secure.php.net/manual/en/function.password-hash.php "Опция salt устарела с версии PHP 7.0.0. Теперь предпочтительнее просто использовать соль, которая генерируется по умолчанию. "

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

}

Ответ №6:

Я создал функцию, которую я постоянно использую для проверки пароля и для создания паролей, например, для их хранения в базе данных MySQL. Он использует случайно сгенерированную соль, которая намного безопаснее, чем использование статической соли.


Комментарии:

1. Лучше всего опустить salt параметр, он будет автоматически сгенерирован функцией password_hash() , следуя рекомендациям. Вместо PASSWORD_BCRYPT этого можно использовать PASSWORD_DEFAULT для написания кода будущего доказательства.

2. Согласно secure.php.net/manual/en/function.password-hash.php «Опция salt устарела с версии PHP 7.0.0. Теперь предпочтительнее просто использовать соль, которая генерируется по умолчанию. »

) !== 0 ) {
printf(«%s::legacy_hashn», __METHOD__);
$res = $userInfo[‘password’] === md5($password . $userInfo[‘salt’]);
} else {
printf(«%s::password_verifyn», __METHOD__);
$res = password_verify($password, $userInfo[‘password’]);
}

// once we’ve passed validation we can check if the hash needs updating.
if( $res amp;amp; password_needs_rehash($userInfo[‘password’], PASSWORD_DEFAULT) ) {
printf(«%s::rehashn», __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;
}Вывод:


В качестве последнего замечания, учитывая, что вы можете повторно хэшировать пароль пользователя только при входе в систему, вам следует рассмотреть возможность «закатывания» небезопасных устаревших хэшей для защиты ваших пользователей. Под этим я подразумеваю, что после определенного льготного периода вы удаляете все небезопасные [например: голые MD5 / SHA / в противном случае слабые] хэши и заставляете своих пользователей полагаться на механизмы сброса пароля вашего приложения.

Комментарии:

1. Ага. Когда я менял нашу защиту паролей на use password_hash , я намеренно использовал низкое cost значение, чтобы позже увеличить его и проверить, что password_needs_rehash() все работает так, как задумано. (Версия с низким cost значением так и не была запущена в производство.)

Ответ №5:

Класс Пароль полный код:


Ответ №6:

Я создал функцию, которую я постоянно использую для проверки пароля и для создания паролей, например, для их хранения в базе данных MySQL. Он использует случайно сгенерированную соль, которая намного безопаснее, чем использование статической соли.


Комментарии:

1. Лучше всего опустить salt параметр, он будет автоматически сгенерирован функцией password_hash() , следуя рекомендациям. Вместо PASSWORD_BCRYPT этого можно использовать PASSWORD_DEFAULT для написания кода будущего доказательства.

2. Согласно secure.php.net/manual/en/function.password-hash.php «Опция salt устарела с версии PHP 7.0.0. Теперь предпочтительнее просто использовать соль, которая генерируется по умолчанию. »