O problema aqui é basicamente um problema de entropia. Então, vamos começar a procurar lá:
Entropia por personagem
O número de bits de entropia por byte são:
- Personagens hexadecimais
- Bits: 4
- Valores: 16
- Entropia em 72 caracteres: 288 bits
- Alfa-numérico
- Bits: 6
- Valores: 62
- Entropia em 72 caracteres: 432 bits
- Símbolos "Comuns"
- Bits: 6,5
- Valores: 94
- Entropia em 72 caracteres: 468 bits
- Bytes completos
- Bits: 8
- Valores: 255
- Entropia em 72 caracteres: 576 bits
Então, como agimos depende do tipo de personagem que esperamos.
O primeiro problema
O primeiro problema com seu código é que sua etapa de hash "pepper" está gerando caracteres hexadecimais (já que o quarto parâmetro para hash_hmac()
não está definido).
Portanto, ao inserir a pimenta, você está efetivamente cortando a entropia máxima disponível para a senha por um fator de 2 (de 576 a 288 bits possíveis ).
O segundo problema
No entanto, em primeiro lugar , sha256
fornece apenas 256
bits de entropia. Portanto, você está efetivamente reduzindo possíveis 576 bits para 256 bits. Seu hash step * imediatamente *, por definição perde
pelo menos 50% da entropia possível na senha.
Você poderia resolver isso parcialmente mudando para SHA512
, onde reduziria a entropia disponível em cerca de 12%. Mas essa ainda é uma diferença não insignificante. Esses 12% reduzem o número de permutações por um fator de 1.8e19
. É um grande número ... E esse é o fator que o reduz em ...
O problema subjacente
O problema subjacente é que existem três tipos de senhas com mais de 72 caracteres. O impacto que este sistema de estilo tem sobre eles será muito diferente:
Nota: de agora em diante, estou assumindo que estamos comparando a um sistema de pimenta que usa SHA512
com saída bruta (não hex).
Senhas aleatórias de alta entropia
Estes são seus usuários usando geradores de senha que geram a quantidade de chaves grandes para senhas. Eles são aleatórios (gerados, não escolhidos por humanos) e têm alta entropia por personagem. Esses tipos usam bytes altos (caracteres> 127) e alguns caracteres de controle.
Para este grupo, sua função de hashing reduzirá significativamente a entropia disponível em bcrypt
.
Deixe-me dizer isso de novo. Para usuários que usam senhas longas e de alta entropia, sua solução reduz significativamente a força de suas senhas em uma quantidade mensurável. (62 bits de entropia perdidos para uma senha de 72 caracteres e mais para senhas mais longas)
Senhas aleatórias de entropia média
Este grupo está usando senhas contendo símbolos comuns, mas sem bytes altos ou caracteres de controle. Estas são suas senhas digitáveis.
Para este grupo, você irá desbloquear um pouco mais entropia (não criá-la, mas permitir que mais entropia caiba na senha bcrypt). Quando digo levemente, quero dizer levemente. O ponto de equilíbrio ocorre quando você atinge o máximo de 512 bits do SHA512. Portanto, o pico é de 78 caracteres.
Deixe-me dizer isso de novo. Para esta classe de senhas, você só pode armazenar 6 caracteres adicionais antes de esgotar a entropia.
Senhas não aleatórias de baixa entropia
Este é o grupo que está usando caracteres alfanuméricos que provavelmente não são gerados aleatoriamente. Algo como uma citação da Bíblia ou algo assim. Essas frases têm aproximadamente 2,3 bits de entropia por caractere.
Para este grupo, você pode desbloquear significativamente mais entropia (não criá-la, mas permitir que mais entropia caiba na entrada de senha bcrypt) por hash. O ponto de equilíbrio é de cerca de 223 caracteres antes de esgotar a entropia.
Vamos dizer isso de novo. Para essa classe de senhas, o pré-hashing definitivamente aumenta a segurança de forma significativa.
De volta ao mundo real
Esses tipos de cálculos de entropia não importam muito no mundo real. O que importa é adivinhar a entropia. Isso é o que afeta diretamente o que os invasores podem fazer. Isso é o que você deseja maximizar.
Embora haja pouca pesquisa para adivinhar a entropia, há alguns pontos que gostaria de apontar.
As chances de adivinhar aleatoriamente 72 caracteres corretos em uma linha são extremamente baixas. É mais provável que você ganhe na loteria Powerball 21 vezes do que essa colisão ... Esse é o número de que estamos falando.
Mas podemos não tropeçar nisso estatisticamente. No caso de frases, a chance dos primeiros 72 caracteres serem iguais é muito maior do que para uma senha aleatória. Mas ainda é trivialmente baixo (é mais provável que você ganhe na loteria Powerball 5 vezes, com base em 2,3 bits por personagem).
Praticamente
Praticamente, isso realmente não importa. As chances de alguém adivinhar os primeiros 72 caracteres corretamente, onde os últimos fazem uma diferença significativa, são tão baixas que não vale a pena se preocupar. Por quê?
Bem, digamos que você esteja pegando uma frase. Se a pessoa conseguir acertar os primeiros 72 caracteres, ela tem muita sorte (pouco provável) ou é uma frase comum. Se for uma frase comum, a única variável é quanto tempo deve ser produzida.
Vamos dar um exemplo. Vamos fazer uma citação da Bíblia (só porque é uma fonte comum de textos longos, não por qualquer outro motivo):
Você não deve cobiçar a casa do seu vizinho. Não cobiçarás a mulher do teu vizinho, nem o seu servo ou serva, o seu boi ou jumento, ou qualquer coisa que pertença ao teu vizinho.
São 180 caracteres. O 73º personagem é g
o segundo neighbor's
. Se você adivinhou isso, provavelmente não está parando em nei
, mas continuando com o resto do versículo (já que é assim que a senha provavelmente será usada). Portanto, seu "hash" não acrescentou muito.
BTW: ABSOLUTAMENTE NÃO estou defendendo o uso de uma citação da Bíblia. Na verdade, exatamente o oposto.
Conclusão
Você não vai ajudar muito as pessoas que usam senhas longas fazendo hash primeiro. Alguns grupos você definitivamente pode ajudar. Alguns você definitivamente pode machucar.
Mas no final, nada disso é excessivamente significativo. Os números com os quais estamos lidando são MUITO altos. A diferença na entropia não será muito.
É melhor você deixar bcrypt como está. É mais provável que você estrague o hashing (literalmente, você já fez isso e não é o primeiro ou o último a cometer esse erro) do que o ataque que está tentando evitar vai acontecer.
Concentre-se em proteger o resto do site. E adicione um medidor de entropia de senha à caixa de senha no registro para indicar a força da senha (e indicar se uma senha é longa demais para que o usuário queira alterá-la) ...
Isso é meu $ 0,02 pelo menos (ou possivelmente bem mais que $ 0,02) ...
Quanto a usar uma pimenta "secreta":
Não há literalmente nenhuma pesquisa sobre alimentar uma função hash em bcrypt. Portanto, não está claro, na melhor das hipóteses, se alimentar um hash "apimentado" em bcrypt algum dia causará vulnerabilidades desconhecidas (sabemos que isso hash1(hash2($value))
pode expor vulnerabilidades significativas em relação à resistência à colisão e ataques de pré-imagem).
Considerando que você já está pensando em armazenar uma chave secreta (a "pimenta"), por que não usá-la de uma forma bem estudada e compreendida? Por que não criptografar o hash antes de armazená-lo?
Basicamente, depois de fazer o hash da senha, coloque toda a saída do hash em um algoritmo de criptografia forte. Em seguida, armazene o resultado criptografado.
Agora, um ataque de injeção de SQL não vazará nada de útil, porque eles não têm a chave de criptografia. E se a chave vazar, os invasores não estarão em melhor situação do que se você usasse um hash simples (o que é provável, algo com a pimenta "pré-hash" não fornece).
Nota: se você decidir fazer isso, use uma biblioteca. Para PHP, eu recomendo fortemente o Zend\Crypt
pacote do Zend Framework 2 . Na verdade, é o único que eu recomendaria neste momento. Foi fortemente revisto e toma todas as decisões por você (o que é uma coisa muito boa) ...
Algo como:
use Zend\Crypt\BlockCipher;
public function createHash($password) {
$hash = password_hash($password, PASSWORD_BCRYPT, ["cost"=>$this->cost]);
$blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
$blockCipher->setKey($this->key);
return $blockCipher->encrypt($hash);
}
public function verifyHash($password, $hash) {
$blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
$blockCipher->setKey($this->key);
$hash = $blockCipher->decrypt($hash);
return password_verify($password, $hash);
}
E é benéfico porque você está usando todos os algoritmos de maneiras que são bem compreendidas e bem estudadas (pelo menos relativamente). Lembrar:
Qualquer pessoa, do amador mais desinformado ao melhor criptógrafo, pode criar um algoritmo que ele mesmo não consegue quebrar.