Skip to content

Hashing

Hashing turns a value into a fixed-size string that cannot be reversed. That is what passwords need: unlike encryption, there is no key that turns the stored value back into the password — at login you hash the attempt and compare.

For passwords the hash must also be slow. General-purpose hashes (SHA-256, MD5) are built for speed, which lets an attacker try billions of guesses per second against a leaked database. bcrypt is designed the other way around: each hash costs a configurable amount of work, and every password gets its own random salt, so equal passwords produce different hashes.

Ludi’s sibling project bcryptlua provides bcrypt as a native module (Rust core), for Ludi or any other Lua application:

Terminal window
luarocks install bcryptlua

bcrypt.hash generates a random salt (from the OS CSPRNG) and returns a standard 60-character $2b$ hash — the only thing you store:

local ludi = require("ludi")
local bcrypt = require("bcryptlua")
local app = ludi.new()
app:post("/users", function(req, res)
local body = req:json()
local hash = bcrypt.hash(body.password)
-- store `hash` next to the user; never store the password
res:status(201):json({ ok = true })
end)

The second argument is the cost (default 12): the log2 of the number of rounds, so each +1 doubles the work. Pick the highest value your login latency budget tolerates — around 200 ms per hash is a common target:

bcrypt.hash(body.password, 14) -- slower, stronger

bcrypt.verify re-hashes the attempt with the salt and cost stored inside the hash and compares in constant time:

app:post("/login", function(req, res)
local body = req:json()
local user = find_user(body.email)
if user and bcrypt.verify(body.password, user.password_hash) then
res:json({ token = issue_token(user) })
else
res:status(401):json({ error = "Invalid credentials" })
end
end)

A wrong password and a malformed hash both return false — on purpose, so a login route cannot leak which accounts exist.

Hardware gets faster; the cost that was right years ago is weak today. bcrypt.cost reads the cost stored in a hash, which enables the rehash-on-login pattern — upgrading each user the moment they authenticate:

if bcrypt.verify(body.password, user.password_hash) then
if bcrypt.cost(user.password_hash) < 12 then
update_password_hash(user, bcrypt.hash(body.password))
end
-- logged in
end