-
Notifications
You must be signed in to change notification settings - Fork 1
Password
Ori Pekelman edited this page May 11, 2026
·
1 revision
PBKDF2-SHA256 password hashing with self-describing storage. The hash format encodes the iteration count + salt + derived bytes in a single string, so verifying doesn't need a separate column for "which params were used."
- PBKDF2-SHA256 with 200,000 iterations (sidekiq/devise/bcrypt default-equivalent at this rev).
- 16-byte random salt per password.
- 32-byte derived key.
- All bytes base64url-encoded.
Stored format (single column, ~85 chars):
pbkdf2-sha256$200000$<salt-b64url>$<key-b64url>
stored = Tep::Password.hash("hunter2")
ok = Tep::Password.verify("hunter2", stored) # true
nope = Tep::Password.verify("guess", stored) # falseBoth calls are constant-time relative to the password length.
The hash string is safe to store in a TEXT column:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE,
password_h TEXT NOT NULL
);db.prepare("INSERT INTO users (username, password_h) VALUES (?, ?)")
db.bind_str(1, params[:user])
db.bind_str(2, Tep::Password.hash(params[:password]))
db.step
db.finalizepost '/signup' do
if params[:password].length < 8
halt 422, "password too short"
end
hashed = Tep::Password.hash(params[:password])
db = Tep::SQLite.new
db.open(DB_PATH)
db.prepare("INSERT INTO users (username, password_h) VALUES (?, ?)")
db.bind_str(1, params[:user])
db.bind_str(2, hashed)
db.step
db.finalize
db.close
redirect "/"
endpost '/login' do
db = Tep::SQLite.new
db.open(DB_PATH)
stored = db.first_str("SELECT password_h FROM users WHERE username = ?", params[:user])
db.close
if stored.length == 0
halt 401, "no such user"
end
if !Tep::Password.verify(params[:password], stored)
halt 401, "wrong password"
end
session["user"] = params[:user]
redirect "/"
end-
200k iters is a CPU cost. A single hash call takes ~80 ms on
modern hardware. That's deliberate (an attacker has to spend the
same per guess) but it means a hot login endpoint will saturate
the worker — prefork (
--workers N) is the right answer. -
Don't truncate the stored hash. The verifier parses the full
string; truncating to fit a column with
VARCHAR(64)will break it. UseTEXT. - The iteration count is parsed from the hash. If you ever bump the default in tep itself, old hashes still verify correctly (they carry their own iter count). Old hashes don't auto-upgrade, though — re-hash on next login.
- The salt is per-password. Two users with the same password produce different hashes. Good — defeats rainbow tables — but also means you can't check "is this password reused" by string equality on the column.