Errores comunes en implementaciones criptográficas y cómo evitarlos

Capítulo 12

Tiempo estimado de lectura: 12 minutos

+ Ejercicio

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.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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 riesgoPatrón recomendado
Concatenar campos sin longitudesUsar 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 hocUsar construcciones autenticadas de alto nivel (AEAD) y APIs que lo integren
Reintentos que cambian el mensajeIncluir 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ón

7) 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

  1. Inventario: localiza puntos donde se usen primitivas directamente (cifrado “raw”, hashes para autenticación, comparaciones manuales, generación de random).

  2. Clasifica por riesgo: (a) autenticación/firmas/tokens, (b) cifrado de datos, (c) transporte/validación de certificados, (d) generación/almacenamiento de claves.

  3. Elige API de alto nivel: prioriza APIs que integren autenticación, manejo de nonces y verificación estricta por defecto.

  4. Define contratos: formato de entrada/salida (bytes vs base64), manejo de errores (fail-closed), y compatibilidad (versionado del formato).

  5. 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.

  6. 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

  1. Especificación mínima: documenta el formato del mensaje (campos, orden, codificación, versionado) y qué se autentica exactamente.

  2. 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.

  3. 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).

  4. 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ón

Criterios 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 ==/equals o 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”).

Ahora responde el ejercicio sobre el contenido:

En una revisión de PR, ¿qué cambio refleja mejor el patrón recomendado para evitar ataques por manipulación de ciphertext al usar cifrado?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

El patrón seguro es usar autenticación criptográfica (AEAD o MAC/tag) y fallar cerrado: si la verificación falla, se aborta. Las pruebas negativas (alterar 1 bit) ayudan a asegurar que no hay degradación silenciosa.

Siguiente capítulo

Evaluación y selección de bibliotecas y estándares criptográficos

Arrow Right Icon
Portada de libro electrónico gratuitaCriptografía aplicada para profesionales: qué usar, cuándo y por qué.
92%

Criptografía aplicada para profesionales: qué usar, cuándo y por qué.

Nuevo curso

13 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.