Crypto
lib/crypto.march — Cryptographic primitives for Bastion.
Calls stdlib builtins (stdlib_random_bytes, stdlib_sha256, etc.) directly rather than aliasing the stdlib Crypto module. This avoids the fragile alias Crypto as StdCrypto dance that caused a Heisenbug at runtime.
Public API summary:
Random
random_bytes(n) → Bytes (CSPRNG, suitable for keys/nonces)
random_hex(n) → String (2n-char lowercase hex)
generate_token(n) → String (URL-safe base64, no padding)
Hashing
sha256(data) → String (lowercase hex, data: String | Bytes)
HMAC
hmac_sha256(key, data) → String (lowercase hex, key/data: String | Bytes)
Key derivation
hkdf_sha256(ikm, salt, info, len) → Bytes (HKDF-SHA256, RFC 5869)
derive_key(secret, context) → String (32-byte HKDF-SHA256 derivation, hex)
Password hashing (PBKDF2-SHA256, 600 000 iterations, 16-byte random salt)
hash_password(plain) → String ("pbkdf2:sha256:…" storable format)
verify_password(plain, hash) → Bool
Comparison
secure_compare(a, b) → Bool (constant-time equality)
Base64
base64_encode(data) → String (standard alphabet + padding)
base64_decode(s) → Result(Bytes, String)
base64_url_encode(data) → String (URL-safe, no padding)
base64_url_decode(s) → Result(Bytes, String)
Utilities
bytes_to_string(b) → String (bytes → String)Functions
Decode a standard Base64 string to bytes.
Base64-encode data using the standard alphabet (padded with =).
Decode a URL-safe Base64 string to bytes.
Base64-encode data using the URL-safe alphabet (no padding, -/_).
Convert a Bytes value to a String (each byte treated as a raw octet).
Derive a 32-byte key from secret and a context label, returned as a 64-char lowercase hex string.
Uses HKDF-SHA256 (extract-then-expand, RFC 5869) with the versioned salt
"bastion.hkdf.v1" and `context` as the expand info, so keys derived for
different contexts are cryptographically independent — recovering one
derived key reveals nothing about the secret or any sibling key.
let k = Crypto.derive_key(secret_key_base, "session_signing")Generate a URL-safe, base64-encoded random token of n bytes entropy. No padding characters; uses -/_ instead of +//.
Crypto.generate_token(32) -- e.g. "dGhpcyBpcyBhIHRlc3Q"Hash a plaintext password using PBKDF2-SHA256 with a random 16-byte salt and 600 000 iterations (OWASP 2023 recommendation).
let h = Crypto.hash_password("my-secret-pass")
-- "pbkdf2:sha256:600000:<salt_hex>:<hash_hex>"HKDF-SHA256 (RFC 5869): derive len bytes of key material from input keying material ikm, a salt, and a context info string.
Thin re-export of `Hkdf.hkdf_sha256` (the implementation lives in its own
module because Crypto's hex-string `hmac_sha256` shadows the Bytes-domain
builtin HKDF needs). `len` must be at most 8160 (255 blocks x 32 bytes).
let okm = Crypto.hkdf_sha256(secret, "my-salt", "encryption", 32)Compute HMAC-SHA256 of data under key, returning a lowercase hex string.
Crypto.hmac_sha256("secret", "message")Generate n cryptographically secure random bytes from the OS CSPRNG.
Crypto.random_bytes(16) -- Bytes with 16 random bytesGenerate n random bytes and return them as a lowercase hex string (2n chars).
Crypto.random_hex(16) -- 32-char lowercase hex stringCompare two strings for equality in constant time (proportional to the longer string length). Prevents timing attacks on secret comparisons.
Crypto.secure_compare(request_token, stored_token)Compute the SHA-256 hash of data, returning a lowercase hex string.
Crypto.sha256("hello")
-- "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"Verify plain against a stored hash produced by hash_password/1.
Crypto.verify_password("correct-horse-battery-staple", stored_hash)