With the bcrypt gem in Ruby, how do BCrypt::Password.create and BCrypt::Password#== store and check a password, and what does cost control?
answer
- "$2a$12$" prefix: version and cost
- salt stored inside the hash string
- wrap with BCrypt::Password.new
- stored == candidate, not the reverse
- cost is log2; default 12, max 31
basics
~20 sBCrypt::Password.create(password) returns a 60-character string holding version, cost, a random salt and the hash. To check, wrap the stored string with BCrypt::Password.new and call == with the candidate, which rehashes it with the stored salt and cost.
solid answer
~40 s`BCrypt::Password.create(secret, cost: 12)` generates a random salt, hashes, and returns a `BCrypt::Password`, a `String` subclass like `"$2a$12$"` plus 53 characters of salt and checksum, so you store one column. To verify, `BCrypt::Password.new(stored)` parses it (raising `BCrypt::Errors::InvalidHash` for malformed input) and `password == candidate` rehashes the candidate with the stored salt and cost and compares in constant time; `is_password?` is an alias. The direction matters: `candidate == password` calls `String#==` and returns `false`. Cost is a base-2 logarithm: each step doubles the work. The default is `BCrypt::Engine::DEFAULT_COST` (12) unless `BCrypt::Engine.cost=` changes it; positive values below 4 are raised to 4, and above 31 `create` raises `ArgumentError`. Secrets longer than 72 bytes are truncated.
code
ruby · 10 linesrequire "bcrypt"
hash = BCrypt::Password.create("correct horse", cost: 12)
stored = hash.to_s # "$2a$12$..." (60 chars)
password = BCrypt::Password.new(stored)
password == "correct horse" # => true
password.is_password?("wrong") # => false
"correct horse" == password # => false: String#==, not bcrypt
password.cost # => 12go deeper
Recall create to hash, BCrypt::Password.new plus == to check, and that the salt and cost live inside the stored string.
Explain why == must be called on the BCrypt::Password object, what cost means numerically, and the default, minimum and maximum values.
Plan cost upgrades on login, measure hashing time under concurrent logins, and handle legacy or malformed hashes that raise InvalidHash.
Set the cost policy as a latency budget per login, revisited as hardware changes, and own the migration path for dormant accounts.
## What the gem gives you The **bcrypt** gem wraps the bcrypt password-hashing function in two classes. `BCrypt::Engine` holds the low-level functions and settings; `BCrypt::Password` is what application code uses. `BCrypt::Password` is a subclass of `String`, so a hash can be stored in a normal text column and passed around like any string. A stored hash is self-describing: | Part | Example | Meaning | |---|---|---| | version | `$2a$` | bcrypt variant the gem produces | | cost | `12$` | log2 of the work factor | | salt | 22 characters | random, from `OpenSSL::Random` | | checksum | 31 characters | the hash itself | The whole string is 60 characters. `Password#version`, `#cost`, `#salt` and `#checksum` read those parts back, where `salt` includes the version and cost prefix. ## Creating and checking 1. **Create:** `BCrypt::Password.create(secret, cost: 12)` generates a salt with `BCrypt::Engine.generate_salt(cost)`, hashes with `BCrypt::Engine.hash_secret`, and wraps the result. Store `password.to_s`. 2. **Load:** `BCrypt::Password.new(stored)` validates the format and raises `BCrypt::Errors::InvalidHash` if it is not a bcrypt hash, for example a legacy digest left in the column. 3. **Check:** `password == candidate` hashes the candidate with the stored salt and cost, then compares the two strings byte by byte in constant time. `is_password?` is an alias that reads better and cannot be reversed by accident. The **direction trap**: `==` is only overridden on `BCrypt::Password`. `candidate == password` calls `String#==`, compares the plain candidate with the stored hash text, and returns `false` for the correct password. Comparing a stored string with a stored string is also `false`, because `==` treats its argument as a password to hash. ## What cost controls - **Cost is logarithmic.** Each increment doubles the time to hash one password, for you and for an attacker who stole the table. - **Defaults:** `BCrypt::Engine::DEFAULT_COST` is 12. `BCrypt::Engine.cost = n` changes the default for the process; `create(..., cost: n)` overrides it per call. - **Bounds:** `BCrypt::Engine::MIN_COST` is 4, and `generate_salt` silently raises smaller values to 4. `MAX_COST` is 31, and `create` raises `ArgumentError` above it. A cost of zero or a non-number raises `BCrypt::Errors::InvalidCost`. - **Calibration:** `BCrypt::Engine.calibrate(ms)` returns the highest cost that hashes within roughly that many milliseconds on the current machine. - **Test suites** often set `BCrypt::Engine.cost = BCrypt::Engine::MIN_COST` so fixtures hash quickly. ## Raising cost for existing users Because every hash records its own cost, you can raise the cost without a reset. On each successful login, check `password.cost` against the current target and, if it is lower, call `BCrypt::Password.create(candidate, cost: target)` and save the new string. Accounts that never log in keep the old cost until you force a reset. ## Edge cases worth naming - **72-byte limit.** `hash_secret` truncates secrets longer than 72 bytes, so two long passphrases sharing their first 72 bytes produce the same hash. Multi-byte characters reach the limit sooner. - **Concurrency.** The C extension releases the GVL while hashing, so other Ruby threads keep running; high cost still ties up a CPU core per concurrent login. - **Choosing bcrypt at all**, versus other password-hashing functions, is a separate design decision; this is about using the gem correctly once chosen. ## Mistakes to catch in review - **Hashing again and comparing.** `BCrypt::Password.create(candidate) == stored` generates a new random salt, so it never matches; always load the stored hash and call `==` or `is_password?` on it. - **Missing hashes.** Accounts created through single sign-on may have no password hash; `BCrypt::Password.new(nil)` raises `BCrypt::Errors::InvalidHash`, so branch before loading rather than rescuing broadly. - **A test-only cost leaking into production.** `BCrypt::Engine.cost = BCrypt::Engine::MIN_COST` is process-wide; set it only in the test helper. - **Comparing stored strings.** `password == other_stored_hash` treats the second hash as a password to hash and returns `false`; to compare two stored values, compare their `to_s`. - **Logging the candidate.** The plaintext exists only during the request; keep it out of logs, error reports and exception messages.
- Why does "secret" == BCrypt::Password.new(stored) return false for the right password?Ruby dispatches `==` on the left operand. With a plain `String` on the left, `String#==` compares the candidate text with the 60-character hash text, which never match. Only `BCrypt::Password#==` rehashes the candidate, so call it on the password object or use `is_password?`.
- How do you move existing users from cost 10 to cost 13 without a password reset?Every hash records its cost, so on each successful login compare `password.cost` with the target and, if lower, store `BCrypt::Password.create(candidate, cost: 13)`. Active users upgrade silently; dormant accounts stay at the old cost until they log in or you force a reset.
- What does the bcrypt gem do with a 100-byte passphrase?`BCrypt::Engine.hash_secret` truncates secrets to 72 bytes before hashing, so bytes beyond that do not affect the result. Two passphrases that share their first 72 bytes produce the same hash.
saying these in an interview costs you the question
- You must store the salt in its own column next to the bcrypt hash.
- Hash the candidate with create again and compare the two strings with ==.
- candidate == stored_password works the same as stored_password == candidate.
- Cost 13 is only about 8 percent slower than cost 12.
- BCrypt::Password.create rejects passwords longer than 72 bytes.