Fumadocs + Code Hike
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 bcryptjs
npm install -D @types/bcryptjs
# Alternativa (binding nativo, mais rápido)
npm install bcrypt
npm install -D @types/bcrypt

Nota: Usamos bcryptjs (JavaScript puro) por compatibilidade, ou bcrypt (binding nativo) para melhor performance.

Conceitos Fundamentais

1. Salt e Rounds:

import bcrypt from 'bcryptjs'
// O "salt rounds" determina o custo computacional
const saltRounds = 12 // Padrão IngenioLab
// bcrypt gera salt automaticamente
const 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ça
const testPassword = 'teste123'
// Rounds baixos (INSEGURO para produção)
console.time('4 rounds')
await bcrypt.hash(testPassword, 4) // ~1ms
console.timeEnd('4 rounds')
// Rounds médios (mínimo recomendado)
console.time('10 rounds')
await bcrypt.hash(testPassword, 10) // ~65ms
console.timeEnd('10 rounds')
// Rounds altos (recomendado IngenioLab)
console.time('12 rounds')
await bcrypt.hash(testPassword, 12) // ~260ms
console.timeEnd('12 rounds')
// Rounds muito altos (para dados ultra-sensíveis)
console.time('15 rounds')
await bcrypt.hash(testPassword, 15) // ~2s
console.timeEnd('15 rounds')

Implementação Padrão IngenioLab

1. Serviço de Hash de Senhas:

// src/services/passwordService.ts
import bcrypt from 'bcryptjs'
import { env } from '../utils/env'
export class PasswordService {
// Cost factor da IngenioLab
private 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ça
console.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.ts
import { 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: string
password: string
name: string
}) {
const { email, password, name } = userData
// Verificar se usuário já existe
const existingUser = await userService.findByEmail(email)
if (existingUser) {
throw new Error('Email já está em uso')
}
// Hash da senha
const hashedPassword = await PasswordService.hashPassword(password)
// Criar usuário
const user = await userService.createUser({
email,
name,
password: hashedPassword
})
// Remover senha do retorno
const { password: _, ...userWithoutPassword } = user
return userWithoutPassword
}
/**
* Login do usuário
*/
static async login(email: string, password: string) {
// Buscar usuário
const user = await userService.findByEmail(email)
if (!user) {
throw new Error('Credenciais inválidas')
}
// Verificar senha
const 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 JWT
const token = jwt.sign(
{
id: user.id,
email: user.email,
role: user.role
},
env.JWT_SECRET,
{ expiresIn: '24h' }
)
const { password: _, ...userWithoutPassword } = user
return {
user: userWithoutPassword,
token
}
}
/**
* Alterar senha
*/
static async changePassword(
userId: string,
currentPassword: string,
newPassword: string
) {
// Buscar usuário
const user = await userService.findById(userId)
if (!user) {
throw new Error('Usuário não encontrado')
}
// Verificar senha atual
const isValidPassword = await PasswordService.comparePassword(
currentPassword,
user.password
)
if (!isValidPassword) {
throw new Error('Senha atual incorreta')
}
// Hash da nova senha
const hashedPassword = await PasswordService.hashPassword(newPassword)
// Atualizar no banco
await userService.updatePassword(userId, hashedPassword)
return { message: 'Senha alterada com sucesso' }
}
}

3. Middleware do Prisma:

// src/lib/prisma-middleware.ts
import { PrismaClient } from '@prisma/client'
import { PasswordService } from '../services/passwordService'
export function addPasswordMiddleware(prisma: PrismaClient) {
// Middleware para hash automático de senhas
prisma.$use(async (params, next) => {
// Interceptar operações em User
if (params.model === 'User') {
if (params.action === 'create' || params.action === 'update') {
const data = params.args.data
// Se está definindo/atualizando senha
if (data.password && typeof data.password === 'string') {
// Verificar se já não é um hash
if (!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.ts
import { PrismaClient } from '@prisma/client'
import { addPasswordMiddleware } from './prisma-middleware'
export const prisma = new PrismaClient()
addPasswordMiddleware(prisma)

4. Validação de Senhas:

// src/utils/passwordValidation.ts
export class PasswordValidator {
private static readonly MIN_LENGTH = 8
private static readonly MAX_LENGTH = 128
/**
* Validar força da senha
*/
static validate(password: string): {
isValid: boolean
errors: string[]
strength: 'weak' | 'medium' | 'strong'
} {
const errors: string[] = []
// Comprimento
if (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órios
if (!/[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 inseguros
const 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ça
let 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.ts
import 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ção
fastify.decorate('authenticate', async function (request, reply) {
try {
await request.jwtVerify()
} catch (err) {
reply.code(401).send({ error: 'Token inválido' })
}
})
// Rotas de autenticação
fastify.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 any
const 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 any
const userId = (request.user as any).id
const 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.ts
import { 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 baixos
const 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.ts
import { 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 12
expect(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 = 5
const 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 resultado
expect(results).toHaveLength(concurrentOperations)
results.forEach(hash => {
expect(hash).toBeDefined()
expect(hash.startsWith('$2b$')).toBe(true)
})
// Todos devem ser diferentes
const uniqueHashes = new Set(results)
expect(uniqueHashes.size).toBe(concurrentOperations)
console.log(`${concurrentOperations} hashes em ${endTime - startTime}ms`)
})
})

Configuração de Ambiente

# .env
BCRYPT_ROUNDS=12 # Cost factor para hashing
JWT_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.ts
export 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
}
}