Como usar o password_hash do PHP para hash e verificar senhas

98

Recentemente, tenho tentado implementar minha própria segurança em um script de log-in que encontrei na Internet. Depois de tentar aprender como fazer meu próprio script para gerar um salt para cada usuário, me deparei compassword_hash .

Pelo que entendi (com base na leitura desta página ), o sal já é gerado na linha quando você usa password_hash. Isso é verdade?

Outra pergunta que eu tinha era: não seria inteligente ter 2 sais? Um direto no arquivo e outro no BD? Dessa forma, se alguém comprometer o seu salt no DB, você ainda tem aquele diretamente no arquivo? Eu li aqui que armazenar sais nunca é uma ideia inteligente, mas sempre me confundiu o que as pessoas querem dizer com isso.

Josh Potter
fonte
8
Não. Deixe a função cuidar do sal. A salga dupla causará problemas e não há necessidade disso.
Funk Forty Niner

Respostas:

184

Usar password_hashé a maneira recomendada de armazenar senhas. Não os separe em banco de dados e arquivos.

Digamos que temos a seguinte entrada:

$password = $_POST['password'];

Primeiro você hash a senha fazendo o seguinte:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Então veja a saída:

var_dump($hashed_password);

Como você pode ver, está hash. (Presumo que você tenha feito essas etapas).

Agora você armazena essa senha em hash em seu banco de dados, garantindo que a coluna de senha seja grande o suficiente para conter o valor em hash (pelo menos 60 caracteres ou mais) . Quando um usuário pede para fazer login, você verifica a entrada de senha com este valor de hash no banco de dados, fazendo o seguinte:

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

Referência Oficial

Akar
fonte
2
Ok, eu apenas tentei isso e funcionou. Eu duvidei da função porque parecia quase fácil demais. Quanto tempo você recomenda que eu faça o comprimento do meu varchar? 225?
Josh Potter de
4
Isso já está nos manuais php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php que o OP provavelmente não leu ou entendeu. Esta pergunta foi feita com mais frequência do que nenhuma.
Funk Forty Niner de
Essa é uma página diferente.
Josh Potter de
@JoshPotter diferente de quê? Além disso, percebi que eles não responderam à sua segunda pergunta. provavelmente estão esperando que você descubra sozinho, ou não sabem.
Funk Forty Niner de
8
@FunkFortyNiner, b / c Josh fez a pergunta, eu encontrei, 2 anos depois, e ela me ajudou. Esse é o ponto do SO. Esse manual é tão claro quanto lama.
toddmo
25

Sim, você entendeu corretamente, a função password_hash () gerará um salt por conta própria e o incluirá no valor hash resultante. Armazenar o salt no banco de dados é absolutamente correto, ele faz seu trabalho mesmo se conhecido.

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

O segundo sal que você mencionou (aquele armazenado em um arquivo), é na verdade uma pimenta ou uma chave do servidor. Se você adicionar antes de hashing (como o sal), você adiciona uma pimenta. Porém, há uma maneira melhor, você pode primeiro calcular o hash e, em seguida, criptografar (bidirecional) o hash com uma chave do lado do servidor. Isso lhe dá a possibilidade de alterar a chave quando necessário.

Em contraste com o sal, essa chave deve ser mantida em segredo. As pessoas costumam misturar tudo e tentar esconder o sal, mas é melhor deixar o sal fazer o seu trabalho e adicionar o segredo com uma chave.

martinstoeckli
fonte
8

Sim, é verdade. Por que você duvida do php faq sobre a função? :)

O resultado da corrida password_hash()tem quatro partes:

  1. o algoritmo usado
  2. parametros
  3. sal
  4. hash de senha real

Como você pode ver, o hash faz parte disso.

Claro, você poderia ter um sal adicional para uma camada adicional de segurança, mas honestamente acho que isso é um exagero em um aplicativo php normal. O algoritmo bcrypt padrão é bom, e o blowfish opcional é indiscutivelmente ainda melhor.

Joel Hinz
fonte
2
BCrypt é uma função de hashing , enquanto Blowfish é um algoritmo para criptografia . No entanto, o BCrypt se origina do algoritmo Blowfish.
martinstoeckli
7

Nunca use md5 () para proteger sua senha, mesmo com sal, é sempre perigoso !!

Proteja sua senha com os algoritmos de hash mais recentes, conforme abaixo.

<?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 correspondência com a senha criptografada do banco de dados e a senha inserida pelo usuário, use a função abaixo.

<?php 

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

Se você quiser usar seu próprio salt, use sua função gerada customizada para o mesmo, apenas siga abaixo, mas eu não recomendo isso porque é considerado obsoleto nas versões mais recentes do PHP.

Leia sobre password_hash () antes de usar o código abaixo.

<?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
fonte
4
A opção salt está obsoleta por boas razões, porque a função faz o melhor para gerar um sal criptograficamente seguro e é quase impossível fazer isso melhor.
martinstoeckli
@martinstoeckli, sim, você está certo, acabei de atualizar minha resposta, obrigado!
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'])); Este é o código usado em login.php .. eu quero fazer sem usar escape e md5. eu quero usar o hash de senha ..
rashmi sm
PASSWORD_DEFAULT - Use o algoritmo bcrypt (Requer PHP 5.5.0). Observe que essa constante foi projetada para mudar com o tempo, conforme algoritmos novos e mais fortes são adicionados ao PHP. Por esse motivo, a duração do resultado do uso desse identificador pode mudar com o tempo.
Adrian P.
5

Há uma falta distinta de discussão sobre compatibilidade com versões anteriores e posteriores que está embutida nas funções de senha do PHP. Notavelmente:

  1. Compatibilidade com versões anteriores: As funções de senha são essencialmente um invólucro bem escrito crypt()e são inerentemente compatíveis com versões anteriores com crypt()hashes -format, mesmo que usem algoritmos de hash obsoletos e / ou inseguros.
  2. Compatibilidade de encaminhamentos: inserir password_needs_rehash()um pouco de lógica em seu fluxo de trabalho de autenticação pode manter seus hashes atualizados com algoritmos atuais e futuros com potencialmente zero alterações futuras no fluxo de trabalho. Nota: Qualquer string que não corresponda ao algoritmo especificado será sinalizada por precisar de um novo hash, incluindo hashes não compatíveis com criptografia.

Por exemplo:

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

Resultado:

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 uma observação final, dado que você só pode refazer o hash de uma senha de usuário no login, você deve considerar "encerrar" hashes legados inseguros para proteger seus usuários. Com isso, quero dizer que, após um certo período de carência, você remove todos os hashes inseguros [por exemplo: MD5 / SHA / de outra forma fracos] e faz com que seus usuários confiem nos mecanismos de redefinição de senha do seu aplicativo.

Sammitch
fonte
0

Código completo da senha da 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
fonte
0

Eu construí uma função que uso o tempo todo para validação de senha e para criar senhas, por exemplo, para armazená-las em um banco de dados MySQL. Ele usa um salt gerado aleatoriamente que é muito mais seguro do que usar um salt estático.

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
fonte
6
É melhor omitir o saltparâmetro, ele será gerado automaticamente pela função password_hash () , seguindo as melhores práticas. Em vez de PASSWORD_BCRYPTum, pode usar PASSWORD_DEFAULTpara escrever código à prova de futuro.
martinstoeckli
Obrigado por esse conselho. Devo ter supervisionado na documentação. Já se passaram algumas noites longas.
Gerrit Fries
1
De acordo com secure.php.net/manual/en/function.password-hash.php "A opção salt tornou-se obsoleta no PHP 7.0.0. Agora é preferível simplesmente usar o salt que é gerado por padrão."
jmng