Arquitectura de Seguridad
Este documento explica las medidas de seguridad implementadas en todo el ecosistema de SwapBits.
Capas de Seguridad​
1. Autenticación y Autorización​
JWT (JSON Web Tokens)​
Estructura de JWT:
// Access Token (15 minutos)
{
userId: "user_123",
email: "user@example.com",
role: "USER",
permissions: ["read:wallets", "write:transactions"],
iat: 1697808000, // Issued at
exp: 1697808900 // Expires 15 min después
}
// Refresh Token (7 dÃas)
{
userId: "user_123",
type: "refresh",
sessionId: "session_abc",
iat: 1697808000,
exp: 1698412800 // 7 dÃas
}
Middleware de Autenticación​
// Middleware JWT
import { verify } from 'jsonwebtoken';
export async function authenticateJWT(req, res, next) {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'No token provided' });
}
const [bearer, token] = authHeader.split(' ');
if (bearer !== 'Bearer' || !token) {
return res.status(401).json({ error: 'Invalid token format' });
}
// Verificar token
const decoded = verify(token, process.env.JWT_SECRET);
// Verificar que no esté en blacklist
const isBlacklisted = await redisClient.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(401).json({ error: 'Token revoked' });
}
// Agregar userId al request
req.userId = decoded.userId;
req.userRole = decoded.role;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
Autorización por Roles​
// Middleware de autorización
export function requireRole(...allowedRoles: string[]) {
return (req, res, next) => {
const userRole = req.userRole;
if (!allowedRoles.includes(userRole)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
}
// Uso en rutas
app.get('/admin/users',
authenticateJWT,
requireRole('ADMIN', 'SUPER_ADMIN'),
getUsersHandler
);
app.delete('/admin/users/:id',
authenticateJWT,
requireRole('SUPER_ADMIN'), // Solo super admin
deleteUserHandler
);
2. Protección de Claves Privadas​
Encriptación AES-256-GCM​
import crypto from 'crypto';
// Encriptar clave privada
function encryptPrivateKey(privateKey: string, userId: string): string {
// 1. Obtener master key de AWS Secrets Manager
const masterKey = await getSecretValue('WALLET_MASTER_KEY');
// 2. Derivar key especÃfica del usuario (PBKDF2)
const userSalt = crypto.createHash('sha256').update(userId).digest();
const userKey = crypto.pbkdf2Sync(
masterKey,
userSalt,
100000, // 100k iteraciones
32, // 32 bytes = 256 bits
'sha256'
);
// 3. Generar IV aleatorio (ÚNICO por encriptación)
const iv = crypto.randomBytes(16);
// 4. Encriptar con AES-256-GCM
const cipher = crypto.createCipheriv('aes-256-gcm', userKey, iv);
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
encrypted += cipher.final('hex');
// 5. Obtener authentication tag
const authTag = cipher.getAuthTag();
// 6. Combinar: IV:AuthTag:Encrypted
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
}
// Desencriptar (SOLO en Lambda)
function decryptPrivateKey(encryptedKey: string, userId: string): string {
const [ivHex, authTagHex, encrypted] = encryptedKey.split(':');
const masterKey = await getSecretValue('WALLET_MASTER_KEY');
const userSalt = crypto.createHash('sha256').update(userId).digest();
const userKey = crypto.pbkdf2Sync(masterKey, userSalt, 100000, 32, 'sha256');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', userKey, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
AWS Secrets Manager​
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const secretsManager = new SecretsManager({ region: 'us-east-1' });
async function getSecretValue(secretName: string): Promise<string> {
try {
const response = await secretsManager.getSecretValue({
SecretId: secretName
});
return response.SecretString;
} catch (error) {
logger.error('Failed to get secret', { secretName, error });
throw error;
}
}
// Secrets almacenados
const Secrets = {
WALLET_MASTER_KEY: 'prod/swapbits/wallet-master-key',
JWT_SECRET: 'prod/swapbits/jwt-secret',
INTERNAL_SERVICE_TOKEN: 'prod/swapbits/internal-token',
BYBIT_API_SECRET: 'prod/swapbits/bybit-secret',
MONGODB_URI: 'prod/swapbits/mongodb-uri',
REDIS_PASSWORD: 'prod/swapbits/redis-password'
};
3. Rate Limiting​
Implementación con Redis​
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// Rate limiter por IP
const ipRateLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:ip:'
}),
windowMs: 60000, // 1 minuto
max: 100, // 100 requests/min
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false
});
// Rate limiter por usuario
const userRateLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:user:'
}),
windowMs: 60000,
max: 200, // 200 requests/min por usuario
keyGenerator: (req) => req.userId,
skip: (req) => !req.userId // Solo para usuarios autenticados
});
// Rate limiter especÃfico por acción
const actionLimiters = {
login: rateLimit({
windowMs: 900000, // 15 minutos
max: 5, // 5 intentos
message: 'Too many login attempts. Try again in 15 minutes.'
}),
sendTransaction: rateLimit({
windowMs: 3600000, // 1 hora
max: 10, // 10 transacciones/hora
keyGenerator: (req) => req.userId
}),
createWallet: rateLimit({
windowMs: 3600000,
max: 5, // 5 wallets/hora
keyGenerator: (req) => req.userId
})
};
// Aplicar en rutas
app.post('/auth/login', actionLimiters.login, loginHandler);
app.post('/wallets/send',
authenticateJWT,
actionLimiters.sendTransaction,
sendTransactionHandler
);
Rate Limiting Personalizado​
// Rate limiting con lógica custom
async function checkRateLimit(
userId: string,
action: string,
maxRequests: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
const key = `custom_rl:${userId}:${action}`;
// Incrementar contador
const count = await redisClient.incr(key);
if (count === 1) {
// Primera request, setear expiración
await redisClient.pexpire(key, windowMs);
}
const ttl = await redisClient.pttl(key);
const resetAt = new Date(Date.now() + ttl);
return {
allowed: count <= maxRequests,
remaining: Math.max(0, maxRequests - count),
resetAt
};
}
// Uso
const limit = await checkRateLimit(userId, 'withdraw', 5, 86400000);
if (!limit.allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
resetAt: limit.resetAt
});
}
4. Validación de Inputs​
Sanitización y Validación​
import { body, param, validationResult } from 'express-validator';
import sanitizeHtml from 'sanitize-html';
// Validación de registro
const registerValidation = [
body('email')
.isEmail().withMessage('Invalid email')
.normalizeEmail()
.custom(async (email) => {
const exists = await userExists(email);
if (exists) throw new Error('Email already in use');
}),
body('password')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
.matches(/[A-Z]/).withMessage('Password must contain uppercase letter')
.matches(/[a-z]/).withMessage('Password must contain lowercase letter')
.matches(/[0-9]/).withMessage('Password must contain number')
.matches(/[^A-Za-z0-9]/).withMessage('Password must contain special character'),
body('firstName')
.trim()
.isLength({ min: 1, max: 50 }).withMessage('First name required')
.customSanitizer(value => sanitizeHtml(value, { allowedTags: [] })),
body('lastName')
.trim()
.isLength({ min: 1, max: 50 }).withMessage('Last name required')
.customSanitizer(value => sanitizeHtml(value, { allowedTags: [] }))
];
// Validación de transacción
const sendTransactionValidation = [
body('to')
.notEmpty()
.custom((value) => {
// Validar formato de address según tipo
if (!isValidAddress(value)) {
throw new Error('Invalid destination address');
}
return true;
}),
body('amount')
.isFloat({ min: 0.0001, max: 1000000 })
.withMessage('Amount must be between 0.0001 and 1,000,000'),
body('pin')
.isLength({ min: 4, max: 6 })
.isNumeric()
.withMessage('PIN must be 4-6 digits')
];
// Middleware para manejar errores de validación
function handleValidationErrors(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
next();
}
// Uso en rutas
app.post('/auth/register',
registerValidation,
handleValidationErrors,
registerHandler
);
app.post('/wallets/send',
authenticateJWT,
sendTransactionValidation,
handleValidationErrors,
sendTransactionHandler
);
Prevención de SQL/NoSQL Injection​
// MAL: Vulnerable a injection
const email = req.body.email;
const user = await db.collection('users').findOne({ email: email });
// Si alguien envÃa: { "$ne": null }
// Query serÃa: { email: { "$ne": null } } -> devuelve TODOS los usuarios
// BIEN: Validar tipo de dato
const email = String(req.body.email); // Forzar a string
const user = await db.collection('users').findOne({ email });
// MEJOR: Usar validación explÃcita
if (typeof req.body.email !== 'string') {
return res.status(400).json({ error: 'Invalid email format' });
}
const email = req.body.email;
const user = await db.collection('users').findOne({ email });
5. Protección CSRF​
CSRF Tokens​
import csrf from 'csurf';
// Middleware CSRF
const csrfProtection = csrf({
cookie: true,
httpOnly: true,
secure: true,
sameSite: 'strict'
});
// Aplicar a rutas que modifican datos
app.post('/wallets/create', csrfProtection, createWalletHandler);
app.post('/wallets/send', csrfProtection, sendTransactionHandler);
app.delete('/wallets/:id', csrfProtection, deleteWalletHandler);
// Endpoint para obtener token CSRF
app.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// En el frontend
async function sendTransaction(data) {
// 1. Obtener CSRF token
const csrfResponse = await fetch('/csrf-token');
const { csrfToken } = await csrfResponse.json();
// 2. Incluir en request
const response = await fetch('/wallets/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
}
6. Seguridad de Headers​
Helmet.js​
import helmet from 'helmet';
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'wss://api.swapbits.com'],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
// HTTP Strict Transport Security
hsts: {
maxAge: 31536000, // 1 año
includeSubDomains: true,
preload: true
},
// X-Frame-Options
frameguard: {
action: 'deny' // Prevenir clickjacking
},
// X-Content-Type-Options
noSniff: true,
// Referrer-Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
}
}));
// CORS configuration
import cors from 'cors';
app.use(cors({
origin: [
'https://swapbits.com',
'https://app.swapbits.com'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'CSRF-Token']
}));
7. Logging y AuditorÃa​
Audit Log System​
// Crear log de auditorÃa
async function createAuditLog(data: AuditLogData) {
await db.collection('audit_logs').insertOne({
userId: data.userId,
action: data.action, // LOGIN, LOGOUT, CREATE_WALLET, SEND_TX, etc.
resource: data.resource, // users, wallets, transactions
resourceId: data.resourceId,
details: data.details,
ip: data.ip,
userAgent: data.userAgent,
success: data.success,
errorMessage: data.errorMessage,
timestamp: new Date()
});
}
// Middleware de auditorÃa
function auditMiddleware(action: string, resource: string) {
return async (req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
// Log después de responder
createAuditLog({
userId: req.userId,
action,
resource,
resourceId: req.params.id,
details: { body: req.body },
ip: req.ip,
userAgent: req.headers['user-agent'],
success: res.statusCode < 400,
errorMessage: res.statusCode >= 400 ? data : null
});
return originalSend.call(this, data);
};
next();
};
}
// Uso
app.post('/wallets/send',
authenticateJWT,
auditMiddleware('SEND_TRANSACTION', 'transactions'),
sendTransactionHandler
);
Eventos Auditados​
const AuditableActions = {
// Autenticación
LOGIN: 'User logged in',
LOGOUT: 'User logged out',
LOGIN_FAILED: 'Failed login attempt',
PASSWORD_RESET: 'Password reset requested',
PASSWORD_CHANGED: 'Password changed',
// Wallets
CREATE_WALLET: 'Wallet created',
IMPORT_WALLET: 'Wallet imported',
DELETE_WALLET: 'Wallet deleted',
EXPORT_PRIVATE_KEY: 'Private key exported',
// Transacciones
SEND_TRANSACTION: 'Transaction sent',
// Banking
ADD_BANK_ACCOUNT: 'Bank account added',
WITHDRAW: 'Withdrawal requested',
// Admin
ADMIN_ACCESS: 'Admin panel accessed',
USER_SUSPENDED: 'User account suspended',
KYC_APPROVED: 'KYC approved',
KYC_REJECTED: 'KYC rejected'
};
8. Detección de Fraude​
Sistema Anti-Fraude​
async function analyzeFraudRisk(
userId: string,
action: string,
data: any
): Promise<{ riskScore: number; flags: string[] }> {
const flags: string[] = [];
let riskScore = 0;
// 1. Cuenta nueva
const user = await getUser(userId);
const accountAge = Date.now() - user.createdAt.getTime();
if (accountAge < 86400000) { // < 24 horas
flags.push('NEW_ACCOUNT');
riskScore += 20;
}
// 2. IP sospechosa
const ipInfo = await getIPInfo(data.ip);
if (ipInfo.isVPN || ipInfo.isProxy) {
flags.push('VPN_DETECTED');
riskScore += 30;
}
if (ipInfo.country !== user.country) {
flags.push('FOREIGN_IP');
riskScore += 15;
}
// 3. Patrón de comportamiento
if (action === 'SEND_TRANSACTION') {
const recentTxs = await getRecentTransactions(userId, 3600000);
// Muchas transacciones en poco tiempo
if (recentTxs.length > 5) {
flags.push('HIGH_FREQUENCY');
riskScore += 25;
}
// Monto inusual
const avgAmount = calculateAverage(recentTxs.map(tx => tx.amount));
if (data.amount > avgAmount * 10) {
flags.push('UNUSUAL_AMOUNT');
riskScore += 20;
}
}
// 4. Retiro inmediato después de depósito
if (action === 'WITHDRAW') {
const lastDeposit = await getLastDeposit(userId);
const timeSinceDeposit = Date.now() - lastDeposit.createdAt.getTime();
if (timeSinceDeposit < 3600000) { // < 1 hora
flags.push('RAPID_WITHDRAWAL');
riskScore += 40;
}
}
return { riskScore, flags };
}
// Middleware de detección de fraude
async function fraudDetectionMiddleware(req, res, next) {
const analysis = await analyzeFraudRisk(
req.userId,
req.route.path,
{ ip: req.ip, ...req.body }
);
// Risk score > 70: Requiere revisión manual
if (analysis.riskScore > 70) {
await createAlert({
type: 'FRAUD_DETECTION',
userId: req.userId,
riskScore: analysis.riskScore,
flags: analysis.flags,
action: req.route.path
});
return res.status(403).json({
error: 'Transaction flagged for review',
message: 'Your transaction requires manual verification'
});
}
// Risk score 50-70: Log y continuar
if (analysis.riskScore > 50) {
logger.warn('Medium fraud risk detected', {
userId: req.userId,
riskScore: analysis.riskScore,
flags: analysis.flags
});
}
next();
}
Vulnerabilidades CrÃticas a Evitar
Top 10 Vulnerabilidades OWASP:
- Inyección (SQL/NoSQL) - Validar TODOS los inputs
- Broken Authentication - Usar JWT correctamente, hash passwords
- Sensitive Data Exposure - Encriptar datos en reposo y en tránsito
- XML External Entities (XXE) - Deshabilitar procesamiento de entities
- Broken Access Control - Validar permisos en cada request
- Security Misconfiguration - No exponer info de debugging en producción
- Cross-Site Scripting (XSS) - Sanitizar outputs
- Insecure Deserialization - No confiar en datos serializados
- Using Components with Known Vulnerabilities - Actualizar dependencias
- Insufficient Logging - Loggear acciones crÃticas
EspecÃfico de Crypto:
- 🔴 NUNCA guardar claves privadas sin encriptar
- 🔴 NUNCA loggear claves privadas o seeds
- 🔴 NUNCA enviar claves privadas por API
- 🔴 NUNCA desencriptar claves en frontend
- 🔴 SIEMPRE validar addresses de blockchain
- 🔴 SIEMPRE usar HTTPS/WSS