Criptografía
Cifrado, codificación y hash: diferencias explicadas
Separa codificación, cifrado y hash con laboratorios de Base64, SHA-256 y OpenSSL. Entiende por qué Base64 no protege y el hash es de un solo sentido.
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.
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.
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.
La sal es un valor aleatorio, distinto para cada contraseña, que se guarda junto al hash. Cumple dos funciones.
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.
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.
m): la RAM, en KiB, que usa cada hash.t): cuántas pasadas hace la función sobre esa memoria.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.
Argon2 ganó la Password Hashing Competition en 2015. Tiene tres variantes.
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.
Cuando una biblioteca de hash de contraseñas guarda una contraseña, ocurren cinco cosas.
El paso 3 es la razón por la que no necesitas una columna aparte para la sal. La cadena codificada tiene este formato.
$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.
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.
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffiEn Windows, activa con .venv\Scripts\activate en PowerShell o cmd, o con . .venv/Scripts/activate en Git 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:
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
Falseverify 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.
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:
mismatch detectedUna 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.
Calcula el hash de la misma contraseña dos veces.
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:
$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+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYgUna 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.
Guarda esto como parts.py.
from argon2 import PasswordHasher
h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))Ejecútalo con python3 parts.py. Salida observada:
['', '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.
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.
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:
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: FalseEl 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.
Guarda esto como bench.py.
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:
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hashSon 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.
verify. No compares cadenas con ==.PasswordHasher, verify y check_needs_rehash.Criptografía
Separa codificación, cifrado y hash con laboratorios de Base64, SHA-256 y OpenSSL. Entiende por qué Base64 no protege y el hash es de un solo sentido.
Criptografía
Claves pública y privada desde cero: qué prueba una firma, por qué la privada no sale de su dueño y un lab Ed25519 donde un archivo alterado falla.
Criptografía
Criptografía desde cero: cifrado, claves, hashes, firmas digitales y por qué el cifrado César enseña ideas pero no protege nada.