Hashing y cripto
Comparativa de funciones hash — MD5, familia SHA, bcrypt, Argon2
No todas las funciones hash hacen el mismo trabajo. Cuándo MD5 sigue siendo válido, por qué SHA-256 es el default, y por qué nunca debes hashear passwords con ninguno.
“Función hash” cubre tres categorías de trabajo que solo se tocan en los bordes: digests criptográficos, funciones de password-hashing, y checksums rápidos no criptográficos. Usar la categoría equivocada para el trabajo es como se produjeron la mayoría de fugas de passwords del sector. Esta guía es la separación práctica.
Las tres categorías
| Categoría | Objetivo | Ejemplos | Velocidad | ¿Vale para passwords? |
|---|---|---|---|---|
| Digest criptográfico | Resistencia a colisión y preimagen | SHA-256, SHA-3, BLAKE3 | Rápido | No |
| Password-hashing | Lento a propósito, memory-hard | bcrypt, Argon2id, scrypt | Lento (parametrizable) | Sí — es el trabajo |
| Checksum no criptográfico | Detectar corrupción accidental | CRC32, xxHash, MurmurHash | Muy rápido | No |
Error común: tirar de SHA-256 para “hashear el password”. SHA-256 es digest criptográfico y es extremadamente rápido. Esa velocidad es el problema: una sola GPU calcula miles de millones de SHA-256 por segundo contra una base de datos de passwords robada. Las funciones de password-hashing (bcrypt, Argon2id) están diseñadas para ser lentas y caras de forma ajustable.
Los digests criptográficos
| Algoritmo | Salida | Estado | Cuándo usar |
|---|---|---|---|
| MD5 | 128 bits | Roto para seguridad | Solo checksums; deduplicación de archivos, claves de caché |
| SHA-1 | 160 bits | Roto (SHAttered 2017) | Solo legacy; Git lo usa pero migra a SHA-256 |
| SHA-256 | 256 bits | Estándar actual | Elección por defecto para cualquier digest criptográfico |
| SHA-512 | 512 bits | Estándar actual | Misma clase de seguridad; a veces más rápido en CPUs 64-bit |
| SHA-3 (Keccak) | 224-512 bits | Estándar actual | Alternativa aprobada por NIST con diseño distinto; úsalo si necesitas primitivas diversas |
| BLAKE3 | Ajustable | Moderno, rápido | Digest paralelo muy rápido; más nuevo, menos batalla |
MD5 no es palabrota. Está roto para uso criptográfico — se pueden construir colisiones barato — pero va bien para detección de corrupción accidental. Si checksumeas un archivo contra corrupción en tránsito, MD5 o incluso CRC32 es más rápido que SHA-256 y el modelo de ataque no importa. Si verificas que un archivo no ha sido manipulado por un atacante, necesitas SHA-256 o superior.
# Estos comandos hashean el mismo archivo con algoritmos distintos
md5sum file.tar
sha256sum file.tar
b3sum file.tar # BLAKE3, si está instalado
SHA-1 sobrevivió algo más que MD5 porque su coste de colisión era mayor, pero Google demostró una colisión práctica en 2017 (el paper SHAttered). Git sigue usando SHA-1 para IDs de objeto pero tiene ruta de migración a SHA-256. Los sistemas nuevos deben defaultear a SHA-256.
Funciones de password-hashing
El modelo de ataque para passwords es offline: un atacante roba tu BD y la crackea en su propio hardware. Tu defensa es hacer que cada computación de hash sea lo bastante cara como para que una granja de GPUs no pueda reventar los passwords débiles antes de que puedas responder.
Dos cosas que necesitas:
- Una función lenta, parametrizada para poder escalarla con el tiempo mientras el hardware mejora.
- Un salt único por usuario, guardado con el hash, para que tablas rainbow precomputadas sean inservibles.
Las opciones recomendadas actuales, en orden aproximado de modernidad:
- Argon2id — Recomendación OWASP 2026. Memory-hard (los atacantes no pueden tirar solo GPUs), resistente a canales laterales, ganó la Password Hashing Competition en 2015. Úsalo si tu stack tiene librería auditada (
argon2-cffien Python,argon2en Node,golang.org/x/crypto/argon2en Go). - bcrypt — El estándar de 1999, sigue bien. Bien auditado, ubicuo. El parámetro de coste (el “work factor”) es ajustable. Las librerías tienen límites de longitud de password (72 bytes para bcrypt) que debes manejar — o truncar o pre-hashear con SHA-256 antes de bcrypt.
- scrypt — Memory-hard como Argon2. Menos usado hoy porque Argon2 subsume sus objetivos de diseño, pero sigue seguro.
- PBKDF2 — Aprobado por FIPS. Úsalo solo si tienes razón de compliance; si no, es estrictamente peor que Argon2id.
# Argon2id en Python (argon2-cffi)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # iteraciones
memory_cost=64_000, # 64 MiB
parallelism=4,
)
hash_ = ph.hash("user-password")
# "$argon2id$v=19$m=65536,t=3,p=4$saltsalt...$hash..."
ph.verify(hash_, "user-password") # True
Regla: nunca uses SHA-256, SHA-3, BLAKE3, o cualquier digest criptográfico plano para hashear passwords. Ni siquiera con salt. Ni siquiera con múltiples iteraciones (estás reimplementando PBKDF2 mal). Usa una función de password-hashing.
Para los tokens de auth construidos sobre estos cimientos, mira la pieza JWT en 2026; para el encoding URL-safe de tokens que los suele acompañar, la guía de Base64 en producción.
Hashes no criptográficos
Estas son herramientas para hash tables, claves de caché, almacenamiento content-addressed, y Bloom filters. Son rápidas y distribuyen bien la entrada, pero se colisionan trivialmente por diseño.
| Algoritmo | Velocidad | Cuándo usar |
|---|---|---|
| CRC32 | Muy rápido, 32 bits | Checksums de archivos en formatos legacy, integridad rápida |
| FNV | Rápido, cualquier ancho | Hashing de strings en apps simples |
| MurmurHash3 | Rápido, bien distribuido | Hash tables, Bloom filters, sharding de caché |
| xxHash | Más rápido no-crypto | Checksums de payloads grandes, dedup de logs |
| CityHash / FarmHash | Rápido | Origen Google; Protocol Buffers y BigQuery usan parientes |
Nunca uses estos contra input no confiable cuando las colisiones importen para seguridad. Un atacante puede fabricar inputs que colisionen en MurmurHash y usarlo para crear ataques HashDoS contra hash tables — razón por la que los runtimes modernos usan semillas de hash aleatorias en startup.
HMAC — lo que en realidad necesita la mayoría
Si estás firmando un API request, un webhook, o una cookie, no quieres un hash crudo. Quieres HMAC (Hash-based Message Authentication Code). HMAC envuelve un hash criptográfico con una clave de forma que demuestra que el mensaje fue firmado por alguien con la clave.
import hmac, hashlib
key = b"secret"
message = b"payload=value&ts=123"
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
Tres cosas que HMAC protege y un sha256(key + message) crudo no:
- Ataques de extensión de longitud contra la concatenación cruda.
- Ataques de truncamiento.
- Mal uso del estado interno del hash.
Default a HMAC-SHA256 para firmar. Stripe, GitHub, la mayoría de providers de webhooks lo usan porque es la opción sin trampas.
Conclusiones
Tres categorías, tres herramientas: un digest criptográfico (SHA-256 por defecto) para integridad; una función de password-hashing (Argon2id o bcrypt) para passwords; un hash no criptográfico (xxHash, Murmur) para hash tables y checksums. MD5 solo está muerto para crypto — sirve para dedup. Usa siempre HMAC cuando firmes, no hash crudo. Nunca hashees passwords con nada que no sea una función dedicada de password-hashing.