ToolPal
Un candado sobre un teclado de laptop, simbolizando la seguridad digital y la protección de contraseñas.

Hashing de contraseñas con Bcrypt: por qué importa y cómo usarlo

📷 Pixabay / Pexels

Hashing 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.

7 de abril de 20268 min de lectura

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 costoIteracionesTiempo aproximado (servidor típico)
101.024~65ms
112.048~130ms
124.096~250ms
138.192~500ms
1416.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

AlgoritmoIntensivo en memoriaResistente a GPUEstado OWASPNotas
BcryptNoParcialRecomendadoProbado, límite de 72 bytes
Argon2idPreferidoGanador PHC, más reciente
scryptAceptableIntensivo en memoria, parámetros complejos
PBKDF2NoNoAceptableAprobado por NIST, compatible con FIPS
SHA-256 (simple)NoNoNo usarNo 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

Preguntas Frecuentes

Compartir

XLinkedIn

Publicaciones relacionadas