Security
Crypt
import crypt;
The crypt module provides hashing, message authentication, password hashing,
JWT generation and verification, and asymmetric key and certificate helpers.
All functions are pure Go - no external tools or system libraries are required.
Hashes
All hash functions accept a string and return a lowercase hex-encoded string.
import crypt;
io.println(crypt.sha256("hello"));
# 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
io.println(crypt.sha512("hello"));
io.println(crypt.sha3_256("hello"));
io.println(crypt.blake2b("hello"));
io.println(crypt.sha1("hello")); # legacy - avoid for security use
io.println(crypt.md5("hello")); # legacy - avoid for security use
crypt.crc32(text) returns an int (not hex) - it is a checksum, not a
cryptographic hash:
io.println(crypt.crc32("hello")); # 907060870
Choosing a hash algorithm:
| Algorithm | Output | Use when |
|---|---|---|
sha256 |
64 hex chars | General-purpose signatures, content addressing |
sha512 |
128 hex chars | Larger security margin needed |
sha3_256 |
64 hex chars | Post-SHA-2 hardening, NIST standard |
blake2b |
64 hex chars | High-speed hashing, file integrity |
sha1 |
40 hex chars | Legacy compatibility only |
md5 |
32 hex chars | Legacy checksums only |
crc32 |
int | Non-cryptographic checksums |
HMAC
crypt.hmacSha256(secret, message) computes an HMAC using SHA-256. Both
arguments are strings; the result is a lowercase hex string:
let sig = crypt.hmacSha256("my-secret-key", "the message");
io.println(sig);
# e.g. 4b2c3d...
# Verify by recomputing and comparing with constant-time equality
let ok = secrets.constantTimeEqual(sig, crypt.hmacSha256("my-secret-key", "the message"));
io.println(ok); # true
HMAC-SHA256 is the standard algorithm for webhook signature verification, API request signing, and message integrity checks.
Password hashing
Never store passwords as plain hashes. Use a dedicated password hashing function that incorporates a salt and a cost factor.
PHP-compatible unified API
crypt.passwordHash(password, opts?) and crypt.passwordVerify(password, hash)
produce and accept hashes that round-trip with PHP's
password_hash / password_verify. The hash strings are also accepted by
Node bcryptjs, Python passlib, and any other PHC-string-compatible library.
/* Defaults: bcrypt at cost 10, $2y$ prefix (PHP's default). */
let hash = crypt.passwordHash("hunter2");
io.println(hash); /* $2y$10$... */
/* Verify a hash produced by PHP, geblang, or any other library: */
io.println(crypt.passwordVerify("hunter2", hash)); /* true */
opts.algorithm picks between "bcrypt" (default), "argon2id", and
"argon2i". opts.cost tunes bcrypt; opts.memory / opts.time /
opts.parallelism / opts.keyLength / opts.saltLength tune the argon2
variants (same shape as crypt.argon2idHash).
let bcrypt12 = crypt.passwordHash("hunter2", {"algorithm": "bcrypt", "cost": 12});
let argon = crypt.passwordHash("hunter2", {"algorithm": "argon2id", "memory": 131072});
passwordVerify auto-detects the algorithm from the hash prefix
($2a$, $2b$, $2y$ for bcrypt; $argon2id$, $argon2i$ for argon2)
and returns false for any unknown or malformed input rather than throwing.
Argon2id (preferred)
crypt.argon2idHash(password) hashes a password using Argon2id and returns a
self-contained encoded string (PHC format) that includes the salt and parameters:
let hash = crypt.argon2idHash("hunter2");
io.println(hash);
# $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
crypt.argon2idVerify(password, hash) verifies a password against the stored
hash. Returns true on match:
let ok = crypt.argon2idVerify("hunter2", hash);
io.println(ok); # true
io.println(crypt.argon2idVerify("wrong", hash)); # false
Default parameters: memory=65536 KiB, time=3, parallelism=4,
keyLength=32, saltLength=16. These can be tuned via an options dict:
let hash = crypt.argon2idHash("hunter2", {
"memory": 131072, # KiB (128 MiB)
"time": 4, # iterations (1-32)
"parallelism": 2, # threads (1-255)
"keyLength": 32, # output bytes (16-1024)
"saltLength": 32 # salt bytes (8-1024)
});
Increase memory and time to make brute-force attacks more expensive. A
good starting point for interactive logins is memory=65536, time=3; for
high-security offline storage, use memory=256000, time=4 or higher.
Bcrypt
crypt.bcryptHash(password) hashes a password with bcrypt at cost 10 (the
default). crypt.bcryptVerify(password, hash) verifies it:
let hash = crypt.bcryptHash("hunter2");
io.println(crypt.bcryptVerify("hunter2", hash)); # true
# Custom cost (4-31; higher = slower)
let strongHash = crypt.bcryptHash("hunter2", 12);
Bcrypt is well-supported but limited to 72-byte passwords and has lower
memory-hardness than Argon2id. Prefer Argon2id for new code. Note that
crypt.bcryptHash emits Go's $2a$ prefix; use crypt.passwordHash
(above) when you need PHP-style $2y$ output for cross-language interop.
JWT - unified sign and verify
crypt.jwtSign(payload, key, opts?) and crypt.jwtVerify(token, key, opts?)
handle every supported algorithm through a single pair. The third opts
argument is optional; when omitted, signing defaults to HS256 and verify
trusts whatever algorithm the token header claims.
Supported algorithms: HS256, HS384, HS512, RS256, RS384, RS512,
ES256, ES384, ES512, EdDSA (Ed25519), plus none (only when
explicitly opted in - see "The none algorithm" below). The default
sign and verify policy never accepts none unless the caller passes
it inside opts.allowedAlgs.
The key shape depends on the algorithm:
| Algorithm family | Key for sign | Key for verify |
|---|---|---|
| HS256 / HS384 / HS512 | shared secret (string or bytes) | same shared secret |
| RS256 / RS384 / RS512 | RSA private key PEM | RSA public key PEM (or certificate PEM) |
| ES256 / ES384 / ES512 | EC private key PEM (P-256 / P-384 / P-521 respectively) | EC public key PEM (or certificate PEM) |
| EdDSA | Ed25519 private key PEM | Ed25519 public key PEM |
Symmetric example (HS256)
let token = crypt.jwtSign({
"sub": "user-42",
"role": "admin",
"exp": datetime.nowUnix() + 3600
}, "my-signing-secret");
let payload = crypt.jwtVerify(token, "my-signing-secret");
if (payload != null) {
io.println(payload["sub"]); # user-42
}
Asymmetric example (RS256 or ES256)
import crypt;
let priv = crypt.generateEcKey("P-256");
let pub = crypt.publicKey(priv);
let token = crypt.jwtSign({"sub": "user-42"}, priv, {"alg": "ES256"});
let claims = crypt.jwtVerify(token, pub);
Algorithm-confusion defence
When opts.allowedAlgs is not given, the verify policy is pinned to
the key type: a raw secret verifies only the HS family, an RSA public
key only RS256/384/512, an EC public key only ES256/384/512, and an
Ed25519 key only EdDSA (none is always excluded). A token whose
header names an algorithm outside the key's family fails verification,
which closes the classic forgery where an HS token is minted using a
verifier's public PEM text as the HMAC secret. To narrow the policy
further (for example to a single algorithm), pass opts.allowedAlgs
and the dispatcher rejects anything outside the allow-list before
loading the key:
let claims = crypt.jwtVerify(token, pubPem, {"allowedAlgs": ["ES256"]});
The same allowedAlgs field is enforced on the sign side, so a
shared "this service only uses ES256" constant can flow through both
calls and a typo on either side raises immediately instead of
producing tokens the matching verifier rejects.
JWK and JWKS
crypt.jwk(pem, opts) builds the RFC 7517 public JWK for a public,
certificate, or private PEM (private keys contribute their public
half). kid defaults to the RFC 7638 thumbprint; alg defaults per
key type (RS256 / ES256-512 by curve / EdDSA); use defaults to
"sig". crypt.jwks(keys) assembles the {"keys": [...]} document
from PEM strings, {pem, kid, alg} dicts, or ready JWK dicts:
let set = crypt.jwks([
{"pem": crypt.publicKey(currentKey), "kid": "2026-06"},
{"pem": crypt.publicKey(previousKey), "kid": "2026-01"},
]);
crypt.jwtVerify accepts a JWKS (or single JWK) dict as its key:
the verification key is selected by the token header's kid (a
kid-less token verifies only against a single-key set), and the
algorithm is pinned to the matched key's alg (or its key-type
family), so key rotation and alg pinning come together:
let token = crypt.jwtSign(payload, currentKey, {"alg": "RS256", "kid": "2026-06"});
let claims = crypt.jwtVerify(token, set);
crypt.jwtSign writes opts.kid into the token header for this
purpose. Symmetric oct JWKs (k member) verify the HS family.
jwtVerify returns null for any invalid token - bad format, disallowed
algorithm, wrong key, or corrupted signature.
The none algorithm
A JWT signed with alg: "none" carries no signature and exists in the
spec only for unsigned tokens. Accepting one is equivalent to trusting
arbitrary input, so the default policy on both sides rejects it:
crypt.jwtSign({"u": "x"}, "shh", {"alg": "none"});
# throws: alg "none" rejected by default; pass opts.allowedAlgs
# containing "none" to opt in
If you genuinely need unsigned tokens (test fixtures, signed-elsewhere formats), opt in explicitly:
let unsigned = crypt.jwtSign(claims, "", {
"alg": "none",
"allowedAlgs": ["none"]
});
let payload = crypt.jwtVerify(unsigned, "", {"allowedAlgs": ["none"]});
A verifier that should accept both signed and unsigned tokens lists each algorithm it tolerates:
crypt.jwtVerify(token, secret, {"allowedAlgs": ["HS256", "none"]});
Expiry checking is not automatic; verify exp yourself:
let payload = crypt.jwtVerify(token, secret, {"allowedAlgs": ["HS256"]});
if (payload == null) {
# reject
} else if (payload["exp"] < datetime.nowUnix()) {
# expired
} else {
# valid
}
crypt.jwtDecode(token) decodes a JWT without verifying the signature. Use
only for debugging or for inspecting the header:
let parts = crypt.jwtDecode(token);
io.println(parts["header"]); # {"alg": "HS256", "typ": "JWT"}
io.println(parts["payload"]); # {"sub": "user-42", ...}
Never trust the payload from jwtDecode for access control - always use
jwtVerify instead.
Deprecated per-algorithm helpers
The earlier per-algorithm API remains as a shim. Prefer the unified surface:
| Deprecated | Replacement |
|---|---|
crypt.jwtSignRS256(payload, priv) |
crypt.jwtSign(payload, priv, {"alg": "RS256"}) |
crypt.jwtVerifyRS256(token, pub) |
crypt.jwtVerify(token, pub, {"allowedAlgs": ["RS256"]}) |
crypt.jwtSignES256(payload, priv) |
crypt.jwtSign(payload, priv, {"alg": "ES256"}) |
crypt.jwtVerifyES256(token, pub) |
crypt.jwtVerify(token, pub, {"allowedAlgs": ["ES256"]}) |
Key generation
RSA keys
crypt.generateRsaKey(bits) generates an RSA private key and returns it as a
PKCS#8 PEM string. The default bit size is 2048; valid range is 1024-8192:
let privPem = crypt.generateRsaKey(); # 2048-bit
let privPem = crypt.generateRsaKey(4096); # 4096-bit
EC keys
crypt.generateEcKey(curve) generates an ECDSA private key. The default curve
is "P-256"; valid values are "P-256", "P-384", and "P-521":
let privPem = crypt.generateEcKey(); # P-256
let privPem = crypt.generateEcKey("P-384"); # stronger curve
Ed25519 keys
crypt.generateEd25519Key() generates an Ed25519 private key:
let privPem = crypt.generateEd25519Key();
Extracting a public key
crypt.publicKey(privatePem) extracts the corresponding public key from any
supported private key type and returns it as a PKCS#8 PEM string:
let pubPem = crypt.publicKey(privPem);
io.println(pubPem); # -----BEGIN PUBLIC KEY-----...
Certificates
Self-signed certificates
crypt.generateSelfSignedCert(options) generates a self-signed X.509
certificate and returns a dict with "cert" and "key" PEM strings:
let result = crypt.generateSelfSignedCert({
"subject": {
"commonName": "localhost",
"organization": "Acme Inc",
"country": "GB"
},
"dnsNames": ["localhost", "api.example.com"],
"ipAddresses": ["127.0.0.1"],
"validDays": 365,
"keyType": "EC-P256" # RSA2048, RSA4096, EC-P256, EC-P384, EC-P521, Ed25519
});
io.println(result["cert"]); # -----BEGIN CERTIFICATE-----...
io.println(result["key"]); # -----BEGIN PRIVATE KEY-----...
Pass an existing "key" PEM to use a pre-generated key instead of generating
one:
let key = crypt.generateEcKey("P-256");
let cert = crypt.generateSelfSignedCert({
"subject": {"commonName": "localhost"},
"key": key
});
Certificate signing requests (CSR)
crypt.generateCsr(options) creates a PKCS#10 CSR for submission to a CA.
The "key" option is required:
let key = crypt.generateEcKey("P-256");
let csr = crypt.generateCsr({
"key": key,
"subject": {
"commonName": "api.example.com",
"organization": "Acme Inc",
"country": "GB",
"state": "London",
"locality": "London"
},
"dnsNames": ["api.example.com", "www.example.com"]
});
io.println(csr); # -----BEGIN CERTIFICATE REQUEST-----...
Signing a CSR with a CA
crypt.signCertificate(options) takes a CSR, a CA certificate, and the CA's
private key, and returns the issued certificate PEM. The CSR's subject,
DNS names, IP addresses, email addresses, and URIs are copied across; the
issuer is the CA's subject.
let caKey = crypt.generateEcKey("P-256");
let caBundle = crypt.generateSelfSignedCert({
"subject": {"commonName": "Acme Root CA"},
"key": caKey
});
let leafKey = crypt.generateEcKey("P-256");
let csr = crypt.generateCsr({
"key": leafKey,
"subject": {"commonName": "api.example.com"},
"dnsNames": ["api.example.com"]
});
let leafPem = crypt.signCertificate({
"csr": csr,
"caCert": caBundle["cert"],
"caKey": caKey,
"validDays": 90
});
isCA: true issues an intermediate CA certificate (sets BasicConstraints isCA=true and adds KeyUsageCertSign). The default is a leaf certificate
suitable for server / client authentication.
Parsing certificates
crypt.parseCert(pem) decodes an X.509 certificate PEM and returns a dict:
let info = crypt.parseCert(certPem);
io.println(info["subject"]); # dict: commonName, organization, etc.
io.println(info["issuer"]); # dict: same fields
io.println(info["dnsNames"]); # list<string>
io.println(info["ipAddresses"]); # list<string>
io.println(info["notBefore"]); # RFC3339 string
io.println(info["notAfter"]); # RFC3339 string
io.println(info["serialNumber"]); # hex string
io.println(info["keyType"]); # "RSA", "EC", or "Ed25519"
io.println(info["isCA"]); # bool
io.println(info["publicKey"]); # SPKI public-key PEM
io.println(info["extensions"]); # list of {oid, critical, value (bytes)}
Validate a certificate's expiry:
let info = crypt.parseCert(certPem);
let expiry = datetime.parseRFC3339(info["notAfter"]);
if (expiry < datetime.nowUnix()) {
io.println("certificate has expired");
}
Verifying a certificate chain
crypt.verifyCertChain(options) checks a chain's signatures up to a trusted
root and throws on any failure (untrusted anchor, broken signature, or
expiry), so a verified result cannot be silently ignored. Options: leaf
(PEM), roots (list of trusted-anchor PEM, required), intermediates (list of
PEM), time (RFC3339, default now), and skipExpiry (verify as of the leaf's
issuance). On success it returns the validated chain, leaf to root, as subject
dicts. There is no hostname check; these are not assumed to be TLS certificates.
let chain = crypt.verifyCertChain({
"leaf": leafPem,
"intermediates": [intermediatePem],
"roots": [trustedRootPem],
});
io.println(chain[0]["commonName"]); # the leaf's subject
Decoding ASN.1 and Android Key Attestation
crypt.asn1Decode(der) walks a DER-encoded value into a nested structure:
sequences and sets become lists, integers become ints, octet strings and bit
strings become bytes, OIDs become dotted strings, and context-tagged values
become {tag, constructed, value} dicts. Read raw extensions from
parseCert(...)["extensions"] and decode the ones you need.
crypt.parseAndroidAttestation(pem) parses the Android Key Attestation
extension (OID 1.3.6.1.4.1.11129.2.1.17) into attestationVersion,
attestationSecurityLevel / keymasterSecurityLevel
(Software / TrustedEnvironment / StrongBox), attestationChallenge,
uniqueId, and keyOrigin. Verify the certificate chain up to the Google
hardware-attestation root with verifyCertChain first; the parsed extension is
only trustworthy once the chain is verified.
Decoding PKCS#12 / PFX archives
crypt.pkcs12Decode(pfx, password) decodes a PFX byte string and returns
a dict carrying the private key, the leaf certificate, and any
intermediate CA certificates. The password defaults to an empty string;
common server PFX bundles use a non-empty password.
let pfx = io.readBytes("server.pfx");
let bundle = crypt.pkcs12Decode(pfx, "changeit");
io.println(bundle["key"]); # PKCS#8 PEM
io.println(bundle["cert"]); # CERTIFICATE PEM (or null)
io.println(bundle["caCerts"].length); # int - intermediates
Encoding to PFX is not currently supported; export from a CA tool
(openssl pkcs12 -export ...) and decode on the Geblang side.
Encrypted JWT (JWE)
crypt.jweEncrypt(payload, key, opts?) and
crypt.jweDecrypt(token, key) round-trip a payload through a JWE compact
token (header.encryptedKey.iv.ciphertext.tag). The payload may be a
string or bytes; jweDecrypt always returns bytes.
Supported key wrap (opts.alg): dir and RSA-OAEP-256.
Supported content encryption (opts.enc): A256GCM only.
Direct mode (shared 32-byte key)
import crypt;
import bytes;
let cek = bytes.fromHex(crypt.randomHex(32)); # 32-byte Content Encryption Key
let token = crypt.jweEncrypt("payload data", cek, {
"alg": "dir",
"enc": "A256GCM"
});
let plain = crypt.jweDecrypt(token, cek);
io.println(bytes.toString(plain)); # payload data
dir requires exactly a 32-byte CEK (AES-256 key size); any other length
is rejected on encrypt and decrypt.
RSA-OAEP-256 key wrap
let priv = crypt.generateRsaKey(2048);
let pub = crypt.publicKey(priv);
let token = crypt.jweEncrypt("payload", pub, {
"alg": "RSA-OAEP-256",
"enc": "A256GCM"
});
let plain = crypt.jweDecrypt(token, priv);
The CEK is generated automatically per token and wrapped with the supplied RSA public key. Decryption uses the matching private key.
A tampered token (modified ciphertext, IV, or tag) fails the AES-GCM
authentication check and jweDecrypt throws.
Symmetric encryption
For protecting data at rest (session cookies, encrypted files, sensitive config) Geblang exposes two authenticated AEAD ciphers. Both accept a 32-byte key, generate a random nonce automatically, and produce a dict containing the nonce and ciphertext.
AES-256-GCM
import crypt;
import bytes;
import secrets;
let key = secrets.randomBytes(32); # 32-byte AES-256 key
let enc = crypt.aesEncrypt(key, "secret data");
# enc is {"nonce": bytes, "ciphertext": bytes}
let plaintext = crypt.aesDecrypt(key, enc["nonce"], enc["ciphertext"]);
io.println(plaintext.toString()); # secret data
Both calls accept an optional associated-data argument that is authenticated but not encrypted (good for metadata that must not be forged):
let aad = bytes.fromString("user-42");
let enc = crypt.aesEncrypt(key, "secret", aad);
let pt = crypt.aesDecrypt(key, enc["nonce"], enc["ciphertext"], aad);
If the key, nonce, ciphertext, or associated data is altered between encrypt
and decrypt, aesDecrypt throws RuntimeError: authentication failed -
authenticity is checked alongside confidentiality, so callers do not need a
separate HMAC.
The key must be exactly 32 bytes (AES-256). Derive a key from a password with
Argon2id (and a stored salt) rather than passing the password directly. For
production secrets, prefer secrets.randomBytes(32) and store the key in a
dedicated secrets manager.
XChaCha20-Poly1305
XChaCha20-Poly1305 is the alternate modern AEAD. The 24-byte nonce can be generated randomly without collision concerns even for very high message volumes, which is convenient for stateless services.
let key = secrets.randomBytes(32);
let enc = crypt.chacha20Encrypt(key, "secret data");
let pt = crypt.chacha20Decrypt(key, enc["nonce"], enc["ciphertext"]);
Use aesEncrypt when interoperating with other systems (AES-GCM is the
broader standard); use chacha20Encrypt when you need the larger nonce or
when the target platform lacks AES hardware acceleration.
Miscellaneous encoding helpers
crypt.base64Encode(text) encodes a string to standard Base64.
crypt.base64Decode(text) decodes it back to a string. For binary-safe
encoding use the bytes module instead.
let encoded = crypt.base64Encode("hello world");
io.println(encoded); # aGVsbG8gd29ybGQ=
io.println(crypt.base64Decode(encoded)); # hello world
crypt.randomHex(n) generates n cryptographically random bytes and returns
them as a hex string of length 2n. For secure random material, prefer
secrets.randomHex which is the canonical API:
let nonce = crypt.randomHex(16); # 32-char hex string
Secrets
import secrets;
The secrets module is the canonical place for reading secret material at
startup and generating cryptographically secure random values.
Reading secrets
secrets.requireEnv(name) reads an environment variable and throws a runtime
error if it is not set. Use this for required secrets at application startup:
import secrets;
let dbUrl = secrets.requireEnv("DATABASE_URL");
let apiKey = secrets.requireEnv("API_KEY");
secrets.getEnv(name) returns the value or null if the variable is not set:
let logLevel = secrets.getEnv("LOG_LEVEL") ?? "info";
secrets.readFile(path) reads a secret from a file and returns the content as
a string with trailing newlines stripped. Useful for Docker secrets and
Kubernetes secret mounts:
let cert = secrets.readFile("/run/secrets/tls.crt");
let key = secrets.readFile("/run/secrets/tls.key");
Prefer secrets.requireEnv and secrets.readFile over sys.getenv when
accessing secrets - it signals intent clearly and makes secret access auditable.
Secure random values
All secrets.random* functions read from the OS cryptographic random source
(/dev/urandom on Linux). They are safe for generating tokens, nonces, salts,
and session IDs.
secrets.randomBytes(n) returns n random bytes as a bytes value:
let salt = secrets.randomBytes(16); # 16 random bytes
secrets.randomHex(n) returns n random bytes encoded as a lowercase hex
string of length 2n:
let token = secrets.randomHex(32); # 64-char hex token
let csrfKey = secrets.randomHex(16); # 32-char key
secrets.randomBase64(n) returns n random bytes encoded as URL-safe Base64
(no padding):
let sessionId = secrets.randomBase64(32); # URL-safe, ~43 chars
secrets.randomInt(min, max) returns a cryptographically random int in the
inclusive range [min, max]:
let otp = secrets.randomInt(100000, 999999); # 6-digit OTP
let die = secrets.randomInt(1, 6); # fair die roll
random vs secrets: which one do I use?
Geblang ships two random number modules. Use the right one for the job:
| Purpose | Module | API |
|---|---|---|
| Security tokens, session IDs, salts, OTPs, anything an attacker shouldn't predict | secrets |
CSPRNG; reads from the OS entropy pool. Never seedable. |
| Simulation, sampling, shuffling, procedural generation, fuzz inputs, tests | random |
Deterministic pseudo-random number generator. Seedable for reproducibility. |
secrets.* is the canonical security choice. random.* (documented in
Utilities / random)
is for everything else where reproducibility matters or cryptographic
guarantees do not.
Constant-time comparison
secrets.constantTimeEqual(a, b) compares two strings or bytes values in
constant time, preventing timing-based side-channel attacks. Both arguments
must be the same type:
let submitted = request.headers["X-Webhook-Signature"];
let expected = crypt.hmacSha256(secret, request.body);
if (secrets.constantTimeEqual(submitted, expected)) {
# signature valid
}
Always use constantTimeEqual when comparing authentication tokens, HMAC
signatures, or any other security-sensitive value. Regular == comparison can
leak information about how many bytes matched.
Complete examples
API key authentication middleware
import secrets;
import crypt;
const API_KEY = secrets.requireEnv("API_KEY");
func checkApiKey(string submitted): bool {
return secrets.constantTimeEqual(submitted, API_KEY);
}
Session token generation and storage
import secrets;
import datetime;
func newSession(string userId): dict<string, string> {
let token = secrets.randomHex(32);
let expireAt = datetime.nowUnix() + 86400; # 24 hours
# store {token: userId, expireAt: expireAt} in your session store
return {"token": token, "expireAt": expireAt as string};
}
Webhook signature verification
import crypt;
import secrets;
const WEBHOOK_SECRET = secrets.requireEnv("WEBHOOK_SECRET");
func verifyWebhook(string body, string signature): bool {
let expected = "sha256=" + crypt.hmacSha256(WEBHOOK_SECRET, body);
return secrets.constantTimeEqual(signature, expected);
}
Password authentication flow
import crypt;
# On registration - store hash, not the password
func hashPassword(string password): string {
return crypt.argon2idHash(password);
}
# On login
func checkPassword(string submitted, string stored): bool {
return crypt.argon2idVerify(submitted, stored);
}
JWT authentication middleware (HS256)
import crypt;
import datetime;
import secrets;
const JWT_SECRET = secrets.requireEnv("JWT_SECRET");
func issueToken(string userId): string {
return crypt.jwtSign({
"sub": userId,
"exp": datetime.nowUnix() + 3600
}, JWT_SECRET);
}
func verifyToken(string token): ?string {
let payload = crypt.jwtVerify(token, JWT_SECRET);
if (payload == null) { return null; }
if (payload["exp"] < datetime.nowUnix()) { return null; }
return payload["sub"] as string;
}
TLS certificate for a local HTTPS server
import crypt;
import io;
let result = crypt.generateSelfSignedCert({
"subject": {"commonName": "localhost"},
"dnsNames": ["localhost"],
"ipAddresses": ["127.0.0.1"],
"validDays": 365
});
io.writeText("server.crt", result["cert"]);
io.writeText("server.key", result["key"]);
Provably-fair RNG (secureRandom)
The secureRandom module (added in 1.6.0) provides a verifiable
random-number stream for use cases where an outcome has to be
auditable after the fact: gaming, lotteries, betting, public
draws, distributed leader elections, any place where "did the
house cheat?" is a real question.
It uses a commit / reveal scheme. The server generates a 32-byte seed, publishes its SHA-256 commitment up front, draws values from an HMAC-SHA-256 stream keyed by that seed, then reveals the seed at the end. Anyone holding the commitment and the audit log can independently re-derive every draw and check that the seed matches its commitment.
For ordinary cryptographic tokens or session IDs, keep using
secrets.*. secureRandom is for the narrower case where the
caller has to prove fairness, not just unpredictability.
Surface
| Function | Purpose |
|---|---|
secureRandom.openSession(opts = {}) |
Opens a session with a fresh 32-byte server seed. opts.clientSeed (string) mixes a caller-supplied nonce into every draw. |
secureRandom.fromSeed(seedHex, clientSeed = "") |
Opens a session from a caller-supplied 64-char hex seed (useful for tests and deterministic replays). |
secureRandom.commitment(s) |
Returns sha256(serverSeed) as a 64-char hex string. Publish this before drawing. |
secureRandom.reveal(s) |
Returns the server seed (hex) and locks the session so no further draws are allowed. |
secureRandom.auditLog(s) |
Returns the per-draw records: {nonce, method, args, output} in draw order. |
secureRandom.auditLogJson(s) |
Returns the audit log + commitment + clientSeed (and serverSeed, once revealed) as a JSON envelope ready to publish. |
secureRandom.bytes(s, n) |
Draws n provably-fair random bytes. |
secureRandom.uintRange(s, lo, hi) |
Unbiased uniform integer in [lo, hi). Uses rejection sampling. |
secureRandom.float(s) |
Uniform float in [0, 1). |
secureRandom.bool(s) |
Fair coin flip. |
secureRandom.choice(s, items) |
Uniformly picks one element of items. |
secureRandom.shuffle(s, items) |
Fisher-Yates shuffled copy. |
secureRandom.weightedChoice(s, items, weights) |
Picks one element with probability proportional to its weight. |
secureRandom.verifyCommitment(commit, seedHex) |
True if sha256(seedHex) == commit. |
secureRandom.replay(seedHex, clientSeed, nonce, method, args) |
Reproduces a single draw outside a session; same inputs always yield the same output. |
Example: a provably-fair dice roll
import secureRandom;
import io;
# The "house" opens a session and publishes the commitment first.
let s = secureRandom.openSession({"clientSeed": "player#42"});
io.println("commitment: " + secureRandom.commitment(s));
# The house draws an outcome.
let outcome = secureRandom.uintRange(s, 1, 7); # 1..6
io.println("you rolled: " + (outcome as string));
# At end of round (or end of day) the house reveals the seed.
let seed = secureRandom.reveal(s);
io.println("seed: " + seed);
io.println(secureRandom.auditLogJson(s));
Verifying after the fact
A third party with just the commitment, the published seed, and the audit log can re-derive every outcome:
import secureRandom;
import json;
import io;
let published = json.parse(io.readText("round-1234.json"));
if (!secureRandom.verifyCommitment(
published["commitment"] as string,
published["serverSeed"] as string)) {
throw RuntimeError("server seed does not match published commitment");
}
for (draw in published["draws"]) {
let expected = secureRandom.replay(
published["serverSeed"] as string,
published["clientSeed"] as string,
draw["nonce"] as int,
draw["method"] as string,
draw["args"] as list<any>
);
if (draw["output"] != expected) {
throw RuntimeError("draw " + (draw["nonce"] as string) + " does not replay");
}
}
Notes
- Per-draw randomness derives from
hmacSha256(serverSeed, json({clientSeed, nonce, method, args})), so identical inputs always produce identical outputs. The caller cannot influence the result without changing the input, and the server cannot retroactively change the result without invalidating the commitment. uintRangeuses rejection sampling so the distribution stays unbiased even for ranges that are not powers of two.- Once
revealhas been called the session refuses further draws. This prevents accidentally leaking entropy from a revealed seed. - For plain unpredictable randomness (session IDs, tokens, OTPs),
use
secrets.*.secureRandomis heavier and only worth the cost when the audit trail matters.