Contraseñas y derivación de claves: almacenamiento seguro y políticas técnicas

Capítulo 6

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

Almacenamiento seguro de contraseñas: qué problema resuelve y qué NO

Una contraseña no es una clave criptográfica uniforme: suele tener baja entropía, patrones humanos y reutilización. Por eso, el objetivo del almacenamiento no es “cifrar contraseñas”, sino guardar un verificador resistente a ataques offline (cuando el atacante roba la base de datos) y que, a la vez, permita verificación eficiente en línea. La técnica estándar es usar funciones de hashing adaptativas y costosas (password hashing / password KDF): Argon2id, bcrypt o scrypt, siempre con sal única por usuario y, opcionalmente, pepper gestionada fuera de la base de datos.

Qué guardar en la base de datos

  • Identificador de algoritmo y versión (p. ej., Argon2id).
  • Parámetros (memoria, iteraciones, paralelismo; o cost en bcrypt; o N/r/p en scrypt).
  • Sal aleatoria por registro (16 bytes o más).
  • Hash/verificador resultante.

En la práctica se almacena en un formato autocontenido (por ejemplo, el “PHC string format”), que incluye algoritmo y parámetros junto al hash.

Funciones recomendadas: Argon2id, bcrypt, scrypt

Argon2id (recomendado por defecto)

Argon2id combina resistencia a ataques de canal lateral (como Argon2i) con resistencia a cracking con GPU/ASIC (como Argon2d). Es la opción preferida cuando puedes asignar memoria por verificación.

  • Sal: 16 bytes aleatorios (mínimo).
  • Memoria (m): el parámetro más importante; sube el coste para GPU/ASIC.
  • Iteraciones (t): incrementa el trabajo CPU.
  • Paralelismo (p): ajusta hilos; no lo uses para “abaratar”, úsalo para aprovechar CPU.

Guía de parametrización por latencia objetivo: calibra en tu hardware real (mismo tipo de instancia/CPU) y define un presupuesto de latencia por verificación (por ejemplo, 100–250 ms en backend). Como punto de partida típico en servidores modernos:

EscenarioLatencia objetivoArgon2id sugerido (aprox.)
Alta concurrencia / login frecuente~50–100 msm=64–128 MiB, t=1–2, p=1–4
Equilibrado (común)~100–250 msm=128–256 MiB, t=2–3, p=1–4
Máxima dureza (baja frecuencia)~250–500 msm=256–1024 MiB, t=3–5, p=2–8

Notas prácticas: (1) aumenta primero memoria y luego iteraciones; (2) evita parámetros que provoquen swapping; (3) mide p95/p99 bajo carga, no solo una ejecución aislada.

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

bcrypt (compatibilidad y despliegues heredados)

bcrypt es robusto y ampliamente soportado, pero su coste es principalmente CPU y no “memory-hard” como Argon2id. Sigue siendo aceptable si necesitas compatibilidad o librerías maduras en muchos entornos.

  • Cost (work factor): se expresa como 2^cost iteraciones internas.
  • Recomendación práctica: elige un cost que dé ~100–250 ms en tu servidor (por ejemplo, 10–14 suele ser un rango común, pero debe calibrarse).
  • Limitación: entrada efectiva truncada en algunas implementaciones (históricamente 72 bytes); evita contraseñas extremadamente largas sin preprocesamiento cuidadoso.

scrypt (alternativa memory-hard)

scrypt también es memory-hard y puede ser una buena alternativa cuando Argon2id no está disponible. Sus parámetros son más propensos a configuraciones peligrosas si no se calibran bien.

  • N: factor de coste (potencia de 2).
  • r: tamaño de bloque (impacta memoria).
  • p: paralelismo.
  • Recomendación práctica: calibra para una latencia objetivo y memoria razonable por verificación; documenta límites para evitar DoS por parámetros excesivos.

Sal (salt) y pepper: roles distintos

Sal: pública, única, no secreta

La sal evita ataques con tablas arcoíris y hace que dos usuarios con la misma contraseña tengan hashes distintos. Reglas:

  • Genera con CSPRNG.
  • Única por usuario/registro.
  • Almacénala junto al hash.
  • No reutilices sal entre usuarios.

Pepper: secreta, global (o por segmento), fuera de la BD

La pepper es un secreto adicional que se mezcla con la contraseña antes de aplicar la función (por ejemplo, concatenando o usando HMAC como preprocesamiento). Su objetivo es elevar el coste del atacante si solo roba la base de datos, porque sin pepper no puede verificar candidatos offline.

Separación de responsabilidades: la pepper no debe vivir en la misma base de datos ni en el mismo plano de acceso que los hashes. Opciones típicas:

  • HSM: la pepper (o una clave para HMAC) no sale del hardware; el servicio solicita una operación (p. ej., HMAC) y recibe el resultado.
  • KMS: almacena y rota claves; el servicio obtiene un token/clave derivada o invoca una operación criptográfica.
  • Secret manager: almacena el secreto con control de acceso y auditoría; el servicio lo carga en memoria al arrancar (menos fuerte que HSM, pero útil).

Patrón recomendado: usa una clave de pepper para HMAC y pre-hashea la contraseña con HMAC antes de Argon2id/bcrypt/scrypt. Esto reduce riesgos de concatenación ambigua y facilita rotación por versión.

peppered = HMAC-SHA-256(key=PEPPER_KEY, msg=password_utf8)  // en HSM/KMS idealmente
stored = Argon2id(password=peppered, salt=user_salt, params=...)

Guarda un pepper_id (versión) con el registro para soportar rotación gradual.

Guía práctica paso a paso: implementar almacenamiento y verificación

1) Registro / cambio de contraseña

  • Normaliza entrada de forma consistente (p. ej., UTF-8) y define reglas claras (longitud mínima/máxima, permitir passphrases).
  • Genera salt aleatoria (≥16 bytes).
  • Obtén pepper o invoca HSM/KMS para calcular HMAC (no registres la contraseña).
  • Calcula el hash con Argon2id/bcrypt/scrypt usando parámetros calibrados.
  • Almacena: algoritmo, parámetros, salt, hash, pepper_id y metadatos de política (p. ej., fecha de actualización).

2) Login / verificación

  • Recupera el registro por identificador (email/username) de forma que no filtre existencia (ver mitigación de enumeración más abajo).
  • Recalcula peppered (HMAC con pepper actual o según pepper_id).
  • Recalcula el hash con los mismos parámetros y salt.
  • Compara con tiempo constante.
// Pseudocódigo conceptual
candidate = KDF(password_input, salt, params, pepper_id)
if constant_time_equal(candidate, stored_hash): success else fail

3) Rehash / actualización de parámetros (migración gradual)

Los parámetros “buenos” cambian con el tiempo. Implementa rehash on login:

  • Si el algoritmo ya no es el preferido (p. ej., bcrypt → Argon2id), o los parámetros son inferiores a los actuales, entonces tras un login exitoso recalcula y actualiza el registro con los parámetros nuevos.
  • Si rotas pepper, acepta temporalmente múltiples pepper_id: verifica con el pepper_id almacenado; si es antiguo y el login es válido, reescribe con el pepper_id nuevo.

Verificación en tiempo constante y errores uniformes

Comparación constante

La comparación del hash calculado con el almacenado debe ser en tiempo constante para evitar filtraciones por timing. Usa funciones de la librería estándar/cripto (p. ej., constant_time_compare) y evita comparaciones de strings que cortan al primer byte distinto.

Mensajes de error uniformes

No devuelvas “usuario no existe” vs “contraseña incorrecta”. Devuelve un error genérico (“credenciales inválidas”) y registra internamente la causa. Esto reduce enumeración y también evita que un atacante optimice ataques online.

Mitigación de enumeración de usuarios desde el punto de vista criptográfico

Incluso con mensajes uniformes, diferencias de tiempo pueden revelar si el usuario existe (porque solo para usuarios existentes haces una verificación costosa). Mitigaciones:

  • Hash “falso” para usuarios inexistentes: si no existe el usuario, ejecuta igualmente una verificación con parámetros equivalentes usando un salt y hash “dummy” constantes del sistema (o precomputados) para igualar tiempos.
  • Ruta de ejecución uniforme: evita early returns antes de la operación costosa; mantén un flujo similar.
  • Jitter controlado: añade pequeñas variaciones aleatorias puede ayudar, pero no sustituye la uniformidad del trabajo criptográfico.
// Idea: siempre ejecutar una verificación costosa
record = find_user(identifier)
if record exists:
  salt, params, stored = record
else:
  salt, params, stored = DUMMY_SALT, DUMMY_PARAMS, DUMMY_HASH
candidate = KDF(password_input, salt, params, pepper_id_or_default)
ok = constant_time_equal(candidate, stored)
if record exists and ok: success else fail

Rate limiting y control de intentos: interacción con KDF

El hashing costoso ya actúa como “freno”, pero no es suficiente: un atacante distribuido puede paralelizar intentos. Combina:

  • Rate limiting por cuenta (p. ej., N intentos por ventana).
  • Rate limiting por IP / ASN / dispositivo con señales de riesgo.
  • Backoff progresivo (incrementa espera tras fallos).
  • Bloqueo temporal con cuidado para evitar DoS contra usuarios (mejor: fricción incremental que bloqueo duro).

Desde el punto de vista criptográfico/operativo: define un presupuesto de CPU/memoria por segundo para verificaciones y aplica colas o “circuit breakers” para proteger el servicio. Si Argon2id usa mucha memoria, limita concurrencia de verificaciones para evitar presión de memoria bajo ataque.

KDF de contraseñas vs KDF de claves: no son intercambiables

Password hashing (Argon2id/bcrypt/scrypt)

Diseñado para entradas de baja entropía y para ser caro de probar masivamente. Se usa para verificar contraseñas, no para expandir material de clave en múltiples subclaves de un protocolo.

HKDF (derivación de claves desde un secreto de alta entropía)

HKDF se usa cuando ya tienes un secreto raíz con entropía suficiente (por ejemplo, una clave maestra generada aleatoriamente, o el resultado de un intercambio de claves). HKDF permite derivar múltiples claves independientes y con contexto, de forma segura y eficiente.

HKDF tiene dos fases:

  • Extract: convierte el input (IKM) en una PRK uniforme usando HMAC con una sal opcional.
  • Expand: deriva una o más claves usando un info que etiqueta propósito y contexto.

Derivar múltiples claves (cifrado, MAC, rotación) desde un secreto raíz con HKDF

Patrón recomendado: una raíz, muchas subclaves con “info” distinto

Supón que tienes un secreto raíz root_secret (32 bytes aleatorios) almacenado en un KMS/HSM o generado al aprovisionar un tenant. Quieres derivar claves separadas para cifrado, autenticación y rotación por versión.

// HKDF-Extract(salt, IKM) -> PRK
PRK = HKDF-Extract(salt = tenant_id_or_random_salt, IKM = root_secret)

// HKDF-Expand(PRK, info, L) -> OKM
K_enc_v1 = HKDF-Expand(PRK, info = "enc:v1:serviceA", L = 32)
K_mac_v1 = HKDF-Expand(PRK, info = "mac:v1:serviceA", L = 32)
K_wrap_v1 = HKDF-Expand(PRK, info = "wrap:v1:serviceA", L = 32)

Buenas prácticas:

  • Separación por propósito: usa etiquetas info diferentes para cada uso (enc/mac/wrap).
  • Separación por contexto: incluye identificadores como servicio, entorno (prod/stage) y tenant.
  • Rotación: incrementa versión en info (v1, v2) o cambia el root_secret. Mantén compatibilidad leyendo versión desde metadatos del dato cifrado.
  • Longitud: deriva exactamente el tamaño requerido por el algoritmo (p. ej., 32 bytes para claves de 256 bits).

Ejemplo de esquema de rotación con metadatos

Cuando cifres datos, guarda junto al ciphertext un encabezado con kdf=HKDF, root_id (o key id del KMS) y version. En descifrado, usas esos metadatos para derivar la clave correcta:

header: { root_id: "tenantRootKey", version: 2, purpose: "enc", context: "serviceA" }
K_enc = HKDF-Expand(PRK(root_id), info = "enc:v2:serviceA", L = 32)

Esto permite rotación sin re-encriptar inmediatamente todo: puedes soportar múltiples versiones en lectura y reescribir en la versión nueva de forma gradual.

Checklist técnico (implementación y operaciones)

  • Usa Argon2id si puedes; si no, bcrypt o scrypt con calibración.
  • Calibra parámetros con pruebas de carga y objetivos p95/p99; documenta el presupuesto de recursos.
  • Sal única por usuario (≥16 bytes) siempre.
  • Pepper opcional pero valiosa: guárdala fuera de la BD (HSM/KMS/secret manager) y versiona (pepper_id).
  • Comparación constante y errores uniformes.
  • Mitiga enumeración igualando trabajo criptográfico para usuarios inexistentes.
  • Rate limiting y control de concurrencia para proteger CPU/memoria.
  • Rehash on login para migrar parámetros/algoritmos y rotar pepper.
  • No uses HKDF para almacenar contraseñas; úsalo para derivar subclaves desde secretos de alta entropía.

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la combinación de prácticas que mejor protege el almacenamiento de contraseñas frente a un robo de base de datos (ataque offline) sin impedir la verificación en línea?

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

¡Tú error! Inténtalo de nuevo.

El objetivo no es cifrar contraseñas, sino guardar un verificador resistente a ataques offline. Para ello se usan KDFs de contraseñas costosas con sal única por usuario; una pepper fuera de la BD añade una barrera adicional si la BD es robada.

Siguiente capítulo

Firmas digitales y certificados: integridad, autoría y cadena de confianza

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

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.