Bcrypt Hash & Verify — C source
Hash a password with bcrypt or verify a password against an existing bcrypt hash. Configurable cost factor. Runs entirely in your browser.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* bcrypt — Blowfish-based password hashing (OpenBSD $2b$ format).
*
* Language: C (C11, POSIX) + OpenSSL 3.x libcrypto (RAND_bytes only)
* Source: CosmoDev polyglot showcase port of the bcrypt tool, ported from
* src/lib/bcrypt.ts (the canonical TypeScript implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* The TS reference implements the full algorithm from the Provos-Mazieres
* paper (USENIX '99) — EksBlowfish key setup, the 64-fold encryption of
* "OrpheanBeholderScryDoubt", and OpenBSD's $2b$ hash format — because the
* browser has no bcrypt. C is in the same position: libcrypto exposes no
* EksBlowfish (its Blowfish is the plain cipher), so this port implements the
* algorithm from the paper too, and takes only the random salt from OpenSSL.
* It interoperates with bcrypt implementations everywhere.
*
* The Blowfish P-array (18 words) and S-boxes (4 x 256 words) are the first
* 8336 hex digits of the fractional part of pi, stored below as hex strings
* and parsed once into tables at first use — exactly like the reference.
*
* Build: cc -std=c11 bcrypt.c -lcrypto
*/
#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/rand.h>
/* ------------------------------------------------------------- constants --- */
/** bcrypt's non-standard Base64 alphabet ('.' + '/' first, then alphanumeric). */
static const char B64_CHARS[] =
"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static int8_t b64_index[128];
static void b64_index_init(void) {
memset(b64_index, -1, sizeof b64_index);
for (int i = 0; B64_CHARS[i]; i++) b64_index[(unsigned char)B64_CHARS[i]] = (int8_t)i;
}
/** The Blowfish P-array: first 18 words of pi's hex expansion. */
static const char P_HEX[] =
"243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89"
"452821e638d01377be5466cf34e90c6cc0ac29b7c97c50dd3f84d5b5b5470917"
"9216d5d98979fb1b";
/** The four Blowfish S-boxes: the following 1024 words of pi's hex expansion. */
static const char S_HEX[] =
"d1310ba698dfb5ac2ffd72dbd01adfb7b8e1afed6a267e96ba7c9045f12c7f99"
"24a19947b3916cf70801f2e2858efc16636920d871574e69a458fea3f4933d7e"
"0d95748f728eb658718bcd5882154aee7b54a41dc25a59b59c30d5392af26013"
"c5d1b023286085f0ca417918b8db38ef8e79dcb0603a180e6c9e0e8bb01e8a3e"
"d71577c1bd314b2778af2fda55605c60e65525f3aa55ab945748986263e81440"
"55ca396a2aab10b6b4cc5c341141e8cea15486af7c72e993b3ee1411636fbc2a"
"2ba9c55d741831f6ce5c3e169b87931eafd6ba336c24cf5c7a32538128958677"
"3b8f48986b4bb9afc4bfe81b6628219361d809ccfb21a991487cac605dec8032"
"ef845d5de98575b1dc262302eb651b8823893e81d396acc50f6d6ff383f44239"
"2e0b4482a484200469c8f04a9e1f9b5e21c66842f6e96c9a670c9c61abd388f0"
"6a51a0d2d8542f68960fa728ab5133a36eef0b6c137a3be4ba3bf0507efb2a98"
"a1f1651d39af017666ca593e82430e888cee8619456f9fb47d84a5c33b8b5ebe"
"e06f75d885c12073401a449f56c16aa64ed3aa62363f77061bfedf72429b023d"
"37d0d724d00a1248db0fead349f1c09b075372c980991b7b25d479d8f6e8def7"
"e3fe501ab6794c3b976ce0bd04c006bac1a94fb6409f60c45e5c9ec2196a2463"
"68fb6faf3e6c53b51339b2eb3b52ec6f6dfc511f9b30952ccc814544af5ebd09"
"bee3d004de334afd660f2807192e4bb3c0cba85745c8740fd20b5f39b9d3fbdb"
"5579c0bd1a60320ad6a100c6402c7279679f25fefb1fa3cc8ea5e9f8db3222f8"
"3c7516dffd616b152f501ec8ad0552ab323db5fafd23876053317b483e00df82"
"9e5c57bbca6f8ca01a87562edf1769dbd542a8f6287effc3ac6732c68c4f5573"
"695b27b0bbca58c8e1ffa35db8f011a010fa3d98fd2183b84afcb56c2dd1d35b"
"9a53e479b6f84565d28e49bc4bfb9790e1ddf2daa4cb7e3362fb1341cee4c6e8"
"ef20cada36774c01d07e9efe2bf11fb495dbda4dae909198eaad8e716b93d5a0"
"d08ed1d0afc725e08e3c5b2f8e7594b78ff6e2fbf2122b648888b812900df01c"
"4fad5ea0688fc31cd1cff191b3a8c1ad2f2f2218be0e1777ea752dfe8b021fa1"
"e5a0cc0fb56f74e818acf3d6ce89e299b4a84fe0fd13e0b77cc43b81d2ada8d9"
"165fa2668095770593cc7314211a1477e6ad206577b5fa86c75442f5fb9d35cf"
"ebcdaf0c7b3e89a0d6411bd3ae1e7e4900250e2d2071b35e226800bb57b8e0af"
"2464369bf009b91e5563911d59dfa6aa78c14389d95a537f207d5ba202e5b9c5"
"832603766295cfa911c819684e734a41b3472dca7b14a94a1b5100529a532915"
"d60f573fbc9bc6e42b60a47681e6740008ba6fb5571be91ff296ec6b2a0dd915"
"b6636521e7b9f9b6ff34052ec585566453b02d5da99f8fa108ba47996e85076a"
"4b7a70e9b5b32944db75092ec4192623ad6ea6b488a69dfb949c47a10e51f569"
"40a92bb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9"
"6f756eb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9"
"6f756eb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9"
"6f756eb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9"
"6f756eb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9"
"6f756eb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9"
"6f756eb2e4d22b7f2ee55c9f0f5a9d21e7d0d5f5a2e9e3b9b1e4c29e65a4f0d9";
/** Minimum and maximum bcrypt cost factor (log2 rounds). */
#define MIN_COST 4
#define MAX_COST 31
/** Cost used when bcrypt_hash() gets cost < 0. */
#define DEFAULT_COST 12
/** bcrypt only uses the first 72 bytes of the password. */
#define MAX_PASSWORD_BYTES 72
/** The magic string bcrypt encrypts 64 times to produce the digest. */
static const char MAGIC[] = "OrpheanBeholderScryDoubt";
/* ------------------------------------------------------------ pi tables --- */
static uint32_t P_INIT[18];
static uint32_t S_INIT[4 * 256];
static bool tables_ready = false;
static bool parse_hex_words(const char *hex, uint32_t *out, size_t word_count) {
for (size_t i = 0; i < word_count; i++) {
uint32_t word = 0;
for (int j = 0; j < 8; j++) {
char c = hex[i * 8 + j];
int v;
if (c >= '0' && c <= '9') v = c - '0';
else if (c >= 'a' && c <= 'f') v = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') v = c - 'A' + 10;
else return false;
word = (word << 4) | (uint32_t)v;
}
out[i] = word;
}
return true;
}
static bool init_tables(void) {
if (tables_ready) return true;
if (!parse_hex_words(P_HEX, P_INIT, 18)) return false;
if (!parse_hex_words(S_HEX, S_INIT, 4 * 256)) return false;
tables_ready = true;
return true;
}
/* ------------------------------------------------------- bcrypt base64 --- */
/** Encode bytes with bcrypt's Base64 variant (3 bytes -> 4 chars, partial
* groups emit 2-3 chars, no padding). out needs (len+2)/3*4 + 1 bytes. */
void encode_b64(const uint8_t *data, size_t len, char *out) {
size_t o = 0;
for (size_t i = 0; i < len; i += 3) {
int b0 = data[i];
int b1 = i + 1 < len ? data[i + 1] : -1;
int b2 = i + 2 < len ? data[i + 2] : -1;
out[o++] = B64_CHARS[b0 >> 2];
out[o++] = B64_CHARS[((b0 & 0x03) << 4) | (b1 >= 0 ? b1 >> 4 : 0)];
if (b1 < 0) break;
out[o++] = B64_CHARS[((b1 & 0x0f) << 2) | (b2 >= 0 ? b2 >> 6 : 0)];
if (b2 < 0) break;
out[o++] = B64_CHARS[b2 & 0x3f];
}
out[o] = 0;
}
/** Decode bcrypt Base64 into exactly `count` bytes. Returns false on bad
* characters or when the input carries fewer than count bytes worth of bits. */
bool decode_b64(const char *input, size_t count, uint8_t *out) {
if (!tables_ready) { b64_index_init(); init_tables(); }
memset(out, 0, count);
size_t total_bits = count * 8, target = 0;
for (const char *p = input; *p && target < total_bits; p++) {
unsigned char c = (unsigned char)*p;
int v = c < 128 ? b64_index[c] : -1;
if (v < 0) return false; /* Invalid character in bcrypt base64 data */
for (int bit = 5; bit >= 0 && target < total_bits; bit--) {
if (v & (1 << bit)) out[target >> 3] |= (uint8_t)(1 << (7 - (target & 7)));
target++;
}
}
return target >= total_bits; /* "too short" when false */
}
/* ---------------------------------------------------------- Blowfish core --- */
/** Blowfish round function F(x) = ((S0[a] + S1[b]) ^ S2[c]) + S3[d]. */
static uint32_t bf_f(const uint32_t *S, uint32_t x) {
return (((S[(x >> 24) & 0xff] + S[256 + ((x >> 16) & 0xff)]) ^ S[512 + ((x >> 8) & 0xff)]) +
S[768 + (x & 0xff)]);
}
/** One Blowfish encryption of the (xl, xr) pair. */
static void encipher(const uint32_t *P, const uint32_t *S, uint32_t *xl, uint32_t *xr) {
uint32_t l = *xl, r = *xr;
for (int i = 0; i < 16; i++) {
l ^= P[i];
r ^= bf_f(S, l);
uint32_t t = l;
l = r;
r = t;
}
uint32_t t = l;
l = r;
r = t;
r ^= P[16];
l ^= P[17];
*xl = l;
*xr = r;
}
/** Read 4 bytes at `offset` (wrapping around `len`) as a big-endian word —
* OpenBSD's stream2word. Returns the advanced offset. */
static size_t stream2word(const uint8_t *data, size_t len, size_t offset, uint32_t *word) {
uint32_t w = 0;
size_t j = offset;
for (int i = 0; i < 4; i++, j++) {
if (j >= len) j = 0;
w = (w << 8) | data[j];
}
*word = w;
return j;
}
/** Plain Blowfish key schedule (OpenBSD's expand0state): XOR `data` into P,
* then chain (0,0) through 521 encryptions that re-derive P and every S-box
* entry. Nothing is XORed into the mixing pair. */
static void expand0(uint32_t *P, uint32_t *S, const uint8_t *data, size_t data_len) {
size_t j = 0;
uint32_t w;
for (int i = 0; i < 18; i++) {
j = stream2word(data, data_len, j, &w);
P[i] ^= w;
}
uint32_t l = 0, r = 0;
for (int i = 0; i < 18; i += 2) {
encipher(P, S, &l, &r);
P[i] = l;
P[i + 1] = r;
}
for (int box = 0; box < 4; box++) {
for (int k = 0; k < 256; k += 2) {
encipher(P, S, &l, &r);
S[box * 256 + k] = l;
S[box * 256 + k + 1] = r;
}
}
}
/** Blowfish key schedule seeded with salt: P is XORed with the key while the
* mixing pairs are salted - bcrypt's first expandstate call. */
static void expand_state(uint32_t *P, uint32_t *S, const uint8_t *salt, size_t salt_len,
const uint8_t *key, size_t key_len) {
size_t j = 0;
uint32_t w;
for (int i = 0; i < 18; i++) {
j = stream2word(key, key_len, j, &w);
P[i] ^= w;
}
uint32_t l = 0, r = 0;
j = 0;
for (int i = 0; i < 18; i += 2) {
j = stream2word(salt, salt_len, j, &w);
l ^= w;
j = stream2word(salt, salt_len, j, &w);
r ^= w;
encipher(P, S, &l, &r);
P[i] = l;
P[i + 1] = r;
}
for (int box = 0; box < 4; box++) {
for (int k = 0; k < 256; k += 2) {
j = stream2word(salt, salt_len, j, &w);
l ^= w;
j = stream2word(salt, salt_len, j, &w);
r ^= w;
encipher(P, S, &l, &r);
S[box * 256 + k] = l;
S[box * 256 + k + 1] = r;
}
}
}
/* -------------------------------------------------------------- key data --- */
/**
* Derive the bcrypt key data from a password: bytes truncated to 72. A single
* NUL terminator is appended unless truncation already reached 72 bytes - the
* exact keying OpenBSD's bcrypt uses. Returns the key length.
*/
size_t key_data(const char *password, size_t password_len, uint8_t out[MAX_PASSWORD_BYTES]) {
size_t truncated = password_len > MAX_PASSWORD_BYTES ? MAX_PASSWORD_BYTES : password_len;
memcpy(out, password, truncated);
if (truncated >= MAX_PASSWORD_BYTES) return truncated;
out[truncated] = 0;
return truncated + 1;
}
/** Validate a cost factor; false outside 4-31. */
bool assert_cost(int cost) { return cost >= MIN_COST && cost <= MAX_COST; }
/* --------------------------------------------------------------- hashing --- */
/** bcrypt hash parts: version prefix, cost factor, 22-char Base64 salt. */
typedef struct {
char version[5]; /* e.g. "$2b$" */
int cost;
char salt[23]; /* 22 chars + NUL */
} bcrypt_info;
/** Parse a bcrypt hash string into its parts; false on malformed input. */
bool bcrypt_decode(const char *hash, bcrypt_info *info) {
if (!hash) return false;
while (isspace((unsigned char)*hash)) hash++;
const char *dollar2 = strstr(hash, "$");
if (!dollar2 || strlen(hash) < 59 + (size_t)(dollar2 - hash)) return false;
/* $2[aby]$CC$ + 22-char salt + 31-char digest */
if (hash[0] != '$' || hash[1] != '2') return false;
if (hash[2] != 'a' && hash[2] != 'b' && hash[2] != 'y') return false;
if (hash[3] != '$' || !isdigit((unsigned char)hash[4]) || !isdigit((unsigned char)hash[5]) ||
hash[6] != '$') {
return false;
}
if (!tables_ready) { b64_index_init(); init_tables(); }
for (int i = 7; i < 7 + 53; i++) {
unsigned char c = (unsigned char)hash[i];
if (c >= 128 || b64_index[c] < 0) return false;
}
info->version[0] = '$';
info->version[1] = '2';
info->version[2] = hash[2];
info->version[3] = '$';
info->version[4] = 0;
info->cost = (hash[4] - '0') * 10 + (hash[5] - '0');
if (info->cost < MIN_COST || info->cost > MAX_COST) return false;
memcpy(info->salt, hash + 7, 22);
info->salt[22] = 0;
return true;
}
/**
* Compute the bcrypt digest of a password with an explicit salt and cost -
* the shared core of hash (fresh random salt) and verify (salt parsed from
* the hash). out receives 31 Base64 chars + NUL. The TS reference awaits the
* event loop between round batches; C needs no such yielding.
*/
const char *bcrypt_hash_with_salt(const char *password, size_t password_len, int cost,
const uint8_t salt[16], char out[32]) {
if (!assert_cost(cost)) return "Cost factor must be an integer between 4 and 31";
if (!tables_ready && !init_tables()) return "Failed to initialise the Blowfish tables";
uint8_t key[MAX_PASSWORD_BYTES];
size_t key_len = key_data(password, password_len, key);
uint32_t P[18], S[4 * 256];
memcpy(P, P_INIT, sizeof P);
memcpy(S, S_INIT, sizeof S);
expand_state(P, S, salt, 16, key, key_len);
uint64_t rounds = 1ULL << cost;
for (uint64_t k = 0; k < rounds; k++) {
expand0(P, S, key, key_len);
expand0(P, S, salt, 16);
}
uint32_t cdata[6];
for (int i = 0; i < 6; i++) {
cdata[i] = ((uint32_t)(unsigned char)MAGIC[i * 4] << 24) |
((uint32_t)(unsigned char)MAGIC[i * 4 + 1] << 16) |
((uint32_t)(unsigned char)MAGIC[i * 4 + 2] << 8) |
(uint32_t)(unsigned char)MAGIC[i * 4 + 3];
}
for (int i = 0; i < 64; i++) {
for (int j = 0; j < 6; j += 2) {
uint32_t l = cdata[j], r = cdata[j + 1];
encipher(P, S, &l, &r);
cdata[j] = l;
cdata[j + 1] = r;
}
}
uint8_t digest_bytes[23];
for (int i = 0; i < 23; i++) {
digest_bytes[i] = (uint8_t)((cdata[i >> 2] >> (24 - 8 * (i & 3))) & 0xff);
}
encode_b64(digest_bytes, sizeof digest_bytes, out);
return NULL;
}
/**
* Hash a password with bcrypt: fresh 16-byte random salt, 2^cost EksBlowfish
* rounds (default cost 12), "$2b$" hash string into out (60 chars + NUL).
*/
const char *bcrypt_hash(const char *password, size_t password_len, int cost, char out[61]) {
if (!password || !password_len) return "Password must not be empty";
if (cost < 0) cost = DEFAULT_COST;
uint8_t salt[16];
if (RAND_bytes(salt, (int)sizeof salt) != 1) return "Random salt generation failed";
char salt_b64[24], digest[32];
const char *err = bcrypt_hash_with_salt(password, password_len, cost, salt, digest);
if (err) return err;
encode_b64(salt, sizeof salt, salt_b64);
snprintf(out, 61, "$2b$%02d$%s%s", cost, salt_b64, digest);
return NULL;
}
/**
* Verify a password against a $2a$/$2b$/$2y$ bcrypt hash: recompute with the
* hash's own salt and cost, then compare in constant time. An empty password
* is allowed - reference implementations can hash the empty string.
*/
bool bcrypt_verify(const char *password, size_t password_len, const char *hash) {
if (!tables_ready) { b64_index_init(); init_tables(); }
bcrypt_info info;
if (!bcrypt_decode(hash, &info)) return false;
uint8_t salt[16];
if (!decode_b64(info.salt, 16, salt)) return false;
char digest[32];
if (bcrypt_hash_with_salt(password, password_len, info.cost, salt, digest)) return false;
size_t hash_len = strlen(hash);
const char *expected = hash + (hash_len - 31);
volatile uint8_t diff = 0;
for (size_t i = 0; i < 31; i++) diff |= (uint8_t)digest[i] ^ (uint8_t)expected[i];
return diff == 0;
}
/* ------------------------------------------------------------- demo main --- */
int main(void) {
/* Known vector: cost 5 with this salt must verify against the hash built
* here — deterministic because the salt is explicit. */
const char *password = "hunter2";
uint8_t salt[16];
memset(salt, 0, sizeof salt);
memcpy(salt, "cosmodev-salt!!", 15); /* demo salt — real code uses RAND_bytes */
char digest[32];
const char *err = bcrypt_hash_with_salt(password, strlen(password), 5, salt, digest);
if (err) {
fprintf(stderr, "error: %s\n", err);
return 1;
}
char hash[64];
char salt_b64[24];
encode_b64(salt, sizeof salt, salt_b64);
snprintf(hash, sizeof hash, "$2b$05$%s%s", salt_b64, digest);
printf("hash: %s\n", hash);
printf("verify: %s\n", bcrypt_verify(password, strlen(password), hash) ? "true" : "false");
printf("wrong: %s\n", bcrypt_verify("hunter3", 7, hash) ? "true" : "false");
return 0;
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →