O salt é usado no processo de hashing para forçar a unicidade do digest. Ele aumenta a entropia do hash, sem penalizar a experiência do usuário, e, com isso, protege contra ataques como rainbow tables.

sal

O Hash

O hash é uma função determinística, ou seja, dada uma mesma entrada, ela sempre produzirá a mesma saída.

Adicionar um salt aleatório garante que o valor de entrada mude, e com isso o hash produzido será diferente mesmo para senhas idênticas. Isso protege seu repositório contra ataques do tipo Rainbow table e reduz a eficácia de brute-force e dictionary attacks.


Entendendo o problema

Suponha que um usuário tenha a senha 12345678. O hash gerado (MD5, no exemplo) fica assim: 25D55AD283AA400AF464C76D713C07AD.

Exemplo de dump de hashes:

usuáriohash
joao7C222FB2927D828AF22F592134E8932480637C0D
mariaA7D579BA76398070EAE654C30FF153A4C273272A
jose4A3487E57D90E2084654B6D23937E75AF5C8EE55
bruno7C222FB2927D828AF22F592134E8932480637C0D
dianaA7D579BA76398070EAE654C30FF153A4C273272A

Repare na tabela: bruno e joao têm exatamente o mesmo hash. Como o hash é determinístico, isso indica, com alta probabilidade, que ambos usam a mesma senha.

Ou seja: sem salt, hashes idênticos = senhas idênticas, e o atacante já ganhou uma informação valiosa de graça.

Antes de continuar veja o que já passou

Se você chegou aqui neste artigo e não leu os anteriores, é leitura obrigatória para entender como chegamos aqui:

Como hashes são crackeados

Os métodos mais conhecidos atualmente para crackear um hash são:

  • Dictionary attack
  • Brute Force attack
  • Lookup Table
  • Reverse Lookup Table
  • Rainbow Tables

O salt protege seu repositório de três deles. Lookup table, reverse lookup e rainbow table.

Dictionary e Brute force attack

Dictionary Attack

Trying 1234578      : failed
Trying 10203040     : failed
Trying meucachorro  : failed
...
Trying minhamae     : failed
Trying minhasenhasecreta: success!
Brute Force Attack

Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed
...
Trying zzab : failed
Trying zzac : success!

A forma mais simples, e mais usada, de crackear uma senha é por tentativa e erro (tentative error). Nesse cenário, os ataques mais comuns são o dictionary attack e o brute-force.

brute-force

O brute-force é o método mais óbvio e, na prática, o mais lento. Porém não subestime velocidade: com hardware razoável e ferramentas como hashcat, um rig doméstico com duas GPUs decentes pode fazer milagres. Por exemplo, uma máquina com duas Radeon 7970 e hashcat consegue testar todo o espaço de senhas de 6 caracteres (lowercase + UPPERCASE + digits + symbols) em míseros 47 segundos, sim, sério. Moral: se o seu hash é rápido, senhas curtas e previsíveis caem num piscar de olhos.

dictionary attack

O dictionary attack é, na prática, mais eficiente que brute-force puro. Em vez de torturar todo o espaço de caracteres, o atacante testa uma lista “inteligente”: palavras comuns, variações previsíveis e coleções de senhas vazadas. Como muitas pessoas reutilizam padrões e senhas óbvias, esse ataque costuma encontrar alvos reais bem mais rápido.

Lookup table

Essa é uma técnica bem eficaz, basicamente é colar as respostas. Em vez de calcular o hash a partir da senha, o atacante consulta uma tabela gigantesca com pares hash → senha já pré-calculados. Se o hash estiver lá, pronto: senha descoberta sem esforço de CPU naquele momento.

É rápido, direto e desagradavelmente eficiente.

Exemplo (simulação de busca numa lookup table):

Searching: 7C222FB2927D828AF22F592134E8932480637C0D: FOUND: 12345678
Searching: 040069E821AF22F61491D2040C481C97:  not in database
Searching: A7D579BA76398070EAE654C30FF153A4C273272A: FOUND: 10203040
Searching: 6E22255F7B6C604A2992FF97E1F5B2CA:  not in database
Searching: 65CC61F8A26C3480CA3C6714D67BBC3F: FOUND: minhamae
Searching: BCD3A64ED1C945565F54FFFBA26071E9: FOUND: minhasupersenha
Searching: F5BB0C8DE146C67B44BABBF4E6584CC0:  not in database
Searching: D017FBF96868A24E9E144E7BF4B2260D:  not in database


Reverse Lookup table

Aqui a ideia é simples, e eficiente demais para quem não gosta de trabalho repetitivo: ao invés de atacar um único usuário até descobrir a senha, o atacante gera um hash (via dictionary ou brute-force) e procura simultaneamente em todo o dump quem tem aquele mesmo hash. Em outras palavras: um hash → muitos usuários verificados de uma vez.

É um salto de eficiência enorme. Em vez de gastar ciclos tentando quebrar userA, userB, userC separadamente, o atacante calcula hash(candidate) uma vez e verifica quais usuários coincidem. Se você não usa salt único por usuário, isso vira um ganho massivo, descobrir uma senha vale para dezenas (ou centenas) de contas ao mesmo tempo.

Searching for hash(12345678) in users' hashes ... : Matches [joao, bruno, ricardo]
Searching for hash(10203040) in users' hashes ... : Matches [jose, maria, diana]
Searching for hash(minhasenhasecreta) in users' hashes ... : Matches [wilson, pedro, ana]
Searching for hash(@Password1) in users' hashes ...   : Matches [mario, manoel, carla]
Searching for hash(@pass1) in users' hashes ...      : No users used this password

Moral da história: sem salts únicos, hashes idênticos são como bilhetes dourados, um só erro dá acesso a vários ingressos. Use per-user salt e uma função de hashing que seja lenta/memory-hard (Argon2id / bcrypt / scrypt) e você transforma esse ataque numa tarefa bem menos atraente para o invasor.

Rainbow table

Um rainbow table combina duas funções: uma hash e uma reduction. A ideia é criar chains (cadeias) alternando hash → reduction → hash → reduction, e só armazenar o início e o fim de cada chain. Assim você reduz drasticamente o consumo de memória em relação a uma lookup table que guarda hash → senha para tudo, mas ainda consegue, com algum trabalho adicional, recuperar senhas a partir de hashes.

Em termos práticos: em vez de armazenar todos os pares possíveis (dependendo do tamanho da base, poderia ocupar muito espaço), a rainbow table guarda menos dados e aceita gastar mais tempo na hora do ataque para recalcular partes da chain. É o clássico time–memory trade-off: você troca espaço por tempo.

Apesar da compactação, as tabelas continuam sendo imensas, e existem coleções prontas para download (sim, pessoas já fizeram o trabalho pesado e disponibilizaram tabelas grandes). Se quiser entender o funcionamento interno, há bons textos que mostram o processo de geração das chains e a lógica de redução.

Por que isso importa para você?

  • Rainbow tables são altamente eficazes contra hashes sem salt.
  • Um per-user salt quebra a utilidade prática dessas tabelas, porque força o atacante a gerar (ou ter) uma tabela por cada salt, o que elimina a vantagem de pré-computação.

Salt

Lookup tables e rainbow tables funcionam porque a mesma senha sempre gera o mesmo hash quando processada da mesma forma. Logo: se dois usuários usam a mesma senha, ambos terão hashes idênticos.

Você evita isso adicionando um salt ao hash.

  • O que é: o salt é uma string aleatória que você concatena à senha (prefixando ou postfixando) antes de calcular o hash. Exemplo:

    hash(password)    -> H1
    hash(salt + password) -> H2  (H2 ≠ H1)
    
  • Como funciona na prática: o salt faz com que a mesma senha produza hashes diferentes para cada usuário. Para verificar a senha, o sistema lê o salt correspondente, aplica hash(salt + candidate) e compara com o digest armazenado.

  • Salt não precisa ser secreto. Ele normalmente é armazenado junto com o hash no repositório. O papel do salt não é esconder a senha, é impedir pré-computação de hashes.

  • Por que quebra Lookup / Rainbow tables: Um atacante não sabe, com antecedência, qual salt foi usado para cada usuário, então não consegue usar uma tabela pré-computada. Com salt único por usuário, o atacante teria que recalcular todos os hashes.

  • E contra Reverse Lookup? Se o salt for verdadeiramente aleatório e único por usuário, o ataque de reverse lookup perde eficiência porque hashes idênticos deixam de existir e o paralelismo simples (comparar o hash de um usuário com outros) não tem mais efeito.

Não crie seu próprio salt

No salt usamos a mesma regra sobre Hash: não inventa moda. Use bibliotecas que já geram salt. Funções de hash e geração de salt precisam de um CSPRNG e muito cuidado, não é lugar pra quem não manja dos paranaue brincar.

Salts ineficientes

Alguns erros comuns que anulam totalmente o propósito do salt:

  • Usar um único salt global: é praticamente o mesmo que não usar salt. Dois usuários com a mesma senha terão o mesmo hash.
  • Reusar salt entre senhas/usuários: gere um salt novo para cada senha criada ou alterada.

Uma prática comum e segura é usar um salt com tamanho comparável à saída da funçãao de hash. Por exemplo, se a sua função produz 256 bits (SHA-256), é recomendado usar um salt de 128–256 bits (16–32 bytes).

Playground

Quer saber o quão poderoso é um Lookup table? Vá até este site e coloque esses hashes:

670F8574BD93DD78BD568DAB84C6733A
25D55AD283AA400AF464C76D713C07AD
7C222FB2927D828AF22F592134E8932480637C0D
61FF76C0A46C9F653F4B1EE3D251AAC860263E15
2A6D337010BFD13831EB441CD9FB763D

Conclusão

Salt, assim como o password hash, é uma segunda linha de defesa. Quando um atacante já tem acesso ao repositório, seja lá como ele teve acesso as senhas.

Nesse caso que a primeira barreira falhou se as senhas estiverem desprotegidas, o estrago não é só no seu sistema: muitos usuários reutilizam senhas, então contas em outros serviços também ficam vulneráveis. Não é só a sua segurança que está em jogo, é a dos seus usuários. Você tem responsabilidade por isso.

Regra prática: não economize na proteção de senhas. Use libs e serviços testados, gere salt por senha com CSPRNG, escolha uma password hashing function adequada (ex.: Argon2id, bcrypt, scrypt). Faça diferente e prepapa o discurso pro seu chefe ou pra UOL.

E lembre-se: uma autenticação feita por amadores é uma autenticação amadora.