Comment utiliser le mot de passe de PHP pour hacher et vérifier les mots de passe

94

Récemment, j'ai essayé de mettre en œuvre ma propre sécurité sur un script de connexion sur lequel je suis tombé par hasard sur Internet. Après avoir lutté pour essayer d'apprendre à créer 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 (basé sur la lecture de cette page ), le sel est déjà généré dans la ligne lorsque vous utilisez password_hash. Est-ce vrai?

Une autre question que j'avais était, ne serait-il pas intelligent d'avoir 2 sels? Un directement dans le fichier et un dans la base de données? De cette façon, si quelqu'un compromet votre sel dans la base de données, vous avez toujours celui directement dans le fichier? J'ai lu ici que stocker des sels n'est jamais une idée intelligente, mais cela m'a toujours confus ce que les gens entendaient par là.

Josh Potter
la source
8
Non. Laissez la fonction s'occuper du sel. Le double salage vous causera des problèmes et vous n'en aurez pas besoin.
Funk Forty Niner

Réponses:

182

L'utilisation password_hashest la méthode recommandée pour stocker les mots de passe. Ne les séparez pas en DB et fichiers.

Disons que nous avons l'entrée suivante:

$password = $_POST['password'];

Vous devez d'abord hacher le mot de passe en procédant comme suit:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Ensuite, voyez la sortie:

var_dump($hashed_password);

Comme vous pouvez le voir, il est haché. (Je suppose que vous avez fait ces étapes).

Maintenant, vous stockez ce mot de passe haché dans votre base de données, en vous assurant que la colonne de votre mot de passe est suffisamment grande pour contenir la valeur hachée (au moins 60 caractères ou plus) . Lorsqu'un utilisateur demande à se connecter, vous vérifiez l'entrée du mot de passe avec cette valeur de hachage dans la base de données, en procédant comme suit:

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

Référence officielle

Akar
la source
2
Ok, j'ai juste essayé et ça a marché. Je doutais de la fonction car cela me paraissait presque trop facile. Combien de temps recommandez-vous que je fasse la longueur de mon varchar? 225?
Josh Potter
4
Cela figure déjà dans les manuels php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php que l'OP n'a probablement pas lu ou compris. Cette question a été posée plus souvent qu’aucune.
Funk Forty Niner
C'est une page différente.
Josh Potter
@JoshPotter différent de quoi? de plus, vous avez remarqué qu'ils n'ont pas répondu à votre deuxième question. ils s'attendent probablement à ce que vous le découvriez vous-même, ou ils ne le savent pas.
Funk Forty Niner
8
@FunkFortyNiner, b / c Josh a posé la question, je l'ai trouvée, 2 ans plus tard, et cela m'a aidé. C'est le but de SO. Ce manuel est à peu près aussi clair que la boue.
toddmo
23

Oui, vous l'avez bien compris, la fonction password_hash () générera elle-même un salt et l'inclura 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 clé côté serveur. Si vous l'ajoutez avant le hachage (comme le sel), vous ajoutez un poivre. Il existe cependant un meilleur moyen, vous pouvez d'abord calculer le hachage, puis chiffrer (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. Les gens le mélangent souvent et essaient de cacher le sel, mais il vaut mieux laisser le sel faire son travail et ajouter le secret avec une clé.

martinstoeckli
la source
8

Oui c'est vrai. Pourquoi doutez-vous de la faq php sur la fonction? :)

Le résultat de la course password_hash()comporte quatre parties:

  1. l'algorithme utilisé
  2. paramètres
  3. sel
  4. hachage du mot de passe réel

Donc, comme vous pouvez le voir, le hachage en fait partie.

Bien sûr, vous pourriez avoir un sel supplémentaire pour une couche de sécurité supplémentaire, mais je pense honnêtement que c'est exagéré dans une application PHP classique. L'algorithme bcrypt par défaut est bon, et celui optionnel blowfish est sans doute encore meilleur.

Joel Hinz
la source
2
BCrypt est une fonction de hachage , tandis que Blowfish est un algorithme de cryptage . Cependant, BCrypt provient de l'algorithme Blowfish.
martinstoeckli
7

N'utilisez jamais md5 () pour sécuriser votre mot de passe, même avec du sel, c'est toujours dangereux !!

Assurez 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 faire correspondre le mot de passe crypté 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 souhaitez utiliser votre propre sel, utilisez votre fonction générée personnalisée pour la même chose, suivez simplement ci-dessous, mais je ne le recommande pas car il est déconseillé dans les dernières versions de PHP.

Lisez à propos de password_hash () avant d'utiliser le 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);
Mahesh Yadav
la source
4
L'option salt est déconseillée pour de bonnes raisons, car la fonction fait de son mieux pour générer un sel cryptographiquement sûr et il est presque impossible de le faire mieux.
martinstoeckli
@martinstoeckli, oui vous avez raison, je viens de mettre à jour ma réponse, merci!
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'])); C'est le code utilisé dans login.php .. je veux faire sans utiliser escape et md5. Je veux utiliser le hachage de mot de passe ..
rashmi sm
PASSWORD_DEFAULT - Utilisez l'algorithme bcrypt (nécessite PHP 5.5.0). Notez que cette constante est conçue pour changer avec le temps à mesure que de nouveaux algorithmes plus puissants sont ajoutés à PHP. Pour cette raison, la longueur du résultat de l'utilisation de cet identifiant peut changer avec le temps.
Adrian P.
5

Il y a un manque évident de discussion sur la compatibilité ascendante et descendante qui est intégrée aux fonctions de mot de passe de PHP. Notamment:

  1. Compatibilité descendante: les fonctions de mot de passe sont essentiellement un wrapper bien écrit crypt()et sont intrinsèquement rétrocompatibles avec les crypt()hachages -format, même si elles utilisent des algorithmes de hachage obsolètes et / ou non sécurisés.
  2. Compatibilité des transferts: l' insertion password_needs_rehash()et un peu de logique dans votre flux de travail d'authentification peuvent vous permettre de garder vos hachages à jour avec les algorithmes actuels et futurs avec potentiellement aucune modification future du flux de travail. Remarque: toute chaîne qui ne correspond pas à l'algorithme spécifié sera marquée pour avoir besoin d'un rehash, y compris les hachages non compatibles avec le cryptage.

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' => '$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;
}

Production:

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)

En guise de note finale, étant donné que vous ne pouvez hacher à nouveau le mot de passe d'un utilisateur qu'à la connexion, vous devriez envisager de «supprimer» les hachages hérités non sécurisés pour protéger vos utilisateurs. J'entends par là qu'après une certaine période de grâce, vous supprimez tous les hachages non sécurisés [par exemple: MD5 / SHA / autrement faibles] et que vos utilisateurs comptent sur les mécanismes de réinitialisation de mot de passe de votre application.

Sammitch
la source
0

Code complet du mot de passe de classe:

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
la source
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 généré aléatoirement qui est bien 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;

}
Frites Gerrit
la source
6
Il est préférable d'omettre le saltparamètre, il sera automatiquement généré par la fonction password_hash () , en suivant les meilleures pratiques. Au lieu d' PASSWORD_BCRYPTun peut utiliser PASSWORD_DEFAULTpour écrire du code de preuve future.
martinstoeckli
Merci pour ce conseil. J'ai dû le surveiller dans la documentation. Ça fait de longues nuits.
Gerrit Fries
1
Selon secure.php.net/manual/en/function.password-hash.php "L'option salt est obsolète depuis PHP 7.0.0. Il est maintenant préférable d'utiliser simplement le salt généré par défaut."
jmng