Algorithms

bcrypt

The OpenBSD password hash from 1999. Blowfish key setup on repeat and the crypt string comes back with every result
IDbcrypt46 / 46bcrypt · 184-bit
Hash / Password

bcrypt

Blowfish's key setup, run a few thousand times on purpose.

Digest
184-bit · 23 bytes
HMAC
no key mode
Security
A password past 72 bytes is refused
Family
1 in bcrypt
3 options, 0 required

Sample

hex
ed8529cb1e4d34bdc37a9952219a05d5f1e78496ab37a4
base64
7YUpyx5NNL3DeplSIZoF1fHnhJarN6Q=

Options

encodingstring
default hex
Output encoding: hex, base64, base64url, binary
saltstring
optional
Salt in hex, 16 bytes; 16 random bytes when omitted
costnumber
default 12
Base-2 logarithm of the rounds, 4 to 31

Access

Createcreate("bcrypt")
CLIhashes bcrypt 'hello world' --salt 73616c7473616c7473616c7473616c74 --cost 4
Tryplayground with the sample above

bcrypt is old. Provos and Mazières built it for OpenBSD in 1999, and it still sits in plenty of password tables. The trick is Blowfish's key setup. It's slow, so bcrypt runs it 2^cost times and calls that a feature. Fair enough.

ts
const result = create("bcrypt").hash("password", {
  salt: "73616c7473616c7473616c7473616c74",
  cost: 4,
});
result.digest;
// "9f72647acd4f9f84df28e21dca2617f0cd69df0b4d8fcb"
result.options.crypt;
// "$2b$04$a0DqbFLfZFPxWUvya0Dqb.l1HicqzNl2RdIMGbwgWV6Kzn1urLh6q"

The salt is saltsaltsaltsalt in hex. bcrypt takes exactly 16 bytes, no more and no less. Leave it out and 16 random bytes come back in result.options. The digest is the 23 bytes the string carries after its salt. Want the thing a database or .htpasswd stores? That's crypt.

Each step of cost doubles the work. The default is 12, OWASP asks for at least 10. The page above runs cost 4, the lowest there is, so the tab doesn't sit and think. Tool calls stop at 16.

Here's where this package parts ways with OpenBSD. A password past 72 bytes is an error. OpenBSD, crypt_blowfish and htpasswd just drop the rest. So any password with the same first 72 bytes passes, and nobody tells you. Python's bcrypt 5.0 refuses too, which feels like the right call.

$2a$, $2b$ and $2y$ hash the same under that limit. A hash htpasswd wrote as $2y$ checks out with the same salt and cost. $2x$ is the odd one: crypt_blowfish's old sign bug, and this package doesn't run it.

Checking a hash from a dump or a config file in your own code? @agntn/hashes/bcrypt has the bytes and the string as plain functions:

ts
import { bcrypt, bcryptString } from "@agntn/hashes/bcrypt";

const text = new TextEncoder();
const salt = text.encode("saltsaltsaltsalt");
const digest = bcrypt(text.encode("password"), salt, 4);
bcryptString(salt, 4, digest);
// the same $2b$ string as above

No default cost and no random salt here. You pass all three. A salt of the wrong size or a cost outside 4 to 31 throws before Blowfish starts.