March Docs

Crypto

Crypto module: general-purpose cryptographic primitives.

Exposes hashing, HMAC, password hashing/verification, random generation, and Base64 encoding as a public API. The underlying implementations are provided by the March runtime (Digestif SHA-256/512, PBKDF2, CSPRNG).

Password hashing uses PBKDF2-SHA256 with a 16-byte random salt and

  1. The stored

format is: "pbkdf2:sha256:600000:<salt_hex>:<hash_hex>" The iteration count is embedded in the stored hash, so existing hashes produced with a different count still verify correctly via verify_password/2. This is intentionally simple to verify without external libraries.

All hex output is lowercase.

Functions

fnbase64_decodebase64_decode(s)#

Decode a standard Base64 string to Bytes.

march> Crypto.base64_decode("SGVsbG8=") == Ok(Bytes.from_string("Hello"))
true
march> Result.is_err(Crypto.base64_decode("!!"))
true

Invalid input gives Err(message); natively Crypto.base64_decode("!!") is Err("base64_decode: invalid character").

fnbase64_encodebase64_encode(data)#

Base64-encode a String (standard alphabet with padding).

march> Crypto.base64_encode("Hello")
"SGVsbG8="

The argument must be a String; pass a Bytes value through Bytes.to_string first.

fnbase64_url_decodebase64_url_decode(s)#

Decode a URL-safe Base64 string (- and _, no padding) to Bytes.

march> Crypto.base64_url_decode("SGVsbG8") == Ok(Bytes.from_string("Hello"))
true
march> Result.is_err(Crypto.base64_url_decode("!!"))
true
fnbase64_url_encodebase64_url_encode(data)#

Base64-encode using the URL-safe alphabet (- and _ instead of + and /), with no padding characters.

march> Crypto.base64_url_encode("Hello?>")
"SGVsbG8_Pg"
fnhash_passwordhash_password(plain)#

Hash a plaintext password using PBKDF2-SHA256 with a random salt.

let h = Crypto.hash_password("my-secret")
-- "pbkdf2:sha256:600000:<salt_hex>:<hash_hex>"

The result is safe to store in a database. Use verify_password/2 to check a candidate password against the stored hash. The salt is random, so two calls with the same password return different strings (no executable example for that reason); each call also costs the full 600 000 iterations, which is deliberately slow.

Uses 600 000 PBKDF2 iterations (OWASP 2023 recommendation for SHA-256) with a 16-byte random salt and 32-byte output. Existing hashes stored with a different iteration count continue to verify correctly because the count is embedded in the hash string.

fnhmachmac(algo, key, data)#

Compute an HMAC using the given algorithm.

march> Crypto.hmac(:sha256, "secret", "message")
"8b5f48702995c1598c573db1e21866a9b825d4a794d169d7060a03605796360b"

Supported algorithms: :sha256. Returns the HMAC as a lowercase hex string. The key and data must each be a String.

fnrandom_bytesrandom_bytes(n : {Int | _ >= 0})#

Generate n cryptographically secure random bytes. The bytes come from the operating system's CSPRNG (/dev/urandom on Unix), so the output is suitable for cryptographic keys, IVs, nonces, salts, and session tokens.

Panics if n is negative or if /dev/urandom cannot be opened.

Crypto.random_bytes(16)  -- Bytes value with 16 random bytes
fnrandom_hexrandom_hex(n : {Int | _ >= 0})#

Generate n random bytes and return them as a lowercase hex string (2n chars), from the same CSPRNG as random_bytes. random_hex(0) is "".

Panics if n is negative or if /dev/urandom cannot be opened.

Crypto.random_hex(16)  -- 32-character lowercase hex string
fnsecure_comparesecure_compare(a, b)#

Compare two strings for equality in time proportional to max(|a|, |b|), never short-circuiting on the first byte difference or on length mismatch.

Prevents timing attacks that would otherwise reveal (a) the length of the expected value or (b) the position of the first differing byte when validating a secret such as a token, HMAC digest, or session cookie.

Returns true iff a and b have the same bytes.

Note: the interpreter does not guarantee constant time at the machine level; this function provides algorithmic constant-time behaviour that translates to machine-level constant time under the native compiler.

Examples

march> Crypto.secure_compare("abc", "abc")
true
march> Crypto.secure_compare("abc", "abd")
false
march> Crypto.secure_compare("abc", "abcd")
false
march> Crypto.secure_compare("", "")
true
fnsha256sha256(data)#

Compute the SHA-256 hash of a String, returning a 64-char lowercase hex string.

march> Crypto.sha256("hello")
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

The argument must be a String; pass a Bytes value through Bytes.to_string first.

fnsha512sha512(data : String) : String#

Compute the SHA-512 hash of a String, returning a 128-char lowercase hex string.

march> Crypto.sha512("hello")
"9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
fnverify_passwordverify_password(plain, stored)#

Verify a plaintext password against a stored hash produced by hash_password/1.

let h = Crypto.hash_password("correct-horse-battery-staple")
Crypto.verify_password("correct-horse-battery-staple", h)  -- true
Crypto.verify_password("wrong-password", h)                -- false

The iteration count is read from the stored hash, so a hash made with a lower count (here 1000, salt "0123456789abcdef") verifies quickly:

march> let h = "pbkdf2:sha256:1000:30313233343536373839616263646566:a63e13df90f6bf8b58982d6c4c9d72e6d70c00339db67406bee1e6c980d08768"
march> Crypto.verify_password("hunter2", h)
true
march> let h = "pbkdf2:sha256:1000:30313233343536373839616263646566:a63e13df90f6bf8b58982d6c4c9d72e6d70c00339db67406bee1e6c980d08768"
march> Crypto.verify_password("hunter3", h)
false
march> Crypto.verify_password("hunter2", "md5:abc")  -- unrecognised format
false

Returns false if the hash format is unrecognised (wrong prefix, wrong number of fields, non-integer iteration count, or a salt that is not even-length hex). The digest is compared as lowercase hex, so a stored hash whose digest was uppercased does not verify. Panics if the stored iteration count is zero or negative (the runtime PBKDF2 call fails).