Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 62 – API JavaScript, Node.js e Express – Password Hashing – bcrypt, scrypt, argon2

Imagem destacada da aula de API

Introdução

Olá, futuro especialista em APIs! Sejam muito bem-vindos à Aula 62, onde mergulharemos em um tópico de segurança cibernética que é absolutamente vital para qualquer aplicação web moderna: o Password Hashing. Seus usuários confiam a você suas informações mais sensíveis, e proteger as senhas é a pedra angular dessa confiança.

Imagine suas senhas como segredos guardados em um cofre digital. Se alguém consegue abrir o cofre e ver o segredo em claro, a segurança está comprometida. O Password Hashing é como transformar esse segredo em um rabisco indecifrável antes de colocá-lo no cofre. Mesmo que o cofre seja arrombado (o banco de dados seja vazado), os invasores terão apenas rabiscos, e não as senhas originais. É uma medida de proteção indispensável.

Nesta aula, vamos aplicar e compreender as técnicas robustas de hashing de senhas usando algoritmos de ponta como bcrypt, scrypt e argon2. Estes não são meros algoritmos de criptografia; são funções de derivação de chave (KDFs) projetadas especificamente para tornar o processo de quebra de senhas exponencialmente mais difícil.

Dentro do ecossistema Node.js e Express, aprender a implementar corretamente o hashing é um diferencial. Ele se integra perfeitamente à lógica de autenticação do seu backend, blindando seus usuários contra vazamentos de dados e ataques de força bruta, elevando a resiliência da sua API a um novo patamar.

Conceito Fundamental

Em sua essência, o hashing de senhas é o processo de transformar uma senha de texto claro (plaintext) em uma sequência de caracteres de tamanho fixo, irreversível e aparentemente aleatória, conhecida como hash ou digest. É crucial entender que hashing não é criptografia. Enquanto a criptografia permite reverter um texto cifrado para o original usando uma chave, o hashing é uma função unidirecional: uma vez que a senha é “hasheada”, não há como obter a senha original a partir do hash.

A terminologia correta da indústria inclui:

    • Hash: O resultado final da transformação da senha.
    • Salt (Sal): Uma sequência de dados aleatórios única que é adicionada à senha antes do hashing. O uso de sal é fundamental para prevenir ataques de dicionário e rainbow tables, que consistem em bancos de dados pré-calculados de hashes para senhas comuns. Com um sal único para cada senha, cada hash se torna distinto, mesmo que duas senhas sejam idênticas.
    • Cost Factor (Fator de Custo) / Work Factor (Fator de Trabalho): Um parâmetro que define o número de iterações ou o esforço computacional que o algoritmo de hashing deve realizar. Um fator de custo mais alto significa que o processo leva mais tempo e consome mais CPU, tornando os ataques de força bruta proibitivamente caros e lentos.
    • Key Derivation Function (KDF): Funções projetadas especificamente para derivar chaves criptográficas (ou hashes de senhas) de senhas de baixa entropia. bcrypt, scrypt e argon2 são exemplos de KDFs.

Casos de uso reais em produção são abundantes. Toda vez que você se registra em um novo serviço online ou faz login, o sistema por trás está aplicando hashing de senhas. Desde grandes plataformas de redes sociais até aplicativos bancários, a segurança das credenciais é mantida através dessas técnicas. No contexto de APIs, isso se integra perfeitamente com a autenticação de usuários: ao registrar um usuário, a senha é hasheada e armazenada; ao tentar fazer login, a senha fornecida é hasheada novamente e comparada com o hash armazenado.

As vantagens de usar bcrypt, scrypt ou argon2 são significativas:

    • Resistência a Força Bruta: O fator de custo torna cada tentativa de adivinhação extremamente lenta.
    • Proteção contra Rainbow Tables: O sal único garante que senhas iguais gerem hashes diferentes.
    • Evolução da Segurança: O fator de custo pode ser aumentado com o tempo para acompanhar o avanço da capacidade computacional dos invasores.

No entanto, há algumas desvantagens a serem consideradas:

    • Consumo de CPU: O processo de hashing é intencionalmente lento, o que pode impactar ligeiramente o desempenho do servidor em picos de autenticação. Contudo, em uma escala de API, o ganho de segurança ultrapassa em muito essa pequena latência.
    • Complexidade de Implementação: Requer um entendimento cuidadoso para ser implementado corretamente, mas esta aula irá facilitar esse caminho.

Implementação Prática

Para esta demonstração, focaremos em bcrypt, pois é amplamente adotado e oferece um excelente equilíbrio entre segurança e desempenho. Mencionaremos as alternativas scrypt e argon2 como opções ainda mais robustas para cenários específicos. Nosso código rodará em um ambiente Node.js/Express e será 100% compatível com seu HostGator Plano M.

Primeiro, crie um novo projeto Node.js e instale as dependências:


mkdir password-hashing-api
cd password-hashing-api
npm init -y
npm install express bcrypt

Agora, vamos criar nosso arquivo principal, por exemplo, app.js:


// app.js
const express = require('express');
const bcrypt = require('bcrypt'); // Importa a biblioteca bcrypt para hashing
const app = express();
const PORT = process.env.PORT || 3000;

// Habilita o parsing de JSON para requisições POST app.use(express.json());

// Nosso "banco de dados" em memória para fins de demonstração. // Em produção, isso seria um banco de dados real como MongoDB, PostgreSQL, etc. const users = [];

// --- Endpoint de Registro de Usuário --- app.post('/register', async (req, res) => { // Validação de entrada básica const { username, password } = req.body; if (!username || !password) { // Logging profissional: registra a tentativa inválida console.warn('Tentativa de registro com dados incompletos.'); return res.status(400).json({ message: 'Nome de usuário e senha são obrigatórios.' }); }

// Verifica se o usuário já existe if (users.find(u => u.username === username)) { console.warn(Tentativa de registro de usuário existente: ${username}); return res.status(409).json({ message: 'Nome de usuário já existe.' }); // 409 Conflict }

try { // --- Melhor prática enterprise: Gerar um salt com um fator de custo adequado --- // O valor de saltRounds determina o trabalho computacional. // Valores entre 10 e 12 são geralmente bons para aplicações web. // Um valor maior aumenta a segurança, mas também o tempo de hashing. const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds);

// Armazena o usuário (apenas o hash da senha, NUNCA a senha em texto claro) users.push({ username, hashedPassword });

// Logging profissional: confirma o registro console.log(Usuário registrado com sucesso: ${username}); res.status(201).json({ message: 'Usuário registrado com sucesso!' }); } catch (error) { // Error handling robusto: registra o erro e retorna uma resposta genérica console.error('Erro ao registrar usuário:', error.message); res.status(500).json({ message: 'Erro interno do servidor ao registrar usuário.' }); } });

// --- Endpoint de Login de Usuário --- app.post('/login', async (req, res) => { // Validação de entrada const { username, password } = req.body; if (!username || !password) { console.warn('Tentativa de login com dados incompletos.'); return res.status(400).json({ message: 'Nome de usuário e senha são obrigatórios.' }); }

// Busca o usuário em nosso "banco de dados" const user = users.find(u => u.username === username); if (!user) { console.warn(Tentativa de login falha para usuário inexistente: ${username}); // É uma boa prática não informar se o usuário existe ou não por questões de segurança (timing attacks) return res.status(401).json({ message: 'Credenciais inválidas.' }); // 401 Unauthorized }

try { // --- Comparação segura da senha --- // Compara a senha fornecida pelo usuário com o hash armazenado. // bcrypt.compare() lida com o salt automaticamente. const isMatch = await bcrypt.compare(password, user.hashedPassword);

if (isMatch) { console.log(Login bem-sucedido para: ${username}); res.status(200).json({ message: 'Login bem-sucedido!' }); } else { console.warn(Tentativa de login falha (senha incorreta) para: ${username}); res.status(401).json({ message: 'Credenciais inválidas.' }); } } catch (error) { console.error('Erro ao fazer login:', error.message); res.status(500).json({ message: 'Erro interno do servidor ao fazer login.' }); } });

// Inicia o servidor app.listen(PORT, () => { console.log(Servidor rodando na porta ${PORT}); console.log(Para testar:); console.log( Registro: POST http://localhost:${PORT}/register); console.log( Login: POST http://localhost:${PORT}/login); });

/ Alternativas para bcrypt:

- scrypt: Geralmente considerado mais robusto que bcrypt, pois exige mais memória além de CPU, dificultando ataques baseados em hardware (ASICs). Ex: require('crypto').scrypt(password, salt, keylen, options, callback); Ou usar uma lib como 'scrypt-js'.

- argon2: Atualmente, considerado o algoritmo mais seguro e recomendado pelo Password Hashing Competition. Também exige memória e CPU, com opções mais avançadas para otimização. Ex: require('argon2').hash(password, options); require('argon2').verify(hash, password);

Para HostGator Plano M: Todas essas bibliotecas (bcrypt, scrypt-js, argon2) são compatíveis. As versões nativas (como a 'bcrypt' que estamos usando) podem precisar de ferramentas de compilação (gcc, python) no ambiente do servidor para serem instaladas. Caso haja problemas, 'bcryptjs' é uma alternativa puramente JavaScript para bcrypt, que não requer compilação e é 100% compatível, mas pode ser um pouco mais lenta. /

Para rodar a aplicação, execute no terminal:


node app.js

Você verá a mensagem: Servidor rodando na porta 3000.

Testes básicos com curl (ou Postman/Insomnia):

1. Registrar um novo usuário:


curl -X POST -H "Content-Type: application/json" -d '{"username": "aluno", "password": "minhasenha123"}' http://localhost:3000/register

Resposta esperada (201 Created):


{"message": "Usuário registrado com sucesso!"}

2. Tentar registrar o mesmo usuário (deve falhar):


curl -X POST -H "Content-Type: application/json" -d '{"username": "aluno", "password": "outrasenha"}' http://localhost:3000/register

Resposta esperada (409 Conflict):


{"message": "Nome de usuário já existe."}

3. Fazer login com credenciais corretas:


curl -X POST -H "Content-Type: application/json" -d '{"username": "aluno", "password": "minhasenha123"}' http://localhost:3000/login

Resposta esperada (200 OK):


{"message": "Login bem-sucedido!"}

4. Fazer login com senha incorreta:


curl -X POST -H "Content-Type: application/json" -d '{"username": "aluno", "password": "senhaerrada"}' http://localhost:3000/login

Resposta esperada (401 Unauthorized):


{"message": "Credenciais inválidas."}

5. Fazer login com usuário inexistente:


curl -X POST -H "Content-Type: application/json" -d '{"username": "naoexiste", "password": "qualquersenha"}' http://localhost:3000/login

Resposta esperada (401 Unauthorized):


{"message": "Credenciais inválidas."}

Exercício Hands-On

Agora é a sua vez de solidificar o conhecimento! Seu desafio é implementar um endpoint para atualização de senha. Este endpoint deve:

    • Aceitar um username, a oldPassword (senha atual) e a newPassword (nova senha).
    • Primeiro, autenticar o usuário verificando se a oldPassword corresponde ao hash armazenado.
    • Se a autenticação for bem-sucedida, hashear a newPassword e atualizar o registro do usuário em nosso “banco de dados”.
    • Retornar mensagens de sucesso ou erro apropriadas.

Solução detalhada passo a passo:

Adicione o seguinte código ao seu app.js, logo abaixo do endpoint /login:


// --- Endpoint de Atualização de Senha ---
app.put('/update-password', async (req, res) => {
    // 1. Validação de entrada robusta
    const { username, oldPassword, newPassword } = req.body;
    if (!username || !oldPassword || !newPassword) {
        console.warn('Tentativa de atualização de senha com dados incompletos.');
        return res.status(400).json({ message: 'Nome de usuário, senha antiga e nova senha são obrigatórios.' });
    }

// 2. Busca o usuário const user = users.find(u => u.username === username); if (!user) { console.warn(Tentativa de atualização de senha para usuário inexistente: ${username}); return res.status(401).json({ message: 'Credenciais inválidas.' }); }

try { // 3. Autentica o usuário com a senha antiga const isMatch = await bcrypt.compare(oldPassword, user.hashedPassword);

if (!isMatch) { console.warn(Falha na atualização de senha (senha antiga incorreta) para: ${username}); return res.status(401).json({ message: 'Senha antiga inválida.' }); }

// 4. Hasheia a nova senha const saltRounds = 10; // Mantém o mesmo fator de custo const newHashedPassword = await bcrypt.hash(newPassword, saltRounds);

// 5. Atualiza o registro do usuário user.hashedPassword = newHashedPassword;

console.log(Senha atualizada com sucesso para o usuário: ${username}); res.status(200).json({ message: 'Senha atualizada com sucesso!' });

} catch (error) { console.error('Erro ao atualizar senha:', error.message); res.status(500).json({ message: 'Erro interno do servidor ao atualizar senha.' }); } });

Como testar e validar o resultado:

Reinicie o seu servidor Node.js (Ctrl+C e node app.js).

1. Tente atualizar a senha com a senha antiga incorreta:


curl -X PUT -H "Content-Type: application/json" -d '{"username": "aluno", "oldPassword": "senhaerrada", "newPassword": "nova_senha_segura"}' http://localhost:3000/update-password

Resposta esperada (401 Unauthorized):


{"message": "Senha antiga inválida."}

2. Atualize a senha com sucesso:


curl -X PUT -H "Content-Type: application/json" -d '{"username": "aluno", "oldPassword": "minhasenha123", "newPassword": "nova_senha_segura"}' http://localhost:3000/update-password

Resposta esperada (200 OK):


{"message": "Senha atualizada com sucesso!"}

3. Verifique se a nova senha funciona para login (e a antiga não):

Login com a senha antiga (deve falhar):


curl -X POST -H "Content-Type: application/json" -d '{"username": "aluno", "password": "minhasenha123"}' http://localhost:3000/login

Login com a nova senha (deve ter sucesso):


curl -X POST -H "Content-Type: application/json" -d '{"username": "aluno", "password": "nova_senha_segura"}' http://localhost:3000/login

Troubleshooting dos erros mais comuns:

    • Erro TypeError: callback is not a function ou similar: Geralmente indica que você está usando bcrypt de forma síncrona (ex: bcrypt.hashSync) ou misturando async/await com callbacks. Certifique-se de usar as versões assíncronas (await bcrypt.hash()) e que seus endpoints Express são async.
    • Sempre falha no bcrypt.compare(): Verifique se você está armazenando e recuperando o hash corretamente do seu “banco de dados”. O hash completo (incluindo o sal) deve ser comparado.
    • Servidor não inicia devido a dependências nativas (bcrypt): Se estiver em um ambiente limitado (como algumas configurações de HostGator), a compilação de módulos nativos pode falhar. Troque bcrypt por bcryptjs (npm uninstall bcrypt; npm install bcryptjs) e ajuste const bcrypt = require('bcryptjs'); no seu código. bcryptjs é uma alternativa puramente JavaScript.

Próximos passos sugeridos:

    • Integre este sistema com um banco de dados real (MongoDB, PostgreSQL) para persistência de dados.
    • Implemente um sistema de token de autenticação como JWT (JSON Web Tokens) para gerenciar sessões de usuários após o login bem-sucedido.
    • Explore a implementação de argon2 ou scrypt para entender suas nuances e benefícios adicionais.
    • Adicione um mecanismo de recuperação de senha (com redefinição via token por e-mail, por exemplo).

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas