Mais um artigo interessante, e bem escrito, sobre criptografia e senhas que merece ser compartilhado…

Teoria

Hash de senhas é sempre uma defesa secundária. Um servidor que faça autenticação precisa de alguma informação para poder validar uma senha. Um sistema simples armazena as próprias senhas literalmente, e a validação neste caso é mera comparação de strings. Neste caso, se alguém der uma mera espiada no arquivo da base de dados, já verá informação demais. Este tipo de brecha acontece na prática. Um backup em lugar errado, um HD trocado e não apagado corretamente, uma injeção SQL, e assim vai. Veja uma discussão detalhada neste blog.

Mesmo assim, como o conteúdo de um servidor que valide senhas necessariamente inclui os dados para esta validação, alguém que tenha mera cópia destes pode fazer um ataque de dicionário offline, tentando senhas em potencial até que alguma coincida, e este tipo de ataque é inevitável. Em sendo assim o que podemos é tentar deixar este ataque o mais difícil possível, e para isto, temos estas ferramentas:

  • Funções criptográficas de hash: são funções matemáticas que ao mesmo tempo que são eficientes, ninguém sabe como reverter. O servidor pode manter um hash de uma senha; quando for fazer a comparação, basta usar o mesmo hash no segundo valor e ver se coincidem; de qualquer forma, olhando o hash não dá pra saber qual é a senha original.
  • Salts: uma das vantagens do atacante é o paralelismo. O atacante pega um monte de senhas protegidas com hash e quer descobrir o máximo que puder delas. Ele pode simplesmente fazer um hash de uma senha em potencial e comparar esse mesmo hash com centenas de registros diferentes. Pode também usar tabelas de hashes pré-calculadas, incluindo asrainbow tables;A característica de ataques com paralelismo é atuar sobre várias senhas com a mesmíssima função de hash. Usar o salt não se trata de ter uma função de hash, mas uma porção delas. Idealmente cada senha deveria usar sua própria função de hash. Um salt é um modo de selecionar uma solução específica de hash entre uma grande família de funções. Se propriamente usado, pode acabar totalmente com o paralelismo.
  • Lentidão: computadores são cada vez mais rápidos, como teorizou Gordon Moore, co-fundador da Intel. Cérebros humanos não. A cada ano os atacantes podem testar mais e mais senhas no mesmo tempo, enquanto usuários não lembram senhas mais complexas (ou se recusam a lembrar). Para isso, podemos fazer com que hashes fiquem extremamente lentas usando funções que demandem muitas iterações.

Nós temos algumas funções criptográficas bem comuns, como MD5 e a família SHA. Construir uma função de hash usando operações elementares não é uma tarefa fácil. Quando criptógrafosquerem fazer isso, pensam profundamente, e organizam torneios onde as funções “brigam entre si violentamente”. Depois de centenas deles virando, mexendo, cutucando uma função por vários anos e não acham nada ruim pra falar dela, eles começam a admitir que, talvez, aquela função possa ser considerada mais ou menos segura. Foi o que aconteceu na competição SHA-3. Nóstemos que usar este meio de construir estas funções por não conhecermos jeito melhor. Matematicamente não sabemos se funções seguras de hash realmente existem, o que temos são candidatas (esta é a diferença entre “não pode ser quebrada” e “ninguém sabe como quebrar”).

Uma função básica de hash, mesmo segura como função de hash não é apropriada para senhas, pelo seguinte:

  • não usa salt, permitindo ataques paralelos (rainbow tables pra MD5 e SHA-1 podem ser obtidas gratuitamente, você não precisa nem ter o trabalho de calcular);
  • são muito rápidas e ficam mais e mais com o tempo. Com uma GPU razoável de mercado, a taxa de hashing é de bilhões de senhas por segundo.

Então, precisamos de coisa melhor. E mais, juntar adequadamente uma função de hash, salt e ficar iterando, não é mais simples do que projetar uma função de hash — ao menos se você quiser um resultado seguro. Novamente você depende de construções padrão que sobreviveram ao massacre contínuo de criptógrafos vingativos.

Funções de hash boas para senhas

PBKDF2

O PBKDF2 vem da PKCS#5. É parametrizado com contador de iterações que iniciando por 1 e não tem limite superior, um salt arbitrário sem restrição de tamanho, e o tamanho requerido de saída. (O PBKDF2 gera uma saída de tamanho configurável), e um PRF. Na prática o PBKDF2 é sempre usado com HMAC, que é construído por sua vez sobre uma função de hash também. Quando dizemos “PBKDF2 com SHA-1”, na verdade significa “PBKDF2 com HMAC com SHA-1”.

Vantagens:

  • Já foi definido há bastante tempo, e ainda está “ileso”.
  • Já está presente em vários frameworks (como .NET, por exemplo).
  • Altamente configurável (apesar de algumas implementações não permitirem a escolha do hash, como o .NET que usa apenas SHA-1).
  • Aprovado pelo NIST (observar a diferença entre hashing e derivação de chave; leia a seguir).
  • Tamanho de saída configurável (leia a seguir).

Desvantagens:

  • É intensivo somente pra CPU, e bem mais acessível para a GPU (o servidor é um pc normal usualmente, para tarefas usuais, enquanto o atacante pode investir mais em hardware especializado e ter vantagem com isso).
  • Você tem que ajustar os parâmetros por sua conta (gerar e gerenciar o salt, número de iterações etc). Há um encoding padrão para os parâmetros do PBKDF2 mas ele usa ASN.1, então as pessoas evitam sempre que podem (ASN.1 é complicado para não-experts).

bcrypt

O bcrypt foi desenvolvido reusando e expandindo elementos de um block cipher chamadoBlowfish. A contagem de iterações é uma potência de dois, que é bem menos configurável que a da PBKDF2, mas suficiente pro uso normal. Esse é o núcleo do mecanismo de hash de senhas doOpenBSD.

Vantagens:

  • Implementação disponível para várias linguagens (veja os links no rodapé da página da Wikipedia)
  • Mais “resistente” à GPU; isso graças a detalhes do seu design interno. Os autores do bcrypt fizeram desta forma voluntariamente. Reusaram o Blowfish por este ser baseado numa tabela RAM interna que é constantemente modificada durante o processamento. Isso complica muito a vida de quem quiser acelerar o bcrypt com GPUs, pois GPUs não são boas para acessar memória em paralelo.
  • A saída padrão inclui o salt e contagem de iterações, simplificando em muito a armazenagem, que é de apenas uma string.

Desvantagens:

  • Tamanho fixo de saída: 192 bits.
  • Apesar de forçar as GPUs, pode ser otimizado com FPGA: Chips FPGA modernos têm blocos embutidos de RAM que são bem convenientes para rodar bcrypt em paralelo no mesmo chip.Já foi feito, inclusive.
  • A senha de entrada é limitada a 51 caracteres. Para senhas maiores, alguém teria quecombinar o bcrypt com uma função de hash (calcula o hash da senha, e usa o resultado com bcrypt). Combinar primitivos criptográficos tem riscos, então isto não é recomendado para uso geral.

scrypt

O scrypt é bem mais novo (projetado em 2009), baseado no PBKDF2 e num stream cipherchamado Salsa20/8. De qualquer forma, estas são apenas ferramentas em torno do núcleo que concentra a força do scrypt, que é a RAM. O scrypt foi feito de forma a usar muita RAM (ele gera alguns bytes pseudo-aleatórios, e os lê repetidamente, numa sequência também pseudo-aleatória). “Muita RAM” é algo difícil de paralelizar. Um PC básico é bom no acesso à RAM, e normalmente não vai tentar ler simultaneamente da RAM dezenas de bytes sem relação aparente. Um atacante com GPU ou FPGA pode querer fazer isso, mas terá dificuldade.

Vantagens:

  • Um PC, ou seja, o que o defensor vai usar pra aplicar o hash normalmente, é uma das melhores (senão a melhor) plataforma para operar o scrypt. O atacante não tem mais vantagem investindo em GPU ou FPGA.
  • Uma opção a mais de ajuste: uso de memória.

Desvantagens:

  • É novo ainda (minha “regra pessoal” é de aguardar ao menos 5 anos de exposição — mas, é claro, é bom que outras pessoas usem o scrypt em produção, para dar maior visibilidade).
  • Não tem tantas implementações à disposição para as diversas linguagens.
  • Não está claro qual o ideal de CPU e RAM a usar. Pra cada acesso pseudo-aleatório o scrypt precisa computar um hash, um cache miss gasta 200 ciclos, uma chamada de SHA-256 gasta 1000. Há espaço para melhorias nesse aspecto.
  • Uma opção a mais para ajustar: quanta memória usar.

Iterated And Salted S2K do OpenPGP

Estou mencionando este, pois você vai usar se for proteger arquivos com senha usando GnuPG. Essa ferramente segue o formato OpenPGP, que define suas próprias funções de hash, chamadas “Simple S2K”, “Salted S2K” e “Iterated and Salted S2K“. Apenas a terceira pode ser considerada “boa” no contexto desta resposta. É definida como uma hash de uma string longa (configurável até uns 65 megabytes) e consiste em repetição de um salt de 8 bytes e a senha.

Em linhas gerais, O “Iterated And Salted S2K” do OpenPGP é decente, similar a um PBKDF2 com menos opções. Você raramente vai encontrar essa implementação fora do OpenPGP.

“crypt” do Unix

Sistemas do tipo Unix atuais (como Linux), usam variantes iteradas e com salt da função crypt(), que é baseada em boas funções de hash, com milhares de iterações. Isso é razoavelmente bom. Alguns sistemas também podem usar bcrypt, que é melhor ainda.

A função crypt() “antiga”, que era baseada no block cipher DES, não é boa:

  • É lenta em software, mas rápida em hardware, e ainda pode ser acelerada em software quando computando várias instâncias em paralelo (técnica conhecida como SWAR ou “bitslicing”), que é vantajoso para o atacante.
  • Ainda é muito rápida com 25 iterações.
  • Seu salt é de 12 bits, implicando em reúso frequente.
  • Trunca a senha a 8 caracteres e ainda trunca o bit alto, transformando em ASCII puro (7 bits).

As variantes novas, que são as que estão em uso atualmente, são OK.

Funções de hash ruins para senhas

Tudo o mais, em especial soluções caseiras que as pessoas insistem em criar, assumindo que “criptografia segura” é “jogar fora toda operação criptográfica ou não que pode ser pensada”. Vejaesta pergunta como exemplo. O princípio nela parece ser de que a complexidade com a bagunça das instruções vai confundir atacantes. Na prática, o desenvolvedor vai sempre estar mais perdido na sua própria criação do que o atacante.

Complexidade é ruim. Solução caseira é ruim. Novidade é ruim. Lembrando disso você vai evitar 99% dos problemas de hash de senhas, e segurança em geral.

Hash de senha no Windows costumava ser terrivelmente medonho, agora é só péssimo (MD4 semsalt e iterações).

Derivação de chave

Até agora nós consideramos a questão de hash das senhas. Um problema próximo é a transformação de uma senha numa chave simétrica que possa ser usada para criptografia. Isto é chamado de derivação de chave e é a primeira coisa que você faz quando encripta um arquivo com senha.

É possível fazer exemplos elaborados de funções de hash que são seguras para guardar um tokende validação, mas que são péssimas para gerar chaves simétricas; o oposto é possível, da mesma maneira. Esses exemplos entretanto são artificiais. Para casos práticos como os descritos acima:

  • A saída de uma hash de senha é aceitável como chave simétrica, após ser truncada no tamanho requerido.
  • Uma função de derivação de chave pode servir como hash de senha, desde que a chave derivada seja longa o suficiente pra evitar “pré-imagens genéricas” (o atacante tem a sorte de achar uma senha que dê o mesmo resultado). Uma saída de 100 bits deve ser suficiente.

Para falar a verdade, PBKDF2 e scrypt são funções de derivação de chave, e não de hashing — e o NIST aprova o PBKDF2 como função de derivação de chave, e não explicitamente como hasher. (mas com um pouquinho só de hipocrisia, dá pra ler o material do NIST de maneira a se entender que o PBKDF2 é bom para hash de senhas).

Ainda, o bcrypt é na verdade um block cipher (a parte de processamento de senha é o “key schedule”) que é depois usado em modo CTR para produzir três blocos de 192 bits de saída pseudo-aleatória, fazendo-o uma espécie de função de hash. O bcrypt pode virar uma função de derivação de chave com uma leve operação, usando o block cypher em modo CTR para gerar mais blocos. Mas, como de costume, não recomentamos remendos caseiros. Felizmente 192 bits são mais que suficientes para a maioria dos propósitos (por exemplo, encriptação simétrica comGCM ou EAX só precisa de 128 bits na chave).

Considerações adicionais

Quantas iterações?

Quanto mais melhor! Essa corrida de lento-e-salted é uma disputa acirrada entre atacante e defensor. Você usa muitas iterações para deixar o hashing mais dificil para todos. Para aumentar a segurança, você deve deixar o número mais alto que seja tolerado no servidor, considerando as outras coisas que ele deve fazer. Quanto mais alto melhor.

Colisões e MD5

O MD5 está quebrado: é muito fácil conseguir vários pares de entradas diferentes com o mesmo valor de saída. Estas são as colisões. Entretanto, colisões não são um problema para hash de senhas. Hash de senhas tem que resistir às pré-imagens, não colisões. Colisões são pares que dão mesma saída sem restrições, enquanto no hash de senhas o atacante tem que achar uma mensagem que dê uma determinada saída, que ele não escolheu. Isso é bem diferente. Até onde sabemos, o MD5 é praticamente tão forte quanto sempre foi em relação a pré-imagens (há umataque teórico que ainda está muito distante de ser viável na prática).

O verdadeiro problema com o MD5 é que seu uso é muito comum para senhas, ele é muito rápido e não tem salt. Mas o PBKDF2 usado com MD5 seria robusto. Você deve usar SHA-1 ou SHA-256, mas por questão de “relações públicas”. As pessoas ficam nervosas quando ouvem “MD5”.

Geração do salt

O objetivo fundamental do salt é ser o mais único possível. Sempre que um salt é reusado, potencialmente pode ajudar a um atacante. Por exemplo, se você usa o nome do usuário como salt, um atacante pode construir rainbow tables usando “admin” e/ou “root”, pois a tabela serviria para muitos lugares que possuem usuários com esses “nomes”.

Da mesma forma, quando um usuário muda a senha, o nome permanece, levando ao reúso dosalt. Senhas velhas são sempre alvos de valor, pois usuários tendem a reaproveitá-las em muitos lugares. (é sempre divulgado que é péssima idéia, todo mundo sabe, mas continua fazendo porque torna a vida mais fácil), e além disso as pessoas tendem a gerar senhas sequenciais. Se a senha velha do Alaor é “SenhaSecreta37”, bem capaz que a nova seja “SenhaSecreta38” ou “SenhaSecreta39”.

O jeito “barato” de obter salts únicos é usar aleatoriedade. Se você gera seu salt com bytes aleatórios de um gerador seguro que seu OS ofereça, (/dev/urandom, CryptGenRandom()…) você terá valores de salt “únicos suficientemente”, se forem de 16 bytes por exemplo, para nunca ver na vida uma colisão de salt.

O UUID é um jeito padrão de se obter valores “únicos”. Lembrando que UUID “versão 4” usa aleatoriedade (122 bits), como mencionado acima. Vários frameworks oferecem funções simples para gerar UUID sob demanda, e eles podem ser usados como salt

O salt tem que ser secreto?

O salt não foi feito pra ser secreto, senão seria chamado de chave. Você não precisa divulgar o salt, mas se precisar (usando uma solução de hash no cliente, por exemplo), não se preocupe. Osalt existe apenas para ser único. Não é nada mais do que a seleção de uma função de hash entre muitas.

Pepper

Criptógrafos não podem deixar uma metáfora quieta. Eles precisam estendê-las com mais analogias e trocadilhos (salt significa “sal”, pepper significa “pimenta”). Se você usa uma pepperna sua função de hash, está usando uma espécie diferente de algoritmo criptográfico; você está calculando um código de autenticação de mensagem (MAC) sobre a senha. A chave do MAC é sua pepper.

“Apimentar” faz sentido se você tiver uma chave secreta que o atacante não é capaz de ler. Lembre-se de que nós usamos o hash por que consideramos que um atacante pode conseguir uma cópia da base de dados do servidor ou até o disco todo. Um caso comum é um servidor com dois discos em RAID 1. Um disco falha, por ter a placa queimada. Acontece a toda hora. Osysadmin troca o disco, o espelho é refeito, e nada é perdido graças à magica do RAID 1. Bom, mas o disco velho não funciona mais, e o sysadmin não tem mais uma maneira fácil de limpar seu conteúdo, então ele simplesmente descarta o disco. O atacante encontra o disco no lixo, troca a placa e, vejam só! Ele tem uma cópia completa do sistema, com base de dados, configuração, executáveis, OS…

Para a pepper funcionar num caso desses, você precisa de algo mais que um PC com discos; você precisa de um dispositivo de segurança por hardware (HSM). Os HSMs são caros, tanto em termos econômicos quanto operacionais, mas com um HSM, basta usar a pepper e processar as senhas com um simples HMAC (por exemplo com SHA-1 ou SHA-256). Isso vai ser muito mais eficiente que bcrypt/PBKDF2/scrypt e suas complexas iterações. Fora que usar um HSM fica com cara bem profissional numa WebTrust audit.

Hashing do lado do cliente

Como o hashing é propositalmente “caro”, faria sentido numa arquitetura cliente-servidor usar a CPU do cliente. Afinal, quando 100 clientes conectam a um servidor só, coletivamente eles têm muito mais poder de processamento. Para isto, o server precisa mandar o salt para o cliente. Isso implica em mais uma ida e volta de dados, o que pode ou não ser fácil de adicionar em casos específicos. Num contexto web é mais complicado, pois o javascript sempre foi mais “anêmico” pra usar a CPU. Num contexto de senha segura remota (SRP), o hashing tem que acontecer de qualquer forma no lado do cliente.

Conclusão

Use bcrypt (há quem seja contra). PBKDF2 também não é ruim. Caso você use scrypt, vai estar “ligeiramente adiantado”, com os riscos implícitos nessa expressão; mas é bom pro progresso da ciência. Ser um “crash dummy” é uma profissão honrada!