Cómo usar password_hash de PHP para hash y verificar contraseñas

94

Recientemente, he intentado implementar mi propia seguridad en un script de inicio de sesión con el que me topé en Internet. Después de luchar por tratar de aprender a hacer mi propio script para generar una sal para cada usuario, me encontré con password_hash.

Por lo que entiendo (basado en la lectura de esta página ), la sal ya se genera en la fila cuando usa password_hash. ¿Es esto cierto?

Otra pregunta que tuve fue, ¿no sería inteligente tener 2 sales? ¿Uno directamente en el archivo y otro en la base de datos? De esa manera, si alguien compromete su sal en la base de datos, ¿todavía tiene la que está directamente en el archivo? Leí aquí que almacenar sales nunca es una buena idea, pero siempre me confundió lo que la gente quería decir con eso.

Josh Potter
fuente
8
No. Deje que la función se encargue de la sal. La doble salazón le causará problemas y no es necesario.
Funk Forty Niner

Respuestas:

182

El uso password_hashes la forma recomendada de almacenar contraseñas. No los separe en DB y archivos.

Digamos que tenemos la siguiente entrada:

$password = $_POST['password'];

Primero hash la contraseña haciendo esto:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Entonces mira el resultado:

var_dump($hashed_password);

Como puede ver, está hash. (Supongo que hiciste esos pasos).

Ahora almacena esta contraseña hash en su base de datos, asegurándose de que la columna de su contraseña sea lo suficientemente grande para contener el valor hash (al menos 60 caracteres o más) . Cuando un usuario pide iniciar sesión, verifica la entrada de la contraseña con este valor hash en la base de datos, haciendo esto:

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

Referencia oficial

Akar
fuente
2
Ok, acabo de probar esto y funcionó. Dudé de la función porque parecía casi demasiado fácil. ¿Cuánto tiempo me recomiendan que haga la longitud de mi varchar? 225?
Josh Potter
4
Esto ya está en los manuales php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php que el OP probablemente no leyó ni entendió. Esta pregunta se ha hecho con más frecuencia que ninguna.
Funk Forty Niner
Esa es una página diferente.
Josh Potter
@JoshPotter diferente de qué? además, notó que no han respondido a su segunda pregunta. probablemente están esperando que lo descubras por ti mismo, o no lo saben.
Funk Forty Niner
8
@FunkFortyNiner, b / c Josh hizo la pregunta, la encontré, 2 años después, y me ayudó. Ese es el punto de SO. Ese manual es tan claro como el barro.
toddmo
23

Sí, lo entendió correctamente, la función password_hash () generará una sal por sí misma y la incluirá en el valor hash resultante. El almacenamiento de la sal en la base de datos es absolutamente correcto, hace su trabajo incluso si se conoce.

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

La segunda sal que mencionaste (la que está almacenada en un archivo), es en realidad una pimienta o una clave del lado del servidor. Si lo agrega antes del hash (como la sal), agrega una pimienta. Sin embargo, hay una mejor manera, primero puede calcular el hash y luego cifrar (bidireccionalmente) el hash con una clave del lado del servidor. Esto le da la posibilidad de cambiar la clave cuando sea necesario.

A diferencia de la sal, esta clave debe mantenerse en secreto. La gente a menudo la mezcla y trata de ocultar la sal, pero es mejor dejar que la sal haga su trabajo y agregar el secreto con una llave.

martinstoeckli
fuente
8

Sí, es verdad. ¿Por qué dudas de las preguntas frecuentes de php sobre la función? :)

El resultado de correr password_hash()tiene cuatro partes:

  1. el algoritmo utilizado
  2. parámetros
  3. sal
  4. hash de contraseña real

Entonces, como puede ver, el hash es parte de eso.

Claro, podría tener una sal adicional para una capa adicional de seguridad, pero honestamente creo que eso es excesivo en una aplicación PHP normal. El algoritmo bcrypt predeterminado es bueno, y el de blowfish opcional es posiblemente incluso mejor.

Joel Hinz
fuente
2
BCrypt es una función de hash , mientras que Blowfish es un algoritmo de cifrado . Sin embargo, BCrypt se origina en el algoritmo Blowfish.
martinstoeckli
7

Nunca use md5 () para asegurar su contraseña, incluso con sal, ¡¡siempre es peligroso !!

Asegure su contraseña con los últimos algoritmos de hash como se muestra a continuación.

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

Para hacer coincidir la contraseña encriptada de la base de datos y la contraseña ingresada por el usuario, use la siguiente función.

<?php 

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

Si desea usar su propia sal, use su función generada personalizada para el mismo, solo siga a continuación, pero no lo recomiendo ya que se encuentra obsoleto en las últimas versiones de PHP.

Lea sobre password_hash () antes de usar el siguiente código.

<?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
fuente
4
La opción de sal está en desuso por buenas razones, porque la función hace todo lo posible para generar una sal criptográficamente segura, y es casi imposible hacerlo mejor.
martinstoeckli
@martinstoeckli, sí, tienes razón, acabo de actualizar mi respuesta, ¡Gracias!
Mahesh Yadav
if (isset ($ _ POST ['btn-signup'])) {$ uname = mysql_real_escape_string ($ _ POST ['uname']); $ correo electrónico = mysql_real_escape_string ($ _ POST ['correo electrónico']); $ upass = md5 (mysql_real_escape_string ($ _ POST ['pasar'])); Este es el código que se usa en login.php ... quiero hacerlo sin usar escape y md5. quiero usar el hash de la contraseña ..
rashmi sm
PASSWORD_DEFAULT: use el algoritmo bcrypt (requiere PHP 5.5.0). Tenga en cuenta que esta constante está diseñada para cambiar con el tiempo a medida que se agregan algoritmos nuevos y más fuertes a PHP. Por esa razón, la duración del resultado del uso de este identificador puede cambiar con el tiempo.
Adrian P.
5

Hay una clara falta de discusión sobre la compatibilidad hacia atrás y hacia adelante que está integrada en las funciones de contraseña de PHP. Notablemente:

  1. Compatibilidad con versiones anteriores: las funciones de contraseña son esencialmente un envoltorio bien escrito crypt()y son inherentemente compatibles con versiones anteriores de crypt()hash de formato, incluso si usan algoritmos de hash obsoletos y / o inseguros.
  2. Compatibilidad con reenvíos: la inserción password_needs_rehash()y un poco de lógica en su flujo de trabajo de autenticación puede mantener sus hashes actualizados con algoritmos actuales y futuros con potencialmente cero cambios futuros en el flujo de trabajo. Nota: Cualquier cadena que no coincida con el algoritmo especificado se marcará para necesitar un refrito, incluidos los hash no compatibles con criptas.

P.ej:

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

Salida:

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)

Como nota final, dado que solo puede volver a aplicar el hash de la contraseña de un usuario al iniciar sesión, debe considerar "eliminar" los hash heredados inseguros para proteger a sus usuarios. Con esto quiero decir que después de un cierto período de gracia eliminas todos los hashes inseguros [por ejemplo: MD5 / SHA / de otra manera débil] y los usuarios confían en los mecanismos de restablecimiento de contraseña de tu aplicación.

Sammitch
fuente
0

Código completo de contraseña de clase:

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
fuente
0

I’ve built a function I use all the time for password validation and to create passwords, e.g. to store them in a MySQL database. It uses a randomly generated salt which is way more secure than using a static salt.

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;

}
Gerrit Fries
fuente
6
It is best to omit the salt parameter, it will be automatically generated by the password_hash() function, following best practices. Instead of PASSWORD_BCRYPT one can use PASSWORD_DEFAULTto write future proof code.
martinstoeckli
Thanks for that advise. I must have overseen it in the documentation. It's been some long nights.
Gerrit Fries
1
According to secure.php.net/manual/en/function.password-hash.php "The salt option has been deprecated as of PHP 7.0.0. It is now preferred to simply use the salt that is generated by default."
jmng