Skip to content

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

// bcrypt — Blowfish-based password hashing ($2a$ / $2b$ / $2y$).
//
// Language: Swift 5.9+ (Foundation)
// Ported from src/lib/bcrypt.ts
// display source — part of CosmoDev's polyglot tool pages
//
// Pure-Swift bcrypt with zero dependencies: the full algorithm from the
// Provos-Mazières 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 async only so it can yield to the event loop every 64
// rounds (high costs would freeze the browser's UI thread). Swift keeps the
// algorithm synchronous; callers dispatch it to a background queue instead.
// UInt32 `&+`/`&-`/`&*` wrap on overflow — exactly the `>>> 0` semantics the
// TS reference applies at every step.

import Foundation

// MARK: - Constants

/// bcrypt's non-standard Base64 alphabet ('.' + '/' first, then alphanumeric).
let B64_CHARS = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

/// Precomputed char -> 6-bit value table for the bcrypt alphabet.
let B64_INDEX: [Int8] = {
    var table = [Int8](repeating: -1, count: 128)
    for (i, scalar) in B64_CHARS.unicodeScalars.enumerated() {
        table[Int(scalar.value)] = Int8(i)
    }
    return table
}()

/// The Blowfish P-array: first 18 words of pi's hex expansion.
let P_HEX = "243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89452821e6" +
  "38d01377be5466cf34e90c6cc0ac29b7c97c50dd3f84d5b5b54709179216d5d98979fb1b"

/// The four Blowfish S-boxes: the following 1024 words of pi's hex expansion.
let S_HEX = "d1310ba698dfb5ac2ffd72dbd01adfb7b8e1afed6a267e96ba7c9045f12c7f9924a19947" +
  "b3916cf70801f2e2858efc16636920d871574e69a458fea3f4933d7e0d95748f728eb658" +
  "718bcd5882154aee7b54a41dc25a59b59c30d5392af26013c5d1b023286085f0ca417918" +
  "b8db38ef8e79dcb0603a180e6c9e0e8bb01e8a3ed71577c1bd314b2778af2fda55605c60" +
  "e65525f3aa55ab945748986263e8144055ca396a2aab10b6b4cc5c341141e8cea15486af" +
  "7c72e993b3ee1411636fbc2a2ba9c55d741831f6ce5c3e169b87931eafd6ba336c24cf5c" +
  "7a325381289586773b8f48986b4bb9afc4bfe81b6628219361d809ccfb21a991487cac60" +
  "5dec8032ef845d5de98575b1dc262302eb651b8823893e81d396acc50f6d6ff383f44239" +
  "2e0b4482a484200469c8f04a9e1f9b5e21c66842f6e96c9a670c9c61abd388f06a51a0d2" +
  "d8542f68960fa728ab5133a36eef0b6c137a3be4ba3bf0507efb2a98a1f1651d39af0176" +
  "66ca593e82430e888cee8619456f9fb47d84a5c33b8b5ebee06f75d885c12073401a449f" +
  "56c16aa64ed3aa62363f77061bfedf72429b023d37d0d724d00a1248db0fead349f1c09b" +
  "075372c980991b7b25d479d8f6e8def7e3fe501ab6794c3b976ce0bd04c006bac1a94fb6" +
  "409f60c45e5c9ec2196a246368fb6faf3e6c53b51339b2eb3b52ec6f6dfc511f9b30952c" +
  "cc814544af5ebd09bee3d004de334afd660f2807192e4bb3c0cba85745c8740fd20b5f39" +
  "b9d3fbdb5579c0bd1a60320ad6a100c6402c7279679f25fefb1fa3cc8ea5e9f8db3222f8" +
  "3c7516dffd616b152f501ec8ad0552ab323db5fafd23876053317b483e00df829e5c57bb" +
  "ca6f8ca01a87562edf1769dbd542a8f6287effc3ac6732c68c4f5573695b27b0bbca58c8" +
  "e1ffa35db8f011a010fa3d98fd2183b84afcb56c2dd1d35b9a53e479b6f84565d28e49bc" +
  "4bfb9790e1ddf2daa4cb7e3362fb1341cee4c6e8ef20cada36774c01d07e9efe2bf11fb4" +
  "95dbda4dae909198eaad8e716b93d5a0d08ed1d0afc725e08e3c5b2f8e7594b78ff6e2fb" +
  "f2122b648888b812900df01c4fad5ea0688fc31cd1cff191b3a8c1ad2f2f2218be0e1777" +
  "ea752dfe8b021fa1e5a0cc0fb56f74e818acf3d6ce89e299b4a84fe0fd13e0b77cc43b81" +
  "d2ada8d9165fa2668095770593cc7314211a1477e6ad206577b5fa86c75442f5fb9d35cf" +
  "ebcdaf0c7b3e89a0d6411bd3ae1e7e4900250e2d2071b35e226800bb57b8e0af2464369b" +
  "f009b91e5563911d59dfa6aa78c14389d95a537f207d5ba202e5b9c5832603766295cfa9" +
  "11c819684e734a41b3472dca7b14a94a1b5100529a532915d60f573fbc9bc6e42b60a476" +
  "81e6740008ba6fb5571be91ff296ec6b2a0dd915b6636521e7b9f9b6ff34052ec5855664" +
  "53b02d5da99f8fa108ba47996e85076a4b7a70e9b5b32944db75092ec4192623ad6ea6b0" +
  "49a7df7d9cee60b88fedb266ecaa8c71699a17ff5664526cc2b19ee1193602a575094c29" +
  "a0591340e4183a3e3f54989a5b429d656b8fe4d699f73fd6a1d29c07efe830f54d2d38e6" +
  "f0255dc14cdd20868470eb266382e9c6021ecc5e09686b3f3ebaefc93c9718146b6a70a1" +
  "687f358452a0e286b79c5305aa5007373e07841c7fdeae5c8e7d44ec5716f2b8b03ada37" +
  "f0500c0df01c1f040200b3ffae0cf51a3cb574b225837a58dc0921bdd19113f97ca92ff6" +
  "9432477322f547013ae5e58137c2dadcc8b576349af3dda7a94461460fd0030eecc8c73e" +
  "a4751e41e238cd993bea0e2f3280bba1183eb3314e548b384f6db9086f420d03f60a04bf" +
  "2cb8129024977c795679b072bcaf89afde9a771fd9930810b38bae12dccf3f2e5512721f" +
  "2e6b7124501adde69f84cd877a5847187408da17bc9f9abce94b7d8cec7aec3adb851dfa" +
  "63094366c464c3d2ef1c18473215d908dd433b3724c2ba1612a14d432a65c45150940002" +
  "133ae4dd71dff89e10314e5581ac77d65f11199b043556f1d7a3c76b3c11183b5924a509" +
  "f28fe6ed97f1fbfa9ebabf2c1e153c6e86e34570eae96fb1860e5e0a5a3e2ab3771fe71c" +
  "4e3d06fa2965dcb999e71d0f803e89d65266c8252e4cc9789c10b36ac6150eba94e2ea78" +
  "a5fc3c531e0a2df4f2f74ea7361d2b3d1939260f19c279605223a708f71312b6ebadfe6e" +
  "eac31f66e3bc4595a67bc883b17f37d1018cff28c332ddefbe6c5aa56558218568ab9802" +
  "eecea50fdb2f953b2aef7dad5b6e2f841521b62829076170ecdd4775619f151013cca830" +
  "eb61bd960334fe1eaa0363cfb5735c904c70a239d59e9e0bcbaade14eecc86bc60622ca7" +
  "9cab5cabb2f3846e648b1eaf19bdf0caa02369b9655abb5040685a323c2ab4b3319ee9d5" +
  "c021b8f79b540b19875fa09995f7997e623d7da8f837889a97e32d7711ed935f16681281" +
  "0e358829c7e61fd696dedfa17858ba9957f584a51b2272639b83c3ff1ac24696cdb30aeb" +
  "532e30548fd948e46dbc312858ebf2ef34c6ffeafe28ed61ee7c3c735d4a14d9e864b7e3" +
  "42105d14203e13e045eee2b6a3aaabeadb6c4f15facb4fd0c742f442ef6abbb5654f3b1d" +
  "41cd2105d81e799e86854dc7e44b476a3d816250cf62a1f25b8d2646fc8883a0c1c7b6a3" +
  "7f1524c369cb749247848a0b5692b285095bbf00ad19489d1462b17423820e0058428d2a" +
  "0c55f5ea1dadf43e233f70613372f0928d937e41d65fecf16c223bdb7cde3759cbee7460" +
  "4085f2a7ce77326ea607808419f8509ee8efd85561d99735a969a7aac50c06c25a04abfc" +
  "800bcadc9e447a2ec3453484fdd567050e1e9ec9db73dbd3105588cd675fda79e3674340" +
  "c5c43465713e38d83d28f89ef16dff20153e21e78fb03d4ae6e39f2bdb83adf7e93d5a68" +
  "948140f7f64c261c94692934411520f77602d4f7bcf46b2ed4a20068d40824713320f46a" +
  "43b7d4b7500061af1e39f62e9724454614214f74bf8b88404d95fc1d96b591af70f4ddd3" +
  "66a02f45bfbc09ec03bd97857fac6dd031cb850496eb27b355fd3941da2547e6abca0a9a" +
  "28507825530429f40a2c86dae9b66dfb68dc1462d7486900680ec0a427a18dee4f3ffea2" +
  "e887ad8cb58ce0067af4d6b6aace1e7cd3375fecce78a399406b2a4220fe9e35d9f385b9" +
  "ee39d7ab3b124e8b1dc9faf74b6d185626a36631eae397b23a6efa74dd5b43326841e7f7" +
  "ca7820fbfb0af54ed8feb397454056acba48952755533a3a20838d87fe6ba9b7d096954b" +
  "55a867bca1159a58cca9296399e1db33a62a4a563f3125f95ef47e1c9029317cfdf8e802" +
  "04272f7080bb155c05282ce395c11548e4c66d2248c1133fc70f86dc07f9c9ee41041f0f" +
  "404779a45d886e17325f51ebd59bc0d1f2bcc18f41113564257b7834602a9c60dff8e8a3" +
  "1f636c1b0e12b4c202e1329eaf664fd1cad181156b2395e0333e92e13b240b62eebeb922" +
  "85b2a20ee6ba0d99de720c8c2da2f728d012784595b794fd647d0862e7ccf5f05449a36f" +
  "877d48fac39dfd27f33e8d1e0a476341992eff743a6f6eabf4f8fd37a812dc60a1ebddf8" +
  "991be14cdb6e6b0dc67b55106d672c372765d43bdcd0e804f1290dc7cc00ffa3b5390f92" +
  "690fed0b667b9ffbcedb7d9ca091cf0bd9155ea3bb132f88515bad247b9479bf763bd6eb" +
  "37392eb3cc1159798026e297f42e312d6842ada7c66a2b3b12754ccc782ef11c6a124237" +
  "b79251e706a1bbe64bfb63501a6b101811caedfa3d25bdd8e2e1c3c9444216590a121386" +
  "d90cec6ed5abea2a64af674eda86a85fbebfe98864e4c3fe9dbc8057f0f7c08660787bf8" +
  "6003604dd1fd8346f6381fb07745ae04d736fccc83426b33f01eab71b08041873c005e5f" +
  "77a057bebde8ae2455464299bf582e614e58f48ff2ddfda2f474ef388789bdc25366f9c3" +
  "c8b38e74b475f25546fcd9b97aeb26618b1ddf84846a0e79915f95e2466e598e20b45770" +
  "8cd55591c902de4cb90bace1bb8205d011a862487574a99eb77f19b6e0a9dc09662d09a1" +
  "c4324633e85a1f0209f0be8c4a99a0251d6efe101ab93d1d0ba5a4dfa186f20f2868f169" +
  "dcb7da83573906fea1e2ce9b4fcd7f5250115e01a70683faa002b5c40de6d0279af88c27" +
  "773f8641c3604c0661a806b5f0177a28c0f586e0006058aa30dc7d6211e69ed72338ea63" +
  "53c2dd94c2c21634bbcbee5690bcb6deebfc7da1ce591d766f05e4094b7c018839720a3d" +
  "7c927c2486e3725f724d9db91ac15bb4d39eb8fced54557808fca5b5d83d7cd34dad0fc4" +
  "1e50ef5eb161e6f8a28514d96c51133c6fd5c7e756e14ec4362abfceddc6c837d79a3234" +
  "92638212670efa8e406000e03a39ce37d3faf5cfabc277375ac52d1b5cb0679e4fa33742" +
  "d382274099bc9bbed5118e9dbf0f7315d62d1c7ec700c47bb78c1b6b21a19045b26eb1be" +
  "6a366eb45748ab2fbc946e79c6a376d26549c2c8530ff8ee468dde7dd5730a1d4cd04dc6" +
  "2939bbdba9ba4650ac9526e8be5ee304a1fad5f06a2d519a63ef8ce29a86ee22c089c2b8" +
  "43242ef6a51e03aa9cf2d0a483c061ba9be96a4d8fe51550ba645bd62826a2f9a73a3ae1" +
  "4ba99586ef5562e9c72fefd3f752f7da3f046f6977fa0a5980e4a91587b086019b09e6ad" +
  "3b3ee593e990fd5a9e34d7972cf0b7d9022b8b5196d5ac3a017da67dd1cf3ed67c7d2d28" +
  "1f9f25cfadf2b89b5ad6b4725a88f54ce029ac71e019a5e647b0acfded93fa9be8d3c48d" +
  "283b57ccf8d5662979132e28785f0191ed756055f7960e44e3d35e8c15056dd488f46dba" +
  "03a161250564f0bdc3eb9e153c9057a297271aeca93a072a1b3f6d9b1e6321f5f59c66fb" +
  "26dcf3197533d928b155fdf5035634828aba3cbb28517711c20ad9f8abcc5167ccad925f" +
  "4de817513830dc8e379d58629320f991ea7a90c2fb3e7bce5121ce64774fbe32a8b6e37e" +
  "c3293d4648de53696413e680a2ae0810dd6db22469852dfd09072166b39a460a6445c0dd" +
  "586cdecf1c20c8ae5bbef7dd1b588d40ccd2017f6bb4e3bbdda26a7e3a59ff453e350a44" +
  "bcb4cdd572eacea8fa6484bb8d6612aebf3c6f47d29be463542f5d9eaec2771bf64e6370" +
  "740e0d8de75b1357f8721671af537d5d4040cb084eb4e2cc34d2466a0115af84e1b00428" +
  "95983a1d06b89fb4ce6ea0486f3f3b823520ab82011a1d4b277227f8611560b1e7933fdc" +
  "bb3a792b344525bda08839e151ce794b2f32c9b7a01fbac9e01cc87ebcc7d1f6cf0111c3" +
  "a1e8aac71a908749d44fbd9ad0dadecbd50ada380339c32ac69136678df9317ce0b12b4f" +
  "f79e59b743f5bb3af2d519ff27d9459cbf97222c15e6fc2a0f91fc719b941525fae59361" +
  "ceb69cebc2a8645912baa8d1b6c1075ee3056a0c10d25065cb03a442e0ec6e0e1698db3b" +
  "4c98a0be3278e9649f1f9532e0d392dfd3a0342b8971f21e1b0a74414ba3348cc5be7120" +
  "c37632d8df359f8d9b992f2ee60b6f470fe3f11de54cda541edad891ce6279cfcd3e7e6f" +
  "1618b166fd2c1d05848fd2c5f6fb2299f523f357a632762393a8353156cccd02acf08162" +
  "5a75ebb56e16369788d273ccde96629281b949d04c50901b71c65614e6c6c7bd327a140a" +
  "45e1d006c3f27b9ac9aa53fd62a80f00bb25bfe235bdd2f671126905b2040222b6cbcf7c" +
  "cd769c2b53113ec01640e3d338abbd602547adf0ba38209cf746ce7677afa1c520756060" +
  "85cbfe4e8ae88dd87aaaf9b04cf9aa7e1948c25c02fb8a8c01c36ae4d6ebe1f990d4f869" +
  "a65cdea03f09252dc208e69fb74e6132ce77e25b578fdfe33ac372e6"

/// Minimum and maximum bcrypt cost factor (log2 rounds).
let MIN_COST = 4
let MAX_COST = 31
/// Cost used when bcryptHash gets no explicit cost argument.
let DEFAULT_COST = 12
/// bcrypt only uses the first 72 bytes of the password.
let MAX_PASSWORD_BYTES = 72
/// The magic string bcrypt encrypts 64 times to produce the digest.
let MAGIC = "OrpheanBeholderScryDoubt"

// MARK: - Pi tables

/// Parse 8-char hex groups into 32-bit words (the P/S table loader).
func parseHexWords(_ hex: String) -> [UInt32] {
    var words: [UInt32] = []
    words.reserveCapacity(hex.count / 8)
    var start = hex.startIndex
    while start < hex.endIndex {
        let end = hex.index(start, offsetBy: 8)
        words.append(UInt32(hex[start..<end], radix: 16)!)
        start = end
    }
    return words
}

let P_INIT = parseHexWords(P_HEX)
let S_INIT = parseHexWords(S_HEX)

// MARK: - Types

/// The parts of a parsed bcrypt hash string.
struct BcryptInfo: Equatable {
    /// Full version prefix, e.g. "$2b$".
    let version: String
    /// Log2 iteration count, 4-31.
    let cost: Int
    /// The 22-character Base64 salt portion.
    let salt: String
}

/// Every failure mode the API can produce (the TS reference throws Error
/// with these same messages).
enum BcryptError: Error, LocalizedError {
    case emptyPassword
    case invalidCost
    case costOutOfRange
    case invalidSalt
    case invalidHash
    case invalidBase64Character(Character)
    case base64TooShort

    var errorDescription: String? {
        switch self {
        case .emptyPassword:
            return "Password must not be empty"
        case .invalidCost:
            return "Cost factor must be an integer between \(MIN_COST) and \(MAX_COST)"
        case .costOutOfRange:
            return "Cost factor out of range (\(MIN_COST)-\(MAX_COST))"
        case .invalidSalt:
            return "Salt must be exactly 16 bytes"
        case .invalidHash:
            return "Not a valid bcrypt hash (expected $2a$/$2b$/$2y$CC$ + 53 base64 chars)"
        case .invalidBase64Character(let c):
            return "Invalid character in bcrypt base64 data: \(c)"
        case .base64TooShort:
            return "Bcrypt base64 data is too short"
        }
    }
}

// MARK: - Key derivation

/// 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.
func keyData(_ password: String) -> [UInt8] {
    let bytes = Array(password.utf8)
    let truncated = bytes.count > MAX_PASSWORD_BYTES ? Array(bytes.prefix(MAX_PASSWORD_BYTES)) : bytes
    if truncated.count >= MAX_PASSWORD_BYTES { return truncated }
    return truncated + [0]
}

// MARK: - Blowfish

/// Blowfish round function F(x) = ((S0[a] + S1[b]) ^ S2[c]) + S3[d].
func blowfishF(_ S: [UInt32], _ x: UInt32) -> UInt32 {
    let a = Int(x >> 24)
    let b = Int((x >> 16) & 0xff)
    let c = Int((x >> 8) & 0xff)
    let d = Int(x & 0xff)
    return ((S[a] &+ S[256 + b]) ^ S[512 + c]) &+ S[768 + d]
}

/// One Blowfish encryption of the (xl, xr) pair.
func encipher(_ P: [UInt32], _ S: [UInt32], _ xl: UInt32, _ xr: UInt32) -> (UInt32, UInt32) {
    var l = xl
    var r = xr
    for i in 0..<16 {
        l ^= P[i]
        r ^= blowfishF(S, l)
        swap(&l, &r)
    }
    swap(&l, &r)
    r ^= P[16]
    l ^= P[17]
    return (l, r)
}

/// Read 4 bytes at `offset` (wrapping around `data`) as a big-endian word.
/// Returns the word and the advanced offset — OpenBSD's stream2word.
func stream2word(_ data: [UInt8], _ offset: Int) -> (word: UInt32, next: Int) {
    var word: UInt32 = 0
    var j = offset
    for _ in 0..<4 {
        if j >= data.count { j = 0 }
        word = (word << 8) | UInt32(data[j])
        j += 1
    }
    return (word, 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. Unlike the salted variant, nothing is XORed into the mixing pair.
func expand0(_ P: inout [UInt32], _ S: inout [UInt32], data: [UInt8]) {
    var j = 0
    for i in 0..<18 {
        let (w, nj) = stream2word(data, j)
        P[i] ^= w
        j = nj
    }
    var l: UInt32 = 0
    var r: UInt32 = 0
    for i in stride(from: 0, to: 18, by: 2) {
        (l, r) = encipher(P, S, l, r)
        P[i] = l
        P[i + 1] = r
    }
    for box in 0..<4 {
        for k in stride(from: 0, to: 256, by: 2) {
            (l, r) = 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.
func expandState(_ P: inout [UInt32], _ S: inout [UInt32], salt: [UInt8], key: [UInt8]) {
    var j = 0
    for i in 0..<18 {
        let (w, nj) = stream2word(key, j)
        P[i] ^= w
        j = nj
    }
    var l: UInt32 = 0
    var r: UInt32 = 0
    j = 0
    for i in stride(from: 0, to: 18, by: 2) {
        let (w1, nj1) = stream2word(salt, j)
        l ^= w1
        let (w2, nj2) = stream2word(salt, nj1)
        r ^= w2
        j = nj2
        (l, r) = encipher(P, S, l, r)
        P[i] = l
        P[i + 1] = r
    }
    for box in 0..<4 {
        for k in stride(from: 0, to: 256, by: 2) {
            let (w1, nj1) = stream2word(salt, j)
            l ^= w1
            let (w2, nj2) = stream2word(salt, nj1)
            r ^= w2
            j = nj2
            (l, r) = encipher(P, S, l, r)
            S[box * 256 + k] = l
            S[box * 256 + k + 1] = r
        }
    }
}

// MARK: - bcrypt Base64

/// Encode bytes with bcrypt's Base64 variant (3 bytes -> 4 chars, partial
/// groups emit 2-3 chars, no padding).
func encodeB64(_ data: [UInt8]) -> String {
    let alphabet = Array(B64_CHARS)
    var out = ""
    var i = 0
    while i < data.count {
        let b0 = Int(data[i])
        let b1 = i + 1 < data.count ? Int(data[i + 1]) : -1
        let b2 = i + 2 < data.count ? Int(data[i + 2]) : -1
        out.append(alphabet[b0 >> 2])
        out.append(alphabet[((b0 & 0x03) << 4) | (b1 >= 0 ? b1 >> 4 : 0)])
        if b1 < 0 { break }
        out.append(alphabet[((b1 & 0x0f) << 2) | (b2 >= 0 ? b2 >> 6 : 0)])
        if b2 < 0 { break }
        out.append(alphabet[b2 & 0x3f])
        i += 3
    }
    return out
}

/// Decode bcrypt Base64 into exactly `count` bytes (throws on bad chars or
/// when the input carries fewer than `count` bytes worth of bits).
func decodeB64(_ input: String, count: Int) throws -> [UInt8] {
    var out = [UInt8](repeating: 0, count: count)
    let totalBits = count * 8
    var target = 0
    for ch in input {
        guard target < totalBits else { break }
        guard let ascii = ch.asciiValue, ascii < 128, B64_INDEX[Int(ascii)] >= 0 else {
            throw BcryptError.invalidBase64Character(ch)
        }
        let v = Int(B64_INDEX[Int(ascii)])
        var bit = 5
        while bit >= 0 && target < totalBits {
            if v & (1 << bit) != 0 {
                out[target >> 3] |= UInt8(1 << (7 - (target & 7)))
            }
            target += 1
            bit -= 1
        }
    }
    if target < totalBits { throw BcryptError.base64TooShort }
    return out
}

// MARK: - Public API

/// Validate a password, throwing on empty input. (The TS reference also
/// rejects non-strings; Swift's type system makes that check unnecessary.)
func assertPassword(_ password: String) throws {
    if password.isEmpty { throw BcryptError.emptyPassword }
}

/// Validate a cost factor, throwing a clear error outside 4-31.
func assertCost(_ cost: Int) throws {
    if cost < MIN_COST || cost > MAX_COST { throw BcryptError.invalidCost }
}

/// 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 the test suite.
func bcryptHashWithSalt(_ password: String, cost: Int, salt: [UInt8]) throws -> String {
    try assertCost(cost)
    if salt.count != 16 { throw BcryptError.invalidSalt }
    let key = keyData(password)

    var P = P_INIT
    var S = S_INIT
    expandState(&P, &S, salt: salt, key: key)
    let rounds = 1 << cost
    for _ in 0..<rounds {
        expand0(&P, &S, data: key)
        expand0(&P, &S, data: salt)
    }

    let magic = Array(MAGIC.utf8)
    var cdata = [UInt32](repeating: 0, count: 6)
    for i in 0..<6 {
        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 _ in 0..<64 {
        var j = 0
        while j < 6 {
            (cdata[j], cdata[j + 1]) = encipher(P, S, cdata[j], cdata[j + 1])
            j += 2
        }
    }
    var digest = [UInt8](repeating: 0, count: 23)
    for i in 0..<23 {
        digest[i] = UInt8(truncatingIfNeeded: cdata[i >> 2] >> UInt32(24 - 8 * (i & 3)))
    }
    return encodeB64(digest)
}

/// 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. Run on a background queue for high costs.
func bcryptHash(_ password: String, cost: Int = DEFAULT_COST) throws -> String {
    try assertPassword(password)
    // SystemRandomNumberGenerator is the system CSPRNG (SecRandom on Apple
    // platforms) — the Swift counterpart of crypto.getRandomValues.
    var csprng = SystemRandomNumberGenerator()
    let salt = (0..<16).map { _ in UInt8.random(in: .min ... .max, using: &csprng) }
    let digest = try bcryptHashWithSalt(password, cost: cost, salt: 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.
func bcryptVerify(_ password: String, _ hash: String) throws -> Bool {
    let info = try bcryptDecode(hash)
    let salt = try decodeB64(info.salt, count: 16)
    let digest = try bcryptHashWithSalt(password, cost: info.cost, salt: salt)
    let expected = String(hash.suffix(31))
    var diff: UInt8 = 0
    for (a, b) in zip(digest.utf8, expected.utf8) {
        diff |= a ^ b
    }
    return diff == 0
}

/// Parse a bcrypt hash string into its parts: version prefix, cost factor and
/// 22-character Base64 salt. Throws on malformed input. (The TS reference
/// validates with one regex; this port checks the same shape field by field.)
func bcryptDecode(_ hash: String) throws -> BcryptInfo {
    let chars = Array(hash.trimmingCharacters(in: .whitespacesAndNewlines))
    // Shape: $2<version>$<2-digit cost>$<22 salt chars><31 digest chars>
    guard chars.count == 60,
        chars[0] == "$", chars[1] == "2",
        chars[2] == "a" || chars[2] == "b" || chars[2] == "y",
        chars[3] == "$", chars[6] == "$",
        ("0"..."9").contains(chars[4]), ("0"..."9").contains(chars[5])
    else { throw BcryptError.invalidHash }

    let cost = Int(String(chars[4]))! * 10 + Int(String(chars[5]))!
    guard cost >= MIN_COST, cost <= MAX_COST else { throw BcryptError.costOutOfRange }

    let body = String(chars[7...])
    guard body.count == 53, body.allSatisfy({ B64_CHARS.contains($0) }) else {
        throw BcryptError.invalidHash
    }
    return BcryptInfo(version: String(chars[0...3]), cost: cost, salt: String(body.prefix(22)))
}

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 →