March Docs

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

fnbase64_decodebase64_decode(s : String) : Result(Bytes, String)#

Decode a standard Base64 string to bytes.

fnbase64_encodebase64_encode(data : String) : String#

Base64-encode data using the standard alphabet (padded with =).

fnbase64_url_decodebase64_url_decode(s : String) : Result(Bytes, String)#

Decode a URL-safe Base64 string to bytes.

fnbase64_url_encodebase64_url_encode(data : String) : String#

Base64-encode data using the URL-safe alphabet (no padding, -/_).

fnbytes_to_stringbytes_to_string(b : Bytes) : String#

Convert a Bytes value to a String (each byte treated as a raw octet).

fnderive_keyderive_key(secret : String, context : String) : String#

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")
fngenerate_tokengenerate_token(n : Int) : String#

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"
fnhash_passwordhash_password(plain : String) : String#

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>"
fnhkdf_sha256hkdf_sha256(ikm : String, salt : String, info : String, len : Int) : Bytes#

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)
fnhmac_sha256hmac_sha256(key : String, data : String) : String#

Compute HMAC-SHA256 of data under key, returning a lowercase hex string.

    Crypto.hmac_sha256("secret", "message")
fnrandom_bytesrandom_bytes(n : Int) : Bytes#

Generate n cryptographically secure random bytes from the OS CSPRNG.

    Crypto.random_bytes(16)  -- Bytes with 16 random bytes
fnrandom_hexrandom_hex(n : Int) : String#

Generate n random bytes and return them as a lowercase hex string (2n chars).

    Crypto.random_hex(16)  -- 32-char lowercase hex string
fnsecure_comparesecure_compare(a : String, b : String) : Bool#

Compare 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)
fnsha256sha256(data : String) : String#

Compute the SHA-256 hash of data, returning a lowercase hex string.

    Crypto.sha256("hello")
    -- "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
fnverify_passwordverify_password(plain : String, stored : String) : Bool#

Verify plain against a stored hash produced by hash_password/1.

    Crypto.verify_password("correct-horse-battery-staple", stored_hash)