
Hashing de contraseñas con Bcrypt: por qué importa y cómo usarlo
📷 Pixabay / PexelsHashing de contraseñas con Bcrypt: por qué importa y cómo usarlo
Las contraseñas almacenadas como hashes SHA-256 pueden descifrarse en minutos. Bcrypt está diseñado para ser lento -- y ese es exactamente el punto. Aprenda cómo funciona bcrypt, cómo elegir un factor de costo y cómo implementarlo en Node.js, Python y PHP.
El problema del almacenamiento de contraseñas
Imagina que estás construyendo un sistema de inicio de sesión. Un usuario crea una cuenta con la contraseña hunter2. Necesitas almacenar algo en tu base de datos para que cuando inicie sesión la próxima semana, puedas verificar que ingresó lo correcto.
La solución ingenua es almacenar hunter2 directamente. Esto es catastrófico. Cualquier violación de base de datos, cualquier inyección SQL, cualquier backup dejado en un bucket S3 mal configurado -- el atacante ahora tiene la contraseña real de cada usuario.
El siguiente paso es hashearlo. SHA-256("hunter2") te da una cadena de longitud fija que no puedes revertir. Almacena eso en su lugar. ¿Mejor, verdad?
Mejor -- pero no suficiente.
El problema es que SHA-256 está diseñado para ser rápido. Las GPU modernas pueden calcular miles de millones de hashes SHA-256 por segundo. Un atacante con una base de datos de contraseñas hasheadas con SHA-256 y una buena GPU puede descifrar una parte significativa de las contraseñas comunes en horas, a veces minutos, usando tablas precalculadas (rainbow tables) o ataques de diccionario.
Bcrypt fue diseñado específicamente para resolver esto. Es intencionalmente lento, y la velocidad es configurable.
Puedes experimentar directamente con hashes bcrypt usando nuestro Bcrypt Hash Generator -- sin configuración requerida.
Qué es realmente Bcrypt
Bcrypt es una función de hashing de contraseñas diseñada por Niels Provos y David Mazieres en 1999, basada en el cifrado Blowfish. A diferencia de SHA-256 o MD5 (funciones de hash criptográficas de propósito general), bcrypt fue construido desde cero específicamente para el almacenamiento de contraseñas.
Tres propiedades lo hacen adecuado para esto:
1. Es lento por diseño
Bcrypt incluye un factor de costo configurable (también llamado factor de trabajo o rondas de sal). La función realiza 2^costo iteraciones internamente. Aumentar el factor de costo en 1 duplica el tiempo de computación. Esto significa que puedes ajustar la velocidad para que coincida con tu hardware -- y a medida que el hardware se vuelve más rápido, puedes aumentar el factor de costo para mantenerte adelante.
Un hash SHA-256 toma microsegundos. Un hash bcrypt con costo 12 toma alrededor de 200-300 milisegundos. Esa diferencia suena pequeña, pero cambia drásticamente la economía del atacante.
2. Maneja el salting automáticamente
Una sal es un valor aleatorio añadido a la contraseña antes del hashing. El salting garantiza que dos usuarios con la misma contraseña obtengan hashes diferentes, y derrota los ataques de rainbow tables precalculadas.
Bcrypt genera automáticamente una sal aleatoria criptográfica de 128 bits y la incrusta en el hash de salida. No necesitas gestionar las sales tú mismo.
3. El hash es autosuficiente
La salida de bcrypt contiene la versión del algoritmo, el factor de costo, la sal y el hash -- todo en una sola cadena. Esto significa que solo necesitas almacenar una sola cadena por usuario, y puedes verificar una contraseña sin recuperar la sal por separado.
Un hash bcrypt típico se ve así:
$2b$12$LJ3m6gEwO/fSFqCVXWLwOeR/dYtTVkRDCwoGLBE0Fg6voFEOB5viy
Desglose:
$2b$ -- versión del algoritmo (2b es el estándar actual)
12$ -- factor de costo (2^12 = 4.096 iteraciones del programa de claves)
LJ3m6gEwO/fSFqCVXWLwOe -- sal codificada en base64 de 22 caracteres (128 bits)
R/dYtTVkRDCwoGLBE0Fg6voFEOB5viy -- hash codificado en base64 de 31 caracteres
Hashing vs. Cifrado: La distinción clave
Esta distinción es lo suficientemente importante como para expresarla claramente.
El hashing es una función unidireccional. Ingresas una contraseña y obtienes un hash. No hay clave, no hay operación inversa. La única forma de comprobar si una contraseña coincide con un hash es hashear la contraseña candidata y comparar el resultado.
El cifrado es una función bidireccional. Cifras datos con una clave y puedes descifrarlos de vuelta al original con la misma (o una relacionada) clave.
Las contraseñas siempre deben hashearse, no cifrarse. Si cifras contraseñas, tu sistema contiene en algún lugar una clave de descifrado, y quien obtenga esa clave tiene todas las contraseñas de tus usuarios. Con el hashing, una violación de base de datos expone hashes, no contraseñas -- y con bcrypt, esos hashes son lentos de descifrar.
El factor de costo: Elegir la lentitud correcta
El factor de costo controla directamente cuánto cálculo realiza bcrypt. Cada incremento duplica el trabajo.
| Factor de costo | Iteraciones | Tiempo aproximado (servidor típico) |
|---|---|---|
| 10 | 1.024 | ~65ms |
| 11 | 2.048 | ~130ms |
| 12 | 4.096 | ~250ms |
| 13 | 8.192 | ~500ms |
| 14 | 16.384 | ~1.000ms |
Los tiempos anteriores varían significativamente según el hardware. Ejecuta benchmarks en tu hardware de producción real antes de decidir.
La recomendación actual de OWASP es un factor de costo de 10 como mínimo, apuntando a un tiempo de hash de 100ms o más. La mayoría de los practicantes usan 12 como valor predeterminado razonable hoy en día.
El compromiso:
- Demasiado bajo (8 o menos): Los hashes son lo suficientemente rápidos para que un atacante con una GPU decente pueda progresar rápidamente contra una base de datos filtrada.
- Demasiado alto (15+): Las solicitudes de inicio de sesión legítimas toman más de un segundo cada una, lo cual es perceptible para los usuarios y crea potencial de denegación de servicio.
- Zona intermedia (~12): ~250ms por hash es lo suficientemente lento para obstaculizar significativamente a los atacantes, lo suficientemente rápido para que los usuarios no lo noten.
Implementando Bcrypt: Ejemplos de código
Node.js
import bcrypt from 'bcrypt';
const COST_FACTOR = 12;
// Hashear una contraseña
async function hashPassword(plaintext) {
const hash = await bcrypt.hash(plaintext, COST_FACTOR);
return hash;
// "$2b$12$..." -- almacenar esta cadena en tu base de datos
}
// Verificar una contraseña en el inicio de sesión
async function verifyPassword(plaintext, storedHash) {
const match = await bcrypt.compare(plaintext, storedHash);
return match; // true o false
}
// Ejemplo de uso
const hash = await hashPassword('hunter2');
console.log(hash); // $2b$12$...
const valid = await verifyPassword('hunter2', hash);
console.log(valid); // true
const invalid = await verifyPassword('wrongpassword', hash);
console.log(invalid); // false
Python (con el paquete bcrypt)
import bcrypt
COST_FACTOR = 12
def hash_password(plaintext: str) -> str:
"""Hashear una contraseña y devolver la cadena de hash bcrypt."""
password_bytes = plaintext.encode('utf-8')
salt = bcrypt.gensalt(rounds=COST_FACTOR)
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def verify_password(plaintext: str, stored_hash: str) -> bool:
"""Verificar una contraseña en texto plano contra un hash bcrypt almacenado."""
password_bytes = plaintext.encode('utf-8')
hash_bytes = stored_hash.encode('utf-8')
return bcrypt.checkpw(password_bytes, hash_bytes)
# Uso
hash_value = hash_password('hunter2')
print(hash_value) # $2b$12$...
print(verify_password('hunter2', hash_value)) # True
print(verify_password('wrongpassword', hash_value)) # False
PHP
<?php
// Hashear una contraseña
function hashPassword(string $plaintext): string {
return password_hash($plaintext, PASSWORD_BCRYPT, ['cost' => 12]);
}
// Verificar una contraseña
function verifyPassword(string $plaintext, string $storedHash): bool {
return password_verify($plaintext, $storedHash);
}
// Uso
$hash = hashPassword('hunter2');
echo $hash; // $2y$12$...
var_dump(verifyPassword('hunter2', $hash)); // bool(true)
var_dump(verifyPassword('wrongpassword', $hash)); // bool(false)
?>
Errores comunes y cómo evitarlos
Almacenar contraseñas en texto plano o con codificación reversible
Base64 es codificación, no hashing. base64("hunter2") es aHVudGVyMg==. Cualquier atacante que vea esto puede decodificarlo en una línea.
Usar funciones de hash rápidas para contraseñas
SHA-256, SHA-512, MD5, SHA-1 -- todos están diseñados para ser rápidos. La rapidez es la propiedad incorrecta para el hashing de contraseñas.
El problema de truncamiento en 72 bytes
La especificación original de bcrypt solo procesa los primeros 72 bytes de entrada. La mitigación estándar es hashear la entrada con SHA-256 primero, luego hashear ese hash con bcrypt:
import crypto from 'crypto';
import bcrypt from 'bcrypt';
async function hashLongPassword(plaintext) {
const prehashed = crypto
.createHash('sha256')
.update(plaintext)
.digest('base64');
return bcrypt.hash(prehashed, 12);
}
Probando hashes Bcrypt
Nuestro Bcrypt Hash Generator es útil para:
- Generar hashes de prueba durante el desarrollo sin configurar un proyecto
- Verificar que tu implementación produce una salida bcrypt válida
- Probar rápidamente la verificación hash-contraseña en tu código
La herramienta se ejecuta completamente en tu navegador y nunca envía datos a un servidor.
Bcrypt vs. Alternativas
| Algoritmo | Intensivo en memoria | Resistente a GPU | Estado OWASP | Notas |
|---|---|---|---|---|
| Bcrypt | No | Parcial | Recomendado | Probado, límite de 72 bytes |
| Argon2id | Sí | Sí | Preferido | Ganador PHC, más reciente |
| scrypt | Sí | Sí | Aceptable | Intensivo en memoria, parámetros complejos |
| PBKDF2 | No | No | Aceptable | Aprobado por NIST, compatible con FIPS |
| SHA-256 (simple) | No | No | No usar | No es un hash de contraseña |
Conclusión
El hashing de contraseñas es una de esas áreas donde la diferencia entre "funciona" y "funciona correctamente" importa mucho. Bcrypt te da tres cosas importantes: lentitud intencional, salting automático y un formato de salida autosuficiente. No es la opción más nueva, pero está bien probada, es universalmente compatible y es correcta.
Usa el factor de costo 12 como punto de partida. Haz benchmarks en tu hardware real. Auméntalo si tu servidor puede hashear cómodamente más rápido que 200ms. Y si empiezas desde cero, echa un vistazo a Argon2id.
Herramientas y recursos relacionados
- Bcrypt Hash Generator -- Generar y verificar hashes bcrypt en el navegador
- Password Generator -- Generar contraseñas aleatorias fuertes
- Password Strength Checker -- Evaluar la fortaleza de una contraseña
- Hash Generator -- Generar hashes MD5, SHA-1, SHA-256 y SHA-512
- Hash Functions Explained -- Análisis profundo de SHA-256, MD5 y hashing criptográfico