Bem, você pode procurar na Wikipedia ... Mas como você quer uma explicação, farei o meu melhor aqui:
Funções de hash
Eles fornecem um mapeamento entre uma entrada arbitrária de comprimento e uma saída (geralmente) de comprimento fixo (ou comprimento menor). Pode ser qualquer coisa, desde um simples crc32 até uma função hash criptográfica completa, como MD5 ou SHA1 / 2/256/512. O ponto é que há um mapeamento unidirecional em andamento. É sempre um mapeamento de muitos: 1 (o que significa que sempre haverá colisões), pois cada função produz uma saída menor do que é capaz de inserir (se você alimentar todos os arquivos de 1 MB no MD5, você terá várias colisões).
A razão pela qual eles são difíceis (ou impossíveis de serem praticados) de reverter é por causa de como eles trabalham internamente. A maioria das funções de hash criptográfico itera sobre a entrada definida muitas vezes para produzir a saída. Portanto, se observarmos cada pedaço de entrada de comprimento fixo (que depende do algoritmo), a função hash chamará esse estado atual. Ele irá percorrer o estado e alterá-lo para um novo e usá-lo como feedback em si mesmo (o MD5 faz isso 64 vezes para cada bloco de dados de 512 bits). De alguma forma, ele combina os estados resultantes de todas essas iterações novamente para formar o hash resultante.
Agora, se você quiser decodificar o hash, primeiro precisará descobrir como dividir o hash especificado em seus estados iterados (1 possibilidade de entradas menores que o tamanho de um pedaço de dados, muitas para entradas maiores). Então você precisaria reverter a iteração para cada estado. Agora, para explicar por que isso é muito difícil, imagine tentar deduzir a
e b
a partir da seguinte fórmula: 10 = a + b
. Existem 10 combinações positivas a
e b
que podem funcionar. Agora, repita isso várias vezes:tmp = a + b; a = b; b = tmp
. Para 64 iterações, você teria mais de 10 ^ 64 possibilidades para tentar. E isso é apenas uma simples adição em que algum estado é preservado de iteração para iteração. Funções hash reais realizam muito mais que 1 operação (o MD5 realiza cerca de 15 operações em 4 variáveis de estado). E como a próxima iteração depende do estado do anterior e o anterior é destruído na criação do estado atual, é praticamente impossível determinar o estado de entrada que levou a um determinado estado de saída (para cada iteração, não menos). Combine isso, com o grande número de possibilidades envolvidas, e decodificar até um MD5 levará uma quantidade quase infinita (mas não infinita) de recursos. Tantos recursos que
Funções de criptografia
Eles fornecem um mapeamento 1: 1 entre uma entrada e saída arbitrárias de comprimento. E eles são sempre reversíveis. O importante a ser observado é que é reversível usando algum método. E é sempre 1: 1 para uma determinada chave. Agora, existem várias entradas: pares de chaves que podem gerar a mesma saída (na verdade, geralmente existem, dependendo da função de criptografia). Bons dados criptografados são indistinguíveis de ruídos aleatórios. Isso é diferente de uma boa saída de hash, que sempre tem um formato consistente.
Casos de Uso
Use uma função de hash quando desejar comparar um valor, mas não puder armazenar a representação simples (por várias razões). As senhas devem se encaixar muito bem neste caso de uso, pois você não deseja armazená-las em texto sem formatação por motivos de segurança (e não deve). Mas e se você quiser verificar um sistema de arquivos em busca de arquivos de música pirateados? Seria impraticável armazenar 3 mb por arquivo de música. Portanto, pegue o hash do arquivo e armazene-o (o md5 armazenaria 16 bytes em vez de 3mb). Dessa forma, você apenas faz o hash de cada arquivo e o compara com o banco de dados armazenado de hashes (isso não funciona tão bem na prática devido à recodificação, alteração de cabeçalhos de arquivos, etc., mas é um exemplo de caso de uso).
Use uma função de hash ao verificar a validade dos dados de entrada. É para isso que eles foram projetados. Se você possui duas partes de entrada e deseja verificar se são iguais, execute as duas por meio de uma função hash. A probabilidade de uma colisão é astronomicamente baixa para tamanhos de entrada pequenos (assumindo uma boa função de hash). É por isso que é recomendado para senhas. Para senhas de até 32 caracteres, o md5 possui 4 vezes o espaço de saída. O SHA1 possui 6 vezes o espaço de saída (aproximadamente). O SHA512 possui cerca de 16 vezes o espaço de saída. Você realmente não ligo para o que a senha foi , você se importa se é o mesmo que aquele que foi armazenado. É por isso que você deve usar hashes para senhas.
Use criptografia sempre que precisar recuperar os dados de entrada. Observe a palavra necessidade . Se você estiver armazenando números de cartão de crédito, precisará recuperá-los em algum momento, mas não deseja armazená-los em texto simples. Portanto, armazene a versão criptografada e mantenha a chave o mais segura possível.
As funções de hash também são ótimas para assinar dados. Por exemplo, se você estiver usando o HMAC, assine um pedaço de dados usando um hash dos dados concatenado com um valor conhecido mas não transmitido (um valor secreto). Então, você envia o texto sem formatação e o hash HMAC. Em seguida, o receptor simplesmente faz o hash dos dados enviados com o valor conhecido e verifica se ele corresponde ao HMAC transmitido. Se for o mesmo, você sabe que não foi adulterado por uma parte sem o valor secreto. Isso é comumente usado em sistemas de cookies seguros por estruturas HTTP, bem como na transmissão de mensagens de dados por HTTP, onde você deseja alguma garantia de integridade nos dados.
Uma observação sobre hashes para senhas:
Um recurso importante das funções de hash criptográfico é que elas devem ser muito rápidas de criar e muito difíceis / lentas para reverter (tanto que é praticamente impossível). Isso representa um problema com senhas. Se você armazena sha512(password)
, não está fazendo nada para se proteger contra tabelas arco-íris ou ataques de força bruta. Lembre-se, a função hash foi projetada para velocidade. Portanto, é trivial para um invasor apenas executar um dicionário através da função hash e testar cada resultado.
A adição de um sal ajuda as coisas, pois adiciona um pouco de dados desconhecidos ao hash. Então, em vez de encontrar algo que corresponda md5(foo)
, eles precisam encontrar algo que, quando adicionado ao sal conhecido, produz md5(foo.salt)
(o que é muito mais difícil de fazer). Mas ainda não resolve o problema da velocidade, pois se eles conhecem o sal, é apenas uma questão de analisar o dicionário.
Então, existem maneiras de lidar com isso. Um método popular é chamado fortalecimento de teclas (ou alongamento de teclas). Basicamente, você itera sobre um hash muitas vezes (geralmente milhares). Isso faz duas coisas. Primeiro, diminui significativamente o tempo de execução do algoritmo de hash. Segundo, se implementado corretamente (passando a entrada e o sal de volta a cada iteração), na verdade, aumenta a entropia (espaço disponível) para a saída, reduzindo as chances de colisões. Uma implementação trivial é:
var hash = password + salt;
for (var i = 0; i < 5000; i++) {
hash = sha512(hash + password + salt);
}
Existem outras implementações mais padrão, como PBKDF2 , BCrypt . Mas essa técnica é usada por muitos sistemas relacionados à segurança (como PGP, WPA, Apache e OpenSSL).
A linha inferior, hash(password)
não é boa o suficiente. hash(password + salt)
é melhor, mas ainda não é bom o suficiente ... Use um mecanismo de hash estendido para produzir hashes de senha ...
Outra nota sobre alongamento trivial
Em nenhuma circunstância alimente a saída de um hash diretamente de volta à função hash :
hash = sha512(password + salt);
for (i = 0; i < 1000; i++) {
hash = sha512(hash); // <-- Do NOT do this!
}
A razão para isso tem a ver com colisões. Lembre-se de que todas as funções de hash têm colisões porque o espaço de saída possível (o número de saídas possíveis) é menor que o espaço de entrada. Para ver o porquê, vejamos o que acontece. Para anteceder isso, vamos supor que haja uma chance de 0,001% de colisão sha1()
(é muito menor na realidade, mas para fins de demonstração).
hash1 = sha1(password + salt);
Agora, hash1
tem uma probabilidade de colisão de 0,001%. Mas quando fazemos o próximo hash2 = sha1(hash1);
, todas as colisões de hash1
automaticamente se tornam colisões dehash2
. Então, agora, temos a taxa de hash1 em 0,001%, e a segunda sha1()
chamada aumenta isso. Então agora, hash2
tem uma probabilidade de colisão de 0,002%. Isso é duas vezes mais chances! Cada iteração adicionará outra 0.001%
chance de colisão ao resultado. Assim, com 1000 iterações, a chance de colisão saltou de 0,001% para 1% trivial. Agora, a degradação é linear e as probabilidades reais são muito menores, mas o efeito é o mesmo (uma estimativa da chance de uma única colisão com md5
é de cerca de 1 / (2 128 ) ou 1 / (3x10 38) Embora isso pareça pequeno, graças ao ataque de aniversário , não é tão pequeno quanto parece).
Em vez disso, ao anexar novamente o salt e a senha a cada vez, você reintroduz os dados na função hash. Portanto, quaisquer colisões de qualquer rodada específica não são mais colisões da próxima rodada. Assim:
hash = sha512(password + salt);
for (i = 0; i < 1000; i++) {
hash = sha512(hash + password + salt);
}
Tem a mesma chance de colisão que a sha512
função nativa . Qual é o que você quer. Use isso em vez disso.