Hashing & verifying
The API is two functions and a config. hash turns a password into a storable string; verify checks a candidate password against one. Both return a Result — see Error handling for the failure side.
Hashing a password
hash takes the password and a Config, and returns Result String BcryptError:
import Bcrypt (hasher, hash)
case hash "hunter2" hasher {
Ok stored -> save_to_db stored # "$2b$12$Nn...."
Err e -> log_error e
}The returned string is a $2b$ modular-crypt hash: it packs the algorithm version, the cost, the salt, and the digest into one string. The salt is generated randomly per call, so hashing the same password twice gives two different strings — both verify correctly. You store exactly this one string; there is no separate salt column to manage.
The cost (work factor)
hasher is a Config with a default cost of 12. The cost is the base-2 logarithm of the number of hashing rounds, so each +1 doubles the work. Override it with with_cost:
import Bcrypt (hasher, with_cost, hash)
let strong = hasher |> with_cost 14
hash "hunter2" strongChoosing a cost is a tradeoff between security and latency:
- Higher is slower to attack — and slower for you. Cost 12 is a common baseline; 10 is on the low end for stored user passwords; 14+ starts to add noticeable per-login latency.
- Tune to your hardware. Pick the highest cost that keeps hashing comfortably under your latency budget (a common target is ~250ms per hash). Re-evaluate as hardware gets faster.
- The valid range is 4 to 31. Anything outside it returns
Err (InvalidCost n)rather than hashing — see Error handling. Cost 4 is only appropriate for tests, where you want speed, not strength.
Because the cost is stored inside the hash, you can raise it over time without a migration: verify still works against old hashes at their original cost, and you can transparently re-hash a password at the new cost the next time a user logs in successfully.
Verifying a password
verify takes the candidate password and a stored hash, and returns Result Bool BcryptError:
import Bcrypt (verify)
case verify attempt stored {
Ok True -> grant_access ()
Ok False -> reject ()
Err e -> log_error e # the stored hash was malformed
}Note the three outcomes, and keep them distinct:
Ok True— the password matches.Ok False— the password does not match. This is a normal, expected result, not an error.Err _— the hash you passed in could not be parsed (corrupted data, wrong column, truncated string). This says nothing about the password.
Do not collapse Ok False and Err into "login failed" without thought: a burst of Err means your stored hashes are broken, which is an operational problem you want to see, whereas Ok False is just someone typing the wrong password.
verify reads the cost and salt from the hash itself, so you never pass a Config to it — it automatically uses whatever parameters the hash was created with. It also accepts the older $2a$ and $2y$ prefixes, not just the $2b$ that hash produces.
A note on password length
bcrypt only considers the first 72 bytes of a password; anything beyond that is ignored by the algorithm. This is a property of bcrypt itself, not this library. If you need to accept arbitrarily long passphrases, the common practice is to pre-hash with SHA-256 before bcrypt — but for typical passwords the 72-byte limit is not something you will hit.