Skip to content

Hashing, encoding & randomness

Hale provides cryptographic digests, HMAC authentication, digital signatures (ECDSA P-256 and Ed25519), hex and Base64 codecs, and random number generation across std::crypto, std::hex, std::text::base64, std::os, and std::rand.

fn main() {
let data = std::bytes::from_string("hello");
let key = std::bytes::from_string("secret");
let digest = std::crypto::sha256(data); // 32 Bytes
let tag = std::crypto::hmac_sha256(key, data); // 32 Bytes
println(std::hex::encode(digest)); // 64 lowercase hex digits
println(std::text::base64::encode(tag)); // padded Base64 string
}

Function / Type Signature Description
std::crypto::sha256 fn(Bytes) -> Bytes 32-byte SHA-256 digest (built-in, no OpenSSL dependency)
std::crypto::Sha256 Sha256 { }: update(Bytes), finish() -> Bytes SHA-256 fed in pieces: the same 32 bytes as sha256 of the pieces joined, holding a fixed 112-byte state
std::crypto::sha256_file fn(String) -> Bytes fallible(IoError) 32-byte SHA-256 of a file, read 1 MiB at a time
std::crypto::sha512 fn(Bytes) -> Bytes 64-byte SHA-512 digest (built-in, no OpenSSL dependency)
std::crypto::sha1 fn(Bytes) -> Bytes 20-byte SHA-1 digest for legacy protocol interop (built-in)
std::crypto::hmac_sha256 fn(Bytes, Bytes) -> Bytes 32-byte HMAC-SHA256 authentication tag ((key, data))
std::crypto::hmac_sha512 fn(Bytes, Bytes) -> Bytes 64-byte HMAC-SHA512 authentication tag ((key, data))
std::crypto::crc32 fn(Bytes) -> Int IEEE 802.3 / zlib CRC-32 checksum
std::crypto::ecdsa_p256_sign fn(Bytes, Bytes) -> Bytes fallible(CryptoError) Signs (pem_key, message) using ECDSA P-256 + SHA-256 (ES256, 64-byte r || s, OpenSSL)
std::crypto::ecdsa_p256_verify fn(Bytes, Bytes, Bytes) -> Bool Verifies (pem_pubkey, message, sig) for a 64-byte ES256 signature
std::crypto::ed25519_keypair fn(Bytes) -> (Bytes, Bytes) fallible(CryptoError) Derives (public: 32B, secret: 64B) from a 32-byte seed (Monocypher, wasm32-compatible)
std::crypto::ed25519_sign fn(Bytes, Bytes) -> Bytes fallible(CryptoError) Deterministically signs (secret_or_seed, message) -> 64-byte Ed25519 signature
std::crypto::ed25519_verify fn(Bytes, Bytes, Bytes) -> Bool Verifies (public, message, sig) for a 64-byte Ed25519 signature
std::hex::encode fn(Bytes) -> String Encodes bytes as lowercase hexadecimal (2 chars per byte)
std::hex::decode fn(String) -> Bytes fallible(ParseError) Strictly decodes hex string (case-insensitive, no 0x or whitespace)
std::hex::digit fn(Int) -> Int Converts an ASCII hex byte to 0..15, or -1 if not a hex digit
std::text::base64::encode fn(Bytes) -> String Standard RFC 4648 Base64 encoding (+/ alphabet, = padding)
std::text::base64::decode fn(String) -> Bytes Decodes standard padded Base64 (skips whitespace; returns b"" on invalid input)
std::text::base64::url_encode fn(Bytes) -> String RFC 4648 §5 URL-safe Base64 encoding (-_ alphabet, unpadded)
std::os::getrandom fn(Int) -> Bytes fallible(IoError) Cryptographically secure random bytes (CSPRNG) from the OS kernel
std::rand::next_int fn(Int) -> Int Fast, non-cryptographic random integer in [0, n)
std::rand::Rng Rng { seed: Int } Deterministic, isolated SplitMix64 PRNG (range, next_int, next_float)

Digest and MAC functions take Bytes and return Bytes (or Int for crc32). All hash, HMAC, and CRC functions are self-contained in the runtime with no external library dependencies and cannot fail:

let data = std::bytes::from_string("hello");
let key = std::bytes::from_string("secret");
let digest = std::crypto::sha256(data); // 32 bytes
let tag = std::crypto::hmac_sha256(key, data); // 32 bytes
let sum = std::crypto::crc32(data); // Int (IEEE 802.3 / zlib)
let digest512 = std::crypto::sha512(data); // 64 bytes
let tag512 = std::crypto::hmac_sha512(key, data); // 64 bytes

Use sha256 / hmac_sha256 (or sha512 / hmac_sha512 for APIs requiring 512-bit tags) for new code. std::crypto::sha1 (20 bytes) is provided for legacy protocol interoperability.

sha256(b) needs all of b in memory. To verify a download against a pinned digest as it arrives, or a file larger than memory, feed a std::crypto::Sha256 the pieces instead, or hash the file by its path:

let h = std::crypto::Sha256 { };
h.update(b"hello, ");
h.update(b"world");
let digest = h.finish(); // == std::crypto::sha256(b"hello, world")
let on_disk = std::crypto::sha256_file("/models/weights.bin") or raise;
  • update(piece) absorbs any number of bytes, an empty piece included; the digest does not depend on how the input was split.
  • finish() answers the 32-byte digest of everything absorbed since the hasher was made or last finished, and starts it over, so one hasher can serve one input after another.
  • The hasher holds a fixed 112-byte state whatever it has absorbed; sha256_file holds one 1 MiB read buffer. Hashing a 10 GiB file either way peaked at about 1 to 2 MB above a program that hashes nothing, as fast as sha256sum.
  • sha256_file fails with IoError (not_found, permission_denied, …, the path in err.path) when the file cannot be opened or read.
  • In a long-lived locus, each piece can be made in the loop body of run() itself: a loop iteration frees what it made when it ends, as long as it keeps nothing in a binding declared before the loop (Memory & Lifetime).

On x86-64 CPUs with the SHA extensions, the compression runs on them. On aarch64 Linux and macOS (a Grace CPU, Apple Silicon), it runs on the ARMv8 SHA-256 instructions when the OS reports them. Everywhere else, wasm32 included, it runs the portable C rounds (about 350 MB/s on the x86-64 machine measured above). All four entry points (sha256, hmac_sha256, Sha256, sha256_file) share the one implementation.


let text = std::hex::encode(digest); // Bytes -> String (lowercase)
let back = std::hex::decode(text) or raise; // String -> Bytes (either case)
let value = std::hex::digit(std::str::byte_at_unchecked("f", 0)); // 15
  • std::hex::decode is strict: input must have an even number of hex digits with no whitespace and no 0x prefix. Odd-length strings or non-hex bytes fail with ParseError (kind == "hex", carrying the offending input).
  • std::hex::digit returns the integer value (0..15) of a single ASCII byte, or -1 if the byte is not a valid hexadecimal digit.
let enc = std::text::base64::encode(data); // Bytes -> String (standard, padded)
let dec = std::text::base64::decode(enc); // String -> Bytes
let url = std::text::base64::url_encode(data); // Bytes -> String (URL-safe, unpadded)
  • encode and decode use the standard RFC 4648 alphabet (+/ with = padding). decode ignores ASCII whitespace and returns empty Bytes (b"") for any non-standard input (including url_encode output; there is no built-in URL-safe decoder).
  • url_encode implements RFC 4648 §5 (-_ alphabet with = padding stripped), matching the format required by JWTs, OAuth, and webhook signatures.

ECDSA P-256 / ES256 (std::crypto::ecdsa_p256_*)

Section titled “ECDSA P-256 / ES256 (std::crypto::ecdsa_p256_*)”

For JWT (ES256), COSE, and API request signing, std::crypto provides OpenSSL-backed ECDSA over NIST P-256 with SHA-256:

// key: PEM EC private key (SEC1 or PKCS#8); message: Bytes
let sig = std::crypto::ecdsa_p256_sign(key, message) or raise;
// pubkey: PEM SPKI; sig: raw r||s, 64 bytes (JWS/COSE format)
let ok = std::crypto::ecdsa_p256_verify(pubkey, message, sig);

ecdsa_p256_sign is fallible(CryptoError) and fails with a structured CryptoError { kind, detail } if the PEM key cannot be parsed or uses a curve other than P-256.

Ed25519 (RFC 8032) is used by NATS NKeys, SSH keys, and modern JWT/webhook schemes. The implementation uses Monocypher compiled directly into the runtime (no system library dependency, and supported on --target wasm32):

let seed = std::os::getrandom(32) or raise; // 32-byte seed
let (public, secret) = std::crypto::ed25519_keypair(seed) or raise;
let sig = std::crypto::ed25519_sign(secret, message) or raise; // 64-byte signature
let ok = std::crypto::ed25519_verify(public, message, sig); // Bool
  • Key formats: public is 32 bytes. secret is 64 bytes (seed || public), matching the key layout used by libsodium and Go’s crypto/ed25519.
  • Signing: ed25519_sign accepts either the 64-byte secret or the 32-byte seed directly, and is deterministic (the same key and message always produce the same 64-byte signature). Passing any other length—or a 64-byte key whose second half does not match the seed’s derived public key—raises CryptoError.
  • Verification: ed25519_verify returns false if the signature, message, public key, or byte lengths are invalid.

Cryptographic Randomness (std::os::getrandom)

Section titled “Cryptographic Randomness (std::os::getrandom)”

For security-sensitive values (cryptographic keys, nonces, salts, session IDs, and tokens), always use the operating system CSPRNG via std::os::getrandom:

let nonce = std::os::getrandom(16) or raise; // 16 cryptographically random Bytes

For jitter, sampling, shuffling, simulations, and property testing, std::rand provides a fast SplitMix64 pseudo-random generator:

// Global non-cryptographic PRNG
let roll = std::rand::next_int(6) + 1; // integer in [1, 6]
// Isolated, reproducible PRNG instance
let rng = std::rand::Rng { seed: 42 };
let die = rng.range(1, 7); // Int in [1, 7)
let pick = rng.next_int(10); // Int in [0, 10)
let prob = rng.next_float(); // Float in [0.0, 1.0)

An Rng initialized with a given seed produces the exact same sequence across all runs and target architectures without sharing state with the global generator or other Rng instances.

Next: CLI & config.