Cryptography
Password hashing, pepper, token generation and verifying webhooks with HMAC.
use crypto
crypto.config.set({provider: "argon2id", pepper: $secret_key})
$hash = crypto.crypt($password)
if crypto.verify($password, $hash) then
out("signed in")
end if| Call | Returns |
|---|---|
crypto.crypt($password[, {...}]) | the hash |
crypto.verify($password, $hash[, {...}]) | true / false |
crypto.needs_rehash($hash) | whether the settings changed |
crypto.token([$bytes]) | a random string (32 bytes by default) |
crypto.uuid() | a version 4 identifier |
crypto.hmac($data, $key) | HMAC-SHA256, hex |
crypto.sha256($data) | digest, hex |
crypto.compare($a, $b) | constant-time comparison |
crypto.md5($data) | digest — legacy compatibility only |
crypto.encrypt / crypto.decrypt | AES-256-CBC — legacy compatibility only |
| Setting | Default |
|---|---|
provider | "argon2id" ("bcrypt" is also available) |
pepper (or key) | none |
memory · iterations · parallelism | 65536 KiB · 3 · core count (≤4) |
cost (bcrypt) | 12 |
Pepper#
A pepper is a secret added to the password before hashing that does not live in the database. A salt differs per record and is stored next to the hash; a pepper is the same for the whole application and exists only in an environment variable.
The benefit is concrete: even if the database leaks, a dictionary attack does not work without the pepper. The hashes the attacker holds are hashes of passwords mixed with a value they do not have.
$env = new env_parser(path.pwd + "/.env")
crypto.config.set({provider: "argon2id", pepper: $env.require("PASSWORD_PEPPER")})Raising the cost#
A hash carries its own settings. When you raise the cost, existing hashes stay valid; you produce a new one the next time the user signs in:
if crypto.verify($password, $record.hash) then
if crypto.needs_rehash($record.hash) then
$new = crypto.crypt($password)
$db.execute("UPDATE users SET hash = $1 WHERE id = $2", $new, $record.id)
end if
end ifTokens#
$key = crypto.token(32) # random, 43 charactersSession ids, password reset links, API keys. Generated from a cryptographically secure source.
When comparing a token, use crypto.compare rather than ==:
if crypto.compare($given, $expected) then
out("valid")
end if== compares up to the first differing byte and returns; that means the time taken reveals how many characters of the token were right. compare spends the same time in every case.
Verifying webhooks#
Payment providers sign the body they send with a shared key:
$signature = crypto.hmac($req.body, $env.require("WEBHOOK_SECRET"))
if not crypto.compare($signature, $req.header("x-signature")) then
$res.status(403)
$res.send("invalid signature")
return
end ifThe signature is computed over the raw body. Re-encoding the decoded JSON and signing that will not match, because of key ordering or whitespace.
Legacy compatibility#
md5, encrypt and decrypt are in the list but are not for new code. MD5 is broken for passwords; AES-256-CBC is secure when used correctly but leaves IV management and authentication to you. They exist for migrating data from an older system.