Skip to content

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 →