Senhas, tokens, assinaturas, webhooks e dados confidenciais exigem operações criptográficas corretas. O módulo Crypto no Node.js, disponível como node:crypto, oferece funções nativas para gerar valores aleatórios, calcular hashes e HMACs, derivar chaves, assinar mensagens e criptografar conteúdo.
Criptografia é uma área em que pequenos erros podem comprometer todo o sistema. Algoritmos antigos, nonces repetidos, chaves em código-fonte e comparações comuns de assinaturas criam vulnerabilidades difíceis de perceber. Por isso, o objetivo não é inventar um protocolo, mas usar primitivas modernas com parâmetros seguros e contratos bem definidos.
Neste guia, você aprenderá hashes, HMAC, randomBytes(), UUIDs, derivação de senha, AES-GCM, assinaturas, comparação em tempo constante, gestão de chaves e testes.
Carregando o módulo crypto
const crypto = require('node:crypto');A documentação oficial do módulo crypto descreve algoritmos, classes e requisitos de OpenSSL. Para revisar a plataforma, consulte o que é Node.js e o que é JavaScript.
Hash não é criptografia reversível
Uma função hash transforma uma entrada em um resumo de tamanho fixo. Ela é usada para verificar integridade, identificar conteúdo e construir assinaturas, mas não foi feita para recuperar o valor original.
const hash = crypto
.createHash('sha256')
.update('conteúdo importante', 'utf8')
.digest('hex');
console.log(hash);Alterar um único byte muda o resultado. Para arquivos grandes, alimente o hash progressivamente por stream em vez de carregar tudo na memória.
Hash de arquivo com pipeline
const fs = require('node:fs');
async function hashFile(file) {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(file);
for await (const chunk of stream) {
hash.update(chunk);
}
return hash.digest('hex');
}O guia de Streams no Node.js mostra como processar arquivos grandes com backpressure.
Não use SHA-256 puro para senhas
Hashes rápidos são inadequados para armazenar senhas porque atacantes conseguem testar bilhões de combinações. Use uma função de derivação lenta e com salt, como scrypt, ou uma biblioteca madura com Argon2.
const { promisify } = require('node:util');
const scrypt = promisify(crypto.scrypt);
async function hashPassword(password) {
const salt = crypto.randomBytes(16);
const derived = await scrypt(password, salt, 64);
return {
salt: salt.toString('base64'),
hash: derived.toString('base64')
};
}Armazene salt, parâmetros e resultado. O salt não precisa ser secreto, mas deve ser único e aleatório por senha.
Verificando uma senha
async function verifyPassword(password, record) {
const salt = Buffer.from(record.salt, 'base64');
const expected = Buffer.from(record.hash, 'base64');
const actual = await scrypt(password, salt, expected.length);
return crypto.timingSafeEqual(expected, actual);
}Use comparação em tempo constante quando os Buffers têm o mesmo tamanho. O artigo sobre Buffer no Node.js explica codificações e memória binária.
Gerando tokens aleatórios
const token = crypto.randomBytes(32).toString('base64url');Trinta e dois bytes oferecem ampla entropia para tokens de redefinição e sessões. Não use Math.random() para segredos, pois ele não é um gerador criptograficamente seguro.
Armazene no banco apenas o hash do token quando possível. Se o banco vazar, o valor original enviado ao usuário não estará disponível diretamente.
UUIDs
const id = crypto.randomUUID();UUID é adequado para identificadores não sequenciais, mas não substitui automaticamente um token secreto em todos os cenários. Avalie a entropia e a finalidade.
HMAC para autenticidade
HMAC combina uma chave secreta com a mensagem. Ele permite verificar se o conteúdo foi alterado e se veio de alguém que conhece a chave.
function signPayload(payload, secret) {
return crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
}Na validação, calcule novamente sobre os bytes exatos e use comparação segura:
function verifyHmac(payload, signature, secret) {
const expected = Buffer.from(
signPayload(payload, secret),
'hex'
);
const received = Buffer.from(signature, 'hex');
if (expected.length !== received.length) return false;
return crypto.timingSafeEqual(expected, received);
}Esse padrão é usado em webhooks seguros com Node.js.
Criptografia autenticada com AES-GCM
AES-GCM protege confidencialidade e integridade. Cada criptografia precisa de um IV único para a mesma chave.
function encrypt(text, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(
'aes-256-gcm',
key,
iv
);
const encrypted = Buffer.concat([
cipher.update(text, 'utf8'),
cipher.final()
]);
return {
iv: iv.toString('base64'),
data: encrypted.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}A chave deve possuir 32 bytes para AES-256. O IV não é secreto, mas nunca deve ser repetido com a mesma chave.
Descriptografando AES-GCM
function decrypt(record, key) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(record.iv, 'base64')
);
decipher.setAuthTag(
Buffer.from(record.tag, 'base64')
);
const plain = Buffer.concat([
decipher.update(Buffer.from(record.data, 'base64')),
decipher.final()
]);
return plain.toString('utf8');
}Se o conteúdo, IV ou tag forem alterados, final() falhará. Não ignore essa exceção.
Dados adicionais autenticados
GCM pode autenticar metadados que não precisam ser criptografados, como versão e identificador:
const aad = Buffer.from(`user:${userId}`);
cipher.setAAD(aad);Na descriptografia, forneça exatamente os mesmos bytes antes de validar a tag.
Chaves derivadas de senha
Não use uma senha diretamente como chave AES. Derive uma chave com salt e parâmetros adequados:
async function deriveKey(password, salt) {
return scrypt(password, salt, 32);
}Para dados persistentes, registre versão e parâmetros de derivação, permitindo aumentar o custo no futuro.
Assinaturas digitais
Assinaturas assimétricas usam chave privada para assinar e chave pública para verificar:
const signature = crypto.sign(
null,
Buffer.from(message),
privateKey
);
const valid = crypto.verify(
null,
Buffer.from(message),
publicKey,
signature
);O algoritmo depende do tipo da chave. Ed25519 é uma opção moderna e simples quando compatível com o ecossistema.
Gerando um par de chaves
const { publicKey, privateKey } =
crypto.generateKeyPairSync('ed25519');Em servidores, prefira a versão assíncrona quando a geração puder ser custosa. Exporte chaves em formatos padronizados e proteja a privada com controle de acesso ou serviço de gestão de chaves.
Web Crypto API
Node.js também oferece crypto.webcrypto e o objeto global crypto.subtle em versões atuais. A API segue padrões da Web e usa Promises. Escolha um estilo consistente e confirme suporte na versão mínima do projeto.
A documentação oficial de Web Crypto detalha algoritmos e formatos aceitos.
Gestão de chaves
Não coloque chaves em repositórios, imagens Docker ou logs. Use variáveis protegidas, arquivos montados com permissões restritas, secret managers ou KMS. Separe chaves por ambiente e finalidade.
Planeje rotação. Um registro criptografado deve indicar qual versão de chave foi usada. Durante uma transição, a aplicação pode descriptografar com versões antigas e gravar novos dados com a atual.
Não reutilize chaves para tudo
Use chaves diferentes para criptografia, HMAC e outros contextos. A separação limita o impacto de um vazamento e evita interações entre protocolos.
Evite algoritmos obsoletos
Não adote MD5 ou SHA-1 para segurança. Evite modos sem autenticação, como ECB, e não invente combinações de cifra, hash e padding. Algoritmos podem ser desabilitados conforme a configuração do OpenSSL.
Criptografia e desempenho
Derivação de senha e geração de chaves consomem CPU. Limite concorrência e evite parâmetros tão altos que permitam negação de serviço. Para tarefas pesadas, monitore o event loop e avalie Worker Threads em casos medidos.
Tratamento de erros
Não devolva detalhes internos de OpenSSL ao cliente. Registre uma categoria segura, um identificador de correlação e o contexto necessário. Diferencie dados inválidos de falhas de configuração.
Como testar
Use vetores conhecidos e confirme que:
- hashes e HMACs possuem resultados esperados;
- criptografar e descriptografar recupera o texto;
- alterar um byte invalida a tag;
- IVs são diferentes em operações sucessivas;
- chave incorreta falha;
- tokens têm tamanho e formato definidos;
- rotação de chave mantém compatibilidade.
Erros comuns
- Usar Math.random para tokens: valores podem ser previsíveis.
- Usar hash rápido para senha: ataques de força bruta ficam baratos.
- Repetir IV no GCM: a segurança da chave é comprometida.
- Confundir Base64 com proteção: conteúdo é facilmente reversível.
- Comparar assinatura com ===: diferenças de tempo podem vazar informação.
- Guardar chave no código: o segredo se espalha pelo histórico e artefatos.
- Não planejar rotação: trocar chave quebra dados existentes.
- Inventar protocolo: detalhes não revisados criam vulnerabilidades.
Boas práticas para produção
- Use primitivas modernas e documentadas.
- Gere tokens com
randomBytes(). - Derive senhas com scrypt ou Argon2.
- Use criptografia autenticada, como AES-GCM.
- Nunca repita nonce ou IV com a mesma chave.
- Compare segredos em tempo constante.
- Separe chaves por finalidade e ambiente.
- Implemente versionamento e rotação.
- Não registre chaves, tokens ou texto confidencial.
- Teste corrupção e parâmetros inválidos.
Conclusão
O Crypto no Node.js oferece recursos sólidos para hashes, HMACs, tokens, derivação de chaves, criptografia e assinaturas. Essas APIs evitam a necessidade de implementar algoritmos manualmente e se integram bem a Buffers e streams.
Segurança depende do uso correto. Escolha algoritmos modernos, mantenha chaves fora do código, garanta IVs únicos e trate autenticação como parte obrigatória da criptografia. Com gestão de segredos e testes, o módulo pode proteger dados e integrações de maneira confiável.




