DEV Community

PETRUS NOLETO
PETRUS NOLETO

Posted on

Blind Indexing — A Arte de Buscar o que Você Não Pode Ler

O perigo do "Texto Claro" (Plaintext)

A maioria dos desenvolvedores segue o padrão clássico: criptografam a senha com Argon2 ou Bcrypt, mas deixam o E-mail, CPF e Telefone em texto puro. No papel, parece correto; na prática, é um desastre anunciado.

{
  name: 'tester 123',
  username: 'teste123',
  email: 'teste@gmail.com',
  password: '$argon2id$v=19$m=65536,t=3,p=4$0fnlxR1QiseB2GrhDX9oPg$Uw8VMWVZ7LELNxEKMFBvxddYzxFtb1lggnyd+PlneOc',
  status: 'ACTIVE'
}
Enter fullscreen mode Exit fullscreen mode

Se um atacante conseguir um dump (cópia) do seu banco de dados, ele terá em mãos uma lista de identidades reais. Em conformidade com a LGPD no Brasil ou a GDPR na Europa, isso não é apenas uma falha técnica, é um risco jurídico que pode custar milhões em multas.

O Dilema: Criptografar vs. Buscar

Você pode pensar: "Basta criptografar o e-mail antes de salvar!".
O problema é que a criptografia padrão (AES-256, por exemplo) gera resultados diferentes a cada execução devido ao uso de IVs (Initialization Vectors) ou Nonces aleatórios. Isso é ótimo para segurança, mas impossível para buscas

Como você daria um SELECT * FROM users WHERE email = '...' se o resultado criptografado muda toda vez?

A Solução: Blind Indexing

O Blind Index (Índice Cego) resolve o paradoxo entre segurança e usabilidade. A ideia central é criar uma "impressão digital" determinística do dado sensível, utilizando uma chave secreta que jamais deve tocar o banco de dados.

Por que usar HMAC-SHA512256 e não apenas SHA-256?

  • O problema do SHA-256: O problema do Hash Simples (SHA-256): Embora o SHA-256 seja um algoritmo extremamente seguro e robusto, ele não foi projetado para proteger dados previsíveis. Por ser um hash público e de alta performance, ele permite que um atacante realize ataques de dicionário ou use Rainbow Tables (listas de hashes pré-calculados de e-mails comuns). Se o seu dado é previsível (como um e-mail), um atacante não precisa "quebrar" o algoritmo; ele só precisa comparar o seu hash com uma lista de hashes que ele já possui.
  • A vantagem do HMAC: O HMAC (Hash-based Message Authentication Code) introduz um fator crítico: uma Secret Key. Sem a sua BLIND_INDEX_SECRET — que permanece protegida e isolada no seu servidor ou container — o hash armazenado torna-se um dado opaco e sem valor para o invasor. Com a tecnologia computacional que temos hoje, esse código dificilmente pode ser quebrado, garantindo a proteção da identidade dos seus usuários mesmo em caso de um vazamento total da base de dados.

Como fica o seu dado agora?

{
  name: 'tester 123',
  username: 'teste123',
  emailCrypto: 'okHMME54xzIh4als7zQJIK5h94k4vfxi0avJtwFnafCIG05AZhZx7/L02V5nhTDe2ReBPH5Q9w==',
  emailHash: '3adeecd4e5fc026c21b62174c53c9a9ead24dbc925d97bad25ec9d301e80a026',
  password: '$argon2id$v=19$m=65536,t=3,p=4$0fnlxR1QiseB2GrhDX9oPg$Uw8VMWVZ7LELNxEKMFBvxddYzxFtb1lggnyd+PlneOc',
  status: 'ACTIVE'
}
Enter fullscreen mode Exit fullscreen mode

Processo de Registro (Verificação de Unicidade)

No registro, o objetivo é a Prevenção. Usamos o operador OR para garantir que nenhum dado colida com o que já existe.

O Fluxo:

  1. Entrada: O usuário envia o e-mail PETRUS@gmail.com.
  2. Normalização: O sistema transforma em petrus@gmail.com (case-insensitive).
  3. Geração do Blind Index: A aplicação aplica o HMAC:

    emailHash = HMAC("petrus@gmail.com", BLIND_INDEX_SECRET)

    Resultado: 3adeec...

  4. Consulta ao Banco: SELECT id FROM accounts WHERE emailHash = '3adeec...' OR identificationHash = '...';

  5. Decisão:

    • Se retornar algo: Erro "Account already exists"
    • Se nulo: Segue para a criptografia do e-mail real e salva.

A Origem do emailCrypto: Criptografia de Via Dupla

Diferente do emailHash, que é uma rua de mão única (você gera o hash mas nunca o "desfaz"), o emailCryptonão usamos um hash comum, mas sim algoritmos de criptografia simétrica autenticada (AEAD). O favorito do mercado de alta segurança é o XChaCha20-Poly1305, pela sua robustez e facilidade de implementação sem erros fatais de gerenciamento de Nonces. Outras alternativas sólidas incluem o AES-256-GCM, amplamente utilizado em setores bancários, e o AEGIS-256, para quem busca a máxima performance em hardware moderno.

No momento do registro, a aplicação utiliza a sua CRYPTO_SECRET para transformar o e-mail legível em uma cifra protegida. É vital entender que:

  1. Blind Index (emailHash): Criado com a BLIND_INDEX_SECRET. Serve apenas para o banco de dados te dizer: "Eu tenho um registro que bate com essa digital".
  2. Armazenamento (emailCrypto): Criado com a CRYPTO_SECRET. Serve para a aplicação dizer: "Agora que encontrei o registro, vou usar minha chave privada para ler o que está escrito aqui".

Exemplo de registro

async function register(emailRaw: string, passwordRaw: string) {
 const emailSearchIndex = await blindIndex.generate(emailRaw);
 const account = await repository.find({ emailHash: emailSearchIndex });

 if (account) throw new Error("Account Already Exists")
 const dataToSave = {
   // Digital determinística para buscas futuras
   emailHash: await blindIndex.generate(emailRaw), 

   // O dado real "trancado" com a chave de criptografia
   emailCrypto: await crypto.encrypt(emailRaw, process.env.CRYPTO_SECRET), 

   password: await argon2.hash(passwordRaw)
 };

  // salva o novo usuario

};
Enter fullscreen mode Exit fullscreen mode

Processo de Login (Busca e Autenticação)

No login, o objetivo é a Identificação Estrita. Usamos o operador AND para garantir precisão.

O Fluxo:

  1. Entrada: Usuário digita e-mail e senha.
  2. Geração do Blind Index: O sistema gera o hash do e-mail digitado usando a mesma BLIND_INDEX_SECRET.
  3. Busca no Banco: SELECT * FROM accounts WHERE emailHash = '3adeec...' AND status = 'ACTIVE';
  4. Extração: O banco retorna a linha contendo o emailCrypto (que você não usa agora) e o password (hash do Argon2) como no exemplo acima.
  5. Verificação de Senha: isValid = Argon2.verify(password_do_banco, senha_digitada, { secret: PASSWORD_SECRET })
  6. Conclusão: Se isValid for true, você completa seu processo de login.

Exemplo de login

async function login(emailRaw: string, passwordRaw: string) {
  // 1. O sistema "fica cego" para o e-mail real e gera o índice de busca
  const emailSearchIndex = await blindIndex.generate(emailRaw);

  // 2. Busca direta no banco pelo índice (performance de O(1) com B-Tree index)
  const account = await repository.find({ emailHash: emailSearchIndex }, "AND");

  if (!account) throw new Error("Invalid credentials");

  // 3. A senha é verificada separadamente com Argon2 + Pepper
  const isPassValid = await passwordService.compare(account.password, passwordRaw);

  if (!isPassValid) throw new Error("Invalid credentials");

  return generateToken(account.id);
}
Enter fullscreen mode Exit fullscreen mode

O Uso do Dado

A dúvida que surge após implementar o Blind Index é: "Se o e-mail está criptografado, como eu envio uma notificação ou um ingresso para o usuário?".

A resposta está na Criptografia Simétrica de Chave Única. Enquanto o emailHash (Blind Index) serve para encontrar o registro, o emailCrypto serve para revelar o dado apenas no momento da execução.

O Fluxo de Recuperação (Ex: Envio de Ingresso)

  1. Recuperação: O sistema busca a conta pelo ID ou pelo emailHash.
  2. Descriptografia em Memória: A aplicação utiliza a CRYPTO_SECRET (que reside apenas nas variáveis de ambiente do servidor ou em algun serviço de injeção de variaveis) para descriptografar o emailCrypto.
  3. Memória Efêmera: O e-mail em texto puro (ex: teste@gmail.com) agora existe apenas na memória RAM do processo por alguns milissegundos.
  4. Consumo: O dado é enviado para o provedor de e-mail (como AWS SES ou SendGrid) via conexão segura (HTTPS/TLS).
  5. Descarte: Assim que a função termina, o dado em texto puro é descartado da memória. Ele nunca toca o disco e jamais deve aparecer nos seus logs.

Exemplo:

async function sendTicket(accountId: string) {
  // 1. Busca os dados protegidos
  const account = await repository.findById(accountId);

  // 2. Transforma o amontoado de caracteres no dado real
  const plaintextEmail = await crypto.decrypt(account.emailCrypto);

  // 3. Uso imediato e seguro
  await emailProvider.send({
    to: plaintextEmail,
    subject: "Seu Ingresso Chegou!",
    body: "..."
  });

  // O dado em texto puro morre aqui.
}
Enter fullscreen mode Exit fullscreen mode

Conclusão: Segurança não é opcional, é fundamento

Implementar Blind Indexing vai muito além de evitar multas regulatórias; trata-se de adotar uma postura de Defesa Proativa. Ao separar a capacidade de identificar um registro da capacidade de ler o dado sensível, transformamos nosso banco de dados em um ambiente hostil para invasores, mas extremamente eficiente para o negócio.

Ao adotar essa arquitetura, você cria um sistema que é Cego para o Banco de Dados, mas Inteligente para a Aplicação. O banco de dados torna-se apenas um depósito de cifras inúteis para invasores, enquanto sua aplicação retém a soberania sobre a identidade dos usuários.

  • Petrus Noleto

Top comments (0)