StackPractices
intermediate Por Mathias Paulenko

Bases de Criptografía — Cifrado, Hashing y Firmas

Guía de criptografía para desarrolladores: cifrado simétrico y asimétrico, hashing, firmas digitales y gestión de claves con ejemplos de código prácticos.

Temas: security

Resumen

La criptografía está debajo de casi todos los controles de seguridad que toca un desarrollador: almacenamiento de contraseñas, sesiones TLS, JWT firmados, backups cifrados, verificación de webhooks. Usarla bien es sobre todo elegir la primitiva correcta para cada trabajo — cifra lo que necesites recuperar después, hashea lo que solo necesites comparar, firma lo que otros deban verificar — y luego no improvisar en los detalles como IVs, almacenamiento de claves y elección de algoritmo.

Esta guía cubre las cuatro primitivas que usa todo desarrollador de aplicaciones — cifrado simétrico, cifrado asimétrico, hashing y firmas digitales — más los dos temas operativos que causan la mayoría de fallos reales: gestión de claves y configuración de TLS. Todos los ejemplos usan pyca/cryptography o el módulo crypto de Node, las dos librerías con menos probabilidades de permitirte hacer algo peligroso por accidente. No hacen falta matemáticas — cuando una decisión depende de detalles internos, lo decimos y te damos el default seguro.

Cuándo usarla

  • Necesitas proteger datos en reposo (campos de base de datos, archivos, backups) o en tránsito (llamadas API, service mesh)
  • Estás implementando autenticación, tokens de sesión o webhooks firmados
  • Necesitas verificar la integridad u origen de datos — checksums, firmas, tags HMAC
  • Estás eligiendo entre librerías criptográficas, algoritmos o tamaños de clave

Para profundizar en proteger datos almacenados — AES-256, integración con KMS, cifrado de sobre — consulta Cifrado en Reposo: AES-256, KMS, Envelope Encryption.

Cifrado Simétrico

La misma clave cifra y descifra. Los cifradores simétricos son rápidos — AES-256 procesa gigabytes por segundo en CPUs modernas — así que soportan casi todo el trabajo de cifrado en volumen. El compromiso es la distribución: todo el que pueda descifrar debe tener la clave, lo que convierte la gestión de claves en la parte difícil.

AES (Advanced Encryption Standard)

AES-256 en modo GCM es el default correcto para sistemas nuevos. GCM es cifrado autenticado: produce un tag que detecta manipulación, así que obtienes confidencialidad e integridad en un paso. Los modos CBC y CTR solo dan confidencialidad — hace falta un HMAC aparte, y equivocar el orden (“encrypt-then-MAC” frente a “MAC-then-encrypt”) ha roto sistemas reales.

from cryptography.fernet import Fernet

# Generar una clave (Fernet = AES-128-CBC + HMAC, API de alto nivel segura)
key = Fernet.generate_key()
cipher = Fernet(key)

# Cifrar
token = cipher.encrypt(b"datos sensibles")

# Descifrar — lanza InvalidToken si el ciphertext fue manipulado
data = cipher.decrypt(token)

Para control de bajo nivel, usa AESGCM directamente. El IV (nonce) debe ser único por clave — nonces aleatorios de 96 bits funcionan hasta aproximadamente 2³² mensajes por clave; reutilizar un nonce con la misma clave destruye por completo la seguridad de GCM.

ModoCaso de usoSeguridad
AES-GCMCasi todo lo nuevoCifrado autenticado — la elección obvia
AES-CBCSolo compatibilidad legacyNecesita HMAC separado para integridad
AES-CTRStreaming, poco comúnNecesita manejo cuidadoso del IV
AES-ECBNuncaFiltra patrones del plaintext — no usar

Gestión de Claves

  • Nunca hardcodees claves ni las subas a un repo; usa un KMS (AWS KMS, GCP KMS, Azure Key Vault) o un gestor de secretos
  • Rota claves periódicamente y siempre ante sospecha de compromiso — diseña para rotación desde el día uno
  • Separa claves por ambiente y propósito: una para campos de BD en dev, otra para backups en prod
  • Guarda las claves en un límite de confianza distinto al de los datos que protegen

Cifrado Asimétrico

Dos claves matemáticamente enlazadas: lo que la clave pública cifra solo la clave privada lo descifra; lo que la clave privada firma, cualquiera con la pública lo verifica. La criptografía asimétrica resuelve el problema de distribución de claves — puedes publicar la mitad pública — pero es miles de veces más lenta que AES y solo cifra payloads pequeños. Por eso los sistemas reales son híbridos: la asimétrica intercambia o protege una clave simétrica, y la simétrica hace el trabajo pesado.

RSA

RSA-2048 es la base; RSA-3072 para márgenes de seguridad más largos. Para cifrar, usa siempre padding OAEP — el RSA de libro de texto y el padding PKCS#1 v1.5 han producido oráculos explotables.

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

# Generar par de claves
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# Cifrar con la clave pública (solo payloads pequeños — normalmente una clave simétrica)
encrypted = public_key.encrypt(
    b"mensaje secreto",
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)

# Descifrar con la clave privada
decrypted = private_key.decrypt(
    encrypted,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)

Curva Elíptica (ECDH/ECDSA)

Seguridad equivalente con claves mucho más pequeñas: una clave EC de 256 bits equivale aproximadamente a una RSA de 3072 bits, y las operaciones son más rápidas. Prefiere SECP256R1 (P-256) por compatibilidad, X25519/Ed25519 donde controlas ambos extremos.

from cryptography.hazmat.primitives.asymmetric import ec

private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

Hashing

Un hash mapea cualquier entrada a una huella de tamaño fijo en una sola dirección — no existe “descifrar”. Se usa para comprobaciones de integridad, direccionamiento por contenido y (con el algoritmo adecuado) almacenamiento de contraseñas.

Algoritmos de Hashing Seguros

AlgoritmoTamaño de salidaEstado
SHA-256256 bitsRecomendado de propósito general
SHA-3VariableRecomendado, diseño diverso
BLAKE2b / BLAKE3256+ bitsRápido, moderno
Argon2idVariableHashing de contraseñas — recomendado
bcrypt184 bitsHashing de contraseñas — sólido legacy
scryptVariableHashing de contraseñas — memory-hard
MD5128 bitsRoto — solo checksums, nunca seguridad
SHA-1160 bitsRoto — no usar
import hashlib

# SHA-256 para comprobaciones de integridad y huellas
digest = hashlib.sha256(b"datos").hexdigest()

# Para contraseñas: usa Argon2id, bcrypt o scrypt — nunca SHA-256 puro.
# Los hashes rápidos están diseñados para velocidad; un atacante prueba miles
# de millones de candidatos por segundo.

Hashing de Contraseñas

Las contraseñas necesitan funciones deliberadamente lentas y memory-hard. Argon2id es la recomendación actual (OWASP, RFC 9106); bcrypt con factor de costo ≥ 12 sigue siendo una opción sólida y muy soportada.

import bcrypt

hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

# Verificar — usa compare de la librería, no ==, para evitar fugas por timing
ok = bcrypt.checkpw(password.encode(), hashed)

HMAC — Hashing con Clave

Para verificar integridad y autenticidad de un mensaje sobre el que compartes un secreto — payloads de webhooks, URLs firmadas — usa HMAC, no un hash simple. HMAC-SHA256 es el estándar; un SHA-256(secreto + mensaje) a secas es vulnerable a ataques de extensión de longitud.

import hmac

tag = hmac.new(secret_key, message, hashlib.sha256).digest()
# Verificar con hmac.compare_digest(tag, received_tag) — comparación de tiempo constante

Firmas Digitales

Una firma demuestra que un mensaje vino del dueño de una clave privada y no fue modificado. Los JWT, releases de software, cadenas de certificados y webhooks firmados dependen de esto.

from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import hashes, serialization

# Firmar con la clave privada
signature = private_key.sign(
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256()
)

# Verificar con la clave pública — lanza InvalidSignature si falla
public_key.verify(
    signature,
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256()
)

Para sistemas nuevos, Ed25519 es más simple y seguro que RSA-PSS: parámetros fijos, más rápido, firmas pequeñas. RSA persiste sobre todo por compatibilidad con JWT (RS256) y PKI legacy.

Transport Layer Security (TLS)

TLS es criptografía híbrida en acción: la asimétrica (certificados + ECDHE) autentica el servidor y negocia claves de sesión, y luego la simétrica AES-GCM o ChaCha20 transporta el tráfico real.

Handshake TLS 1.3 — cliente y servidor intercambian mensajes hello, el servidor presenta su certificado, se derivan las claves y luego fluye tráfico simétrico cifrado.
  • Usa TLS 1.3; desactiva 1.0 y 1.1 por completo y prefiere 1.2 solo por compatibilidad
  • Habilita HSTS para que los navegadores rechacen intentos de downgrade
  • Considera certificate pinning en apps móviles (evalúa bien el costo operativo)
  • Monitorea el vencimiento de certificados a 30, 14 y 7 días — los certificados vencidos son la causa más común de caídas TLS
  • Termina TLS lo más cerca del borde que sea práctico, y cifra también los saltos internos si tu modelo de amenazas incluye movimiento lateral

Certificados y Fundamentos de PKI

Un certificado vincula una clave pública con una identidad — un dominio, una organización — a través de una cadena de firmas que termina en una Certificate Authority que tu cliente ya confía. Cuando la verificación TLS falla, el problema casi siempre está en esa cadena, no en la criptografía.

Las piezas que conviene conocer:

  • Hoja vs cadena: el servidor presenta su certificado hoja más los intermedios; el cliente verifica cada firma hasta llegar a una CA raíz en su trust store. Un intermedio que falta es el clásico “funciona en Chrome, falla en curl” — los navegadores cachean intermedios, otros clientes no.
  • SAN (Subject Alternative Name): el certificado debe listar cada hostname que protege. ejemplo.com y api.ejemplo.com necesitan entradas separadas o un wildcard.
  • Autofirmado vs emitido por CA: autofirmado vale para servicios internos donde controlas el trust store; cualquier cosa pública necesita una CA real. Let’s Encrypt emite certificados gratuitos y automatizados de 90 días — las vidas más cortas son una ventaja, no un defecto, porque la rotación está automatizada.
  • Revocación: OCSP stapling es el mecanismo práctico; las CRL aún importan en PKI interna. Si una clave se compromete, rota el certificado de inmediato — no esperes al vencimiento.

La PKI interna merece la misma disciplina: certificados hoja de vida corta, un emisor automatizado (Vault PKI, smallstep) y una historia clara de qué pasa cuando rota un intermedio.

Elegir la Primitiva Correcta

La mayoría de preguntas de “qué algoritmo” son en realidad de “qué primitiva”. Parte de lo que necesitas garantizar:

NecesidadPrimitivaElección típica
Leer los datos despuésCifrado simétricoAES-256-GCM
Intercambiar claves / cifrar para un destinatarioCifrado asimétricoECDH, RSA-OAEP
Comparar sin guardar el originalHashingSHA-256; Argon2id para contraseñas
Demostrar que un mensaje no se alteróMAC / firmaHMAC-SHA256 (secreto compartido), Ed25519 (verificación pública)
Combinar integridad + secretoCifrado autenticadoAES-GCM, ChaCha20-Poly1305

El patrón híbrido de fondo — asimétrica para intercambio de claves, simétrica para datos — vale la pena memorizarlo porque explica TLS, PGP, el cifrado de sobre y la mayoría de diseños de gestión de claves.

Errores Comunes

  • Inventar tu propia criptografía — las construcciones caseras fallan de formas que los tests unitarios nunca muestran. Usa librerías validadas: pyca/cryptography, libsodium, OpenSSL, Bouncy Castle, crypto de Node.
  • Usar modo ECB — bloques de plaintext idénticos producen ciphertext idéntico; el pingüino sigue visible. GCM en todo.
  • Reutilizar IVs/nonces — catastrófico en CTR y GCM: misma clave + mismo nonce filtra relaciones del plaintext y permite falsificar tags.
  • Hashear contraseñas con SHA-256 — los hashes rápidos son un regalo para la fuerza bruta. Solo Argon2id o bcrypt.
  • Guardar claves junto a los datos — un volcado de la base de datos no debería entregar también las claves. Separa los límites de confianza.
  • Implementar tu propio compare_digest — == sobre secretos filtra por timing. Usa la comparación de tiempo constante de la librería.
  • Ignorar side channels — el timing y los mensajes de error pueden filtrar más de lo que protegen las matemáticas; los padding oracles (POODLE, Lucky13) explotaron exactamente eso.

Solución de Problemas

  • InvalidToken / InvalidTag al descifrar: el ciphertext, la clave, el IV o el auth tag no coinciden — datos corruptos en almacenamiento, versión de clave incorrecta tras una rotación, o manipulación. Registra qué versión de clave se usó.
  • Bad padding / errores OAEP: estás descifrando con la clave privada equivocada o el modo de padding incorrecto. Confirma que el par de certificados y el algoritmo hash de OAEP coinciden en ambos extremos.
  • signature verification failed en JWTs: desfase de reloj (exp/nbf), algoritmo equivocado (la confusión RS256 vs HS256 es un ataque clásico) o caché JWKS desactualizado. Refresca claves, revisa iss/aud, fija el algoritmo.
  • Fallos de handshake tras habilitar TLS 1.3: un middlebox o cliente viejo en el camino. Habilita 1.2+1.3 durante la migración y revisa el ClientHello con una captura de paquetes.
  • cert expired a las 3 de la mañana: no había alertas de vencimiento. Pon la renovación 30 días antes en el runbook y monitorea el certificado hoja y la cadena.
  • Caída de rendimiento bajo carga: firmas RSA en la ruta crítica. Cambia a Ed25519/ECDSA, o firma una vez y cachea.

Ejemplo Avanzado: Cifrado de Sobre en una App Fintech

Un diseño común en producción: una app fintech que maneja transacciones y PII necesita cifrado en tres capas — tránsito, almacenamiento y nivel de campo para las columnas más sensibles.

Capa           Tecnología             Propósito
----           ----------             ---------
Tránsito       TLS 1.3                Comunicación de red
Reposo (BD)    AES-256-GCM            Cifrado de disco/volumen
Nivel campo    AES-256-GCM + sobre    Columnas sensibles (PII)
Backups        AES-256 + KMS          Archivos de backup
Secretos       KMS + rotación         Custodia de claves

El cifrado de sobre mantiene la clave de datos cerca de los datos: genera una Data Encryption Key (DEK) aleatoria, cifra el campo con ella y luego cifra la propia DEK con una Key Encryption Key (KEK) custodiada por KMS. El almacén guarda ciphertext + DEK cifrada; para descifrar hay que pedir a KMS que desenvuelva la DEK primero. Comprometer la base de datos sola no expone nada — la KEK nunca sale del KMS.

// Cifrado de sobre con AWS KMS (Node.js)
const { KMSClient, EncryptCommand, DecryptCommand } = require("@aws-sdk/client-kms");
const crypto = require("crypto");

async function encryptField(plaintext, kmsKeyId) {
  // 1. Generar una DEK aleatoria de 256 bits y un IV único de 96 bits
  const dek = crypto.randomBytes(32);
  const iv = crypto.randomBytes(12);

  // 2. Cifrar el campo con la DEK (AES-256-GCM)
  const cipher = crypto.createCipheriv("aes-256-gcm", dek, iv);
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
  const authTag = cipher.getAuthTag();

  // 3. Cifrar la DEK bajo la KEK de KMS
  const kms = new KMSClient();
  const response = await kms.send(new EncryptCommand({
    KeyId: kmsKeyId,
    Plaintext: dek,
  }));

  // 4. Almacenar: DEK cifrada + IV + authTag + ciphertext
  return {
    encryptedDek: response.CiphertextBlob.toString("base64"),
    iv: iv.toString("base64"),
    authTag: authTag.toString("base64"),
    ciphertext: ciphertext.toString("base64"),
  };
}

async function decryptField(encrypted) {
  // 1. Desenvolver la DEK vía KMS
  const kms = new KMSClient();
  const response = await kms.send(new DecryptCommand({
    CiphertextBlob: Buffer.from(encrypted.encryptedDek, "base64"),
  }));
  const dek = response.Plaintext;

  // 2. Descifrar el campo
  const decipher = crypto.createDecipheriv(
    "aes-256-gcm", dek, Buffer.from(encrypted.iv, "base64")
  );
  decipher.setAuthTag(Buffer.from(encrypted.authTag, "base64"));
  const plaintext = Buffer.concat([
    decipher.update(Buffer.from(encrypted.ciphertext, "base64")),
    decipher.final()
  ]);
  return plaintext.toString("utf8");
}

Conclusiones del diseño:

  • Una DEK por registro (o por campo) limita el radio de impacto; una KEK las envuelve a todas
  • La DEK solo existe en texto plano dentro de la memoria del proceso
  • KMS rota las KEK automáticamente y registra cada desenvoltura en el audit log
  • El IV debe ser único por DEK; guardarlo junto al ciphertext está bien — no es secreto

Lecturas Adicionales

Preguntas frecuentes

¿Cuál es la diferencia entre cifrado y hashing?

El cifrado es reversible — quien tenga la clave puede recuperar el original. El hashing es unidireccional — solo puedes verificar una hipótesis hasheándola y comparando. Cifra lo que necesites después; hashea lo que solo compares, como las contraseñas (con Argon2id o bcrypt, no SHA-256).

¿Debería usar AES-256 o AES-128?

Cualquiera de los dos vale hoy; ambos resisten la fuerza bruta mucho más allá de los presupuestos de ataque prácticos. AES-256 añade margen frente a avances futuros y cuesta algo más de CPU. Los sistemas nuevos normalmente eligen AES-256-GCM y listo.

¿Qué es el cifrado autenticado?

Cifrado que además detecta manipulación. AES-GCM produce un auth tag junto al ciphertext; el descifrado falla ruidosamente si algo se modificó. AES-CBC puro solo oculta los datos — un atacante aún puede voltear bits sin ser detectado salvo que añadas un HMAC.

¿Cuándo uso cifrado simétrico frente a asimétrico?

Simétrico (AES) para datos en volumen en reposo — es rápido y simple pero exige compartir la clave. Asimétrico (RSA, ECDH) para intercambio de claves y firmas — resuelve la distribución pero es lento y con payloads limitados. Los sistemas reales combinan ambos: la asimétrica protege o intercambia una clave simétrica, que luego hace el trabajo pesado. Ese patrón híbrido es exactamente lo que hacen TLS y el cifrado de sobre.

¿Por qué no puedo hashear contraseñas con SHA-256?

SHA-256 está construido para velocidad — las GPUs prueban miles de millones de candidatos por segundo. El hashing de contraseñas necesita lo contrario: funciones deliberadamente lentas y memory-hard como Argon2id o bcrypt que hacen caro cada intento. Misma familia de primitivas, objetivos de diseño opuestos.

¿Es seguro escribir mi propio formato de cifrado?

Casi nunca. Los fallos viven en los detalles: reutilización de IV, padding oracles, falta de autenticación, manejo de claves. APIs de alto nivel como Fernet (Python), secretbox de libsodium o AWS Encryption SDK hacen que la opción segura sea el default — úsalas antes de componer primitivas por tu cuenta.