Por qué los fallos de implementación son más comunes que los fallos del algoritmo
En entornos profesionales, la criptografía suele fallar por detalles operativos: fuentes de aleatoriedad inadecuadas, estados mal gestionados, validaciones incompletas, manejo inseguro de errores o “atajos” para acelerar entregas. Este capítulo cataloga anti-patrones frecuentes y propone patrones de prevención y criterios concretos para revisiones de pull request (PR) centradas en criptografía.
Catálogo de errores comunes (y cómo reconocerlos en código)
1) Generar claves con PRNG débil o no criptográfico
Anti-patrón: usar generadores pseudoaleatorios pensados para simulación/estadística (o semillas predecibles como hora, PID, etc.) para crear claves, nonces o tokens. Suele aparecer como Math.random(), rand(), Random() sin CSPRNG, o como “sembrar” manualmente.
Riesgo: claves/tokens predecibles, posibilidad de enumeración o reconstrucción del estado del PRNG.
Señales en PR: llamadas a APIs de “random” genéricas; código que acepta una seed externa; tests que fijan semillas en producción; “optimización” para evitar llamadas al sistema.
Corrección (patrón): usar CSPRNG del sistema o librerías de alto nivel que lo encapsulan. Evitar inventar wrappers propios salvo que estén muy justificados y revisados.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
// Anti-patrón (JavaScript): NO usar para claves/tokens
const token = Array.from({length: 32}, () => Math.floor(Math.random()*256));
// Correcto (Node.js): CSPRNG
import { randomBytes } from 'crypto';
const token = randomBytes(32);// Anti-patrón (Java): Random no es criptográfico
byte[] key = new byte[32];
new java.util.Random().nextBytes(key);
// Correcto (Java): SecureRandom
byte[] key = new byte[32];
java.security.SecureRandom.getInstanceStrong().nextBytes(key);2) Reutilizar nonces/IVs o derivarlos de datos predecibles
Anti-patrón: nonces constantes, contadores reiniciables sin persistencia, nonces derivados de timestamps, IDs de usuario o hashes del mensaje. También: reusar el mismo nonce tras un reinicio del servicio o en despliegues paralelos.
Riesgo: pérdida de confidencialidad y/o integridad según el esquema; posibilidad de correlación de mensajes y ataques de recuperación.
Señales en PR: nonce = 0; nonce = timestamp; nonce = messageId; contador en memoria sin almacenamiento estable; nonce generado con PRNG no criptográfico.
Corrección (patrón): delegar el manejo de nonces a APIs de alto nivel cuando sea posible; si el diseño exige contadores, asegurar unicidad global (persistencia, particionado por instancia, o prefijos aleatorios por proceso). Añadir tests que fallen si se repite un nonce para la misma clave.
// Anti-patrón (pseudocódigo): nonce predecible
nonce = currentTimeMillis();
aeadEncrypt(key, nonce, plaintext, aad);
// Mejor: nonce aleatorio criptográfico (si el esquema lo permite)
nonce = CSPRNG(12);
aeadEncrypt(key, nonce, plaintext, aad);
// Mejor (si se usa contador): prefijo aleatorio por proceso + contador persistente
processPrefix = CSPRNG(4);
counter = loadAndIncrementPersistentCounter();
nonce = processPrefix || uint64(counter);3) Comparar hashes/tokens con fugas de timing
Anti-patrón: comparar secretos con ==, equals o bucles que salen al primer byte distinto. Esto filtra información por tiempo (timing side-channel) en escenarios remotos o locales.
Riesgo: adivinación incremental de MACs, tokens de sesión, firmas HMAC, etc.
Señales en PR: comparaciones directas de arrays/strings; conversión a hex/base64 y comparación de strings; early-return en comparación byte a byte.
Corrección (patrón): usar comparación en tiempo constante provista por la plataforma/librería. Evitar normalizaciones peligrosas (trim, lowercasing) sobre secretos.
// Anti-patrón (Node.js)
if (providedSig === expectedSig) allow();
// Correcto (Node.js)
import { timingSafeEqual } from 'crypto';
const a = Buffer.from(providedSig, 'hex');
const b = Buffer.from(expectedSig, 'hex');
if (a.length === b.length && timingSafeEqual(a, b)) allow();// Anti-patrón (Java)
if (Arrays.equals(provided, expected)) allow(); // no garantiza tiempo constante
// Mejor (Java)
if (java.security.MessageDigest.isEqual(provided, expected)) allow();4) Construir protocolos propios (“crypto DIY”)
Anti-patrón: diseñar un “mini-protocolo” de autenticación/cifrado con concatenaciones, campos sin delimitar, orden ambiguo, reintentos que cambian semántica, o “doble cifrado” sin análisis. Ejemplos típicos: “cifro y luego hago hash”, “firmo el JSON tal cual”, “uso RSA para cifrar directamente payloads grandes”, “mezclo compresión y cifrado sin pensar en oráculos”.
Riesgo: vulnerabilidades de composición, confusión de formatos, ataques de replay, downgrade, oráculos por errores, incompatibilidades entre implementaciones.
Señales en PR: concatenación manual de campos; ausencia de especificación formal del mensaje; “versión 1” sin plan de versionado; falta de pruebas de interoperabilidad; uso directo de primitivas en vez de construcciones estándar.
Corrección (patrón): usar protocolos y formatos estandarizados o librerías de alto nivel. Si se requiere un formato propio, documentar un esquema canónico (serialización determinista), versionado explícito, y pruebas de interoperabilidad entre al menos dos implementaciones.
| Señal de riesgo | Patrón recomendado |
|---|---|
| Concatenar campos sin longitudes | Usar TLV/CBOR/Protobuf con canonicalización definida |
| Firmar “el JSON” | Firmar bytes canónicos (p.ej., JSON canonicalizado o CBOR canonical) |
| “Encriptar y hashear” ad hoc | Usar construcciones autenticadas de alto nivel (AEAD) y APIs que lo integren |
| Reintentos que cambian el mensaje | Incluir nonce/requestId y reglas claras anti-replay |
5) Almacenar claves junto a los datos o en el mismo perímetro
Anti-patrón: guardar la clave de cifrado en el mismo repositorio, en variables de entorno sin control, en el mismo bucket/volumen que los datos cifrados, o en la misma base de datos “por comodidad”. También: incluir claves en logs, dumps, tickets, o snapshots.
Riesgo: una sola brecha compromete datos y claves; escalado lateral; exposición accidental por observabilidad.
Señales en PR: secretos en .env versionado; claves en config; “temporalmente” en logs; scripts de migración que imprimen claves; tests que usan claves reales.
Corrección (patrón): separar almacenamiento y control de acceso; usar gestores de secretos/KMS/HSM según el entorno; aplicar políticas de mínimo privilegio; redacción (redaction) en logs; escaneo automático de secretos en CI.
6) Usar cifrado sin autenticación (o ignorar la verificación)
Anti-patrón: cifrar y luego no autenticar; o autenticar pero no verificar correctamente (p.ej., no comprobar el tag, capturar la excepción y continuar, o devolver plaintext parcial). También: usar “checksum” no criptográfico como sustituto.
Riesgo: manipulación de ciphertext, ataques de padding/oráculo, corrupción silenciosa, escalada a ejecución lógica si el plaintext controla decisiones.
Señales en PR: APIs de cifrado “raw”; ausencia de tag/MAC; manejo de errores que “degrada” a modo inseguro; tests que no cubren ciphertext modificado.
Corrección (patrón): usar APIs autenticadas de alto nivel; fallar de forma cerrada (fail-closed) ante cualquier error de autenticación; añadir tests negativos que modifiquen 1 bit del ciphertext y exijan fallo.
// Anti-patrón: ignorar fallo de autenticación
try {
plaintext = decrypt(ciphertext);
} catch (e) {
plaintext = ciphertext; // o continuar con datos corruptos
}
// Correcto: fail-closed
plaintext = decrypt(ciphertext); // si falla, abortar la operación7) Validar mal certificados (o desactivar validación “temporalmente”)
Anti-patrón: desactivar verificación de hostname, aceptar cualquier certificado, confiar en certificados autofirmados sin pinning controlado, o implementar validación parcial. También: “workarounds” para entornos internos que terminan en producción.
Riesgo: MITM, suplantación de servicios, robo de credenciales/tokens.
Señales en PR: flags tipo insecureSkipVerify, rejectUnauthorized: false, trust managers permisivos, stores de CA embebidos sin control, excepciones amplias en validación.
Corrección (patrón): usar el validador por defecto de la plataforma; si se requiere pinning, hacerlo con rotación planificada y telemetría; separar configuración de dev/test de producción con guardrails (p.ej., checks en CI que bloqueen flags inseguros).
// Anti-patrón (Node.js)
https.request({ rejectUnauthorized: false }, ...)
// Correcto: validación por defecto (no tocar rejectUnauthorized)
https.request({ /* defaults */ }, ...)8) Mensajes de error que filtran información
Anti-patrón: errores distintos para “usuario no existe” vs “password incorrecta”, o para “MAC inválido” vs “formato inválido”, o devolver trazas que revelan claves, rutas, configuraciones, IDs internos. También: logs con ciphertext + claves/IVs + tags en la misma línea.
Riesgo: enumeración de cuentas, oráculos de validación, ayuda al atacante para afinar entradas, exposición accidental de secretos.
Señales en PR: mensajes de error muy específicos en endpoints sensibles; códigos de error diferentes según etapa; logging de excepciones con payloads completos; respuestas que incluyen detalles criptográficos.
Corrección (patrón): respuestas externas uniformes (“credenciales inválidas”, “solicitud inválida”); detalles solo en logs internos con redacción; correlación por ID de incidente; límites de rate y alertas para patrones anómalos.
Guías prácticas paso a paso (patrones de prevención)
Guía 1: Sustituir implementaciones de bajo nivel por librerías de alto nivel
Inventario: localiza puntos donde se usen primitivas directamente (cifrado “raw”, hashes para autenticación, comparaciones manuales, generación de random).
Clasifica por riesgo: (a) autenticación/firmas/tokens, (b) cifrado de datos, (c) transporte/validación de certificados, (d) generación/almacenamiento de claves.
Elige API de alto nivel: prioriza APIs que integren autenticación, manejo de nonces y verificación estricta por defecto.
Define contratos: formato de entrada/salida (bytes vs base64), manejo de errores (fail-closed), y compatibilidad (versionado del formato).
Plan de migración: soporta lectura dual si hay datos históricos; escribe siempre con el esquema nuevo; añade telemetría para saber cuándo retirar el viejo.
Pruebas negativas: modifica 1 bit del ciphertext/tag/firma y verifica que falla; prueba replays; prueba nonces repetidos si aplica.
Guía 2: Revisiones de seguridad y pruebas de interoperabilidad
Especificación mínima: documenta el formato del mensaje (campos, orden, codificación, versionado) y qué se autentica exactamente.
Interoperabilidad: crea un conjunto de vectores de prueba (inputs/outputs) y ejecútalos en al menos dos entornos (p.ej., backend y cliente móvil) para detectar diferencias de codificación.
Revisión cruzada: exige al menos un revisor con experiencia en seguridad para cambios criptográficos; usa una etiqueta en PR (p.ej.,
security-crypto).Fuzzing/propiedad: añade tests que generen entradas aleatorias y verifiquen invariantes (p.ej., “si se altera cualquier byte, la verificación falla”).
Guía 3: Checklist de código para criptografía (lista pegable en PR)
Aleatoriedad: ¿todas las claves/nonces/tokens provienen de CSPRNG? ¿no hay seeds manuales en producción?
Nonces/IV: ¿se garantiza unicidad por clave? ¿hay riesgo de reinicio que repita contadores? ¿se testea la no repetición?
Autenticación: ¿se usa cifrado autenticado o MAC? ¿se verifica siempre y se falla cerrado?
Comparaciones: ¿comparaciones de secretos en tiempo constante? ¿sin conversiones innecesarias a string?
Errores: ¿mensajes externos uniformes? ¿logs sin secretos (claves, nonces, tags, tokens)?
Certificados: ¿no hay flags inseguros? ¿validación de hostname activa? ¿pinning (si existe) con rotación?
Formato: ¿serialización canónica definida? ¿versionado explícito? ¿sin concatenaciones ambiguas?
Dependencias: ¿librerías mantenidas y actualizadas? ¿configuración segura por defecto?
Interoperabilidad: ¿vectores de prueba compartidos? ¿tests en CI para múltiples plataformas?
Gestión de secretos: ¿no se almacenan claves junto a datos? ¿permisos mínimos? ¿rotación planificada?
Anti-patrones típicos con corrección (casos prácticos)
Caso A: Token de reseteo de contraseña predecible
Anti-patrón: token derivado de userId y timestamp, o generado con PRNG débil.
// Anti-patrón (pseudocódigo)
token = base64( sha256(userId + ":" + nowMillis) );Corrección: token aleatorio con CSPRNG, longitud suficiente, almacenado como valor de verificación (idealmente hash del token) y con expiración; comparación en tiempo constante al validar.
// Correcto (pseudocódigo)
token = base64url( CSPRNG(32) );
store(userId, hash(token), expiresAt);
// al validar: constantTimeEqual(hash(provided), storedHash)Caso B: Firma/HMAC sobre JSON no canónico
Anti-patrón: firmar el string JSON tal cual se serializa en cada plataforma (orden de claves, espacios, unicode) y luego comparar.
// Anti-patrón
sig = HMAC(key, JSON.stringify(obj));Corrección: definir canonicalización (p.ej., JSON canónico o CBOR canónico) y firmar bytes canónicos; añadir vectores de prueba compartidos.
// Correcto (pseudocódigo)
bytes = canonicalSerialize(obj); // reglas documentadas
sig = HMAC(key, bytes);Caso C: Validación de certificado desactivada en “staging” y filtrada a producción
Anti-patrón: bandera de configuración que permite desactivar validación y termina habilitada por error.
// Anti-patrón
if (config.insecure) tlsVerify = false;Corrección: eliminar la opción insegura o bloquearla por entorno (p.ej., el binario de producción no la compila, o CI falla si aparece). Si se necesita para pruebas locales, aislarla con guardrails.
// Correcto (idea)
assert(!isProduction() || config.insecure !== true);
// y preferir no exponer la opción en producciónCriterios para revisión de Pull Request enfocados en criptografía
Preguntas que el revisor debe poder responder
¿Qué secreto protege esto y dónde vive? Identifica claves, tokens, credenciales, materiales de sesión y su superficie de exposición (memoria, logs, métricas, backups).
¿Qué entradas controla el atacante? Campos del request, headers, payloads, parámetros, tiempos, reintentos, y cualquier dato que afecte nonces/serialización.
¿Qué pasa si se altera un bit? Debe fallar la verificación y abortar; no debe haber degradación silenciosa.
¿Qué pasa si se repite un mensaje? ¿hay replay? ¿hay identificadores únicos y expiración?
¿Qué pasa si el sistema reinicia? Especialmente para contadores de nonces, caches de claves, y estados de sesión.
¿Qué se registra? Confirmar que no se loguean secretos ni materiales que permitan reconstruirlos.
Red flags (motivos para pedir cambios antes de aprobar)
Uso de primitivas “raw” cuando existe alternativa de alto nivel en la misma librería.
Comparaciones de secretos con
==/equalso conversión a string para comparar.Flags de “insecure”, “skip verify”, “accept all certs”, aunque sea “solo para debug”.
Nonces/IVs deterministas sin justificación y sin pruebas de unicidad.
Manejo de errores que continúa tras fallo criptográfico.
Formato de mensaje no especificado o dependiente de serialización implícita.
Secretos en logs, métricas, trazas, o en el repositorio.
Qué exigir como evidencia en el PR
Tests negativos: alteración de ciphertext/tag/firma y verificación de fallo.
Vectores de prueba: archivos con casos conocidos para interoperabilidad (incluyendo edge cases de codificación).
Documentación breve: README técnico del formato/versionado y decisiones (qué se autentica, qué se cifra, cómo se manejan errores).
Escaneo automático: reglas de CI para detectar secretos y patrones inseguros (p.ej., “rejectUnauthorized: false”).