Skip to content

Bcrypt Hash & Verify — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package bcrypt ports src/lib/bcrypt.ts (dual source: the web lib is
// TypeScript, the CLI lib is Go — kept in lock-step). Pure and deterministic
// for a given salt, never panics. The table-driven tests in bcrypt_test.go
// share vectors with src/lib/bcrypt.test.ts so the two implementations are
// held to the same contract — including reference digests generated by a
// third, independent implementation (Go's golang.org/x/crypto/bcrypt).
//
// The algorithm mirrors the TS lib exactly (Provos-Mazieres USENIX '99):
// EksBlowfish key setup, the 64-fold encryption of "OrpheanBeholderScryDoubt",
// and OpenBSD's $2b$ hash format. The TS lib is async only so high costs can
// yield to the browser event loop; the Go port is plain synchronous code.
//
// 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, parsed once at package init.
package bcrypt

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

// b64Chars is bcrypt's non-standard Base64 alphabet ('.' + '/' first, then
// alphanumeric). Mirrors B64_CHARS in the TS lib.
const b64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

// b64Index maps ASCII bytes to their value in b64Chars (-1 = not in alphabet).
var b64Index = func() [128]int8 {
	var t [128]int8
	for i := range t {
		t[i] = -1
	}
	for i := 0; i < len(b64Chars); i++ {
		t[b64Chars[i]] = int8(i)
	}
	return t
}()

const pHex = "243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89" +
	"452821e638d01377be5466cf34e90c6cc0ac29b7c97c50dd3f84d5b5b5470917" +
	"9216d5d98979fb1b"

const sHex = "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" +
	"4b7a70e9b5b32944db75092ec4192623ad6ea6b049a7df7d9cee60b88fedb266" +
	"ecaa8c71699a17ff5664526cc2b19ee1193602a575094c29a0591340e4183a3e" +
	"3f54989a5b429d656b8fe4d699f73fd6a1d29c07efe830f54d2d38e6f0255dc1" +
	"4cdd20868470eb266382e9c6021ecc5e09686b3f3ebaefc93c9718146b6a70a1" +
	"687f358452a0e286b79c5305aa5007373e07841c7fdeae5c8e7d44ec5716f2b8" +
	"b03ada37f0500c0df01c1f040200b3ffae0cf51a3cb574b225837a58dc0921bd" +
	"d19113f97ca92ff69432477322f547013ae5e58137c2dadcc8b576349af3dda7" +
	"a94461460fd0030eecc8c73ea4751e41e238cd993bea0e2f3280bba1183eb331" +
	"4e548b384f6db9086f420d03f60a04bf2cb8129024977c795679b072bcaf89af" +
	"de9a771fd9930810b38bae12dccf3f2e5512721f2e6b7124501adde69f84cd87" +
	"7a5847187408da17bc9f9abce94b7d8cec7aec3adb851dfa63094366c464c3d2" +
	"ef1c18473215d908dd433b3724c2ba1612a14d432a65c45150940002133ae4dd" +
	"71dff89e10314e5581ac77d65f11199b043556f1d7a3c76b3c11183b5924a509" +
	"f28fe6ed97f1fbfa9ebabf2c1e153c6e86e34570eae96fb1860e5e0a5a3e2ab3" +
	"771fe71c4e3d06fa2965dcb999e71d0f803e89d65266c8252e4cc9789c10b36a" +
	"c6150eba94e2ea78a5fc3c531e0a2df4f2f74ea7361d2b3d1939260f19c27960" +
	"5223a708f71312b6ebadfe6eeac31f66e3bc4595a67bc883b17f37d1018cff28" +
	"c332ddefbe6c5aa56558218568ab9802eecea50fdb2f953b2aef7dad5b6e2f84" +
	"1521b62829076170ecdd4775619f151013cca830eb61bd960334fe1eaa0363cf" +
	"b5735c904c70a239d59e9e0bcbaade14eecc86bc60622ca79cab5cabb2f3846e" +
	"648b1eaf19bdf0caa02369b9655abb5040685a323c2ab4b3319ee9d5c021b8f7" +
	"9b540b19875fa09995f7997e623d7da8f837889a97e32d7711ed935f16681281" +
	"0e358829c7e61fd696dedfa17858ba9957f584a51b2272639b83c3ff1ac24696" +
	"cdb30aeb532e30548fd948e46dbc312858ebf2ef34c6ffeafe28ed61ee7c3c73" +
	"5d4a14d9e864b7e342105d14203e13e045eee2b6a3aaabeadb6c4f15facb4fd0" +
	"c742f442ef6abbb5654f3b1d41cd2105d81e799e86854dc7e44b476a3d816250" +
	"cf62a1f25b8d2646fc8883a0c1c7b6a37f1524c369cb749247848a0b5692b285" +
	"095bbf00ad19489d1462b17423820e0058428d2a0c55f5ea1dadf43e233f7061" +
	"3372f0928d937e41d65fecf16c223bdb7cde3759cbee74604085f2a7ce77326e" +
	"a607808419f8509ee8efd85561d99735a969a7aac50c06c25a04abfc800bcadc" +
	"9e447a2ec3453484fdd567050e1e9ec9db73dbd3105588cd675fda79e3674340" +
	"c5c43465713e38d83d28f89ef16dff20153e21e78fb03d4ae6e39f2bdb83adf7" +
	"e93d5a68948140f7f64c261c94692934411520f77602d4f7bcf46b2ed4a20068" +
	"d40824713320f46a43b7d4b7500061af1e39f62e9724454614214f74bf8b8840" +
	"4d95fc1d96b591af70f4ddd366a02f45bfbc09ec03bd97857fac6dd031cb8504" +
	"96eb27b355fd3941da2547e6abca0a9a28507825530429f40a2c86dae9b66dfb" +
	"68dc1462d7486900680ec0a427a18dee4f3ffea2e887ad8cb58ce0067af4d6b6" +
	"aace1e7cd3375fecce78a399406b2a4220fe9e35d9f385b9ee39d7ab3b124e8b" +
	"1dc9faf74b6d185626a36631eae397b23a6efa74dd5b43326841e7f7ca7820fb" +
	"fb0af54ed8feb397454056acba48952755533a3a20838d87fe6ba9b7d096954b" +
	"55a867bca1159a58cca9296399e1db33a62a4a563f3125f95ef47e1c9029317c" +
	"fdf8e80204272f7080bb155c05282ce395c11548e4c66d2248c1133fc70f86dc" +
	"07f9c9ee41041f0f404779a45d886e17325f51ebd59bc0d1f2bcc18f41113564" +
	"257b7834602a9c60dff8e8a31f636c1b0e12b4c202e1329eaf664fd1cad18115" +
	"6b2395e0333e92e13b240b62eebeb92285b2a20ee6ba0d99de720c8c2da2f728" +
	"d012784595b794fd647d0862e7ccf5f05449a36f877d48fac39dfd27f33e8d1e" +
	"0a476341992eff743a6f6eabf4f8fd37a812dc60a1ebddf8991be14cdb6e6b0d" +
	"c67b55106d672c372765d43bdcd0e804f1290dc7cc00ffa3b5390f92690fed0b" +
	"667b9ffbcedb7d9ca091cf0bd9155ea3bb132f88515bad247b9479bf763bd6eb" +
	"37392eb3cc1159798026e297f42e312d6842ada7c66a2b3b12754ccc782ef11c" +
	"6a124237b79251e706a1bbe64bfb63501a6b101811caedfa3d25bdd8e2e1c3c9" +
	"444216590a121386d90cec6ed5abea2a64af674eda86a85fbebfe98864e4c3fe" +
	"9dbc8057f0f7c08660787bf86003604dd1fd8346f6381fb07745ae04d736fccc" +
	"83426b33f01eab71b08041873c005e5f77a057bebde8ae2455464299bf582e61" +
	"4e58f48ff2ddfda2f474ef388789bdc25366f9c3c8b38e74b475f25546fcd9b9" +
	"7aeb26618b1ddf84846a0e79915f95e2466e598e20b457708cd55591c902de4c" +
	"b90bace1bb8205d011a862487574a99eb77f19b6e0a9dc09662d09a1c4324633" +
	"e85a1f0209f0be8c4a99a0251d6efe101ab93d1d0ba5a4dfa186f20f2868f169" +
	"dcb7da83573906fea1e2ce9b4fcd7f5250115e01a70683faa002b5c40de6d027" +
	"9af88c27773f8641c3604c0661a806b5f0177a28c0f586e0006058aa30dc7d62" +
	"11e69ed72338ea6353c2dd94c2c21634bbcbee5690bcb6deebfc7da1ce591d76" +
	"6f05e4094b7c018839720a3d7c927c2486e3725f724d9db91ac15bb4d39eb8fc" +
	"ed54557808fca5b5d83d7cd34dad0fc41e50ef5eb161e6f8a28514d96c51133c" +
	"6fd5c7e756e14ec4362abfceddc6c837d79a323492638212670efa8e406000e0" +
	"3a39ce37d3faf5cfabc277375ac52d1b5cb0679e4fa33742d382274099bc9bbe" +
	"d5118e9dbf0f7315d62d1c7ec700c47bb78c1b6b21a19045b26eb1be6a366eb4" +
	"5748ab2fbc946e79c6a376d26549c2c8530ff8ee468dde7dd5730a1d4cd04dc6" +
	"2939bbdba9ba4650ac9526e8be5ee304a1fad5f06a2d519a63ef8ce29a86ee22" +
	"c089c2b843242ef6a51e03aa9cf2d0a483c061ba9be96a4d8fe51550ba645bd6" +
	"2826a2f9a73a3ae14ba99586ef5562e9c72fefd3f752f7da3f046f6977fa0a59" +
	"80e4a91587b086019b09e6ad3b3ee593e990fd5a9e34d7972cf0b7d9022b8b51" +
	"96d5ac3a017da67dd1cf3ed67c7d2d281f9f25cfadf2b89b5ad6b4725a88f54c" +
	"e029ac71e019a5e647b0acfded93fa9be8d3c48d283b57ccf8d5662979132e28" +
	"785f0191ed756055f7960e44e3d35e8c15056dd488f46dba03a161250564f0bd" +
	"c3eb9e153c9057a297271aeca93a072a1b3f6d9b1e6321f5f59c66fb26dcf319" +
	"7533d928b155fdf5035634828aba3cbb28517711c20ad9f8abcc5167ccad925f" +
	"4de817513830dc8e379d58629320f991ea7a90c2fb3e7bce5121ce64774fbe32" +
	"a8b6e37ec3293d4648de53696413e680a2ae0810dd6db22469852dfd09072166" +
	"b39a460a6445c0dd586cdecf1c20c8ae5bbef7dd1b588d40ccd2017f6bb4e3bb" +
	"dda26a7e3a59ff453e350a44bcb4cdd572eacea8fa6484bb8d6612aebf3c6f47" +
	"d29be463542f5d9eaec2771bf64e6370740e0d8de75b1357f8721671af537d5d" +
	"4040cb084eb4e2cc34d2466a0115af84e1b0042895983a1d06b89fb4ce6ea048" +
	"6f3f3b823520ab82011a1d4b277227f8611560b1e7933fdcbb3a792b344525bd" +
	"a08839e151ce794b2f32c9b7a01fbac9e01cc87ebcc7d1f6cf0111c3a1e8aac7" +
	"1a908749d44fbd9ad0dadecbd50ada380339c32ac69136678df9317ce0b12b4f" +
	"f79e59b743f5bb3af2d519ff27d9459cbf97222c15e6fc2a0f91fc719b941525" +
	"fae59361ceb69cebc2a8645912baa8d1b6c1075ee3056a0c10d25065cb03a442" +
	"e0ec6e0e1698db3b4c98a0be3278e9649f1f9532e0d392dfd3a0342b8971f21e" +
	"1b0a74414ba3348cc5be7120c37632d8df359f8d9b992f2ee60b6f470fe3f11d" +
	"e54cda541edad891ce6279cfcd3e7e6f1618b166fd2c1d05848fd2c5f6fb2299" +
	"f523f357a632762393a8353156cccd02acf081625a75ebb56e16369788d273cc" +
	"de96629281b949d04c50901b71c65614e6c6c7bd327a140a45e1d006c3f27b9a" +
	"c9aa53fd62a80f00bb25bfe235bdd2f671126905b2040222b6cbcf7ccd769c2b" +
	"53113ec01640e3d338abbd602547adf0ba38209cf746ce7677afa1c520756060" +
	"85cbfe4e8ae88dd87aaaf9b04cf9aa7e1948c25c02fb8a8c01c36ae4d6ebe1f9" +
	"90d4f869a65cdea03f09252dc208e69fb74e6132ce77e25b578fdfe33ac372e6"

// parseHexWords turns a hex string into big-endian 32-bit words. Mirrors
// parseHexWords() in the TS lib.
func parseHexWords(h string) []uint32 {
	raw, err := hex.DecodeString(h)
	if err != nil {
		// Unreachable: pHex/sHex are fixed compile-time constants.
		panic("bcrypt: invalid pi hex constant: " + err.Error())
	}
	words := make([]uint32, len(raw)/4)
	for i := range words {
		words[i] = binary.BigEndian.Uint32(raw[i*4:])
	}
	return words
}

var (
	pInit = parseHexWords(pHex)
	sInit = parseHexWords(sHex)
)

// Cost bounds and defaults. Mirror MIN_COST / MAX_COST / DEFAULT_COST /
// MAX_PASSWORD_BYTES in the TS lib. Hash has no default-parameter sugar in
// Go: callers wanting the TS default pass DefaultCost explicitly.
const (
	MinCost          = 4  // minimum bcrypt cost factor (log2 rounds)
	MaxCost          = 31 // maximum bcrypt cost factor (log2 rounds)
	DefaultCost      = 12 // the TS lib's default cost parameter
	MaxPasswordBytes = 72 // bcrypt only uses the first 72 password bytes
)

// magic is the string bcrypt encrypts 64 times to produce the digest.
const magic = "OrpheanBeholderScryDoubt"

var hashRe = regexp.MustCompile(`^\$2[aby]\$(\d{2})\$([./A-Za-z0-9]{22})([./A-Za-z0-9]{31})$`)

// Info is a bcrypt hash parsed into its parts. Mirrors BcryptInfo.
type Info struct {
	Version string // full version prefix, e.g. "$2b$"
	Cost    int    // log2 iteration count, 4-31
	Salt    string // the 22-character Base64 salt portion
}

// KeyData derives 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.
// Mirrors keyData() in the TS lib.
func KeyData(password string) []byte {
	b := []byte(password) // Go strings are UTF-8, like TextEncoder's output
	if len(b) > MaxPasswordBytes {
		b = b[:MaxPasswordBytes]
	}
	if len(b) >= MaxPasswordBytes {
		return b
	}
	key := make([]byte, len(b)+1)
	copy(key, b)
	key[len(b)] = 0
	return key
}

// f is the Blowfish round function: F(x) = ((S0[a] + S1[b]) ^ S2[c]) + S3[d].
func f(s []uint32, x uint32) uint32 {
	return ((s[(x>>24)&0xff] + s[256+((x>>16)&0xff)]) ^ s[512+((x>>8)&0xff)]) + s[768+(x&0xff)]
}

// encipher runs one Blowfish encryption of the (xl, xr) pair.
// Mirrors encipher() in the TS lib.
func encipher(p, s []uint32, xl, xr uint32) (uint32, uint32) {
	l, r := xl, xr
	for i := 0; i < 16; i++ {
		l ^= p[i]
		r ^= f(s, l)
		l, r = r, l
	}
	l, r = r, l
	r ^= p[16]
	l ^= p[17]
	return l, r
}

// stream2word reads 4 bytes at off (wrapping around data) as a big-endian
// word. Returns the word and the advanced offset — OpenBSD's stream2word.
func stream2word(data []byte, off int) (uint32, int) {
	var word uint32
	j := off
	for i := 0; i < 4; i, j = i+1, j+1 {
		if j >= len(data) {
			j = 0
		}
		word = word<<8 | uint32(data[j])
	}
	return word, j
}

// expand0 is the 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. Unlike the salted variant, nothing is XORed into the
// mixing pair. Mirrors expand0() in the TS lib.
func expand0(p, s []uint32, data []byte) {
	j := 0
	for i := 0; i < 18; i++ {
		w, nj := stream2word(data, j)
		p[i] ^= w
		j = nj
	}
	var l, r uint32
	for i := 0; i < 18; i += 2 {
		l, r = encipher(p, s, l, r)
		p[i] = l
		p[i+1] = r
	}
	for box := 0; box < 4; box++ {
		for k := 0; k < 256; k += 2 {
			l, r = encipher(p, s, l, r)
			s[box*256+k] = l
			s[box*256+k+1] = r
		}
	}
}

// expandState is the Blowfish key schedule seeded with salt: P is XORed with
// the key while the mixing pairs are salted — bcrypt's first expandstate
// call. The salt word offset j keeps cycling across the P loop and all four
// S-box loops without resetting. Mirrors expandState() in the TS lib.
func expandState(p, s []uint32, salt, key []byte) {
	j := 0
	for i := 0; i < 18; i++ {
		w, nj := stream2word(key, j)
		p[i] ^= w
		j = nj
	}
	var l, r uint32
	j = 0
	for i := 0; i < 18; i += 2 {
		var w uint32
		w, j = stream2word(salt, j)
		l ^= w
		w, j = stream2word(salt, j)
		r ^= w
		l, r = encipher(p, s, l, r)
		p[i] = l
		p[i+1] = r
	}
	for box := 0; box < 4; box++ {
		for k := 0; k < 256; k += 2 {
			var w uint32
			w, j = stream2word(salt, j)
			l ^= w
			w, j = stream2word(salt, j)
			r ^= w
			l, r = encipher(p, s, l, r)
			s[box*256+k] = l
			s[box*256+k+1] = r
		}
	}
}

// EncodeB64 encodes bytes with bcrypt's Base64 variant (3 bytes -> 4 chars,
// partial groups emit 2-3 chars, no padding). Mirrors encodeB64() in the TS
// lib.
func EncodeB64(data []byte) string {
	var out strings.Builder
	for i := 0; i < len(data); i += 3 {
		b0 := data[i]
		b1, b2 := -1, -1
		if i+1 < len(data) {
			b1 = int(data[i+1])
		}
		if i+2 < len(data) {
			b2 = int(data[i+2])
		}
		out.WriteByte(b64Chars[b0>>2])
		second := int(b0&0x03) << 4
		if b1 >= 0 {
			second |= b1 >> 4
		}
		out.WriteByte(b64Chars[second])
		if b1 < 0 {
			break
		}
		third := (b1 & 0x0f) << 2
		if b2 >= 0 {
			third |= b2 >> 6
		}
		out.WriteByte(b64Chars[third])
		if b2 < 0 {
			break
		}
		out.WriteByte(b64Chars[b2&0x3f])
	}
	return out.String()
}

// DecodeB64 decodes bcrypt Base64 into exactly count bytes. It errors on bad
// characters, on a negative count, or when the input carries fewer than
// count bytes worth of bits. Mirrors decodeB64() in the TS lib (which throws).
func DecodeB64(input string, count int) ([]byte, error) {
	if count < 0 {
		return nil, fmt.Errorf("invalid byte count %d", count)
	}
	out := make([]byte, count)
	totalBits := count * 8
	target := 0
	for i := 0; i < len(input) && target < totalBits; i++ {
		c := input[i]
		v := -1
		if c < 128 {
			v = int(b64Index[c])
		}
		if v < 0 {
			return nil, fmt.Errorf("invalid character in bcrypt base64 data: %q", input[i:i+1])
		}
		for bit := 5; bit >= 0 && target < totalBits; bit-- {
			if v&(1<<bit) != 0 {
				out[target>>3] |= 1 << (7 - (target & 7))
			}
			target++
		}
	}
	if target < totalBits {
		return nil, errors.New("bcrypt base64 data is too short")
	}
	return out, nil
}

// AssertCost validates a cost factor, returning a clear error outside 4-31.
// Mirrors assertCost() in the TS lib (Go ints are always integers, so the
// Number.isInteger half of the TS check does not apply).
func AssertCost(cost int) error {
	if cost < MinCost || cost > MaxCost {
		return fmt.Errorf("cost factor must be an integer between %d and %d", MinCost, MaxCost)
	}
	return nil
}

// HashWithSalt computes the bcrypt digest of a password with an explicit salt
// and cost — the deterministic entry point used by the test suite, shared by
// Hash (fresh random salt) and Verify (salt parsed from the hash). An empty
// password is allowed here, exactly like the TS lib: reference bcrypt
// implementations can hash the empty string. Mirrors bcryptHashWithSalt().
func HashWithSalt(password string, cost int, salt []byte) (string, error) {
	if err := AssertCost(cost); err != nil {
		return "", err
	}
	if len(salt) != 16 {
		return "", errors.New("salt must be exactly 16 bytes")
	}
	key := KeyData(password)

	p := make([]uint32, len(pInit))
	copy(p, pInit)
	s := make([]uint32, len(sInit))
	copy(s, sInit)
	expandState(p, s, salt, key)
	rounds := uint64(1) << uint(cost)
	for k := uint64(0); k < rounds; k++ {
		expand0(p, s, key)
		expand0(p, s, salt)
	}

	var cdata [6]uint32
	for i := 0; i < 6; i++ {
		cdata[i] = uint32(magic[i*4])<<24 | uint32(magic[i*4+1])<<16 |
			uint32(magic[i*4+2])<<8 | uint32(magic[i*4+3])
	}
	for i := 0; i < 64; i++ {
		for j := 0; j < 6; j += 2 {
			l, r := encipher(p, s, cdata[j], cdata[j+1])
			cdata[j] = l
			cdata[j+1] = r
		}
	}
	digestBytes := make([]byte, 23)
	for i := 0; i < 23; i++ {
		digestBytes[i] = byte(cdata[i>>2] >> (24 - 8*(i&3)))
	}
	return EncodeB64(digestBytes), nil
}

// Hash hashes a password with bcrypt: a fresh 16-byte crypto-random salt,
// 2^cost EksBlowfish rounds, and a "$2b$" hash string. Callers wanting the
// TS lib's default cost pass DefaultCost. Mirrors bcryptHash() — the empty
// password is rejected here (but verifies, see Verify).
func Hash(password string, cost int) (string, error) {
	if password == "" {
		return "", errors.New("password must not be empty")
	}
	salt := make([]byte, 16)
	if _, err := rand.Read(salt); err != nil {
		return "", fmt.Errorf("generating salt: %w", err)
	}
	digest, err := HashWithSalt(password, cost, salt)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("$2b$%02d$%s%s", cost, EncodeB64(salt), digest), nil
}

// Verify verifies a password against a $2a$/$2b$/$2y$ bcrypt hash. It
// recomputes the digest with the hash's own salt and cost, then compares in
// constant time. An empty password is allowed — reference bcrypt
// implementations can hash the empty string, so their hashes must verify.
// A malformed hash returns an error; a well-formed hash with the wrong
// password returns (false, nil). Mirrors bcryptVerify().
func Verify(password, hash string) (bool, error) {
	info, err := Decode(hash)
	if err != nil {
		return false, err
	}
	salt, err := DecodeB64(info.Salt, 16)
	if err != nil {
		return false, err
	}
	digest, err := HashWithSalt(password, info.Cost, salt)
	if err != nil {
		return false, err
	}
	expected := hash[len(hash)-31:]
	return subtle.ConstantTimeCompare([]byte(digest), []byte(expected)) == 1, nil
}

// Decode parses a bcrypt hash string into its parts: version prefix, cost
// factor and 22-character Base64 salt. Mirrors bcryptDecode().
func Decode(hash string) (Info, error) {
	m := hashRe.FindStringSubmatch(strings.TrimSpace(hash))
	if m == nil {
		return Info{}, errors.New("not a valid bcrypt hash (expected $2a$/$2b$/$2y$CC$ + 53 base64 chars)")
	}
	cost, err := strconv.Atoi(m[1])
	if err != nil || cost < MinCost || cost > MaxCost {
		return Info{}, fmt.Errorf("cost factor out of range (%d-%d)", MinCost, MaxCost)
	}
	return Info{Version: m[0][:4], Cost: cost, Salt: m[2]}, nil
}

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 →