Security is the one area where "just install a package" is genuinely dangerous. Every year, compromised npm packages steal credentials. Outdated dependencies expose known vulnerabilities. Developers copy SHA-256 password hashing from Stack Overflow, not knowing that SHA-256 is catastrophically wrong for passwords. Security requires correct defaults, and correct defaults require the language itself to provide them.
In Sessions 183 and 184, we built FLIN's security functions: password hashing with Argon2, JWT creation and verification, HMAC signatures, symmetric encryption, and cryptographically secure random generation. All built-in. All using the correct algorithms by default. All making the secure path the easy path.
Password Hashing: Argon2 by Default
The single most important security function in any web application is password hashing. Get it wrong, and a database breach exposes every user's password in plaintext. Get it right, and a breach reveals nothing useful.
FLIN uses Argon2id -- the current winner of the Password Hashing Competition and the recommended algorithm by OWASP -- as the only built-in password hashing function:
// Hash a password (one function call)
hashed = hash_password("user_password_here")
// "$argon2id$v=19$m=19456,t=2,p=1$random_salt$hash_output"// Verify a password against a hash is_valid = verify_password("user_password_here", hashed) // true ```
Two functions. That is the entire password API. No algorithm selection. No parameter tuning. No salt generation. FLIN handles all of it with secure defaults.
The hash output is a single string in the standard PHC format, containing the algorithm identifier, version, parameters, salt, and hash. You store this string in your database. When verifying, you pass the plaintext password and the stored hash. The function extracts the parameters and salt from the hash string and recomputes. If they match, it returns true.
Why No Algorithm Choice
Most password hashing libraries let you choose between bcrypt, scrypt, PBKDF2, and Argon2. This is a footgun. A developer who does not understand the differences might choose PBKDF2 (the weakest option) because it has the simplest name, or bcrypt (adequate but outdated) because a 2015 blog post recommended it.
FLIN removes the choice. You get Argon2id. It is the best algorithm available. The parameters are tuned for a balance between security and performance suitable for web applications:
- Memory: 19,456 KiB (19 MB)
- Iterations: 2
- Parallelism: 1
These parameters produce a hash in approximately 100-200 milliseconds on a modern server. Fast enough that users do not notice a delay during login. Slow enough that an attacker brute-forcing stolen hashes would need years per password.
The Deliberate Absence of SHA-256
FLIN does not provide a sha256_password or md5_password function. This is deliberate. SHA-256 and MD5 are cryptographic hash functions designed for integrity verification, not password storage. They are fast by design -- billions of hashes per second on a GPU. Using them for passwords is a security vulnerability, not a feature.
If a developer truly needs SHA-256 for non-password purposes (verifying file integrity, computing checksums), it is available as a general-purpose hash function:
// General-purpose hashing (NOT for passwords)
checksum = sha256("file contents here")
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"digest = sha512("data to hash") ```
But hash_password and verify_password -- the functions whose names a developer will search for when implementing authentication -- always use Argon2id. The naming itself guides developers toward the correct choice.
JWT: Token Creation and Verification
JSON Web Tokens are the standard for API authentication in modern web applications. FLIN includes built-in JWT support:
// Create a JWT
token = jwt_sign({
user_id: 42,
role: "admin",
email: "[email protected]"
}, env("JWT_SECRET"), {
expires_in: 30.days
})
// "eyJhbGciOiJIUzI1NiIs..."// Verify and decode a JWT claims = jwt_verify(token, env("JWT_SECRET")) {if claims != none} print("User: {claims['user_id']}, Role: {claims['role']}") {else} print("Invalid or expired token") {/if} ```
jwt_sign takes three arguments: a claims map, a secret key, and options. The claims map can contain any data you want to embed in the token. The secret key is used for HMAC-SHA256 signing (the most common JWT algorithm for single-server applications). The options map supports expires_in (a duration) and issuer (a string).
jwt_verify returns the decoded claims map if the token is valid and not expired, or none if verification fails for any reason (invalid signature, expired, malformed). This is another instance of FLIN's "return none instead of throwing" philosophy -- the developer checks for none instead of wrapping every verification in a try-catch.
JWT Options
token = jwt_sign(claims, secret, {
expires_in: 1.hour, // Token expiration
issuer: "flin.dev", // iss claim
audience: "api.flin.dev", // aud claim
not_before: now + 5.minutes // nbf claim (token not valid before)
})Standard JWT claims (exp, iss, aud, nbf, iat) are set automatically from the options. iat (issued at) is always set to the current time. exp is set based on expires_in. The developer never needs to manually compute Unix timestamps for these claims.
Algorithm Selection
The default algorithm is HS256 (HMAC-SHA256), which is appropriate for applications where the same server creates and verifies tokens. For applications that need asymmetric signing (one server creates tokens, multiple servers verify them), FLIN supports RS256:
// Asymmetric JWT (RS256)
token = jwt_sign(claims, env("JWT_PRIVATE_KEY"), {
algorithm: "RS256",
expires_in: 1.hour
})// Verify with public key claims = jwt_verify(token, env("JWT_PUBLIC_KEY"), { algorithm: "RS256" }) ```
HMAC Signatures
HMAC (Hash-based Message Authentication Code) is used for verifying that a message was not tampered with. It is the standard mechanism for webhook verification -- when Stripe or GitHub sends a webhook to your application, they include an HMAC signature that you verify against your shared secret.
// Sign a message
signature = hmac_sha256("message to sign", env("WEBHOOK_SECRET"))// Verify a webhook signature expected = hmac_sha256(request_body, env("STRIPE_WEBHOOK_SECRET")) is_valid = secure_compare(expected, received_signature) ```
secure_compare is a constant-time string comparison function that prevents timing attacks. Regular string comparison (==) short-circuits on the first differing character, which leaks information about how much of the signature matched. secure_compare always compares every character, taking the same amount of time regardless of how many characters match. This is a subtle but critical security property.
Symmetric Encryption
For encrypting sensitive data at rest (database fields, configuration values, user data that must be stored securely), FLIN provides AES-256-GCM encryption:
// Encrypt
key = env("ENCRYPTION_KEY") // 32-byte key
encrypted = encrypt("sensitive data here", key)// Decrypt decrypted = decrypt(encrypted, key) print(decrypted) // "sensitive data here" ```
AES-256-GCM provides both confidentiality (the data is unreadable without the key) and authenticity (the data has not been tampered with). The initialization vector (IV/nonce) is generated randomly for each encryption and prepended to the ciphertext. The developer does not need to manage IVs manually.
The encrypted output is a base64-encoded string containing the IV and the ciphertext. You can store it in a database text column, send it as a JSON field, or write it to a file. Decryption extracts the IV automatically.
// Encrypting entity fields
entity User {
name: text
email: text
ssn: text // Sensitive!
}// Store encrypted user = User.create( name: "Juste", email: "[email protected]", ssn: encrypt("123-45-6789", env("ENCRYPTION_KEY")) )
// Retrieve and decrypt raw_ssn = decrypt(user.ssn, env("ENCRYPTION_KEY")) ```
Secure Random Generation
Cryptographically secure random values are essential for tokens, session IDs, password reset codes, and API keys:
// Random bytes (base64 encoded)
token = random_bytes(32)
// "dGhpcyBpcyBhIHJhbmRvbSBieXRlIHN0cmluZw=="// Random hex string api_key = random_hex(32) // "a1b2c3d4e5f6789012345678abcdef01"
// UUID v4 id = uuid() // "550e8400-e29b-41d4-a716-446655440000" ```
These functions use the operating system's cryptographic random number generator (/dev/urandom on Linux/macOS, BCryptGenRandom on Windows). They are safe for security-critical purposes like token generation. This is different from random() and random_int() in the math library, which use a PRNG that is fast but not suitable for security.
Hashing for Non-Password Purposes
General-purpose hashing is available for checksums, content addressing, and data deduplication:
sha256("hello")
// "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"sha512("hello") // "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca..."
md5("hello") // "5d41402abc4b2a76b9719d911017c592" ```
MD5 is included despite being cryptographically broken because it is still widely used for non-security purposes (checksum verification, cache key generation, legacy API compatibility). It is explicitly documented as unsuitable for security.
Real-World Example: Complete Authentication Flow
Putting the security functions together, here is a complete authentication system:
entity User {
email: text where is_email
password_hash: text
name: text
created_at: time = now
}// Registration fn register(email: text, password: text, name: text) { // Check if user exists existing = User.find_by(email: email) {if existing != none} return { error: "Email already registered" } {/if}
// Hash password and create user user = User.create( email: email, password_hash: hash_password(password), name: name )
// Generate JWT token = jwt_sign({ user_id: user.id, email: user.email, role: "user" }, env("JWT_SECRET"), { expires_in: 30.days })
return { user: user, token: token } }
// Login fn login(email: text, password: text) { user = User.find_by(email: email) {if user == none} return { error: "Invalid credentials" } {/if}
{if not verify_password(password, user.password_hash)} return { error: "Invalid credentials" } {/if}
token = jwt_sign({ user_id: user.id, email: user.email }, env("JWT_SECRET"), { expires_in: 30.days })
return { user: user, token: token } }
// Protected route fn get_profile(token: text) { claims = jwt_verify(token, env("JWT_SECRET")) {if claims == none} return { error: "Unauthorized" } {/if}
user = User.find(claims["user_id"]) return { user: user } } ```
Registration, login, and token-based authentication in 50 lines. Argon2 password hashing. JWT tokens with 30-day expiry. No security library imports. No middleware configuration. No authentication framework. The language provides the primitives. The developer composes them.
Implementation: Rust's Crypto Ecosystem
FLIN's security functions are built on Rust's battle-tested cryptography crates:
argon2crate for password hashingjsonwebtokencrate for JWT operationsaes-gcmcrate for symmetric encryptionhmacandsha2crates for HMAC and hashingrandcrate withOsRngfor secure random generation
These crates are maintained by the RustCrypto project, one of the most active and security-focused open-source groups in the Rust ecosystem. They undergo regular security audits and follow Rust's memory safety guarantees, eliminating buffer overflow vulnerabilities that plague C-based crypto libraries.
fn builtin_hash_password(vm: &mut Vm, args: &[Value]) -> Result<Value, VmError> {
let password = vm.get_string(args[0])?;let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::new( Algorithm::Argon2id, Version::V0x13, Params::new(19_456, 2, 1, None).unwrap(), );
let hash = argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| VmError::CryptoError(e.to_string()))? .to_string();
Ok(vm.alloc_string_value(hash)) } ```
The implementation is straightforward because Rust's type system ensures the parameters are valid and the salt is properly generated. There is no possibility of forgetting to generate a salt, using a weak random source, or passing parameters in the wrong order. The compiler catches all of these at build time.
Eighteen Functions, Zero Vulnerabilities
The complete security API:
- Password:
hash_password,verify_password - JWT:
jwt_sign,jwt_verify - HMAC:
hmac_sha256,hmac_sha512 - Encryption:
encrypt,decrypt - Hashing:
sha256,sha512,md5 - Random:
random_bytes,random_hex,uuid - Comparison:
secure_compare - Encoding:
base64_encode,base64_decode - Environment:
env
Eighteen functions that replace bcrypt, jsonwebtoken, crypto-js, uuid, dotenv, and half a dozen other security-related packages. All using the correct algorithms by default. All making the secure path the easy path.
---
This is Part 76 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built production-grade security into a programming language.
Series Navigation: - [75] HTTP Client Built Into the Language - [76] Security Functions: Crypto, JWT, Argon2 (you are here) - [77] Introspection and Reflection at Runtime - [78] Reduce, Map, Filter: Higher-Order Functions