Abres una tabla de usuarios y encuentras una contraseña almacenada como $2b$12$…. Lo que sigue parece una secuencia arbitraria de letras, números y puntos. Sin embargo, la cadena ya te está diciendo cómo debe verificarse: qué variante declara, cuánto trabajo solicita y qué salt utilizó.
No necesitas adivinar dónde termina cada parte. Vamos a separar sus 60 caracteres con un explorador que funciona en tu navegador. La idea es que puedas leer ese registro sin confundirlo con una contraseña cifrada ni con una prueba de que alguien conoce la contraseña.
Si buscas elegir un mecanismo para una aplicación nueva, la guía de almacenamiento de contraseñas con Argon2id desarrolla esa decisión. Aquí nos concentraremos en algo más específico: entender el formato que ya tienes delante.
Una cadena que incluye las instrucciones para comprobarla
bcrypt es una función de hashing de contraseñas con costo ajustable, construida a partir de una preparación de claves deliberadamente costosa basada en Blowfish. Busca encarecer cada intento de adivinar una contraseña. Aunque emplea un cifrador internamente, el registro que produce no es una contraseña que después puedas descifrar. El diseño original explica esa elección en A Future-Adaptable Password Scheme.
Cuando decimos «hash bcrypt» solemos referirnos a toda la cadena almacenada. Conviene distinguirla del resultado del cálculo, que ocupa únicamente su parte final. El resto son parámetros necesarios para repetir la operación durante una verificación.
La forma habitual pertenece a la familia de formatos modulares de crypt: el identificador y los delimitadores permiten reconocer el esquema y leer sus parámetros. En bcrypt se ve así:
$2b$12$[salt de 22 caracteres][resultado de 31 caracteres]Los corchetes son anotaciones de este esquema; no aparecen en un registro real. Los signos $ sí pertenecen a la cadena. Separan el identificador y el costo del bloque final, donde salt y resultado están concatenados. El manual de OpenBSD describe esa organización.
Explora las cuatro partes
Reconocer el formato de una cadena bcrypt no verifica una contraseña ni demuestra que el resultado sea auténtico.
Activa JavaScript para explorar los segmentos. La explicación y el ejemplo completo siguen disponibles sin él. Usa únicamente cadenas ficticias.
El ejemplo precargado se generó y verificó localmente con bcrypt para Node.js y la contraseña ficticia Solo-para-el-ejemplo-2026:
$2b$12$JJOzEMMAVBLx9UaU.o4sQeYceTVniHAeS8kvCbjNd1v5aRd.pyv0WSelecciona Salt y después Resultado. El cambio de color marca una frontera que en el registro original no lleva separador. Prueba también a quitar un carácter: ahora ni siquiera coincide con la estructura esperada. Si cambias una letra interior del resultado por otra del alfabeto permitido, el formato puede seguir reconociéndose, pero eso no comprueba que la contraseña del ejemplo siga correspondiendo.
El explorador lee $2a$, $2b$ y $2y$, costos de 04 a 31 y codificaciones canónicas. Rechaza espacios, entradas incompletas, otros prefijos y caracteres ajenos al alfabeto. Es una inspección deliberadamente acotada; no sustituye al verificador de una biblioteca ni promete reproducir la tolerancia de todos los sistemas heredados.
La entrada permanece en memoria del navegador. La herramienta no la envía a servidores, no la incorpora a la URL ni la guarda en cookies o almacenamiento web; se limpia al abandonar la página. Aun así, utiliza datos ficticios: un hash de una cuenta real también es información sensible que permite probar candidatos offline si se filtra.
La cuenta exacta: 4 + 3 + 22 + 31
En el ejemplo anterior, contando desde el primer carácter:
| Posiciones | Contenido | Longitud | Qué representa |
|---|---|---|---|
| 1–4 | $2b$ | 4 | Identificador 2b y dos delimitadores |
| 5–7 | 12$ | 3 | Costo de dos dígitos y un delimitador |
| 8–29 | JJOzEMMAVBLx9UaU.o4sQe | 22 | Salt codificado |
| 30–60 | YceTVniHAeS8kvCbjNd1v5aRd.pyv0W | 31 | Resultado codificado |
Son 60 caracteres ASCII en total: dos de identificador, dos de costo, tres signos $, 22 de salt y 31 de resultado. La documentación del formato de node.bcrypt.js detalla esta distribución.
¿Por qué no hay otro $ entre salt y resultado? Porque no hace falta para distinguirlos: después del tercer delimitador, los primeros 22 caracteres son el salt y los 31 restantes son el resultado. Un lector del formato conoce esas longitudes.
Por eso, hacer cadena.split('$') no te entrega salt y resultado por separado. El último elemento todavía contiene ambos. Y guardar el registro completo permite conservar el salt junto a los parámetros, sin una columna adicional para él. La API de PHP password_hash también documenta esa propiedad de sus resultados autocontenidos.
Los 60 caracteres describen este formato bcrypt, no una longitud universal para todos los hashes de contraseñas. Si diseñas una columna pensando en futuras migraciones, no limites su capacidad a bcrypt por accidente.
$2a$, $2b$ y $2y$ tienen historia
El prefijo no indica la fortaleza de la contraseña ni una cantidad de bits. Identifica una revisión del esquema y, en ocasiones, una decisión histórica de compatibilidad.
$2a$ es el identificador histórico utilizado desde 1997. El problema no es que toda cadena con ese prefijo esté mal: hubo implementaciones con distinto comportamiento ante bytes con el bit más alto activado. En 2011, crypt_blowfish corrigió un fallo de ese tipo y necesitó distinguir registros antiguos de resultados corregidos. El manual del proyecto Openwall conserva ese contexto.
$2y$ se introdujo en crypt_blowfish para identificar el tratamiento corregido de esos bytes. No es «una versión de bcrypt exclusiva de PHP», aunque puedes encontrarla allí: PASSWORD_BCRYPT de PHP genera cadenas con ese identificador. Anuncio de crypt_blowfish 1.2, manual de PHP.
$2b$ llegó con OpenBSD 5.5. Openwall documenta que su comportamiento coincide con $2y$ de crypt_blowfish. Esa equivalencia en el cálculo no obliga a que todas las bibliotecas acepten ambos textos como entrada. Compatibilidad de crypt_blowfish.
Por ejemplo, node.bcrypt.js declara soporte para $2a$ y $2b$. Que el playground pueda separar un $2y$ no amplía ese soporte. Al migrar registros, verifica la biblioteca de origen, la de destino, la codificación de las contraseñas y vectores representativos; no sustituyas prefijos a ciegas. Notas de compatibilidad de node.bcrypt.js.
El 12 no significa doce vueltas ni doce milisegundos
El costo es el exponente que determina las repeticiones de la fase costosa de preparación:
| Costo declarado | 2^costo | Trabajo de esa fase respecto a costo 10 |
|---|---|---|
10 | 1 024 | 1× |
11 | 2 048 | 2× |
12 | 4 096 | 4× |
13 | 8 192 | 8× |
Estas cifras salen de la fórmula; no son tiempos medidos. El manual de crypt_newhash de OpenBSD expresa el parámetro como un logaritmo en base dos y admite valores de 4 a 31. El formato conserva dos dígitos, de ahí 04 y no 4.
El algoritmo también realiza trabajo fijo antes y después de esa fase. Por eso, duplicar sus repeticiones no promete duplicar exactamente la latencia total de una petición HTTP. La preparación y el cálculo final se distinguen en el diseño original de bcrypt.
Un costo mayor encarece los intentos de un atacante, pero también los inicios de sesión legítimos. Para elegirlo, mide en el entorno donde vas a desplegar y con concurrencia representativa: duración de las verificaciones, CPU, espera en las colas de trabajo y capacidad disponible para otras peticiones. El máximo admitido por el formato no es un objetivo operativo. OWASP recomienda equilibrar el factor de trabajo con la capacidad del servicio en su guía de almacenamiento de contraseñas.
Tampoco puedes actualizar un registro cambiando 12 por 13: estarías cambiando las instrucciones de cálculo sin recalcular el resultado. La cadena seguiría teniendo 60 caracteres, pero ya no sería la que generaste.
Salt público, codificación particular
El salt de bcrypt contiene 16 bytes, es decir, 128 bits, representados mediante 22 caracteres. Debe generarse aleatoriamente al crear un registro; en el ejemplo de Node.js dejaremos esa tarea a la biblioteca. Durante la comprobación se reutiliza el salt almacenado, en lugar de elegir otro. OpenBSD: formato bcrypt.
Que sea público no es un descuido. Su función no es ocultar la contraseña, sino variar el cálculo para que los mismos candidatos no tengan un único resultado reutilizable entre todos los registros. Dos ejecuciones con salts distintos pueden generar cadenas diferentes para una misma contraseña. El salt no impide probar candidatos individualmente; ese trabajo sigue siendo posible. Para ampliar salt y pepper, continúa con la guía de contraseñas y Argon2id.
Hay otro detalle fácil de pasar por alto: bcrypt usa su propio alfabeto y orden de Base64:
./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789No es Base64 estándar ni Base64URL. No basta con pasar el texto a atob() o cambiar un carácter por otro: importa qué valor numérico representa cada símbolo. Tampoco aparece relleno =. El alfabeto y su codificador pueden comprobarse en el código fuente de bcrypt para Node.js.
¿Y los 31 caracteres finales cuántos bytes representan?
La descripción de alto nivel de bcrypt habla de un resultado interno de 24 bytes. Pero el formato habitual serializa 23 bytes, que se codifican en 31 caracteres. La implementación lo hace explícito al codificar 4 * BCRYPT_BLOCKS - 1 bytes; el manual de Openwall describe 184 bits de resultado almacenado.
Esta distinción evita una cuenta imposible: no puedes representar 24 bytes completos en 31 símbolos de seis bits. Los 31 caracteres tampoco son 31 bytes del resultado original; son su representación textual.
Además, al no completar grupos de seis bits, quedan bits sin usar en el último símbolo. En la codificación canónica del salt, el último carácter es uno de .Oeu; en la del resultado, uno de .CGKOSWaeimquy26. El explorador comprueba esas restricciones, derivadas del codificador de bcrypt. Eso sigue siendo una comprobación de representación, no una operación criptográfica.
Generar dos registros y verificar la misma contraseña
Una prueba pequeña permite ver por qué comparar cadenas completas entre sí no equivale a verificar una contraseña. Necesitas Node.js 22 o superior y pnpm. En una carpeta nueva:
mkdir bcrypt-anatomia-demo
cd bcrypt-anatomia-demo
pnpm init
pnpm add bcrypt@6.0.0 --allow-build=bcryptLa opción --allow-build de pnpm autoriza el script de instalación de ese paquete. No habilites indiscriminadamente los scripts de todas tus dependencias.
Guarda este archivo como demo.mjs:
import bcrypt from 'bcrypt';
import { Buffer } from 'node:buffer';
const password = 'Solo-para-el-ejemplo-2026';
const cost = 12; // Parámetro de la demostración, no un valor universal.
function checkLength(value) {
if (Buffer.byteLength(value, 'utf8') > 72) {
throw new Error('La contraseña supera los 72 bytes admitidos por este ejemplo.');
}
}
checkLength(password);
const first = await bcrypt.hash(password, cost);
const second = await bcrypt.hash(password, cost);
console.log('Primero:', first);
console.log('Segundo:', second);
console.log('Longitudes:', first.length, second.length);
console.log('Cadenas diferentes:', first !== second);
console.log('Coincide con el primero:', await bcrypt.compare(password, first));
console.log('Coincide con el segundo:', await bcrypt.compare(password, second));
console.log('Contraseña incorrecta:', await bcrypt.compare('Otra-clave-ficticia', first));Ejecuta node demo.mjs. En la prueba local, ambas cadenas tuvieron 60 caracteres, fueron diferentes y ambas verificaciones con la contraseña original devolvieron true; la contraseña incorrecta devolvió false. Tus cadenas serán distintas porque se generan salts nuevos.
hash(password, cost) genera el salt y realiza el cálculo; compare(password, registro) lee el registro completo para comprobar el candidato. Usamos la API asíncrona para evitar ejecutar el trabajo intensivo en el hilo principal. No elimina su consumo de CPU ni convierte una capacidad limitada en infinita. API y uso asíncrono de node.bcrypt.js.
Los console.log muestran exclusivamente datos ficticios del ejercicio. No traslades esos logs a un servicio con cuentas reales. Para limpiar, elimina la carpeta de la demostración cuando ya no la necesites; no levanta servicios ni modifica una base de datos.
Lo que la cadena no te cuenta
Puedes leer versión, costo y salt. No puedes deducir que la contraseña sea fuerte, que el sistema limite los intentos de acceso o que la biblioteca esté mantenida. Tampoco el formato demuestra que el salt se generó aleatoriamente: una cadena escrita a mano puede superar la inspección sintáctica.
bcrypt tiene un límite habitual de 72 bytes de entrada. No son 72 caracteres visibles: UTF-8 puede representar un carácter con varios bytes. El ejemplo comprueba Buffer.byteLength, documentado en Node.js, para rechazar entradas demasiado largas en vez de truncarlas silenciosamente.
Para sistemas nuevos, OWASP prioriza Argon2id y reserva bcrypt para situaciones heredadas en las que no estén disponibles las alternativas preferidas; para bcrypt recomienda un factor de trabajo de al menos 10. Es una recomendación que debe acompañarse de mediciones, no una certificación de seguridad por alcanzar ese número. OWASP: bcrypt.
Si mantienes registros bcrypt, conserva una biblioteca actualizada y comprueba compatibilidad antes de migrarlos. La guía de almacenamiento seguro desarrolla el siguiente paso. Entender los 60 caracteres te permite inspeccionar los parámetros; verificar contraseñas debe seguir siendo trabajo de una biblioteca especializada.
Referencias
- node.bcrypt.js: formato, API y compatibilidad.
- Implementación de bcrypt 6.0.0: codificación y serialización.
- OpenBSD:
cryptycrypt_newhash. - Provos y Mazières: A Future-Adaptable Password Scheme.
- Openwall: crypt_blowfish y revisiones del formato.
- Openwall: vectores de prueba, incluidos
$2y$. - OWASP Password Storage Cheat Sheet.
Solo datos ficticios. El análisis es local: no envía ni almacena lo que escribes.
Selecciona una parte para entenderla. También puedes usar Tab y Enter o Espacio.
