Comment utiliser le hachage de mot de passe
récemment, j'ai essayé d'implémenter ma propre sécurité sur un script de connexion que j'ai trouvé sur internet. Après avoir lutté d'essayer d'apprendre à faire mon propre script pour générer un sel pour chaque utilisateur, je suis tombé sur password_hash.
D'après ce que je comprends (d'après la lecture de cette page:http://php.net/manual/en/faq.passwords.php), le sel est déjà généré dans la rangée lorsque vous utilisez password_hash. Est-ce vrai?
une Autre la question que j'avais était, ne serait-il pas intelligent d'avoir deux sels? Un directement dans le fichier et un dans le DB? De cette façon, si quelqu'un compromet votre sel dans le DB, vous avez toujours celui directement dans le fichier? J'ai lu ici que stocker des sels n'est jamais une idée intelligente, mais ça m'a toujours embrouillé sur ce que les gens entendaient par là.
7 réponses
en utilisant password_hash
est la méthode recommandée pour stocker les mots de passe. Ne les séparez pas de la base de données et des fichiers.
disons que nous avons les entrées suivantes:
$password = $_POST['password'];
Je ne valide pas l'entrée juste pour comprendre le concept.
vous Hachez d'abord le mot de passe en faisant ceci:
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
puis voir la sortie:
var_dump($hashed_password);
Comme vous pouvez le voir, c'est haché. (Je suppose que vous avez fait ces étapes).
maintenant vous stockez ceci hashed_password dans votre base de données, et ensuite disons quand un utilisateur demande de les connecter. Vous vérifiez l'entrée du mot de passe avec cette valeur de hachage dans la base de données, en faisant ceci:
// 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.
Oui vous l'avez bien compris, la fonction password_hash () générera un sel par elle-même, et l'inclut dans la valeur de hachage résultante. Le stockage du sel dans la base de données est absolument correct, il fait son travail même s'il est connu.
// 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);
le second sel que vous avez mentionné (celui stocké dans un fichier), est en fait un poivre ou une touche côté serveur. Si vous l'ajoutez avant le hachage (comme le sel), alors vous ajoutez un poivre. Il ya une meilleure façon, vous pourriez d'abord calculer la valeur de hachage, et ensuite crypter (dans les deux sens) le hachage avec une clé côté serveur. Cela vous donne la possibilité de changer la clé si nécessaire.
contrairement au sel, cette clé doit être gardée secrète. Souvent, les gens mélanger et essayer de cacher le sel, mais il est préférable de laisser le sel de son travail, et ajoutez le secret avec une clé.
Oui, c'est vrai. Pourquoi doutez-vous de la faq de php sur la fonction? :)
Le résultat de l'exécution password_hash()
a a quatre parties:
- l'algorithme utilisé
- paramètres
- sel
- hachage du mot de passe actuel
comme vous pouvez le voir, le hachage en fait partie.
bien sûr, vous pourriez avoir un sel supplémentaire pour une couche supplémentaire de sécurité, mais je pense honnêtement que c'est exagéré dans une application php régulière. L'algorithme par défaut de bcrypt est bon, et l'option blowfish one est sans doute encore mieux.
N'utilisez jamais md5 () pour sécuriser votre mot de passe, même avec du sel, c'est toujours dangereux!!
votre mot de passe sécurisé avec les derniers algorithmes de hachage comme ci-dessous.
<?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);
?>
pour établir une correspondance avec le mot de passe chiffré de la base de données et le mot de passe saisi par l'utilisateur, utilisez la fonction ci-dessous.
<?php
if (password_verify($password_inputted_by_user, $password_encrypted)) {
// Success!
echo 'Password Matches';
}else {
// Invalid credentials
echo 'Password Mismatch';
}
?>
si vous voulez utiliser votre propre sel, utilisez votre fonction générée sur mesure pour le même, il suffit de suivre ci-dessous, mais je ne le recommande pas car il est trouvé déprécié dans la dernière les versions de PHP.
lire ce http://php.net/manual/en/function.password-hash.php avant de l'utiliser de code ci-dessous.
<?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);
?>
espérons que tout cela aide!!
il y a un manque évident de discussion sur la compatibilité vers l'arrière et vers l'avant qui est intégrée aux fonctions de mot de passe de PHP. Notamment:
- Rétrocompatibilité: les fonctions password sont essentiellement un wrapper bien écrit autour de
crypt()
, et sont intrinsèquement vers l'arrière-compatible aveccrypt()
-formater les hachures, même si elles utilisent des algorithmes de hachage obsolètes et/ou peu sûrs. - Transfère Compatibilité: Insérer
password_needs_rehash()
et un peu de logique dans votre flux d'authentification peut vous permettre de garder vos hachages à jour avec les algorithmes actuels et futurs avec potentiellement zéro changements futurs dans le flux de travail. Note: toute chaîne qui ne correspond pas à l'algorithme spécifié sera signalée pour avoir besoin d'une reprise, y compris des hachages non compatibles avec la crypt.
par exemple:
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' => '$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
// old salted md5 format
2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
// current bcrypt format
3 => ['password' => 'yeUn9Rnf04DR.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;
}
Sortie:
MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["y$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)
MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["y$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)
MyAuth::authUser::password_verify
bool(true)
Comme note finale, étant donné que vous ne pouvez re-hachage d'un mot de passe de l'utilisateur lors de la connexion, vous devriez envisager de "temporiser" les anciens hachages non sécurisés pour protéger vos utilisateurs. Par ceci je veux dire qu'après une certaine période de Grâce vous supprimez tous les [par exemple: nu MD5/SHA/autrement faible] hashes et demandez à vos utilisateurs de compter sur les mécanismes de réinitialisation de mot de passe de votre application.
mot de passe de la classe code complet:
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("y$%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) == 'y$' && strlen($hash) == 60) {
$return['algo'] = PASSWORD_BCRYPT;
$return['algoName'] = 'bcrypt';
list($cost) = sscanf($hash, "y$%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;
}
}
j'ai construit une fonction que j'utilise tout le temps pour la validation des mots de passe et pour créer des mots de passe, par exemple pour les stocker dans une base de données MySQL. Il utilise un sel produit au hasard qui est beaucoup plus sûr que l'utilisation d'un sel statique.
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;
}