Estrategia de Cifrado y Almacenamiento Sensible — Finnova
Finnova almacena datos financieros personales (saldos, movimientos, inversiones) que requieren protección en todas las capas del sistema. Este documento define qué se cifra, cómo, dónde se almacena y quién puede acceder.
1. Principios base
| Principio | Descripción |
|---|---|
| Cifrado por defecto | Todo dato sensible se cifra en tránsito y en reposo, sin excepciones |
| Mínimo privilegio | Cada módulo accede únicamente a los datos que necesita |
| Separación de secretos | Claves, tokens y credenciales nunca viven en el código fuente ni en la DB |
| Defense in depth | Cifrado a nivel de disco, de base de datos y de campo para datos críticos |
| Rotación periódica | Las claves criptográficas tienen ciclo de vida definido y se rotan |
2. Mapa de datos por nivel de sensibilidad
| Nivel | Datos | Almacenamiento | Cifrado requerido |
|---|---|---|---|
| Crítico | Tokens de acceso bancario, refresh tokens OAuth, secrets de integración | Variables de entorno / AWS Secrets Manager | En reposo (Secrets Manager) + en memoria solamente |
| Alto | Movimientos bancarios, saldos, deudas, inversiones | PostgreSQL — Finnova DB | Cifrado de disco (EBS) + cifrado de campo (columnas sensibles) |
| Medio | Nombre, email, teléfono, CURP, fecha de nacimiento | PostgreSQL — Finnova DB | Cifrado de disco (EBS) |
| Bajo | Logs de auditoría, metadatos de dispositivo, contenido de cursos | PostgreSQL / S3 | Cifrado de disco |
| No almacenar | Números de tarjeta, CVV, datos PAN | — | Stripe los tokeniza; Finnova nunca los persiste |
3. Cifrado en tránsito
3.1 Comunicación cliente ↔ servidor
- Protocolo: TLS 1.2 mínimo, TLS 1.3 preferido
- Terminación: En el HTTPS Listener del Load Balancer (AWS ALB)
- Certificado: Gestionado vía AWS Certificate Manager (ACM) con renovación automática
- HSTS: Habilitar
Strict-Transport-Securityen Nginx conmax-age=31536000; includeSubDomains - Cipher suites: Deshabilitar suites débiles (RC4, 3DES, export ciphers); usar AES-GCM y ChaCha20-Poly1305
3.2 Comunicación interna (API ↔ DB)
- La comunicación entre el API Server y PostgreSQL ocurre dentro de la misma VPC privada
- Habilitar SSL en la conexión PostgreSQL (
sslmode=requireen el connection string) - La cadena de conexión se inyecta como variable de entorno, nunca hardcodeada
3.3 Diagrama de cifrado en tránsito
4. Cifrado en reposo
4.1 Disco — AWS EBS Encryption
Todos los volúmenes EBS donde corren PostgreSQL (principal y réplica) deben tener cifrado habilitado:
- Algoritmo: AES-256
- Gestión de claves: AWS KMS (Key Management Service) con CMK (Customer Managed Key)
- Alcance: Volumen principal, volumen de réplica y snapshots de backup
- Configuración: Habilitar al crear las instancias EC2; no se puede activar retroactivamente en un volumen existente (requiere snapshot + nuevo volumen)
Tarea pendiente: Verificar que los volúmenes EBS actuales tienen cifrado activado. Si no, planificar migración con downtime controlado.
4.2 Base de datos — Cifrado de campos sensibles
Para datos financieros de especial criticidad, aplicar cifrado a nivel de columna además del cifrado de disco. Esto protege contra accesos directos a la DB por parte de administradores no autorizados.
Columnas candidatas a cifrado de campo:
| Tabla (referencial) | Columna | Estrategia |
|---|---|---|
accounts | balance, account_number | AES-256-GCM, clave por usuario |
transactions | amount, description, merchant | AES-256-GCM, clave por usuario |
investments | portfolio_value, holdings | AES-256-GCM, clave por usuario |
users | curp, phone | AES-256-GCM, clave global de aplicación |
Implementación recomendada (Node.js / Express):
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.FIELD_ENCRYPTION_KEY!, 'hex'); // 32 bytes
export function encrypt(plaintext: string): string {
const iv = randomBytes(12);
const cipher = createCipheriv(ALGORITHM, KEY, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
// Formato: iv(12) + authTag(16) + ciphertext — todo en hex
return Buffer.concat([iv, authTag, encrypted]).toString('hex');
}
export function decrypt(ciphertext: string): string {
const buf = Buffer.from(ciphertext, 'hex');
const iv = buf.subarray(0, 12);
const authTag = buf.subarray(12, 28);
const encrypted = buf.subarray(28);
const decipher = createDecipheriv(ALGORITHM, KEY, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
}
AES-256-GCM provee cifrado autenticado: detecta si el ciphertext fue manipulado, lo que previene ataques de alteración de datos.
4.3 S3 — Contenido de cursos
- Server-Side Encryption: SSE-S3 (AES-256 gestionado por AWS) activado por defecto en el bucket
- Acceso: Bucket privado; acceso solo vía URLs prefirmadas (signed URLs) con expiración corta (ej. 15 min)
- Bloqueo de acceso público: Habilitar "Block all public access" en la configuración del bucket
5. Gestión de secretos y claves
5.1 Qué se considera un secreto
- Claves de cifrado de campo (
FIELD_ENCRYPTION_KEY) - Cadenas de conexión a la base de datos
- JWT secret / claves privadas RSA para firma de tokens
- API keys de Stripe y otros terceros
- Credenciales de AWS (si no se usa IAM Role)
- Tokens OAuth de conexión bancaria (si aplica)
5.2 Regla fundamental
Ningún secreto debe estar en el código fuente, en archivos .env commiteados, ni en logs.
5.3 Gestión con AWS Secrets Manager
Configuración recomendada:
| Secreto | Rotación | Almacenamiento |
|---|---|---|
DB_CONNECTION_STRING | 30 días | AWS Secrets Manager |
FIELD_ENCRYPTION_KEY | 90 días (con re-cifrado de datos) | AWS Secrets Manager |
JWT_SECRET | 30 días | AWS Secrets Manager |
STRIPE_SECRET_KEY | Al comprometerse / anualmente | AWS Secrets Manager |
| Variables no sensibles | N/A | Variables de entorno en EC2 |
5.4 Variables de entorno en desarrollo local
Los desarrolladores usan un archivo .env.local (en .gitignore) obtenido de forma segura:
# .env.local — NUNCA committear este archivo
DB_CONNECTION_STRING=postgresql://...
FIELD_ENCRYPTION_KEY=<hex-32-bytes>
JWT_SECRET=<string-largo>
STRIPE_SECRET_KEY=sk_test_...
El repositorio incluye un .env.example con los nombres de las variables pero sin valores reales.
6. Autenticación y tokens de sesión
6.1 JWT — Configuración segura
| Parámetro | Valor recomendado | Razón |
|---|---|---|
| Algoritmo | RS256 (asimétrico) | La clave privada para firmar permanece solo en el servidor |
| Expiración access token | 15 minutos | Minimiza ventana de uso si es robado |
| Expiración refresh token | 7 días | Balance entre UX y seguridad |
| Almacenamiento en app | SecureStorage (Keychain/Keystore) | Evita acceso por otras apps en el dispositivo |
| Transmisión | Header Authorization: Bearer | Nunca en query params ni body |
| Revocación | Lista negra de jti en Redis/DB | Permite invalidar tokens antes de expirar |
6.2 Almacenamiento de contraseñas
import * as argon2 from 'argon2';
// Hash al registrar
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 1,
});
// Verificar al hacer login
const valid = await argon2.verify(hash, password);
- Algoritmo: Argon2id (ganador de Password Hashing Competition, resistente a ataques GPU)
- Alternativa aceptable: bcrypt con cost factor ≥ 12 (si Argon2 no está disponible)
- Prohibido: MD5, SHA-1, SHA-256 sin salt para contraseñas
7. Almacenamiento en el dispositivo móvil (React Native)
| Dato | Almacenamiento | Librería |
|---|---|---|
| JWT access token | SecureStore (Expo) / Keychain (iOS) / Keystore (Android) | expo-secure-store |
| JWT refresh token | SecureStore | expo-secure-store |
| Preferencias de UI no sensibles | AsyncStorage | @react-native-async-storage/async-storage |
| Datos financieros cacheados | No cachear en disco | Siempre consultar la API |
Regla: Ningún dato financiero (saldos, movimientos, inversiones) se persiste en el dispositivo. Solo vive en memoria durante la sesión activa. Al cerrar la app o expirar la sesión, se descarta.
8. Backups y recuperación
| Aspecto | Configuración |
|---|---|
| Frecuencia de backup | Diario (automático via réplica PostgreSQL) |
| Retención | 30 días de backups rotatorios |
| Cifrado de backups | AES-256 (hereda cifrado del volumen EBS) |
| Prueba de restauración | Mensual — restaurar backup en entorno staging y verificar integridad |
| RTO objetivo | < 4 horas |
| RPO objetivo | < 24 horas (backup diario) |
Los backups cifrados son inútiles si no se prueba la restauración. Establecer ritual mensual de drill.
9. Checklist de implementación
Antes del MVP
- Verificar / habilitar cifrado EBS en volúmenes de producción
- Migrar todos los secretos a AWS Secrets Manager (eliminar
.envhardcodeados en servidor) - Configurar
sslmode=requireen conexión PostgreSQL - Implementar
SecureStorepara tokens en la app mobile - Habilitar SSE-S3 y bloqueo de acceso público en bucket de cursos
- Configurar HSTS en Nginx
Post-MVP (primer mes)
- Implementar cifrado de campo (AES-256-GCM) para columnas de datos financieros
- Migrar firma de JWT de HS256 a RS256
- Implementar mecanismo de revocación de refresh tokens
- Configurar rotación automática de secretos en AWS Secrets Manager
- Documentar procedimiento de rotación de
FIELD_ENCRYPTION_KEYcon re-cifrado de datos
Primer trimestre post-MVP
- Establecer drill mensual de restauración de backups
- Auditoría externa de configuración criptográfica
- Implementar alertas de acceso inusual a datos sensibles (AWS CloudTrail + CloudWatch)