Skip to content
LexerLangv0.1.0

Cryptography

Password hashing, pepper, token generation and verifying webhooks with HMAC.

LexerLang
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
CallReturns
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.decryptAES-256-CBC — legacy compatibility only
SettingDefault
provider"argon2id" ("bcrypt" is also available)
pepper (or key)none
memory · iterations · parallelism65536 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.

LexerLang
$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:

LexerLang
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 if

Tokens#

LexerLang
$key = crypto.token(32)          # random, 43 characters

Session ids, password reset links, API keys. Generated from a cryptographically secure source.

When comparing a token, use crypto.compare rather than ==:

LexerLang
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:

LexerLang
$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 if

The 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.

Next#

Middleware.