Saltar al contenido
Nuevo

DeepSeek V4.1 Flash gratis por 7 días: 10M tokens al día tras cualquier recarga. Hasta el 30/11.

Cómo funciona

Contraseñas y hash con Argon2id

Por qué los hashes rápidos fallan con contraseñas, qué hacen la sal y el factor de trabajo, y cómo hashear y verificar con Argon2id en Python.

Intermedio11 min de lecturaPor NotHoly

Qué cubre este artículo

Las bases de contraseñas se filtran con más frecuencia de lo que los equipos planean. Cuando pasa, el atacante obtiene hashes, no contraseñas. Cuánto trabajo le cuesta pasar de uno a otro depende de dos decisiones: la función que usaste y la configuración que le diste. Este artículo explica por qué el almacenamiento de contraseñas necesita un tipo de hash distinto, qué hacen la sal y el factor de trabajo, y por qué Argon2id es la opción que señalan las recomendaciones actuales. Vas a generar y verificar el hash de una contraseña en Python, leer cada parte de la cadena codificada y recalcular los hashes guardados cuando cambien tus parámetros.

Necesitas una terminal y Python 3. El laboratorio instala un solo paquete, argon2-cffi.

Por qué un hash rápido es la herramienta equivocada

Una función hash convierte una entrada en una salida de tamaño fijo. SHA-256 lo hace muy rápido, y eso es justo lo que quieres para sumas de verificación e integridad de archivos. En contraseñas, la velocidad es el problema. Una tabla de contraseñas robada se ataca adivinando: se toma un candidato, se calcula su hash, se compara y se repite. Si cada intento cuesta poco, el atacante puede hacer miles de millones en tarjetas gráficas.

La guía OWASP Password Storage Cheat Sheet es directa en este punto. SHA-256 es rápido, y la rapidez es exactamente lo que necesita un atacante cuando la entrada es una contraseña, una cadena corta que elige una persona.

Puedes medir la diferencia en tu propia máquina. El benchmark del laboratorio calcula el hash de una contraseña con SHA-256 salado y con Argon2id, y muestra la velocidad de cada uno.

Qué hace la sal

La sal es un valor aleatorio, distinto para cada contraseña, que se guarda junto al hash. Cumple dos funciones.

  • Dos usuarios con la misma contraseña terminan con valores almacenados distintos. Una tabla filtrada no revela qué cuentas comparten contraseña.
  • Inutiliza las tablas precalculadas. Sin sal, el atacante calcula una vez los hashes de una lista de contraseñas comunes y busca cada hash filtrado. Con una sal única por usuario, cada contraseña hay que atacarla por separado.

La sal no es un secreto y no vuelve fuerte una contraseña débil. Su trabajo es impedir que el atacante reutilice trabajo entre usuarios. La RFC 9106 recomienda una sal de 128 bits y admite 64 bits cuando el espacio es limitado. La NIST SP 800-63B exige al menos 32 bits. El valor por defecto de argon2-cffi son 16 bytes, es decir, 128 bits.

Qué hace el factor de trabajo

El factor de trabajo es una configuración que hace que cada intento sea caro a propósito. Los esquemas más antiguos, como PBKDF2, cuentan sobre todo iteraciones. Argon2 tiene tres parámetros.

  • Costo de memoria (m): la RAM, en KiB, que usa cada hash.
  • Costo de tiempo (t): cuántas pasadas hace la función sobre esa memoria.
  • Paralelismo (p): cuántos carriles pueden ejecutarse a la vez.

La memoria es lo que más pesa. Una GPU tiene miles de núcleos pequeños, y cada núcleo tiene poca memoria propia. Si cada intento necesita decenas de megabytes, el atacante no puede hacer tantos intentos en paralelo como haría contra una función que necesita pocos kilobytes. A esta propiedad se la llama dureza de memoria (memory-hardness).

El factor de trabajo es una perilla, no una constante. Súbelo a medida que el hardware se vuelve más rápido y que tu presupuesto de inicio de sesión lo permita. Guarda la configuración junto a cada hash, para que los hashes viejos se puedan reconocer y actualizar después. Los últimos pasos del laboratorio lo muestran.

Por qué Argon2id

Argon2 ganó la Password Hashing Competition en 2015. Tiene tres variantes.

  • Argon2d elige las direcciones de memoria según los datos. Resiste bien los ataques con GPU, pero ese patrón de acceso dependiente de los datos puede filtrar información por tiempos.
  • Argon2i usa un patrón de acceso que no depende de la entrada. Eso protege contra las filtraciones por tiempo, pero cede algo de resistencia frente a ataques que cambian memoria por tiempo.
  • Argon2id usa el patrón independiente de los datos en la primera mitad de la primera pasada y luego pasa al dependiente.

Argon2id cubre las dos preocupaciones. La RFC 9106 lo recomienda como variante de uso general, y OWASP lo pone primero entre sus opciones para hashear contraseñas.

La RFC 9106 da dos configuraciones recomendadas. La primera, para sistemas con memoria de sobra, usa 2 GiB, 1 pasada y 4 carriles. La segunda, para entornos con memoria limitada, usa 64 MiB, 3 pasadas y 4 carriles. El mínimo de OWASP es 19 MiB, 2 iteraciones y 1 carril. OWASP también lista alternativas que cambian memoria por pasadas, como 12 MiB con 3 pasadas o 7 MiB con 5 pasadas, todas con 1 carril. Los valores por defecto de argon2-cffi coinciden con la segunda configuración de la RFC.

Trata estos valores como mínimos y puntos de partida, no como metas. Una regla práctica habitual es ajustar la configuración hasta que un hash tarde unos cientos de milisegundos en tu hardware de producción. Después, comprueba que la memoria por hash, multiplicada por el número de inicios de sesión que pueden llegar al mismo tiempo, siga cabiendo en tus servidores. Esa regla viene de la práctica, no de ninguno de los dos estándares.

La NIST enfoca el tema de otra forma. La SP 800-63B exige que las contraseñas se salen y se hasheen con un esquema adecuado, y dice que se debe usar un esquema aprobado de la SP 800-132, que cubre PBKDF2. No menciona Argon2id. Si un programa de cumplimiento exige algoritmos aprobados por la NIST, consulta con tu auditor antes de elegir Argon2id.

Cómo funciona el hash guardado, paso a paso

Cuando una biblioteca de hash de contraseñas guarda una contraseña, ocurren cinco cosas.

  1. La biblioteca genera una sal aleatoria.
  2. Ejecuta Argon2id sobre la contraseña, la sal y los parámetros de costo.
  3. Codifica la variante, la versión, los parámetros, la sal y la salida en una sola cadena.
  4. Al iniciar sesión, lee la cadena, toma de ella la sal y los parámetros, recalcula la salida y compara.
  5. Si los parámetros de la cadena son más antiguos que tu configuración actual, recalcula el hash después de un inicio de sesión correcto.

El paso 3 es la razón por la que no necesitas una columna aparte para la sal. La cadena codificada tiene este formato.

text
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
  • argon2id es la variante.
  • v=19 es la versión de Argon2, 1.3, escrita en decimal. En hexadecimal es 0x13.
  • m, t y p son memoria en KiB, costo de tiempo y paralelismo.
  • <salt> y <hash> van en base64 sin relleno. El hash es la salida derivada, de 32 bytes por defecto.

La cadena del laboratorio tiene 97 caracteres. Usa una columna que admita al menos 255 caracteres, o un tipo de texto. No la partas en columnas, a menos que disfrutes escribiendo tu propio parser.

Laboratorio práctico

El laboratorio usa un entorno virtual local y no necesita ningún servicio de red. La salida que aparece abajo viene de una máquina Windows 11 con Python 3.12.10, argon2-cffi 25.1.0 y argon2-cffi-bindings 26.1.0. En esa máquina, python3 no estaba en el PATH de Git Bash, así que los comandos se ejecutaron como python. Usa python3 si en tu equipo funciona. Tus cadenas de hash serán distintas de las mostradas, porque la sal es aleatoria.

1. Crea el entorno

bash
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffi

En Windows, activa con .venv\Scripts\activate en PowerShell o cmd, o con . .venv/Scripts/activate en Git Bash.

2. Calcula el hash, verifica y revisa los parámetros

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h); print(ph.verify(h,'correct horse battery')); print(ph.check_needs_rehash(h))"

Salida observada:

text
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
False

verify devuelve True con la contraseña correcta. check_needs_rehash devuelve False porque este hash se generó con los valores por defecto actuales. Esos valores son costo de tiempo 3, costo de memoria 65536 KiB (64 MiB), paralelismo 4, hash de 32 bytes, sal de 16 bytes y la variante ID.

3. Detecta una contraseña incorrecta

bash
python3 -c "from argon2 import PasswordHasher; from argon2.exceptions import VerifyMismatchError; ph=PasswordHasher(); h=ph.hash('a');
try: ph.verify(h,'b')
except VerifyMismatchError: print('mismatch detected')"

Salida observada:

text
mismatch detected

Una contraseña incorrecta no devuelve False. verify lanza VerifyMismatchError, así que la comprobación va dentro de un bloque except. Una cadena guardada mal formada lanza otra excepción, InvalidHashError. Trátala como un problema de datos del servidor y no dejes que llegue a la pantalla del usuario.

4. Misma contraseña, sales distintas

Calcula el hash de la misma contraseña dos veces.

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"

Salida observada:

text
$argon2id$v=19$m=65536,t=3,p=4$p8OU0plYOCO2/v70mjYTFA$FwhMW3ECJ0F/8rulKrJ158HjyNekURguaCIfoVk0i/o
$argon2id$v=19$m=65536,t=3,p=4$MTgAgrtBHJmc1eL2iKy8nA$h/7+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYg

Una contraseña, dos cadenas distintas. Los parámetros coinciden porque ambas ejecuciones usaron los mismos valores por defecto. Solo cambian la sal y la salida. Quien lea la tabla no puede saber que esas dos cuentas usan la misma contraseña.

5. Lee las partes de la cadena codificada

Guarda esto como parts.py.

python
from argon2 import PasswordHasher

h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))

Ejecútalo con python3 parts.py. Salida observada:

text
['', 'argon2id', 'v=19', 'm=65536,t=3,p=4', 'waS+Ec4iQCNIuhRQbv2sWQ', 'yLioGsHJoVv+vTu2kRoYmXTLjbbjJSe2tPSExz3Sd5A']

El primer elemento vacío viene del $ inicial. Después están la variante, la versión, los parámetros, la sal y el hash. Verificar no necesita nada más, por eso una sola columna basta.

6. Recalcula el hash cuando cambien los parámetros

Las configuraciones cambian con el tiempo. Este script genera un hash con una configuración antigua y más débil, y luego lo compara con los valores actuales. Guárdalo como rehash.py.

python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

old = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
stored = old.hash('correct horse battery')
print('stored:', stored)

new = PasswordHasher()  # valores por defecto actuales
print('needs rehash under new params:', new.check_needs_rehash(stored))
try:
    new.verify(stored, 'correct horse battery')
    print('verify ok')
    if new.check_needs_rehash(stored):
        stored = new.hash('correct horse battery')
        print('rehashed:', stored)
except VerifyMismatchError:
    print('wrong password')
print('after rehash needs rehash:', new.check_needs_rehash(stored))

Ejecuta python3 rehash.py. Salida observada:

text
stored: $argon2id$v=19$m=19456,t=2,p=1$EWji+p6+3HbAKY+wkykcXg$NxPq/zGPsx9okK4vQCKfZ4BVoSOqeF1QkHxd3f2g2uE
needs rehash under new params: True
verify ok
rehashed: $argon2id$v=19$m=65536,t=3,p=4$FkifxeWUHV9eU7d8JFt6mg$ZFoMkNjYqo4D/bax/jPz80Hvr4mjLWRi4TVoqCoR6ng
after rehash needs rehash: False

El orden importa. Primero verifica y después recalcula, para que solo una contraseña correcta llegue a la actualización. En una aplicación real, guarda la cadena nueva en la misma fila justo después de un inicio de sesión correcto. Si omites ese paso, las configuraciones antiguas se quedan en la tabla para siempre.

7. Mide el costo en tu hardware

Guarda esto como bench.py.

python
import hashlib, time, os
from argon2 import PasswordHasher

pw = b'correct horse battery'
salt = os.urandom(16)
n = 1_000_000
t = time.perf_counter()
for _ in range(n):
    hashlib.sha256(salt + pw).digest()
dt = time.perf_counter() - t
print(f"sha256 salted: {n} hashes in {dt:.2f} s, {n/dt:,.0f} per second")

ph = PasswordHasher()
m = 10
t = time.perf_counter()
for _ in range(m):
    ph.hash('correct horse battery')
dt = time.perf_counter() - t
print(f"argon2id default: {m} hashes in {dt:.2f} s, {dt/m*1000:.0f} ms per hash")

Ejecuta python3 bench.py. Salida observada:

text
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hash

Son mediciones únicas en una máquina Windows. La línea de SHA-256 mide un bucle de Python, no código optimizado, así que subestima lo que hace el hardware dedicado con SHA-256. La línea de Argon2id es el costo que paga cada inicio de sesión. Importa más la diferencia que las cifras exactas. Una llamada de SHA-256 salado tarda unos 4 microsegundos aquí. Una llamada de Argon2id tarda unos 640 milisegundos, y eso es el diseño. Ajusta m, t y p en hardware parecido al de producción, hasta que un inicio de sesión cueste algunos cientos de milisegundos.

Errores comunes

  • Hash rápido. Usar SHA-256, SHA-512 o MD5 para contraseñas. Una contraseña no es una suma de verificación de archivo.
  • Sal compartida o predecible. Una sola sal para todos los usuarios, o una sal derivada del nombre de usuario. Genera una sal aleatoria para cada contraseña.
  • Copiar la configuración sin medir. Los valores por defecto de la biblioteca son un buen comienzo, no un veredicto para tu servidor. Mide primero.
  • Memoria sin presupuesto de concurrencia. 64 MiB por inicio de sesión parecen poco hasta que llegan 100 a la vez, unos 6,4 GB. Dimensiona la memoria para el pico de inicios de sesión simultáneos.
  • Comparar cadenas de hash a mano. Llama a verify. No compares cadenas con ==.
  • Contraseñas en los logs. Los logs, los reportes de error y la analítica nunca deben contener la contraseña, el hash ni el cuerpo completo de una petición de inicio de sesión.
  • Sin ruta de actualización. Deja los parámetros dentro de la cadena guardada y recalcula el hash en cada inicio de sesión correcto.
  • Tratar la pimienta como solución. OWASP sugiere una pimienta, un secreto guardado fuera de la base de datos, como defensa en profundidad. La guía también señala que la pimienta, por sí sola, aporta poco. Guárdala en un gestor de secretos si tienes uno, y no la uses en lugar de un hash adecuado.

Cómo seguir

  • Lee las secciones de parámetros y de seguridad de la RFC 9106 y compáralas con tu modelo de amenazas.
  • Lee la documentación de argon2-cffi sobre PasswordHasher, verify y check_needs_rehash.
  • Si tu framework web tiene un módulo de contraseñas, revisa si ya usa Argon2id antes de escribir un wrapper propio.
  • Antes de diseñar un flujo de inicio de sesión, aclara la diferencia entre hash, codificación y cifrado. El artículo anterior lo cubre: cifrado vs codificación vs hash. El siguiente avanza con pares de claves y firmas: claves públicas y privadas explicadas.

Fuentes

Fuentes

  1. RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
  2. OWASP Cheat Sheet Series: Password Storage Cheat Sheet
  3. NIST SP 800-63B-4: Authentication and Authenticator Management
  4. argon2-cffi documentation

Más en Criptografía