Expand description
Password hashing (PBKDF2-HMAC-SHA256).
Like Rails’ has_secure_password, without bcrypt: Workers have no CPU
budget for bcrypt or argon2 in WebAssembly (10 ms per request on the free
plan), but WebCrypto (crypto.subtle.deriveBits) runs PBKDF2 natively,
outside the WebAssembly module. Workers cap it at 100,000 iterations;
Ocre uses the cap (ITERATIONS). Native builds (tests, tools) compute
the same function in pure Rust.
Digests are self-describing, like Django’s:
pbkdf2_sha256$100000$<salt, base64>$<hash, base64> (16-byte random salt,
32-byte hash, standard base64 without padding). The iteration count is
stored, so it can grow later without invalidating existing passwords
(iterations reads it back).
Cost: one hash measured at 5.5 ms of CPU in wrangler dev (see the
cost model), about half of the free plan’s 10 ms per request, so
only sign-up, login and password changes should hash. hash and
verify each run PBKDF2 once. Passwords are never logged: errors name
the operation only.
use axum::{Form, extract::State};
use ocre::{Ctx, Error, Result};
// Sign up: store the digest, never the password.
async fn sign_up(Form(form): Form<Login>) -> Result<String> {
let password_digest = ocre::password::hash(&form.password).await?;
Ok(password_digest) // INSERT INTO users (email, password_digest) ...
}
// Log in: compare in constant time.
async fn log_in(State(ctx): State<Ctx>, Form(form): Form<Login>) -> Result<&'static str> {
let user = find_by_email(&ctx, &form.email).await?.ok_or(Error::Unauthorized)?;
if ocre::password::verify(&form.password, &user.password_digest).await? {
Ok("signed in")
} else {
Err(Error::Unauthorized)
}
}Constants§
- ITERATIONS
- Iterations for new digests: 100,000, the most Workers’ WebCrypto accepts.
Functions§
- hash
- Hashes
passwordwith a new random 16-byte salt into a self-describing digest. - iterations
- The iteration count stored in
digest, orNonewhen it is not in Ocre’s format. - verify
- Whether
passwordmatchesdigest(made byhash), compared in constant time.