
Hashing de contraseñas con bcrypt: la guía práctica del desarrollador
📷 Pixabay / PexelsHashing de contraseñas con bcrypt: la guía práctica del desarrollador
Todo lo que necesitas saber sobre bcrypt para la seguridad de contraseñas — factores de costo, implementación en Node.js/Python/PHP, y errores comunes que debes evitar.
Si alguna vez has tenido que almacenar contraseñas de usuarios, probablemente te hayan dicho que uses bcrypt. Ha sido la recomendación estándar durante más de dos décadas, y por buenas razones. Pero entender por qué funciona — y cómo usarlo correctamente — es lo que separa una implementación segura de una que solo parece segura.
Esta guía cubre todo lo práctico: cómo funciona bcrypt en realidad, qué factor de costo elegir, cómo implementarlo en múltiples lenguajes, y los errores que minan silenciosamente la seguridad de contraseñas.
Qué es bcrypt (y por qué existe)
bcrypt fue diseñado por Niels Provos y David Mazières en 1999, específicamente para el hashing de contraseñas. Ese contexto importa. No fue construido para ser rápido — fue construido para ser lento de manera controlada y ajustable.
La idea fundamental detrás de bcrypt es que el hashing rápido es una vulnerabilidad cuando se trata de contraseñas. Un hash de propósito general como SHA-256 puede calcular miles de millones de hashes por segundo en hardware moderno. Eso es excelente para sumas de verificación e integridad de datos. Es terrible para contraseñas, porque un atacante que roba tu base de datos de contraseñas puede intentar miles de millones de suposiciones por segundo con GPUs comunes.
bcrypt resuelve esto introduciendo un factor de costo — un parámetro que controla cuánto trabajo hace el algoritmo. Tú decides qué tan lento es el hashing. El hardware se vuelve más rápido cada año, así que puedes aumentar el factor de costo con el tiempo para mantenerte al día.
Cómo funciona bcrypt en realidad
Cuando llamas a bcrypt.hash("mypassword", 10), varias cosas ocurren internamente:
- Se genera una sal aleatoria de 16 bytes. Esta sal es única para cada operación de hash.
- La contraseña y la sal se introducen en la configuración de clave Eksblowfish. Este es el algoritmo central de bcrypt — una versión modificada del cifrado Blowfish con una fase de expansión de clave costosa.
- La configuración de clave se repite 2^costo veces. Con factor de costo 10, son 1.024 iteraciones. Con costo 12, son 4.096 iteraciones. Cada incremento duplica el trabajo.
- El hash resultante se codifica como una cadena de 60 caracteres que contiene la versión del algoritmo, el factor de costo, la sal y el hash, todo en uno.
La salida se ve así:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
Desglose:
$2b$— versión de bcrypt10— factor de costo- Los siguientes 22 caracteres — la sal codificada en base64
- Los caracteres restantes — el hash
Dado que la sal está incrustada en la cadena de hash, no necesitas almacenarla por separado. La función bcrypt.compare() extrae la sal del hash almacenado y la usa para hashear la contraseña de entrada — no se requiere gestión manual de sales.
Por qué las funciones de hash rápidas son la herramienta incorrecta
Los desarrolladores a veces almacenan contraseñas con MD5, SHA-1 o SHA-256. Estos algoritmos están ampliamente disponibles y bien entendidos, por lo que la elección parece razonable. No lo es.
El problema no es que estos algoritmos estén rotos para sus propósitos previstos. El problema es que están diseñados para ser rápidos, y rápido es exactamente lo incorrecto para el almacenamiento de contraseñas.
En 2012, el investigador de seguridad Jeremi Gosney demostró el craqueo del 90% de una base de datos filtrada de 6,4 millones de hashes SHA-1 en menos de una hora. Con hardware moderno y herramientas como Hashcat, un atacante con una buena GPU puede probar hashes MD5 a más de 10 mil millones de intentos por segundo. SHA-256 no es mucho mejor — alrededor de 4 mil millones por segundo.
bcrypt con costo 10 reduce esto a aproximadamente 20.000 intentos por segundo. Ese es el punto.
Elegir el factor de costo correcto
Costo 10 es el valor predeterminado ampliamente recomendado. En un servidor moderno, produce un hash en aproximadamente 100-300ms. Es imperceptiblemente lento para un usuario que inicia sesión, pero significa que un atacante solo puede probar unos pocos miles de contraseñas por segundo por núcleo.
Costo 12 cuadruplica aproximadamente el tiempo de computación en comparación con el costo 10. Es una elección razonable para aplicaciones con requisitos de seguridad elevados donde se pueden aceptar tiempos de inicio de sesión de 400-1000ms.
Costo 14 y superior generalmente es excesivo para aplicaciones web. Con costo 14, el hashing tarda varios segundos.
Nunca bajes de 8 en producción. Algunos tutoriales antiguos muestran costo 5 o 6 como ejemplos. Esos valores son demasiado rápidos y proporcionan protección inadecuada.
Implementar bcrypt en Node.js
const bcrypt = require('bcrypt');
const COST_FACTOR = 10;
// Hashear una contraseña
async function hashPassword(plaintext) {
const hash = await bcrypt.hash(plaintext, COST_FACTOR);
return hash; // Almacena esta cadena en tu base de datos
}
// Verificar al iniciar 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$10$...
const valid = await verifyPassword('hunter2', hash);
console.log(valid); // true
Implementar bcrypt en Python
import bcrypt
COST_FACTOR = 10
def hash_password(plaintext: str) -> bytes:
password_bytes = plaintext.encode('utf-8')
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=COST_FACTOR))
return hashed
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
password_bytes = plaintext.encode('utf-8')
return bcrypt.checkpw(password_bytes, stored_hash)
# Uso
hashed = hash_password("hunter2")
print(hashed) # b'$2b$10$...'
print(verify_password("hunter2", hashed)) # True
print(verify_password("wrong", hashed)) # False
Implementar bcrypt en PHP
<?php
$options = ['cost' => 10];
// Hashear
$hash = password_hash($plaintext, PASSWORD_BCRYPT, $options);
// Verificar
$valid = password_verify($plaintext, $hash);
// Verificar si el hash necesita actualizarse
if (password_needs_rehash($hash, PASSWORD_BCRYPT, $options)) {
$newHash = password_hash($plaintext, PASSWORD_BCRYPT, $options);
// Guardar $newHash en la base de datos
}
?>
Implementar bcrypt en Go
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
Errores comunes que socavan bcrypt
Volver a hashear la contraseña en cada verificación de inicio de sesión. Cuando un usuario inicia sesión, debes comparar su contraseña con el hash almacenado usando bcrypt.compare(). No hashees la contraseña nuevamente y compares los dos hashes — eso nunca coincidirá, porque se genera una nueva sal aleatoria cada vez.
Almacenar la contraseña en texto plano. Obvio, pero sucede.
Usar MD5 o SHA para el almacenamiento de contraseñas. Incluso con una sal añadida manualmente, los hashes rápidos dan a los atacantes demasiado rendimiento.
No manejar el límite de 72 bytes. bcrypt trunca silenciosamente la entrada en 72 bytes.
Usar un factor de costo demasiado bajo. Si usas costo 4 porque es más rápido, estás perdiendo el punto. La lentitud es una propiedad de seguridad, no un error.
Cuándo usar Argon2 en su lugar
bcrypt ha sido el valor predeterminado seguro durante 25 años, pero no es la mejor opción moderna. Argon2 — específicamente Argon2id — ganó el Password Hashing Competition en 2015 y aborda la principal debilidad de bcrypt.
bcrypt está ligado a la CPU: un atacante con GPUs puede paralelizar el craqueo de manera relativamente económica porque el algoritmo no requiere mucha memoria. Argon2 es intensivo en memoria: requiere una cantidad configurable de RAM por operación de hash. Las GPUs tienen mucho menos memoria por núcleo de cómputo que una CPU, por lo que los algoritmos intensivos en memoria nivelan significativamente el campo de juego.
Resumen
- La fortaleza de bcrypt proviene de ser deliberadamente lento, no de la complejidad criptográfica
- Siempre usa
bcrypt.compare()(o el equivalente de tu lenguaje) para la verificación — nunca hashear y comparar - El factor de costo 10 es el valor predeterminado correcto para la mayoría de las aplicaciones; revísalo cada pocos años
- bcrypt maneja el salting automáticamente — no necesitas gestionar las sales
- Sé consciente del límite de truncamiento de 72 bytes para contraseñas inusualmente largas
- Para nuevos proyectos, vale la pena considerar Argon2id; para sistemas bcrypt existentes, mantente en el curso
El almacenamiento de contraseñas es una de las pocas áreas donde la opción segura — bcrypt con un factor de costo razonable — también es simple de implementar correctamente. La mayoría de las vulnerabilidades en este espacio provienen de ignorar el consejo, no de fallas en el propio algoritmo.