Capa do Ebook gratuito Criptografia Aplicada para Profissionais: o que usar, quando e por quê

Criptografia Aplicada para Profissionais: o que usar, quando e por quê

Novo curso

22 páginas

Hashing, derivação de chaves e armazenamento seguro de senhas

Capítulo 5

Tempo estimado de leitura: 13 minutos

+ Exercício

O que é hashing e por que ele não é “criptografia”

Hashing é o processo de transformar uma entrada (mensagem, arquivo, senha) em uma saída de tamanho fixo (o “hash” ou “digest”), usando uma função determinística. Determinística significa: a mesma entrada sempre gera o mesmo hash. Uma boa função de hash criptográfica tem propriedades específicas que a tornam útil para integridade e para certos tipos de armazenamento seguro.

É comum confundir hash com criptografia. A diferença prática é: criptografia é reversível (com a chave correta você recupera o texto original); hashing criptográfico é projetado para ser irreversível na prática. Você não “desfaz” um hash para obter a entrada; você apenas verifica se uma entrada candidata produz o mesmo hash.

Propriedades importantes de funções de hash criptográficas

  • Resistência a pré-imagem: dado um hash H, deve ser inviável encontrar uma mensagem M tal que hash(M)=H.
  • Resistência a segunda pré-imagem: dado M1, deve ser inviável encontrar M2≠M1 com o mesmo hash.
  • Resistência a colisões: deve ser inviável encontrar quaisquer duas mensagens diferentes M1 e M2 com o mesmo hash.
  • Efeito avalanche: pequenas mudanças na entrada produzem mudanças grandes e aparentemente aleatórias no hash.

Essas propriedades são essenciais para integridade de dados, deduplicação segura (com ressalvas), impressão digital de arquivos e como componente em protocolos. Para senhas, porém, “hash criptográfico genérico” (como SHA-256) não é suficiente por si só: ele é rápido demais e facilita ataques offline em massa quando um banco de hashes vaza.

Hashing para integridade: onde ele entra no dia a dia

Em cenários de integridade, você calcula o hash de um arquivo e compara com um valor esperado. Isso detecta alterações acidentais e muitas alterações maliciosas, desde que o valor esperado seja obtido por um canal confiável. Exemplo: você publica um instalador e também publica o SHA-256 do arquivo em um local protegido; quem baixa verifica se o hash bate.

Um cuidado: hash sozinho não autentica a origem. Se um atacante consegue substituir o arquivo e também o hash publicado, a verificação passa. Para autenticação, você precisa de um mecanismo adicional (por exemplo, assinatura digital ou um MAC), mas aqui o foco é entender o papel do hash como “impressão digital” e como base para derivação de chaves e armazenamento de senhas.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Derivação de chaves (KDF): transformar segredos em chaves úteis

Derivação de chaves é o processo de gerar uma ou mais chaves criptográficas a partir de um segredo inicial. Esse segredo pode ser uma senha, um segredo compartilhado, um material de chave obtido de um acordo de chaves, ou uma chave mestra. Uma KDF (Key Derivation Function) serve para:

  • Expandir material de chave em várias chaves independentes (por exemplo, uma para criptografia e outra para autenticação).
  • Uniformizar a distribuição (produzir bytes que “parecem aleatórios” mesmo que a entrada tenha estrutura).
  • Endurecer senhas (tornar caro testar tentativas), quando a entrada é de baixa entropia.

É útil separar mentalmente dois casos: (1) derivar chaves a partir de material já forte (alta entropia), e (2) derivar chaves a partir de senhas (baixa entropia). No primeiro caso, você quer uma KDF de extração/expansão (como HKDF). No segundo, você quer uma KDF de hashing de senha (como Argon2, scrypt, bcrypt, PBKDF2), que introduz custo computacional e/ou de memória para dificultar força bruta.

HKDF: quando você já tem um segredo forte

HKDF (HMAC-based Key Derivation Function) é usada quando você tem um segredo com boa entropia (por exemplo, um segredo compartilhado resultante de um acordo de chaves) e precisa derivar chaves específicas para diferentes finalidades. HKDF tem duas fases conceituais:

  • Extract: “limpa” e normaliza o material de entrada, produzindo um pseudo-random key (PRK).
  • Expand: gera uma ou mais chaves derivadas, usando um contexto (info) para separar finalidades.

O parâmetro info é crucial para evitar reutilização indevida: ele “rotula” a chave derivada (por exemplo, “chave de criptografia do canal A” vs “chave de autenticação do canal A”). Assim, mesmo com o mesmo segredo base, as chaves derivadas ficam separadas.

KDFs para senha: quando o segredo é fraco

Senhas humanas têm baixa entropia e padrões previsíveis. Se você usar SHA-256(senha) para armazenar ou derivar uma chave, um atacante com o hash consegue testar bilhões de tentativas por segundo em GPU/ASIC. Por isso, para senha você usa funções de hashing de senha (password hashing) que são deliberadamente caras.

As opções mais comuns:

  • Argon2id: recomendado em muitos cenários modernos; combina resistência a ataques por GPU e ataques por canal lateral, com custo de memória configurável.
  • scrypt: também memory-hard; bom histórico, amplamente suportado.
  • bcrypt: muito usado; custo configurável, mas menos “memory-hard” que Argon2/scrypt.
  • PBKDF2: amplamente disponível; depende de muitas iterações; não é memory-hard, então tende a ser menos resistente a hardware especializado, mas ainda é aceitável quando Argon2/bcrypt/scrypt não estão disponíveis.

Em todos os casos, você deve usar um salt (valor aleatório único por senha) e armazená-lo junto do hash. O salt impede ataques com tabelas pré-computadas (rainbow tables) e garante que senhas iguais resultem em hashes diferentes.

Armazenamento seguro de senhas: o que fazer e o que evitar

O objetivo prático

Quando um usuário cria uma senha, o sistema deve armazenar algo que permita verificar a senha no login, mas que não permita recuperar a senha original. Além disso, o armazenamento deve tornar caro para um atacante testar muitas senhas caso ele obtenha o banco de dados (ataque offline).

O que evitar (erros comuns)

  • Guardar senha em texto puro: qualquer vazamento vira comprometimento imediato.
  • Criptografar a senha e guardar a chave no servidor: se o servidor for comprometido, o atacante pode obter a chave e descriptografar todas as senhas. Além disso, você não precisa recuperar a senha, apenas verificar.
  • Usar hash rápido (MD5, SHA-1, SHA-256) diretamente: rápido demais; facilita força bruta em massa.
  • Usar salt global (um único salt para todos): melhora pouco; o ideal é salt único por usuário.
  • Parâmetros fracos ou fixos para sempre: custo deve ser revisado e aumentado com o tempo.

O que fazer (prática recomendada)

  • Use um algoritmo de hashing de senha (preferencialmente Argon2id; alternativas: scrypt, bcrypt; PBKDF2 se necessário).
  • Gere um salt aleatório único por senha (por exemplo, 16 bytes ou mais).
  • Configure custo (tempo/iterações e, quando aplicável, memória) para que a verificação leve um tempo aceitável para o usuário, mas caro para o atacante.
  • Armazene parâmetros junto do hash (algoritmo, salt, custo). Muitos formatos já incluem isso na string final.
  • Use comparação em tempo constante ao comparar hashes para evitar vazamentos por timing.

Passo a passo prático: cadastro e login com Argon2id

A seguir está um fluxo prático e implementável, independente de linguagem. A ideia é registrar e verificar senhas de forma segura, armazenando apenas o necessário.

1) Cadastro (criação/alteração de senha)

  • Entrada: senha em texto (recebida via canal seguro), e um gerador de números aleatórios criptograficamente seguro (CSPRNG).
  • Gere um salt: 16 bytes (ou 32) aleatórios.
  • Defina parâmetros: por exemplo, memória (em KiB), iterações (time cost) e paralelismo. Ajuste conforme seu ambiente.
  • Calcule o hash de senha com Argon2id usando (senha, salt, parâmetros).
  • Armazene: uma string que inclua algoritmo + parâmetros + salt + hash. Muitos bindings retornam algo como: $argon2id$v=19$m=65536,t=3,p=1$<salt_base64>$<hash_base64>.
// Pseudocódigo de cadastro com Argon2id (conceitual, não específico de linguagem)
function registerPassword(userId, passwordPlaintext):    salt = CSPRNG.randomBytes(16)    params = { memoryKiB: 65536, timeCost: 3, parallelism: 1, hashLen: 32 }    encoded = Argon2id.hash(passwordPlaintext, salt, params)    // encoded inclui params + salt + hash em formato padronizado    database.save(userId, encoded)

Observações práticas: (1) não “normalize” senha (por exemplo, lowercasing) — isso reduz o espaço de busca e enfraquece; (2) trate a senha como segredo: evite logs, evite persistir em memória por mais tempo que o necessário quando a linguagem permitir.

2) Login (verificação de senha)

  • Recupere do banco a string armazenada (encoded) para aquele usuário.
  • Use a função de verificação do algoritmo, que extrai salt e parâmetros do encoded e recalcula o hash com a senha fornecida.
  • Compare usando comparação em tempo constante (normalmente a biblioteca já faz).
function verifyLogin(userId, passwordCandidate):    encoded = database.load(userId)    ok = Argon2id.verify(encoded, passwordCandidate)    return ok

3) Rehash automático (atualização de parâmetros ao longo do tempo)

Com o tempo, hardware fica mais rápido e você deve aumentar o custo. Um padrão útil é: após um login bem-sucedido, verificar se o hash armazenado está com parâmetros antigos; se estiver, recalcular com parâmetros novos e atualizar no banco.

function verifyAndMaybeRehash(userId, passwordCandidate):    encoded = database.load(userId)    if !Argon2id.verify(encoded, passwordCandidate):        return false    if Argon2id.needsRehash(encoded, newParams):        newEncoded = Argon2id.hash(passwordCandidate, CSPRNG.randomBytes(16), newParams)        database.save(userId, newEncoded)    return true

Como escolher parâmetros de custo sem “chutar”

Parâmetros precisam equilibrar segurança e experiência do usuário. Uma abordagem prática:

  • Defina um orçamento de tempo por verificação (por exemplo, 100–300 ms em servidor, dependendo do volume de logins e do hardware).
  • Meça em produção ou em ambiente equivalente (mesma CPU, mesma configuração de contêiner/VM).
  • Ajuste memória e iterações para atingir o orçamento. Em Argon2id, aumentar memória costuma aumentar o custo para atacantes com GPU.
  • Considere picos: logins simultâneos podem causar contenção. Se você usar muita memória por verificação, pode derrubar o serviço sob carga.

Não existe um número universal. O importante é: (1) usar um algoritmo adequado, (2) usar salt único, (3) medir e ajustar, (4) ter estratégia de rehash.

Pepper: um “segredo do servidor” para reduzir impacto de vazamentos

Além do salt (público e único por usuário), você pode usar um pepper: um segredo global mantido fora do banco de dados (por exemplo, em um HSM, KMS, ou variável de ambiente protegida). O pepper é combinado com a senha antes do hashing (por exemplo, concatenando ou via HMAC), de modo que um atacante que roube apenas o banco de dados não consiga verificar tentativas sem também obter o pepper.

Cuidados:

  • Se o atacante comprometer o servidor e obtiver o pepper, o benefício diminui.
  • Rotação de pepper é complexa: pode exigir rehash de todas as senhas ou uma estratégia de múltiplos peppers válidos.
  • Pepper não substitui um bom password hashing; é uma camada adicional.
// Exemplo conceitual: senha + pepper antes do Argon2id (não é um padrão único)
passwordInput = passwordPlaintext || pepperSecretencoded = Argon2id.hash(passwordInput, salt, params)

Derivação de chaves a partir de senha: quando você precisa de uma chave, não de um hash

Às vezes você não está armazenando senha para login, mas precisa derivar uma chave para criptografar dados do usuário (por exemplo, um cofre local, um arquivo protegido, ou uma chave para desbloquear outra chave). Nesse caso, você quer uma chave com tamanho específico (por exemplo, 32 bytes) derivada da senha. Você ainda deve usar uma KDF de senha (Argon2id/scrypt/PBKDF2), mas com atenção a alguns detalhes:

  • Use um salt e armazene-o junto dos dados criptografados.
  • Separe finalidades: se você precisa de duas chaves (ex.: uma para criptografia e outra para autenticação), derive material suficiente e separe com um KDF de expansão (ou derive duas vezes com contextos diferentes).
  • Não reutilize a mesma saída para múltiplos propósitos sem separação de domínio.

Passo a passo: derivar chave para criptografar um arquivo

Fluxo típico (conceitual):

  • Usuário escolhe senha.
  • Você gera um salt aleatório e deriva uma chave com Argon2id.
  • Você usa essa chave para criptografar o arquivo (com um esquema autenticado, por exemplo, um AEAD).
  • Você armazena junto do arquivo: salt, parâmetros da KDF e o nonce/IV do esquema de criptografia.
function encryptFileWithPassword(fileBytes, passwordPlaintext):    salt = CSPRNG.randomBytes(16)    kdfParams = { memoryKiB: 65536, timeCost: 3, parallelism: 1 }    key = Argon2id.deriveKey(passwordPlaintext, salt, kdfParams, keyLen=32)    nonce = CSPRNG.randomBytes(12)    ciphertext = AEAD.encrypt(key, nonce, plaintext=fileBytes, aad=metadata)    output = { kdfParams, salt, nonce, aad, ciphertext }    return output
function decryptFileWithPassword(bundle, passwordCandidate):    key = Argon2id.deriveKey(passwordCandidate, bundle.salt, bundle.kdfParams, 32)    plaintext = AEAD.decrypt(key, bundle.nonce, bundle.ciphertext, bundle.aad)    return plaintext

Note como o salt e os parâmetros precisam estar disponíveis na descriptografia. Isso é normal e esperado: salt não é segredo. O segredo é a senha (e eventualmente um pepper, se aplicável).

HMAC e “hash com chave”: autenticidade e derivação com contexto

HMAC é uma construção que usa uma função de hash (como SHA-256) junto com uma chave secreta para produzir um código de autenticação de mensagem. Ele é útil quando você precisa garantir integridade e autenticidade de dados entre partes que compartilham uma chave. Também é usado como bloco de construção em KDFs (como HKDF).

Um erro comum é tentar “inventar” autenticação fazendo hash(conteúdo + chave) de forma ingênua. HMAC existe justamente para evitar armadilhas sutis e deve ser preferido quando você precisa de um MAC baseado em hash.

// Exemplo conceitual de uso de HMAC para autenticar um token interno
tag = HMAC_SHA256(key=serverSecretKey, message=tokenPayload)verify: recompute tag and compare in constant time

Checklist rápido para implementação segura de senhas

  • Use Argon2id (preferencial), scrypt ou bcrypt; PBKDF2 se não houver alternativa.
  • Salt aleatório único por senha; armazene junto do hash.
  • Parâmetros calibrados por medição; revise periodicamente.
  • Comparação em tempo constante (use verify da biblioteca).
  • Considere pepper armazenado fora do banco para reduzir impacto de vazamento do DB.
  • Implemente rehash automático após login bem-sucedido quando parâmetros estiverem defasados.
  • Não exponha detalhes em mensagens de erro (por exemplo, “usuário não existe” vs “senha incorreta”) se isso facilitar enumeração de usuários; trate isso na camada de autenticação.

Agora responda o exercício sobre o conteúdo:

Ao verificar a integridade de um arquivo baixado, por que comparar apenas o hash do arquivo com um valor publicado pode falhar em autenticar a origem?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Hash detecta alterações apenas se o valor esperado vier de um canal confiável. Se o atacante conseguir trocar o arquivo e também o hash publicado, a verificação ainda passa. Para autenticar a origem, é necessário usar assinatura digital ou um MAC.

Próximo capitúlo

Autenticidade de mensagens com HMAC e comparação com assinaturas

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.