age
stableFile encryption and cryptographic utilities using AES-256-GCM, PBKDF2 key derivation, SHA hashing, and PEM-style armoring.
use plugin age::{generate_key, encrypt_symmetric, decrypt_symmetric, …} Functions (11)
- generate_key Generate an X25519 key pair
- encrypt_symmetric Encrypt text with a passphrase
- decrypt_symmetric Decrypt passphrase-encrypted text
- derive_key Derive a 32-byte key from passphrase and salt
- generate_salt Generate a random hex-encoded salt
- encrypt Encrypt text with a raw hex key
- decrypt Decrypt text with a raw hex key
- armor Wrap base64 data in PEM-style headers
- dearmor Strip PEM-style headers from armored data
- hash Hash a string with SHA-256 or SHA-512
- random_bytes Generate cryptographically random bytes
Overview
The age plugin bundles the small set of cryptographic primitives most apps actually need: authenticated symmetric encryption (AES-256-GCM), passphrase-based key derivation (PBKDF2-HMAC-SHA256, 100,000 iterations), SHA-256/512 hashing, secure random bytes, and PEM-style text armoring. It is stateless — every function takes its inputs and returns a value, so there are no handles or sessions to manage. Keys and salts travel as hex strings, ciphertext travels as base64, and all randomness comes from the operating system's secure RNG.
Reach for it whenever you need to protect a secret at rest (an API key, a config blob, a token) or verify integrity with a hash. Use the *_symmetric pair when you only have a human passphrase, and the raw encrypt/decrypt pair when you manage a 32-byte key yourself.
Common patterns
Encrypt and recover a secret with nothing but a passphrase — salt, nonce, and derivation are handled internally:
use plugin age::{encrypt_symmetric, decrypt_symmetric}
let blob = encrypt_symmetric("api-key: abc123", "correct-horse-battery-staple")
print("stored: {blob}")
let recovered = decrypt_symmetric(blob, "correct-horse-battery-staple")
print("recovered: {recovered}")
Encrypt with a raw key you manage directly, using a freshly generated salt and a key hex you derive elsewhere:
use plugin age::{generate_salt, random_bytes, encrypt, decrypt}
let key_hex = random_bytes(32) // 32 random bytes -> 64 hex chars
let blob = encrypt("secret message", key_hex)
let plain = decrypt(blob, key_hex)
print(plain)
Wrap ciphertext in PEM-style armor for safe transport in text channels, then strip it back off before decrypting:
use plugin age::{encrypt_symmetric, armor, dearmor, decrypt_symmetric}
let pem = armor(encrypt_symmetric("hello", "pass"))
print(pem)
let plain = decrypt_symmetric(dearmor(pem), "pass")
print(plain)
Generate an X25519 key pair
Generates a fresh X25519 key pair and returns a table with private_hex and public_hex fields. Both keys are hex-encoded 32-byte values.
use plugin age::{generate_key}
let keypair = generate_key()
print("private: {keypair["private_hex"]}")
print("public: {keypair["public_hex"]}")
Encrypt text with a passphrase
Encrypts plaintext using AES-256-GCM with a key derived from passphrase via PBKDF2 (100,000 iterations). Returns a base64-encoded blob containing the random salt, nonce, ciphertext, and authentication tag. A fresh salt and nonce are generated on every call, so encrypting the same text twice yields different blobs.
use plugin age::{encrypt_symmetric, decrypt_symmetric}
let secret = "api-key: abc123"
let blob = encrypt_symmetric(secret, "my-strong-passphrase")
print(blob)
let recovered = decrypt_symmetric(blob, "my-strong-passphrase")
print(recovered)
Because each call randomizes the salt, two encryptions of the same plaintext differ:
use plugin age::{encrypt_symmetric}
let a = encrypt_symmetric("same text", "pass")
let b = encrypt_symmetric("same text", "pass")
print("blobs differ: {a != b}")
Decrypt passphrase-encrypted text
Decrypts a base64 blob produced by encrypt_symmetric. The passphrase must match exactly; a wrong passphrase fails the GCM authentication check and raises an error rather than returning garbage.
use plugin age::{decrypt_symmetric}
let plaintext = decrypt_symmetric(stored_blob, "my-strong-passphrase")
print(plaintext)
Derive a 32-byte key from passphrase and salt
Derives a 32-byte key from passphrase and the hex-encoded salt_hex using PBKDF2-HMAC-SHA256 with 100,000 iterations. Pair it with generate_salt to produce the salt, and store the salt alongside the ciphertext so you can re-derive the same key later.
use plugin age::{generate_salt, derive_key}
let salt = generate_salt(16)
let key = derive_key("my-passphrase", salt)
print("derived key from salt {salt}")
The same passphrase and salt always produce the same key — that determinism is what lets you reconstruct the key on the next run:
use plugin age::{derive_key}
let salt = "0102030405060708090a0b0c0d0e0f10"
let k1 = derive_key("pass", salt)
let k2 = derive_key("pass", salt)
print("stable derivation: {k1 == k2}")
Generate a random hex-encoded salt
Generates len cryptographically random bytes (default 16) and returns them hex-encoded. Use this salt when calling derive_key or for your own key derivation.
use plugin age::{generate_salt}
let salt16 = generate_salt()
let salt32 = generate_salt(32)
print(salt16)
Encrypt text with a raw hex key
Encrypts plaintext with AES-256-GCM using a raw 32-byte key supplied as exactly 64 hex characters. Returns a base64-encoded blob of nonce + ciphertext + tag. Use this when you manage keys directly (e.g., a key from random_bytes or one you persist yourself). Supplying a key that is not 32 bytes raises an error.
use plugin age::{random_bytes, encrypt, decrypt}
let key_hex = random_bytes(32)
let blob = encrypt("secret message", key_hex)
let plain = decrypt(blob, key_hex)
print(plain)
The key length is enforced — a short key is rejected before any encryption happens:
use plugin age::{encrypt}
let key64 = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
let blob = encrypt("payload", key64)
print(blob)
Decrypt text with a raw hex key
Decrypts a base64 blob produced by encrypt using the same 64-char hex key. Wrong keys fail authentication and raise an error.
use plugin age::{decrypt}
let message = decrypt(ciphertext_blob, key_hex_64_chars)
print(message)
Wrap base64 data in PEM-style headers
Wraps a base64 string in -----BEGIN AGE ENCRYPTED DATA----- / -----END AGE ENCRYPTED DATA----- PEM-style headers, breaking lines at 76 characters. Useful for embedding ciphertext in text files, emails, or logs.
use plugin age::{encrypt_symmetric, armor, dearmor, decrypt_symmetric}
let blob = encrypt_symmetric("hello", "pass")
let pem = armor(blob)
print(pem)
let raw = dearmor(pem)
let plain = decrypt_symmetric(raw, "pass")
print(plain)
Strip PEM-style headers from armored data
Strips PEM-style headers and whitespace from an armored string, returning the raw base64 payload. It is the exact inverse of armor, so dearmor(armor(x)) returns x.
use plugin age::{armor, dearmor}
let raw = dearmor(armor("dGVzdA=="))
print(raw)
Hash a string with SHA-256 or SHA-512
Hashes data using "sha256" (default) or "sha512" and returns the hex-encoded digest. The algorithm name is case-insensitive and accepts both "sha256" and "sha-256" spellings; an unsupported name raises an error.
use plugin age::{hash}
let digest = hash("hello world")
print(digest)
let digest512 = hash("hello world", "sha512")
print(digest512)
Hashing is deterministic, which makes it handy for checksums and integrity checks:
use plugin age::{hash}
let expected = hash("config-v1")
let actual = hash("config-v1")
print("integrity ok: {expected == actual}")
Generate cryptographically random bytes
Generates len cryptographically random bytes from the OS RNG and returns them hex-encoded (so the string is len * 2 characters). Useful for nonces, tokens, session IDs, or raw 32-byte keys. Unlike generate_salt, the length argument is required.
use plugin age::{random_bytes}
let token = random_bytes(32)
print(token)
let nonce = random_bytes(12)
print("nonce: {nonce}")