🔐 Security hardening: auth, rate limiting, brute force protection

- Add comprehensive security package with:
  - API Key generation and validation (SHA256 hash)
  - Password policy enforcement (min 12 chars, complexity)
  - Rate limiting with presets (auth, api, ingest, export)
  - Brute force protection (5 attempts, 15min lockout)
  - Security headers middleware
  - IP whitelisting
  - Audit logging structure
  - Secure token generation

- Enhanced auth middleware:
  - JWT + API Key dual authentication
  - Token revocation via Redis
  - Scope-based authorization
  - Role-based access control

- Updated installer with:
  - Interactive setup for client customization
  - Auto-generated secure credentials
  - Docker all-in-one image
  - Agent installer script

- Added documentation:
  - SECURITY.md - Complete security guide
  - INSTALL.md - Installation guide
  - .env.example - Configuration reference
This commit is contained in:
2026-02-05 23:02:06 -03:00
parent dbf9f0497f
commit a94809c812
11 changed files with 2637 additions and 444 deletions

View File

@@ -1,27 +1,265 @@
package api
import (
"fmt"
"strconv"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
)
func RateLimitMiddleware() fiber.Handler {
return limiter.New(limiter.Config{
Max: 100, // 100 requests
Expiration: 1 * time.Minute, // per minute
// ═══════════════════════════════════════════════════════════
// 🚦 RATE LIMITING MIDDLEWARE
// ═══════════════════════════════════════════════════════════
// RateLimitConfig configuração do rate limiter
type RateLimitConfig struct {
// Requests máximos por janela
Max int
// Janela de tempo
Window time.Duration
// Função para extrair identificador (default: IP)
KeyGenerator func(*fiber.Ctx) string
// Função para resposta quando limitado
LimitReached func(*fiber.Ctx) error
// Pular rate limit para certos paths
SkipPaths []string
// Headers customizados
Headers RateLimitHeaders
}
// RateLimitHeaders headers do rate limit
type RateLimitHeaders struct {
Limit string // X-RateLimit-Limit
Remaining string // X-RateLimit-Remaining
Reset string // X-RateLimit-Reset
RetryAfter string // Retry-After
}
// DefaultRateLimitConfig configuração padrão
func DefaultRateLimitConfig() RateLimitConfig {
return RateLimitConfig{
Max: 100,
Window: time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
// Use API key or IP for rate limiting
if key := c.Locals("api_key"); key != nil {
return key.(string)
}
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(fiber.Map{
"error": "Rate limit exceeded",
"error": "Too Many Requests",
"message": "Rate limit exceeded. Please slow down.",
"retry_after": 60,
})
},
Headers: RateLimitHeaders{
Limit: "X-RateLimit-Limit",
Remaining: "X-RateLimit-Remaining",
Reset: "X-RateLimit-Reset",
RetryAfter: "Retry-After",
},
}
}
// rateLimitStore armazena contagem de requests
type rateLimitStore struct {
sync.RWMutex
entries map[string]*rateLimitEntry
}
type rateLimitEntry struct {
count int
expiresAt time.Time
}
var store = &rateLimitStore{
entries: make(map[string]*rateLimitEntry),
}
// Limpar entries expirados periodicamente
func init() {
go func() {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
store.cleanup()
}
}()
}
func (s *rateLimitStore) cleanup() {
s.Lock()
defer s.Unlock()
now := time.Now()
for key, entry := range s.entries {
if now.After(entry.expiresAt) {
delete(s.entries, key)
}
}
}
// RateLimit middleware de rate limiting
func RateLimit(config ...RateLimitConfig) fiber.Handler {
cfg := DefaultRateLimitConfig()
if len(config) > 0 {
cfg = config[0]
}
return func(c *fiber.Ctx) error {
// Verificar skip paths
path := c.Path()
for _, skip := range cfg.SkipPaths {
if path == skip {
return c.Next()
}
}
// Gerar key
key := cfg.KeyGenerator(c)
// Verificar/atualizar contagem
store.Lock()
now := time.Now()
entry, exists := store.entries[key]
if !exists || now.After(entry.expiresAt) {
// Nova janela
store.entries[key] = &rateLimitEntry{
count: 1,
expiresAt: now.Add(cfg.Window),
}
store.Unlock()
// Headers
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1))
c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10))
return c.Next()
}
entry.count++
remaining := cfg.Max - entry.count
resetTime := entry.expiresAt.Unix()
store.Unlock()
// Headers
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining)))
c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10))
// Verificar se excedeu
if remaining < 0 {
retryAfter := int(time.Until(entry.expiresAt).Seconds())
c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter)))
return cfg.LimitReached(c)
}
return c.Next()
}
}
// ═══════════════════════════════════════════════════════════
// 🎯 RATE LIMIT PRESETS
// ═══════════════════════════════════════════════════════════
// RateLimitAuth rate limit para endpoints de autenticação (mais restritivo)
func RateLimitAuth() fiber.Handler {
return RateLimit(RateLimitConfig{
Max: 5,
Window: time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
// Combinar IP + email para prevenir ataques distribuídos
email := c.FormValue("email")
if email == "" {
email = c.Query("email")
}
return fmt.Sprintf("auth:%s:%s", c.IP(), email)
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(fiber.Map{
"error": "Too Many Login Attempts",
"message": "You have exceeded the maximum number of login attempts. Please wait before trying again.",
"retry_after": 60,
})
},
})
}
// RateLimitAPI rate limit para API geral
func RateLimitAPI() fiber.Handler {
return RateLimit(RateLimitConfig{
Max: 100,
Window: time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
// Usar API key ou IP
if apiKey := c.Locals("api_key_id"); apiKey != nil {
return fmt.Sprintf("api:%v", apiKey)
}
if userID := c.Locals("user_id"); userID != nil {
return fmt.Sprintf("user:%v", userID)
}
return fmt.Sprintf("ip:%s", c.IP())
},
})
}
// RateLimitIngest rate limit para ingestão de dados (mais permissivo)
func RateLimitIngest() fiber.Handler {
return RateLimit(RateLimitConfig{
Max: 1000,
Window: time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
if orgID := c.Locals("org_id"); orgID != nil {
return fmt.Sprintf("ingest:%v", orgID)
}
return fmt.Sprintf("ingest:ip:%s", c.IP())
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(fiber.Map{
"error": "Ingest Rate Limit Exceeded",
"message": "Your organization has exceeded the data ingestion rate limit.",
"retry_after": 10,
})
},
})
}
// RateLimitExport rate limit para exportação de dados
func RateLimitExport() fiber.Handler {
return RateLimit(RateLimitConfig{
Max: 10,
Window: time.Hour,
KeyGenerator: func(c *fiber.Ctx) string {
if userID := c.Locals("user_id"); userID != nil {
return fmt.Sprintf("export:%v", userID)
}
return fmt.Sprintf("export:ip:%s", c.IP())
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(fiber.Map{
"error": "Export Rate Limit Exceeded",
"message": "You have exceeded the maximum number of exports per hour.",
"retry_after": 3600,
})
},
})
}
// ═══════════════════════════════════════════════════════════
// 🔧 HELPERS
// ═══════════════════════════════════════════════════════════
func max(a, b int) int {
if a > b {
return a
}
return b
}

View File

@@ -1,93 +1,373 @@
package auth
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/bigtux/ophion/internal/security"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/redis/go-redis/v9"
)
var jwtSecret []byte
// ═══════════════════════════════════════════════════════════
// 🔐 AUTH CONFIG
// ═══════════════════════════════════════════════════════════
func Init(secret string) {
jwtSecret = []byte(secret)
type AuthConfig struct {
JWTSecret []byte
JWTExpiration time.Duration
RefreshExpiration time.Duration
Issuer string
}
// GenerateAPIKey creates a new API key for agents
func GenerateAPIKey() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return "ophion_" + hex.EncodeToString(bytes)
type AuthService struct {
config AuthConfig
redis *redis.Client
rateLimiter *security.RateLimiter
loginTracker *security.LoginAttemptTracker
apiKeyStore APIKeyStore
}
// GenerateJWT creates a JWT token for users
func GenerateJWT(userID string, email string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"email": email,
"iat": time.Now().Unix(),
"exp": time.Now().Add(24 * time.Hour).Unix(),
// APIKeyStore interface para storage de API keys
type APIKeyStore interface {
ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error)
UpdateLastUsed(ctx context.Context, keyID string) error
}
type APIKeyInfo struct {
ID string
OrgID string
Scopes []string
Name string
}
// NewAuthService cria serviço de autenticação
func NewAuthService(config AuthConfig, redis *redis.Client, apiKeyStore APIKeyStore) *AuthService {
return &AuthService{
config: config,
redis: redis,
rateLimiter: security.NewRateLimiter(security.AuthRateLimit, time.Minute),
loginTracker: security.NewLoginAttemptTracker(),
apiKeyStore: apiKeyStore,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// ValidateJWT validates a JWT token
func ValidateJWT(tokenString string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
// ═══════════════════════════════════════════════════════════
// 🎫 JWT TOKENS
// ═══════════════════════════════════════════════════════════
type TokenClaims struct {
jwt.RegisteredClaims
UserID string `json:"uid"`
OrgID string `json:"oid"`
Email string `json:"email"`
Role string `json:"role"`
Scopes []string `json:"scopes,omitempty"`
TokenID string `json:"jti"`
}
// GenerateTokenPair gera access + refresh tokens
func (s *AuthService) GenerateTokenPair(userID, orgID, email, role string) (accessToken, refreshToken string, err error) {
now := time.Now()
tokenID := generateTokenID()
// Access Token (curta duração)
accessClaims := TokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: s.config.Issuer,
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.JWTExpiration)),
ID: tokenID,
},
UserID: userID,
OrgID: orgID,
Email: email,
Role: role,
TokenID: tokenID,
}
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.JWTSecret)
if err != nil {
return "", "", fmt.Errorf("failed to sign access token: %w", err)
}
// Refresh Token (longa duração)
refreshID := generateTokenID()
refreshClaims := jwt.RegisteredClaims{
Issuer: s.config.Issuer,
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.RefreshExpiration)),
ID: refreshID,
}
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.config.JWTSecret)
if err != nil {
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
}
// Armazenar refresh token no Redis (permite revogação)
if s.redis != nil {
ctx := context.Background()
key := fmt.Sprintf("refresh_token:%s", refreshID)
s.redis.Set(ctx, key, userID, s.config.RefreshExpiration)
}
return accessToken, refreshToken, nil
}
// ValidateAccessToken valida access token
func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.config.JWTSecret, nil
})
if err != nil || !token.Valid {
return nil, err
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, jwt.ErrInvalidKey
claims, ok := token.Claims.(*TokenClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token claims")
}
return &claims, nil
// Verificar se token foi revogado
if s.redis != nil {
ctx := context.Background()
revoked, _ := s.redis.Get(ctx, fmt.Sprintf("revoked_token:%s", claims.TokenID)).Result()
if revoked != "" {
return nil, fmt.Errorf("token revoked")
}
}
return claims, nil
}
// AuthMiddleware protects routes
func AuthMiddleware() fiber.Handler {
// RevokeToken revoga um token
func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error {
if s.redis == nil {
return nil
}
ctx := context.Background()
return s.redis.Set(ctx, fmt.Sprintf("revoked_token:%s", tokenID), "1", expiration).Err()
}
// ═══════════════════════════════════════════════════════════
// 🛡️ MIDDLEWARE
// ═══════════════════════════════════════════════════════════
// AuthMiddleware middleware de autenticação
func (s *AuthService) AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
// Obter IP real (considerando proxies)
ip := c.IP()
if forwarded := c.Get("X-Forwarded-For"); forwarded != "" {
ip = strings.Split(forwarded, ",")[0]
}
// Rate limiting por IP
if !s.rateLimiter.Allow(ip) {
return c.Status(429).JSON(fiber.Map{
"error": "Too many requests",
"message": "Rate limit exceeded. Try again later.",
})
}
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(401).JSON(fiber.Map{
"error": "Missing authorization header",
"error": "Unauthorized",
"message": "Missing authorization header",
})
}
// Support both "Bearer <token>" and API keys
token := strings.TrimPrefix(authHeader, "Bearer ")
// Check if it's an API key
if strings.HasPrefix(token, "ophion_") {
// TODO: Validate API key against database
c.Locals("auth_type", "api_key")
c.Locals("api_key", token)
return c.Next()
// API Key authentication
if strings.HasPrefix(token, security.APIKeyPrefix) {
return s.authenticateAPIKey(c, token)
}
// JWT authentication
return s.authenticateJWT(c, token)
}
}
// Validate JWT
claims, err := ValidateJWT(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{
"error": "Invalid token",
// authenticateAPIKey valida API key
func (s *AuthService) authenticateAPIKey(c *fiber.Ctx, apiKey string) error {
// Validar formato
if !security.ValidateAPIKeyFormat(apiKey) {
return c.Status(401).JSON(fiber.Map{
"error": "Unauthorized",
"message": "Invalid API key format",
})
}
// Hash da key para busca
keyHash := security.HashAPIKey(apiKey)
// Buscar no storage
keyInfo, err := s.apiKeyStore.ValidateKey(c.Context(), keyHash)
if err != nil || keyInfo == nil {
return c.Status(401).JSON(fiber.Map{
"error": "Unauthorized",
"message": "Invalid API key",
})
}
// Atualizar last_used (async)
go s.apiKeyStore.UpdateLastUsed(context.Background(), keyInfo.ID)
// Setar contexto
c.Locals("auth_type", "api_key")
c.Locals("org_id", keyInfo.OrgID)
c.Locals("api_key_id", keyInfo.ID)
c.Locals("scopes", keyInfo.Scopes)
return c.Next()
}
// authenticateJWT valida JWT token
func (s *AuthService) authenticateJWT(c *fiber.Ctx, token string) error {
claims, err := s.ValidateAccessToken(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{
"error": "Unauthorized",
"message": err.Error(),
})
}
// Setar contexto
c.Locals("auth_type", "jwt")
c.Locals("user_id", claims.UserID)
c.Locals("org_id", claims.OrgID)
c.Locals("email", claims.Email)
c.Locals("role", claims.Role)
c.Locals("token_id", claims.TokenID)
return c.Next()
}
// RequireScopes middleware que exige scopes específicos
func RequireScopes(required ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
scopes, ok := c.Locals("scopes").([]string)
if !ok {
// JWT tokens têm acesso total por padrão
if c.Locals("auth_type") == "jwt" {
return c.Next()
}
return c.Status(403).JSON(fiber.Map{
"error": "Forbidden",
"message": "Missing required scopes",
})
}
c.Locals("auth_type", "jwt")
c.Locals("user_id", (*claims)["sub"])
c.Locals("email", (*claims)["email"])
scopeMap := make(map[string]bool)
for _, s := range scopes {
scopeMap[s] = true
}
for _, req := range required {
if !scopeMap[req] && !scopeMap["*"] {
return c.Status(403).JSON(fiber.Map{
"error": "Forbidden",
"message": fmt.Sprintf("Missing required scope: %s", req),
})
}
}
return c.Next()
}
}
// RequireRole middleware que exige role específico
func RequireRole(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
userRole, ok := c.Locals("role").(string)
if !ok {
return c.Status(403).JSON(fiber.Map{
"error": "Forbidden",
"message": "Role not found",
})
}
for _, r := range roles {
if userRole == r {
return c.Next()
}
}
// Admin tem acesso a tudo
if userRole == "admin" {
return c.Next()
}
return c.Status(403).JSON(fiber.Map{
"error": "Forbidden",
"message": "Insufficient permissions",
})
}
}
// ═══════════════════════════════════════════════════════════
// 🔐 LOGIN PROTECTION
// ═══════════════════════════════════════════════════════════
// CheckLoginAllowed verifica se login é permitido (brute force protection)
func (s *AuthService) CheckLoginAllowed(identifier string) (bool, time.Duration, error) {
locked, remaining := s.loginTracker.IsLocked(identifier)
return !locked, remaining, nil
}
// RecordLoginAttempt registra tentativa de login
func (s *AuthService) RecordLoginAttempt(identifier string, success bool) (blocked bool, remaining int) {
if success {
s.loginTracker.RecordSuccess(identifier)
return false, 0
}
return s.loginTracker.RecordFailure(identifier)
}
// ═══════════════════════════════════════════════════════════
// 🔧 HELPERS
// ═══════════════════════════════════════════════════════════
func generateTokenID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// GetUserID obtém user ID do contexto
func GetUserID(c *fiber.Ctx) string {
if id, ok := c.Locals("user_id").(string); ok {
return id
}
return ""
}
// GetOrgID obtém org ID do contexto
func GetOrgID(c *fiber.Ctx) string {
if id, ok := c.Locals("org_id").(string); ok {
return id
}
return ""
}
// GetRole obtém role do contexto
func GetRole(c *fiber.Ctx) string {
if role, ok := c.Locals("role").(string); ok {
return role
}
return ""
}

View File

@@ -0,0 +1,558 @@
package security
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"net"
"regexp"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
)
// ═══════════════════════════════════════════════════════════
// 🔐 CONSTANTES DE SEGURANÇA
// ═══════════════════════════════════════════════════════════
const (
// Bcrypt cost (12-14 recomendado para produção)
BcryptCost = 12
// Tamanho mínimo de senha
MinPasswordLength = 12
// Tamanho da API Key
APIKeyLength = 32
// Prefixo da API Key
APIKeyPrefix = "ophion_"
// Rate limit padrão (requests por minuto)
DefaultRateLimit = 100
// Rate limit para auth (tentativas por minuto)
AuthRateLimit = 5
// Tempo de bloqueio após falhas (minutos)
LockoutDuration = 15
// Máximo de tentativas antes do bloqueio
MaxFailedAttempts = 5
)
// ═══════════════════════════════════════════════════════════
// 🔑 API KEY MANAGEMENT
// ═══════════════════════════════════════════════════════════
// GenerateAPIKey cria uma nova API key segura
func GenerateAPIKey() (key string, hash string, prefix string) {
bytes := make([]byte, APIKeyLength)
if _, err := rand.Read(bytes); err != nil {
panic("failed to generate random bytes")
}
key = APIKeyPrefix + hex.EncodeToString(bytes)
hash = HashAPIKey(key)
prefix = key[:len(APIKeyPrefix)+8] // ophion_xxxxxxxx
return key, hash, prefix
}
// HashAPIKey cria hash SHA256 da API key
func HashAPIKey(key string) string {
hash := sha256.Sum256([]byte(key))
return hex.EncodeToString(hash[:])
}
// ValidateAPIKeyFormat verifica formato da API key
func ValidateAPIKeyFormat(key string) bool {
if !strings.HasPrefix(key, APIKeyPrefix) {
return false
}
// ophion_ + 64 hex chars
return len(key) == len(APIKeyPrefix)+64
}
// SecureCompare compara strings em tempo constante (previne timing attacks)
func SecureCompare(a, b string) bool {
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
// ═══════════════════════════════════════════════════════════
// 🔒 PASSWORD MANAGEMENT
// ═══════════════════════════════════════════════════════════
// PasswordPolicy define regras de senha
type PasswordPolicy struct {
MinLength int
RequireUppercase bool
RequireLowercase bool
RequireNumbers bool
RequireSpecial bool
}
// DefaultPasswordPolicy retorna política padrão
func DefaultPasswordPolicy() PasswordPolicy {
return PasswordPolicy{
MinLength: 12,
RequireUppercase: true,
RequireLowercase: true,
RequireNumbers: true,
RequireSpecial: true,
}
}
// ValidatePassword verifica se senha atende à política
func ValidatePassword(password string, policy PasswordPolicy) (bool, []string) {
var errors []string
if len(password) < policy.MinLength {
errors = append(errors, fmt.Sprintf("Senha deve ter no mínimo %d caracteres", policy.MinLength))
}
if policy.RequireUppercase && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
errors = append(errors, "Senha deve conter letra maiúscula")
}
if policy.RequireLowercase && !regexp.MustCompile(`[a-z]`).MatchString(password) {
errors = append(errors, "Senha deve conter letra minúscula")
}
if policy.RequireNumbers && !regexp.MustCompile(`[0-9]`).MatchString(password) {
errors = append(errors, "Senha deve conter número")
}
if policy.RequireSpecial && !regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password) {
errors = append(errors, "Senha deve conter caractere especial")
}
// Verificar senhas comuns
if isCommonPassword(password) {
errors = append(errors, "Senha muito comum, escolha outra")
}
return len(errors) == 0, errors
}
// HashPassword cria hash bcrypt da senha
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
return string(bytes), err
}
// VerifyPassword verifica senha contra hash
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// isCommonPassword verifica senhas comuns
func isCommonPassword(password string) bool {
common := []string{
"password", "123456", "12345678", "qwerty", "abc123",
"monkey", "1234567", "letmein", "trustno1", "dragon",
"baseball", "iloveyou", "master", "sunshine", "ashley",
"passw0rd", "shadow", "123123", "654321", "superman",
"senha", "mudar123", "admin123", "root123",
}
lower := strings.ToLower(password)
for _, p := range common {
if strings.Contains(lower, p) {
return true
}
}
return false
}
// ═══════════════════════════════════════════════════════════
// 🚦 RATE LIMITING
// ═══════════════════════════════════════════════════════════
// RateLimiter controla taxa de requests
type RateLimiter struct {
sync.RWMutex
requests map[string]*rateLimitEntry
limit int
window time.Duration
}
type rateLimitEntry struct {
count int
firstSeen time.Time
blocked bool
blockedAt time.Time
}
// NewRateLimiter cria novo rate limiter
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
requests: make(map[string]*rateLimitEntry),
limit: limit,
window: window,
}
// Cleanup goroutine
go rl.cleanup()
return rl
}
// Allow verifica se request é permitido
func (rl *RateLimiter) Allow(identifier string) bool {
rl.Lock()
defer rl.Unlock()
now := time.Now()
entry, exists := rl.requests[identifier]
if !exists {
rl.requests[identifier] = &rateLimitEntry{
count: 1,
firstSeen: now,
}
return true
}
// Verificar se está bloqueado
if entry.blocked {
if now.Sub(entry.blockedAt) < time.Duration(LockoutDuration)*time.Minute {
return false
}
// Desbloquear
entry.blocked = false
entry.count = 0
entry.firstSeen = now
}
// Verificar janela de tempo
if now.Sub(entry.firstSeen) > rl.window {
entry.count = 1
entry.firstSeen = now
return true
}
entry.count++
if entry.count > rl.limit {
return false
}
return true
}
// Block bloqueia um identificador
func (rl *RateLimiter) Block(identifier string) {
rl.Lock()
defer rl.Unlock()
entry, exists := rl.requests[identifier]
if !exists {
entry = &rateLimitEntry{}
rl.requests[identifier] = entry
}
entry.blocked = true
entry.blockedAt = time.Now()
}
// cleanup remove entradas antigas
func (rl *RateLimiter) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
rl.Lock()
now := time.Now()
for id, entry := range rl.requests {
if now.Sub(entry.firstSeen) > rl.window*2 && !entry.blocked {
delete(rl.requests, id)
}
}
rl.Unlock()
}
}
// ═══════════════════════════════════════════════════════════
// 🛡️ BRUTE FORCE PROTECTION
// ═══════════════════════════════════════════════════════════
// LoginAttemptTracker rastreia tentativas de login
type LoginAttemptTracker struct {
sync.RWMutex
attempts map[string]*loginAttempt
}
type loginAttempt struct {
failures int
lastAttempt time.Time
lockedUntil time.Time
}
// NewLoginAttemptTracker cria tracker
func NewLoginAttemptTracker() *LoginAttemptTracker {
return &LoginAttemptTracker{
attempts: make(map[string]*loginAttempt),
}
}
// RecordFailure registra falha de login
func (t *LoginAttemptTracker) RecordFailure(identifier string) (blocked bool, remainingAttempts int) {
t.Lock()
defer t.Unlock()
now := time.Now()
attempt, exists := t.attempts[identifier]
if !exists {
t.attempts[identifier] = &loginAttempt{
failures: 1,
lastAttempt: now,
}
return false, MaxFailedAttempts - 1
}
// Reset se última tentativa foi há muito tempo
if now.Sub(attempt.lastAttempt) > time.Duration(LockoutDuration)*time.Minute {
attempt.failures = 1
attempt.lastAttempt = now
attempt.lockedUntil = time.Time{}
return false, MaxFailedAttempts - 1
}
attempt.failures++
attempt.lastAttempt = now
if attempt.failures >= MaxFailedAttempts {
attempt.lockedUntil = now.Add(time.Duration(LockoutDuration) * time.Minute)
return true, 0
}
return false, MaxFailedAttempts - attempt.failures
}
// RecordSuccess registra sucesso de login
func (t *LoginAttemptTracker) RecordSuccess(identifier string) {
t.Lock()
defer t.Unlock()
delete(t.attempts, identifier)
}
// IsLocked verifica se está bloqueado
func (t *LoginAttemptTracker) IsLocked(identifier string) (bool, time.Duration) {
t.RLock()
defer t.RUnlock()
attempt, exists := t.attempts[identifier]
if !exists {
return false, 0
}
if attempt.lockedUntil.IsZero() {
return false, 0
}
remaining := time.Until(attempt.lockedUntil)
if remaining <= 0 {
return false, 0
}
return true, remaining
}
// ═══════════════════════════════════════════════════════════
// 🔍 INPUT VALIDATION & SANITIZATION
// ═══════════════════════════════════════════════════════════
// SanitizeEmail limpa e valida email
func SanitizeEmail(email string) (string, error) {
email = strings.TrimSpace(strings.ToLower(email))
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(email) {
return "", fmt.Errorf("email inválido")
}
return email, nil
}
// SanitizeHostname limpa hostname
func SanitizeHostname(hostname string) (string, error) {
hostname = strings.TrimSpace(hostname)
// Permitir apenas alfanuméricos, hífens e pontos
hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$`)
if !hostnameRegex.MatchString(hostname) && len(hostname) > 1 {
return "", fmt.Errorf("hostname inválido")
}
return hostname, nil
}
// SanitizeSQL previne SQL injection (para queries dinâmicas)
func SanitizeSQL(input string) string {
// Remover caracteres perigosos
dangerous := []string{"'", "\"", ";", "--", "/*", "*/", "xp_", "sp_"}
result := input
for _, d := range dangerous {
result = strings.ReplaceAll(result, d, "")
}
return result
}
// ValidateIPAddress valida endereço IP
func ValidateIPAddress(ip string) bool {
return net.ParseIP(ip) != nil
}
// ═══════════════════════════════════════════════════════════
// 🛡️ SECURITY HEADERS MIDDLEWARE
// ═══════════════════════════════════════════════════════════
// SecurityHeaders adiciona headers de segurança
func SecurityHeaders() fiber.Handler {
return func(c *fiber.Ctx) error {
// Prevenir clickjacking
c.Set("X-Frame-Options", "DENY")
// Prevenir MIME type sniffing
c.Set("X-Content-Type-Options", "nosniff")
// XSS Protection
c.Set("X-XSS-Protection", "1; mode=block")
// Content Security Policy
c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:")
// HSTS (apenas se HTTPS)
if c.Protocol() == "https" {
c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
}
// Referrer Policy
c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions Policy
c.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
// Remove Server header
c.Set("Server", "")
return c.Next()
}
}
// ═══════════════════════════════════════════════════════════
// 📝 AUDIT LOGGING
// ═══════════════════════════════════════════════════════════
// AuditEvent representa um evento de auditoria
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
UserID string `json:"user_id,omitempty"`
OrgID string `json:"org_id,omitempty"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Resource string `json:"resource"`
Action string `json:"action"`
Status string `json:"status"`
Details map[string]string `json:"details,omitempty"`
}
// AuditEventType tipos de eventos
const (
AuditLogin = "auth.login"
AuditLogout = "auth.logout"
AuditLoginFailed = "auth.login_failed"
AuditAPIKeyCreated = "apikey.created"
AuditAPIKeyRevoked = "apikey.revoked"
AuditUserCreated = "user.created"
AuditUserDeleted = "user.deleted"
AuditConfigChanged = "config.changed"
AuditAlertCreated = "alert.created"
AuditDataExport = "data.export"
)
// AuditLogger interface para logging de auditoria
type AuditLogger interface {
Log(event AuditEvent) error
}
// ═══════════════════════════════════════════════════════════
// 🔐 SECRETS MANAGEMENT
// ═══════════════════════════════════════════════════════════
// GenerateSecureToken gera token seguro
func GenerateSecureToken(length int) string {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
panic("failed to generate secure token")
}
return base64.URLEncoding.EncodeToString(bytes)[:length]
}
// MaskSecret mascara segredos para logging
func MaskSecret(secret string) string {
if len(secret) <= 8 {
return "****"
}
return secret[:4] + "****" + secret[len(secret)-4:]
}
// ═══════════════════════════════════════════════════════════
// 🌐 IP FILTERING
// ═══════════════════════════════════════════════════════════
// IPWhitelist gerencia whitelist de IPs
type IPWhitelist struct {
sync.RWMutex
allowed map[string]bool
cidrs []*net.IPNet
}
// NewIPWhitelist cria whitelist
func NewIPWhitelist(ips []string, cidrs []string) *IPWhitelist {
wl := &IPWhitelist{
allowed: make(map[string]bool),
}
for _, ip := range ips {
wl.allowed[ip] = true
}
for _, cidr := range cidrs {
_, network, err := net.ParseCIDR(cidr)
if err == nil {
wl.cidrs = append(wl.cidrs, network)
}
}
return wl
}
// IsAllowed verifica se IP está permitido
func (wl *IPWhitelist) IsAllowed(ip string) bool {
wl.RLock()
defer wl.RUnlock()
// Verificar lista direta
if wl.allowed[ip] {
return true
}
// Verificar CIDRs
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false
}
for _, network := range wl.cidrs {
if network.Contains(parsedIP) {
return true
}
}
return false
}