Introducing librage: A Safe, Simple, and Uniform Rust Wrapper for Age Encryption
Modern applications need encryption that is fast, secure, and easy to integrate. The age format delivers the cryptography, and rage provides a superb Rust implementation. But the raw rage API, while powerful, can feel low‑level when all you want is a straightforward encrypt‑this / decrypt‑that interface with no surprises. That’s where librage steps in.
librage is a thin, idiomatic Rust wrapper around rage that gives every operation a uniform response envelope (LibrageResponse<T>), automatically zeroizes all sensitive material, and exposes the full power of age without requiring you to juggle error types or boxed trait objects manually. It even works with streaming, SSH keys, and ASCII‑armoured output right out of the box.
In this post I’ll explain why I built librage, how it makes your life easier, and how you can start encrypting in three lines of code.
The Problem: Great Library, Unfriendly API
The rage crate is undoubtedly well‑designed. But using it directly often means:
- Manually parsing key strings into
x25519::Recipientorssh::Identity. - Boxing trait objects for identities and recipients.
- Handling diverse error variants (
EncryptError,DecryptError,ParseRecipientKeyError) and mapping them into your application’s error model. - Remembering to zeroize plaintext and secrets yourself.
I found myself writing the same boilerplate in every project: key parsing, error conversion, zeroize wrappers, and a custom result type that could speak JSON. librage consolidates all of that into one ergonomic crate.
Uniform API: Everything Returns LibrageResponse<T>
The central design decision is that every public function returns a LibrageResponse<T>:
pub struct LibrageResponse<T: Serialize> {
pub success: bool,
pub data: Option<T>,
pub error: Option<ErrorBody>,
}
If the call succeeds, success is true and data contains the result. If it fails, success is false and error holds a machine‑readable error code (INVALID_PUBLIC_KEY, DECRYPTION_FAILED, etc.) and a human‑readable message. A simple .to_json() call gives you a ready‑to‑send JSON string—perfect for APIs, CLIs, or FFI.
Feature‑Rich, Minimalist Surface
librage exposes everything you need, and nothing you don’t:
- X25519 keys: generate keypairs, encrypt/decrypt with one or multiple recipients.
- Passphrase (scrypt): encrypt with a passphrase—no keys to manage.
- SSH keys: encrypt to an SSH public key, decrypt with the corresponding private key (passphrase‑protected keys supported).
- Tag & tagpq recipients: full support for the age tagging mechanism.
- Streaming: incremental encryption and decryption for large files or network streams.
- ASCII armour: opt‑in PEM encoding for safe transport over text channels.
- File utilities: read recipients and identities from files, including multi‑line SSH private keys, ignoring comments and blank lines.
-
Zeroize by default: secret keys (
KeyGenData::secret_key), ciphertexts, and plaintexts are all wrapped inZeroizingand automatically wiped on drop. -
Clean error model:
LibrageErrorimplementsstd::error::Errorand transparently converts from all relevant rage error types.
No need to box identities yourself—librage handles it internally. You just pass &str keys or slices.
Quick Example: From Keygen to Decrypted Text
use librage::*;
// Generate a new X25519 keypair
let res = generate_keypair();
assert!(res.success);
let kp = res.data.unwrap();
// Encrypt some bytes
let plaintext = b"Hello, librage!";
let enc = encrypt(plaintext, &kp.public_key).unwrap();
println!("Ciphertext: {}", enc.data.as_string()); // hex or PEM
// Decrypt it back
let dec = decrypt(&enc.data.ciphertext, &kp.secret_key).unwrap();
assert_eq!(dec.data.as_bytes(), plaintext);
The same pattern holds for passphrase encryption, SSH, and streaming—just swap encrypt for encrypt_with_passphrase, encrypt_with_ssh, etc. Every function returns the same LibrageResponse, so error handling stays consistent.
Security Without Headaches
I baked several safety measures directly into the library:
-
Zeroization –
KeyGenData::secret_keyisZeroizing<String>. When the struct is dropped, the memory is overwritten. Similarly,EncryptOutputandDecryptOutputuseZeroizing<Vec<u8>>for ciphertext and plaintext. -
No hidden panics – all fallible operations propagate
LibrageError. The library never callsunwrap()orexpect()on user‑facing code paths. - Delegation to rage – cryptographic operations are left entirely to the well‑audited rage crate; librage only smoothes the interface.
-
Passphrase‑handling notes – functions that accept a
&strpassphrase do not zeroize the original input (Rust strings are immutable); for long‑lived secrets, consider usingSecretStringexternally. The documentation is explicit about this.
Who Should Use librage?
- Backend services that need to encrypt payloads or tokens before storing them.
- CLI tools that want a JSON‑based interface for key management and encryption.
- Desktop or mobile apps (via Rust bindings) that require secure local storage.
- Developers learning age who want a gentle, high‑level introduction without sacrificing access to the full feature set.
- Anyone who’s tired of re‑implementing the same error mapping and zeroize wrappers.
Because librage re‑exports the underlying age types where needed (e.g., Box<dyn age::Identity> for advanced use), you can still drop down to the raw rage API if you ever need to.
What’s Next?
The library is feature‑complete and ready for production. Potential future additions include:
- A dedicated
EncryptOutput/DecryptOutputto‑file helpers. - Integration with the
secrecycrate for even stricter passphrase handling. - WASM support for browser‑based applications.
Contributions are welcome! The code is MIT licensed and lives on GitHub:
If you need encryption that’s secure by design and simple by choice, give librage a try. It removes the ceremony so you can focus on your application’s logic. I’d love to hear your feedback or see what you build with it—drop a comment or open an issue on the repo!
Top comments (0)