prática recomendada para gerar token aleatório para senha esquecida

94

Quero gerar um identificador para esqueci a senha. Eu li que posso fazer isso usando timestamp com mt_rand (), mas algumas pessoas estão dizendo que o timestamp pode não ser exclusivo todas as vezes. Estou um pouco confuso aqui. Posso fazer isso usando carimbo de hora com isso?

Pergunta
Qual é a prática recomendada para gerar tokens aleatórios / exclusivos de comprimento personalizado?

Eu sei que há muitas perguntas feitas por aqui, mas estou ficando mais confuso depois de ler opiniões diferentes de pessoas diferentes.

perspicaz
fonte
@AlmaDoMundo: Um computador não pode dividir o tempo sem limites.
julho de
@juergend - desculpe, não entendo.
Alma Do de
Você obterá o mesmo carimbo de data / hora se chamá-lo, por exemplo, de nano segundo de intervalo. Algumas funções de tempo, por exemplo, só podem retornar o tempo em etapas de 100 ns, outras apenas em etapas de segundos.
julho de
@juergend ah, isso. Sim. Mencionei o carimbo de data / hora 'clássico' apenas com segundos. Mas se agir como você disse - sim (isso só nos deixa uma opção com a máquina do tempo para obter um carimbo de data / hora não exclusivo)
Alma Do de
1
Atenção, a resposta aceita não aproveita um CSPRNG .
Scott Arciszewski

Respostas:

147

Em PHP, use random_bytes(). Motivo: você está procurando uma maneira de obter um token de lembrete de senha e, se for uma credencial de login única, então você realmente tem dados para proteger (que é - conta de usuário inteira)

Portanto, o código será o seguinte:

//$length = 78 etc
$token = bin2hex(random_bytes($length));

Atualização : referia-se a versões anteriores desta resposta uniqid()e isso é incorreto se for uma questão de segurança e não apenas de exclusividade. uniqid()é essencialmente apenas microtime()com alguma codificação. Existem maneiras simples de obter previsões precisas do microtime()em seu servidor. Um invasor pode emitir uma solicitação de redefinição de senha e, em seguida, tentar alguns tokens prováveis. Isso também é possível se more_entropy for usado, pois a entropia adicional é igualmente fraca. Obrigado a @NikiC e @ScottArciszewski por apontar isso.

Para mais detalhes veja

Alma Do
fonte
21
Observe que random_bytes()só está disponível a partir do PHP7. Para versões mais antigas, a resposta por @yesitsme parece ser a melhor opção.
Gerald Schneider
3
@GeraldSchneider ou random_compat , que é o polyfill para esses recursos que recebeu a revisão mais pares;)
Scott Arciszewski
Fiz um campo varchar (64) em meu banco de dados sql para armazenar esse token. Defini $ length como 64, mas a string retornada tem 128 caracteres. Como posso obter uma string com tamanho fixo (aqui, 64 então)?
gordie
2
@gordie Defina o comprimento para 32, cada byte tem 2 caracteres hexadecimais
JohnHoulderUK
O que deve ser $length? O id do usuário? Ou o que?
pilha de
71

Isso responde à solicitação de 'melhor aleatório':

A resposta 1 de Adi de Security.StackExchange tem uma solução para isso:

Certifique-se de ter suporte OpenSSL, e você nunca vai dar errado com este one-liner

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, seg, 12 de novembro de 2018, Celeritas, "Generating an unguessable token for confirm e-mails", set 20 '13 at 7:06, https://security.stackexchange.com/a/40314/

Sim, sou eu
fonte
24
openssl_random_pseudo_bytes($length)- suporte: PHP 5> = 5.3.0, ....................................... ................... (Para PHP 7 e superior, use random_bytes($length)) ...................... .................... (Para PHP abaixo de 5.3 - não use PHP abaixo de 5.3)
jave.web
54

A versão anterior da resposta aceita ( md5(uniqid(mt_rand(), true))) é insegura e oferece apenas cerca de 2 ^ 60 resultados possíveis - bem dentro do intervalo de uma busca de força bruta em cerca de uma semana para um atacante de baixo orçamento:

Como uma chave DES de 56 bits pode sofrer força bruta em cerca de 24 horas , e um caso médio teria cerca de 59 bits de entropia, podemos calcular 2 ^ 59/2 ^ 56 = cerca de 8 dias. Dependendo de como essa verificação de token é implementada, pode ser possível vazar praticamente informações de tempo e inferir os primeiros N bytes de um token de redefinição válido .

Já que a pergunta é sobre "melhores práticas" e começa com ...

Quero gerar um identificador para esqueci a senha

... podemos inferir que este token tem requisitos de segurança implícitos. E quando você adiciona requisitos de segurança a um gerador de números aleatórios, a prática recomendada é sempre usar um gerador de números pseudo-aleatórios criptograficamente seguro (abreviado CSPRNG).


Usando um CSPRNG

No PHP 7, você pode usar bin2hex(random_bytes($n))(onde $né um número inteiro maior que 15).

No PHP 5, você pode usar random_compatpara expor a mesma API.

Alternativamente, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))se você ext/mcryptinstalou. Outra boa linha é bin2hex(openssl_random_pseudo_bytes($n)).

Separando a pesquisa do validador

Puxando do meu trabalho anterior sobre cookies "lembrar-me" seguros em PHP , a única maneira eficaz de mitigar o vazamento de tempo mencionado anteriormente (normalmente introduzido pela consulta de banco de dados) é separar a pesquisa da validação.

Se sua tabela for assim (MySQL) ...

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id)
);

... você precisa adicionar mais uma coluna, selectorassim:

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    selector CHAR(16),
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id),
    KEY(selector)
);

Use um CSPRNG Quando um token de redefinição de senha é emitido, envie ambos os valores para o usuário, armazene o seletor e um hash SHA-256 do token aleatório no banco de dados. Use o seletor para obter o hash e a ID do usuário, calcule o hash SHA-256 do token que o usuário fornece com aquele armazenado no banco de dados usando hash_equals().

Código de exemplo

Gerando um token de redefinição no PHP 7 (ou 5.6 com random_compat) com PDO:

$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);

$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
    'selector' => $selector,
    'validator' => bin2hex($token)
]);

$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour

$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
    'userid' => $userId, // define this elsewhere!
    'selector' => $selector,
    'token' => hash('sha256', $token),
    'expires' => $expires->format('Y-m-d\TH:i:s')
]);

Verificando o token de redefinição fornecido pelo usuário:

$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
    $calc = hash('sha256', hex2bin($validator));
    if (hash_equals($calc, $results[0]['token'])) {
        // The reset token is valid. Authenticate the user.
    }
    // Remove the token from the DB regardless of success or failure.
}

Esses fragmentos de código não são soluções completas (evitei a validação de entrada e as integrações de estrutura), mas devem servir como um exemplo do que fazer.

Scott Arciszewski
fonte
Ao verificar o token de redefinição fornecido pelo usuário, por que você usa a representação binária do token aleatório? Você acha que será possível (e seguro?): 1) armazenar no banco de dados o valor hexadecimal com hash do token com hash('sha256', bin2hex($token)), 2) verificar com if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Obrigado!
Guicara
Sim, comparar strings hexadecimais também é seguro. É realmente uma questão de preferência. Eu prefiro fazer todas as operações criptográficas no binário bruto e apenas converter para hex / base64 para transmissão ou armazenamento.
Scott Arciszewski
Olá, Scott, é basicamente uma pergunta não apenas para sua resposta, mas para todo o artigo sobre o recurso "Lembre-se de mim". Por que não usar o único idcomo seletor? Quer dizer, a chave primária da account_recoverytabela. Não precisamos de camada adicional de segurança para o seletor, precisamos? Obrigado!
Andre Polykanine
id:secretestá bem. selector:secretestá bem. secretem si não é. O objetivo é separar a consulta do banco de dados (que tem perda de tempo) do protocolo de autenticação (que deve ser de tempo constante).
Scott Arciszewski
Existe algum mal em usar openssl_random_pseudo_bytesem vez random_bytesse está rodando o PHP 5.6? Além disso, você não deveria anexar apenas o seletor e não o validador na string de consulta do link?
greg
7

Você também pode usar DEV_RANDOM, onde 128 = 1/2 do comprimento do token gerado. O código abaixo gera 256 token.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Graham T
fonte
4
Eu sugeriria MCRYPT_DEV_URANDOMsobre MCRYPT_DEV_RANDOM.
Scott Arciszewski
2

Isso pode ser útil sempre que você precisar de um token muito aleatório

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ir Calif
fonte