Backend
bcrypt
Biblioteca para hash de senhas seguro com salt adaptativo
O que é bcrypt?
bcrypt é uma função de hash de senha baseada no cipher Blowfish, incorporando um salt para proteger contra ataques de rainbow table e força bruta. É projetada para ser computacionalmente custosa, tornando-se mais lenta conforme o poder computacional aumenta.
Por que utilizamos bcrypt na IngenioLab?
- Segurança comprovada: Padrão da indústria há mais de 20 anos
- Salt adaptativo: Protege contra rainbow tables automaticamente
- Cost adjustable: Permite ajustar complexidade conforme hardware
- Timing attack resistant: Tempo de execução consistente
- Widely supported: Disponível em múltiplas linguagens
- Future-proof: Resistente ao aumento do poder computacional
Instalação
npm install bcryptjsnpm install -D @types/bcryptjs# Alternativa (binding nativo, mais rápido)npm install bcryptnpm install -D @types/bcrypt
Nota: Usamos
bcryptjs(JavaScript puro) por compatibilidade, oubcrypt(binding nativo) para melhor performance.
Conceitos Fundamentais
1. Salt e Rounds:
import bcrypt from 'bcryptjs'// O "salt rounds" determina o custo computacionalconst saltRounds = 12 // Padrão IngenioLab// bcrypt gera salt automaticamenteconst password = 'minhasenha123'const hashedPassword = await bcrypt.hash(password, saltRounds)console.log(hashedPassword)// $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdHdw1KGFM7x.62// | | | |// | | |-- Salt (22 chars) |-- Hash (31 chars)// | |-- Cost factor (12)// |-- Algorithm version (2b)
2. Cost Factor (Rounds):
// Comparação de performance vs segurançaconst testPassword = 'teste123'// Rounds baixos (INSEGURO para produção)console.time('4 rounds')await bcrypt.hash(testPassword, 4) // ~1msconsole.timeEnd('4 rounds')// Rounds médios (mínimo recomendado)console.time('10 rounds')await bcrypt.hash(testPassword, 10) // ~65msconsole.timeEnd('10 rounds')// Rounds altos (recomendado IngenioLab)console.time('12 rounds')await bcrypt.hash(testPassword, 12) // ~260msconsole.timeEnd('12 rounds')// Rounds muito altos (para dados ultra-sensíveis)console.time('15 rounds')await bcrypt.hash(testPassword, 15) // ~2sconsole.timeEnd('15 rounds')
Implementação Padrão IngenioLab
1. Serviço de Hash de Senhas:
// src/services/passwordService.tsimport bcrypt from 'bcryptjs'import { env } from '../utils/env'export class PasswordService {// Cost factor da IngenioLabprivate static readonly SALT_ROUNDS = env.BCRYPT_ROUNDS || 12/*** Gera hash da senha com salt*/static async hashPassword(plainPassword: string): Promise<string> {if (!plainPassword) {throw new Error('Senha é obrigatória')}if (plainPassword.length < 8) {throw new Error('Senha deve ter pelo menos 8 caracteres')}try {return await bcrypt.hash(plainPassword, this.SALT_ROUNDS)} catch (error) {throw new Error('Erro ao processar senha')}}/*** Verifica se senha bate com hash*/static async comparePassword(plainPassword: string,hashedPassword: string): Promise<boolean> {if (!plainPassword || !hashedPassword) {return false}try {return await bcrypt.compare(plainPassword, hashedPassword)} catch (error) {// Log do erro mas retorna false por segurançaconsole.error('Erro na comparação de senha:', error)return false}}/*** Verifica se hash precisa ser atualizado (rehashing)*/static needsRehash(hashedPassword: string): boolean {try {const rounds = bcrypt.getRounds(hashedPassword)return rounds < this.SALT_ROUNDS} catch {return true // Hash inválido, precisa refazer}}/*** Gera senha aleatória segura*/static generateSecurePassword(length: number = 16): string {const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'let password = ''for (let i = 0; i < length; i++) {password += charset.charAt(Math.floor(Math.random() * charset.length))}return password}}
2. Integração com Autenticação:
// src/services/authService.tsimport { PasswordService } from './passwordService'import { userService } from './userService'import jwt from 'jsonwebtoken'import { env } from '../utils/env'export class AuthService {/*** Registrar novo usuário*/static async register(userData: {email: stringpassword: stringname: string}) {const { email, password, name } = userData// Verificar se usuário já existeconst existingUser = await userService.findByEmail(email)if (existingUser) {throw new Error('Email já está em uso')}// Hash da senhaconst hashedPassword = await PasswordService.hashPassword(password)// Criar usuárioconst user = await userService.createUser({email,name,password: hashedPassword})// Remover senha do retornoconst { password: _, ...userWithoutPassword } = userreturn userWithoutPassword}/*** Login do usuário*/static async login(email: string, password: string) {// Buscar usuárioconst user = await userService.findByEmail(email)if (!user) {throw new Error('Credenciais inválidas')}// Verificar senhaconst isValidPassword = await PasswordService.comparePassword(password,user.password)if (!isValidPassword) {throw new Error('Credenciais inválidas')}// Verificar se precisa rehash (upgrade de segurança)if (PasswordService.needsRehash(user.password)) {const newHashedPassword = await PasswordService.hashPassword(password)await userService.updatePassword(user.id, newHashedPassword)}// Gerar JWTconst token = jwt.sign({id: user.id,email: user.email,role: user.role},env.JWT_SECRET,{ expiresIn: '24h' })const { password: _, ...userWithoutPassword } = userreturn {user: userWithoutPassword,token}}/*** Alterar senha*/static async changePassword(userId: string,currentPassword: string,newPassword: string) {// Buscar usuárioconst user = await userService.findById(userId)if (!user) {throw new Error('Usuário não encontrado')}// Verificar senha atualconst isValidPassword = await PasswordService.comparePassword(currentPassword,user.password)if (!isValidPassword) {throw new Error('Senha atual incorreta')}// Hash da nova senhaconst hashedPassword = await PasswordService.hashPassword(newPassword)// Atualizar no bancoawait userService.updatePassword(userId, hashedPassword)return { message: 'Senha alterada com sucesso' }}}
3. Middleware do Prisma:
// src/lib/prisma-middleware.tsimport { PrismaClient } from '@prisma/client'import { PasswordService } from '../services/passwordService'export function addPasswordMiddleware(prisma: PrismaClient) {// Middleware para hash automático de senhasprisma.$use(async (params, next) => {// Interceptar operações em Userif (params.model === 'User') {if (params.action === 'create' || params.action === 'update') {const data = params.args.data// Se está definindo/atualizando senhaif (data.password && typeof data.password === 'string') {// Verificar se já não é um hashif (!data.password.startsWith('$2b$')) {data.password = await PasswordService.hashPassword(data.password)}}}}return next(params)})}// Uso no arquivo de configuração do Prisma// src/lib/prisma.tsimport { PrismaClient } from '@prisma/client'import { addPasswordMiddleware } from './prisma-middleware'export const prisma = new PrismaClient()addPasswordMiddleware(prisma)
4. Validação de Senhas:
// src/utils/passwordValidation.tsexport class PasswordValidator {private static readonly MIN_LENGTH = 8private static readonly MAX_LENGTH = 128/*** Validar força da senha*/static validate(password: string): {isValid: booleanerrors: string[]strength: 'weak' | 'medium' | 'strong'} {const errors: string[] = []// Comprimentoif (password.length < this.MIN_LENGTH) {errors.push(`Senha deve ter pelo menos ${this.MIN_LENGTH} caracteres`)}if (password.length > this.MAX_LENGTH) {errors.push(`Senha deve ter no máximo ${this.MAX_LENGTH} caracteres`)}// Caracteres obrigatóriosif (!/[a-z]/.test(password)) {errors.push('Deve conter pelo menos uma letra minúscula')}if (!/[A-Z]/.test(password)) {errors.push('Deve conter pelo menos uma letra maiúscula')}if (!/\d/.test(password)) {errors.push('Deve conter pelo menos um número')}if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {errors.push('Deve conter pelo menos um caractere especial')}// Padrões insegurosconst commonPasswords = ['password', '123456', 'admin', 'qwerty', 'letmein']if (commonPasswords.some(common =>password.toLowerCase().includes(common))) {errors.push('Não use senhas comuns ou previsíveis')}// Calcular forçalet strength: 'weak' | 'medium' | 'strong' = 'weak'if (errors.length === 0) {if (password.length >= 12 && this.hasVariedCharacters(password)) {strength = 'strong'} else {strength = 'medium'}}return {isValid: errors.length === 0,errors,strength}}private static hasVariedCharacters(password: string): boolean {const charSets = [/[a-z]/, // minúsculas/[A-Z]/, // maiúsculas/\d/, // números/[!@#$%^&*(),.?":{}|<>]/, // especiais]return charSets.filter(regex => regex.test(password)).length >= 3}}
Integração com Fastify
1. Plugin de Autenticação:
// src/plugins/auth.tsimport fp from 'fastify-plugin'import jwt from '@fastify/jwt'import { FastifyInstance } from 'fastify'import { AuthService } from '../services/authService'import { env } from '../utils/env'export default fp(async function (fastify: FastifyInstance) {await fastify.register(jwt, {secret: env.JWT_SECRET})// Decorator para autenticaçãofastify.decorate('authenticate', async function (request, reply) {try {await request.jwtVerify()} catch (err) {reply.code(401).send({ error: 'Token inválido' })}})// Rotas de autenticaçãofastify.post('/auth/register', {schema: {body: {type: 'object',required: ['email', 'password', 'name'],properties: {email: { type: 'string', format: 'email' },password: { type: 'string', minLength: 8 },name: { type: 'string', minLength: 1 }}}}}, async (request, reply) => {try {const result = await AuthService.register(request.body as any)reply.code(201).send(result)} catch (error) {reply.code(400).send({ error: error.message })}})fastify.post('/auth/login', {schema: {body: {type: 'object',required: ['email', 'password'],properties: {email: { type: 'string', format: 'email' },password: { type: 'string', minLength: 1 }}}}}, async (request, reply) => {try {const { email, password } = request.body as anyconst result = await AuthService.login(email, password)reply.send(result)} catch (error) {reply.code(401).send({ error: error.message })}})fastify.put('/auth/change-password', {preHandler: [fastify.authenticate],schema: {body: {type: 'object',required: ['currentPassword', 'newPassword'],properties: {currentPassword: { type: 'string' },newPassword: { type: 'string', minLength: 8 }}}}}, async (request, reply) => {try {const { currentPassword, newPassword } = request.body as anyconst userId = (request.user as any).idconst result = await AuthService.changePassword(userId,currentPassword,newPassword)reply.send(result)} catch (error) {reply.code(400).send({ error: error.message })}})})
Testes de Segurança
1. Testes Unitários:
// src/services/__tests__/passwordService.test.tsimport { describe, it, expect, vi } from 'vitest'import { PasswordService } from '../passwordService'describe('PasswordService', () => {describe('hashPassword', () => {it('should hash password successfully', async () => {const password = 'MinhaSenh@123'const hash = await PasswordService.hashPassword(password)expect(hash).toBeDefined()expect(hash).not.toBe(password)expect(hash.startsWith('$2b$')).toBe(true)})it('should generate different hashes for same password', async () => {const password = 'MinhaSenh@123'const hash1 = await PasswordService.hashPassword(password)const hash2 = await PasswordService.hashPassword(password)expect(hash1).not.toBe(hash2)})it('should throw error for empty password', async () => {await expect(PasswordService.hashPassword('')).rejects.toThrow('Senha é obrigatória')})it('should throw error for short password', async () => {await expect(PasswordService.hashPassword('123')).rejects.toThrow('Senha deve ter pelo menos 8 caracteres')})})describe('comparePassword', () => {it('should return true for correct password', async () => {const password = 'MinhaSenh@123'const hash = await PasswordService.hashPassword(password)const isValid = await PasswordService.comparePassword(password, hash)expect(isValid).toBe(true)})it('should return false for incorrect password', async () => {const password = 'MinhaSenh@123'const wrongPassword = 'SenhaErrada456'const hash = await PasswordService.hashPassword(password)const isValid = await PasswordService.comparePassword(wrongPassword, hash)expect(isValid).toBe(false)})it('should return false for empty inputs', async () => {const result1 = await PasswordService.comparePassword('', 'hash')const result2 = await PasswordService.comparePassword('password', '')expect(result1).toBe(false)expect(result2).toBe(false)})})describe('needsRehash', () => {it('should return true for low-cost hash', async () => {// Simular hash com rounds baixosconst lowCostHash = '$2b$04$abcdefghijklmnopqrstuv.0123456789012345678901'const needsRehash = PasswordService.needsRehash(lowCostHash)expect(needsRehash).toBe(true)})it('should return false for current-cost hash', async () => {const password = 'teste123'const currentHash = await PasswordService.hashPassword(password)const needsRehash = PasswordService.needsRehash(currentHash)expect(needsRehash).toBe(false)})})})
2. Testes de Performance:
// src/services/__tests__/passwordPerformance.test.tsimport { describe, it, expect } from 'vitest'import { PasswordService } from '../passwordService'describe('Password Performance', () => {it('should hash password within acceptable time', async () => {const password = 'MinhaSenh@123'const startTime = Date.now()await PasswordService.hashPassword(password)const endTime = Date.now()const duration = endTime - startTime// Deve ser menor que 1 segundo para rounds 12expect(duration).toBeLessThan(1000)// Mas maior que 100ms (garantindo que não está muito fraco)expect(duration).toBeGreaterThan(100)})it('should handle concurrent hashing', async () => {const password = 'MinhaSenh@123'const concurrentOperations = 5const startTime = Date.now()const promises = Array(concurrentOperations).fill(0).map(() =>PasswordService.hashPassword(password))const results = await Promise.all(promises)const endTime = Date.now()// Todos devem ter resultadoexpect(results).toHaveLength(concurrentOperations)results.forEach(hash => {expect(hash).toBeDefined()expect(hash.startsWith('$2b$')).toBe(true)})// Todos devem ser diferentesconst uniqueHashes = new Set(results)expect(uniqueHashes.size).toBe(concurrentOperations)console.log(`${concurrentOperations} hashes em ${endTime - startTime}ms`)})})
Configuração de Ambiente
# .envBCRYPT_ROUNDS=12 # Cost factor para hashingJWT_SECRET=sua-chave-jwt-super-secreta-aqui
Boas Práticas de Segurança
1. ✅ Faça:
- Use salt rounds ≥ 12 em produção
- Sempre use async/await para operações bcrypt
- Implemente rate limiting para login
- Valide força da senha no frontend E backend
- Use rehashing para upgrades de segurança
- Nunca logue senhas em texto puro
2. ❌ Evite:
- Usar MD5, SHA1 ou hashes simples
- Implementar seu próprio algoritmo de hash
- Armazenar senhas em texto puro
- Usar salt rounds < 10
- Confiar apenas em validação frontend
- Retornar detalhes específicos em erros de login
3. Monitoramento:
// src/utils/securityMonitoring.tsexport class SecurityMonitoring {static logFailedLogin(email: string, ip: string) {console.warn(`Failed login attempt: ${email} from ${ip}`)// Integrar com sistema de monitoramento}static logPasswordChange(userId: string) {console.info(`Password changed for user: ${userId}`)}static alertBruteForce(email: string, attempts: number) {console.error(`Brute force detected: ${email} - ${attempts} attempts`)// Alertar equipe de segurança}}