Bcrypt Hash & Verify — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Pure-Java bcrypt (Blowfish-based password hashing) - zero dependencies.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/bcrypt.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// 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. Produces hashes that interoperate with
// bcrypt implementations everywhere (the TS reference is verified against
// OpenBSD/Go reference vectors; this port is the same algorithm).
//
// 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 int[] at class load. Java int arithmetic wraps at 32
// bits by definition, so the TS reference's explicit `>>> 0` unsigned
// normalizations are implicit here.
/** bcrypt's non-standard Base64 alphabet ('.' + '/' first, then alphanumeric). */
// P_HEX_PLACEHOLDER
// S_HEX_PLACEHOLDER
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.regex.Pattern;
public final class Bcrypt {
public static final String B64_CHARS = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final int[] B64_INDEX = buildB64Index();
private static int[] buildB64Index() {
int[] t = new int[128];
Arrays.fill(t, -1);
for (int i = 0; i < B64_CHARS.length(); i++) t[B64_CHARS.charAt(i)] = i;
return t;
}
private static int[] parseHexWords(String hex) {
int[] out = new int[hex.length() / 8];
for (int i = 0; i < out.length; i++) {
out[i] = (int) Long.parseLong(hex.substring(i * 8, i * 8 + 8), 16);
}
return out;
}
/** The Blowfish P-array: first 18 words of pi's hex expansion. */
private static final int[] P_INIT = parseHexWords(P_HEX);
/** The four Blowfish S-boxes: the following 1024 words of pi's hex expansion. */
private static final int[] S_INIT = parseHexWords(S_HEX);
/** Minimum and maximum bcrypt cost factor (log2 rounds). */
public static final int MIN_COST = 4;
public static final int MAX_COST = 31;
/** Cost used when bcryptHash gets no explicit cost argument. */
public static final int DEFAULT_COST = 12;
/** bcrypt only uses the first 72 bytes of the password. */
public static final int MAX_PASSWORD_BYTES = 72;
/** The magic string bcrypt encrypts 64 times to produce the digest. */
private static final String MAGIC = "OrpheanBeholderScryDoubt";
private static final Pattern HASH_RE =
Pattern.compile("^\\$2[aby]\\$(\\d{2})\\$([./A-Za-z0-9]{22})([./A-Za-z0-9]{31})$");
private static final SecureRandom RANDOM = new SecureRandom();
public record BcryptInfo(
/** Full version prefix, e.g. "$2b$". */
String version,
/** Log2 iteration count, 4-31. */
int cost,
/** The 22-character Base64 salt portion. */
String salt) {
}
/**
* Derive the bcrypt key data from a password: UTF-8 bytes, truncated to 72.
* A single NUL terminator is appended unless truncation already reached 72
* bytes - the exact keying OpenBSD's bcrypt uses.
*/
public static byte[] keyData(String password) {
byte[] bytes = password.getBytes(StandardCharsets.UTF_8);
byte[] truncated = bytes.length > MAX_PASSWORD_BYTES
? Arrays.copyOfRange(bytes, 0, MAX_PASSWORD_BYTES) : bytes;
if (truncated.length >= MAX_PASSWORD_BYTES) return truncated;
byte[] key = new byte[truncated.length + 1];
System.arraycopy(truncated, 0, key, 0, truncated.length);
key[truncated.length] = 0;
return key;
}
/** Blowfish round function F(x) = ((S0[a] + S1[b]) ^ S2[c]) + S3[d]. */
private static int f(int[] S, int 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; returns {l, r}. */
private static int[] encipher(int[] P, int[] S, int xl, int xr) {
int l = xl;
int r = xr;
for (int i = 0; i < 16; i++) {
l ^= P[i];
r ^= f(S, l);
int t = l;
l = r;
r = t;
}
int t = l;
l = r;
r = t;
r ^= P[16];
l ^= P[17];
return new int[] {l, r};
}
/**
* Read 4 bytes at {@code offset} (wrapping around {@code data}) as a
* big-endian word. Returns {word, advancedOffset} - OpenBSD's stream2word.
*/
private static int[] stream2word(byte[] data, int offset) {
int word = 0;
int j = offset;
for (int i = 0; i < 4; i++, j++) {
if (j >= data.length) j = 0;
word = (word << 8) | (data[j] & 0xff);
}
return new int[] {word, j};
}
/**
* Plain Blowfish key schedule (OpenBSD's expand0state): XOR {@code data}
* into P, then chain (0,0) through 521 encryptions that re-derive P and
* every S-box entry. Unlike the salted variant, nothing is XORed into the
* mixing pair.
*/
private static void expand0(int[] P, int[] S, byte[] data) {
int j = 0;
for (int i = 0; i < 18; i++) {
int[] wr = stream2word(data, j);
P[i] ^= wr[0];
j = wr[1];
}
int l = 0;
int r = 0;
for (int i = 0; i < 18; i += 2) {
int[] lr = encipher(P, S, l, r);
l = lr[0];
r = lr[1];
P[i] = l;
P[i + 1] = r;
}
for (int box = 0; box < 4; box++) {
for (int k = 0; k < 256; k += 2) {
int[] lr = encipher(P, S, l, r);
l = lr[0];
r = lr[1];
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.
*/
private static void expandState(int[] P, int[] S, byte[] salt, byte[] key) {
int j = 0;
for (int i = 0; i < 18; i++) {
int[] wr = stream2word(key, j);
P[i] ^= wr[0];
j = wr[1];
}
int l = 0;
int r = 0;
j = 0;
for (int i = 0; i < 18; i += 2) {
int[] wr = stream2word(salt, j);
l ^= wr[0];
j = wr[1];
wr = stream2word(salt, j);
r ^= wr[0];
j = wr[1];
int[] lr = encipher(P, S, l, r);
l = lr[0];
r = lr[1];
P[i] = l;
P[i + 1] = r;
}
for (int box = 0; box < 4; box++) {
for (int k = 0; k < 256; k += 2) {
int[] wr = stream2word(salt, j);
l ^= wr[0];
j = wr[1];
wr = stream2word(salt, j);
r ^= wr[0];
j = wr[1];
int[] lr = encipher(P, S, l, r);
l = lr[0];
r = lr[1];
S[box * 256 + k] = l;
S[box * 256 + k + 1] = r;
}
}
}
/**
* Encode bytes with bcrypt's Base64 variant (3 bytes -> 4 chars, partial
* groups emit 2-3 chars, no padding).
*/
public static String encodeB64(byte[] data) {
StringBuilder out = new StringBuilder();
for (int i = 0; i < data.length; i += 3) {
int b0 = data[i] & 0xff;
int b1 = i + 1 < data.length ? data[i + 1] & 0xff : -1;
int b2 = i + 2 < data.length ? data[i + 2] & 0xff : -1;
out.append(B64_CHARS.charAt(b0 >> 2));
out.append(B64_CHARS.charAt(((b0 & 0x03) << 4) | (b1 >= 0 ? b1 >> 4 : 0)));
if (b1 < 0) break;
out.append(B64_CHARS.charAt(((b1 & 0x0f) << 2) | (b2 >= 0 ? b2 >> 6 : 0)));
if (b2 < 0) break;
out.append(B64_CHARS.charAt(b2 & 0x3f));
}
return out.toString();
}
/**
* Decode bcrypt Base64 into exactly {@code count} bytes (throws on bad
* chars or when the input carries fewer than {@code count} bytes of bits).
*/
public static byte[] decodeB64(String input, int count) {
byte[] out = new byte[count];
int totalBits = count * 8;
int target = 0;
for (int i = 0; i < input.length() && target < totalBits; i++) {
char c = input.charAt(i);
int v = c < 128 ? B64_INDEX[c] : -1;
if (v < 0) {
throw new IllegalArgumentException("Invalid character in bcrypt base64 data: " + c);
}
for (int bit = 5; bit >= 0 && target < totalBits; bit--) {
if ((v & (1 << bit)) != 0) out[target >> 3] |= (byte) (1 << (7 - (target & 7)));
target++;
}
}
if (target < totalBits) {
throw new IllegalArgumentException("Bcrypt base64 data is too short");
}
return out;
}
private static void assertPassword(String password) {
if (password == null) throw new IllegalArgumentException("Password must be a string");
if (password.isEmpty()) throw new IllegalArgumentException("Password must not be empty");
}
/** Validate a cost factor, throwing a clear error outside 4-31. */
public static void assertCost(int cost) {
if (cost < MIN_COST || cost > MAX_COST) {
throw new IllegalArgumentException(
"Cost factor must be an integer between " + MIN_COST + " and " + MAX_COST);
}
}
/**
* Compute the bcrypt digest of a password with an explicit salt and cost.
* Shared by hash (fresh random salt) and verify (salt parsed from the
* hash) - also the deterministic entry point used by test suites.
* Synchronous: no event loop to yield to.
*/
public static String bcryptHashWithSalt(String password, int cost, byte[] salt) {
assertCost(cost);
if (salt == null || salt.length != 16) {
throw new IllegalArgumentException("Salt must be exactly 16 bytes");
}
byte[] key = keyData(password);
int[] P = P_INIT.clone();
int[] S = S_INIT.clone();
expandState(P, S, salt, key);
long rounds = 1L << cost;
for (long k = 0; k < rounds; k++) {
expand0(P, S, key);
expand0(P, S, salt);
}
int[] cdata = new int[6];
for (int i = 0; i < 6; i++) {
cdata[i] = (MAGIC.charAt(i * 4) << 24)
| (MAGIC.charAt(i * 4 + 1) << 16)
| (MAGIC.charAt(i * 4 + 2) << 8)
| MAGIC.charAt(i * 4 + 3);
}
for (int i = 0; i < 64; i++) {
for (int j = 0; j < 6; j += 2) {
int[] lr = encipher(P, S, cdata[j], cdata[j + 1]);
cdata[j] = lr[0];
cdata[j + 1] = lr[1];
}
}
byte[] digestBytes = new byte[23];
for (int i = 0; i < 23; i++) {
digestBytes[i] = (byte) ((cdata[i >> 2] >>> (24 - 8 * (i & 3))) & 0xff);
}
return encodeB64(digestBytes);
}
/**
* Hash a password with bcrypt. Generates a fresh 16-byte crypto-random
* salt, runs 2^cost EksBlowfish rounds (default cost 12), and returns a
* "$2b$" hash string.
*/
public static String bcryptHash(String password) {
return bcryptHash(password, DEFAULT_COST);
}
public static String bcryptHash(String password, int cost) {
assertPassword(password);
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
String digest = bcryptHashWithSalt(password, cost, salt);
return "$2b$" + String.format("%02d", cost) + "$" + encodeB64(salt) + digest;
}
/**
* Verify a password against a $2a$ / $2b$ / $2y$ bcrypt hash. Recomputes
* the digest with the hash's own salt and cost, then compares in constant
* time. An empty password is allowed here - reference bcrypt
* implementations can hash the empty string, so their hashes must verify.
*/
public static boolean bcryptVerify(String password, String hash) {
BcryptInfo info = bcryptDecode(hash);
byte[] salt = decodeB64(info.salt(), 16);
String digest = bcryptHashWithSalt(password, info.cost(), salt);
String expected = hash.substring(hash.length() - 31);
int diff = 0;
for (int i = 0; i < digest.length(); i++) {
diff |= digest.charAt(i) ^ expected.charAt(i);
}
return diff == 0;
}
/**
* Parse a bcrypt hash string into its parts: version prefix, cost factor
* and 22-character Base64 salt. Throws on malformed input.
*/
public static BcryptInfo bcryptDecode(String hash) {
if (hash == null) throw new IllegalArgumentException("Hash must be a string");
var m = HASH_RE.matcher(hash.trim());
if (!m.matches()) {
throw new IllegalArgumentException(
"Not a valid bcrypt hash (expected $2a$/$2b$/$2y$CC$ + 53 base64 chars)");
}
int cost = Integer.parseInt(m.group(1));
if (cost < MIN_COST || cost > MAX_COST) {
throw new IllegalArgumentException(
"Cost factor out of range (" + MIN_COST + "-" + MAX_COST + ")");
}
return new BcryptInfo(m.group(0).substring(0, 4), cost, m.group(2));
}
private Bcrypt() {
}
}
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 →