Skip to content

mroczect/librage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

librage

A robust, idiomatic Rust wrapper around the rage encryption library (the Rust implementation of age).
librage exposes a safe, simple, and consistent API for encrypting and decrypting data using X25519 keys, scrypt passphrases, SSH keys, tag recipients, and more. Every public function returns a uniform LibrageResponse<T> that can be easily serialized to JSON.

Features

  • X25519, passphrase (scrypt), SSH (RSA, Ed25519), tag, and tagpq recipients – all core recipient types supported by the age specification.
  • Multiple recipients – encrypt once and let any of the specified private keys decrypt.
  • Multiple identities – provide several identities when decrypting, and the library will try each one.
  • ASCII armour (PEM) – optional armoured output for text-safe storage and transmission.
  • Streaming – incremental encryption and decryption for large files or network streams.
  • Memory safety
    • Secret keys (KeyGenData::secret_key) are wrapped in Zeroizing<String> and automatically zeroized on drop.
    • Ciphertext and plaintext outputs are also wrapped in Zeroizing<Vec<u8>>.
  • File helpers – read recipients and identities directly from files (ignoring comments and blank lines), including multi‑line SSH private keys.
  • Clean error handling – every error includes a machine‑readable code and a human‑readable message; LibrageError implements std::error::Error.
  • Uniform JSON‑ready responses – all functions return LibrageResponse<T> with success, data, and error fields. Call .to_json() to obtain a JSON string.
  • Full test coverage and lint‑free code (cargo fmt, cargo clippy -- -D warnings).

Installation

Add the following to your Cargo.toml:

[dependencies]
librage = { git = "https://github.com/mroczect/librage.git" }

Required dependencies (age, serde, serde_json, thiserror, zeroize, hex) are pulled in automatically.

Quick Start

// examples/simple_tests.rs
use librage::*;

fn main() {
    let res = generate_keypair();
    assert!(res.success);
    let kp = res.data.unwrap();
    println!("Public key : {}", kp.public_key);

    let plaintext = b"Hello, world!";
    let enc = encrypt(plaintext, &kp.public_key);
    assert!(enc.success);
    let dec = decrypt(&enc.data.unwrap().ciphertext, &kp.secret_key);
    assert!(dec.success);
    println!(
        "Decrypted X25519 : {}",
        dec.data.unwrap().as_string()
    );

    let enc = encrypt_with_passphrase(b"secret data", "my password");
    assert!(enc.success);
    let dec = decrypt_with_passphrase(&enc.data.unwrap().ciphertext, "my password");
    assert!(dec.success);
    println!(
        "Decrypted passphrase : {}",
        dec.data.unwrap().as_string()
    );

    let ssh_pub = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7iefA+0Lc9U8bU7NKov97K+I3b5DODVCow//16cpcPpBmUCCeP6f2x1lU+v30irOAy3RQ8BzZG8asG6hK95EI84RzxBPtaR5GpxRDTE3upGxZxa5ZWhjo51DLyoPduYHc35SFtDx9VtMmdk0Rc8o0CvTtsunt8Xg6ZZSoAK1C3PYS8WbkcZRUtZ88lezQ4SyYD2o+0YRWvd9ws8MGNbXAG7NOiPJA2eJDjAqBXSpDqf8t+7ULY17fjeJ9FWy7E8lgR5VbBB6zpv1UEXuG38zcVjUOEreKciKmJ+gRcay+W9sX8iR7CZKXBk6YFO1v4s3tWO1NKydl6LNa49C9AnuoXna80sIDs8Pf8A+Yn7i8I/5LnSuRtIQn5hAAsUkJxkvzAU+jbWyJbNgRJTvL98iJ2FoGVglGMLu6BhAaGCIMrRhl9KeKMOlwufSs+6j0dnyvKGa0J4L4jvyTH8pCGt0bY4zEHUIU6ZpHq22vYP3kP8qRyW8eCVceV4CCMCMZ+Uc=";
    let ssh_private_key = include_str!("../tests/common/test_rsa.asc");

    let enc = encrypt_with_ssh(b"data", ssh_pub);
    assert!(enc.success);
    let dec = decrypt_with_ssh(&enc.data.unwrap().ciphertext, ssh_private_key, None);
    assert!(dec.success);
    println!(
        "Decrypted SSH : {}",
        dec.data.unwrap().as_string()
    );
}

API Reference

Key Generation

Function Signature Description
generate_keypair () -> LibrageResponse<KeyGenData> Generates a new X25519 keypair.

KeyGenData contains:

  • public_key: String
  • secret_key: Zeroizing<String> (automatically zeroized on drop)

X25519 Encryption / Decryption

Function Signature Description
encrypt (plaintext: &[u8], public_key: &str) -> LibrageResponse<EncryptOutput> Encrypts data to a single X25519 public key.
encrypt_armored (plaintext: &[u8], public_key: &str) -> LibrageResponse<EncryptOutput> Encrypts and returns ASCII‑armoured output (EncryptOutput.armored = true).
encrypt_multiple (plaintext: &[u8], public_keys: &[&str]) -> LibrageResponse<EncryptOutput> Encrypts to multiple X25519 public keys.
encrypt_multiple_armored (plaintext: &[u8], public_keys: &[&str]) -> LibrageResponse<EncryptOutput> Multiple recipients with armoured output.
encrypt_stream (reader: R, writer: W, public_key: &str) -> LibrageResponse<()> Streaming encryption to one recipient.
encrypt_stream_armored (reader: R, writer: W, public_key: &str) -> LibrageResponse<()> Streaming encryption with armoured output.
decrypt (ciphertext: &[u8], secret_key: &str) -> LibrageResponse<DecryptOutput> Decrypts with an X25519 secret key (auto‑detects armour).
decrypt_armored (ciphertext: &[u8], secret_key: &str) -> LibrageResponse<DecryptOutput> Alias for decrypt.
decrypt_with_identities (ciphertext: &[u8], identities: &[Box<dyn age::Identity>]) -> LibrageResponse<DecryptOutput> Decrypts using multiple identities (mix of X25519 and SSH).
decrypt_stream (reader: R, writer: W, secret_key: &str) -> LibrageResponse<()> Streaming decryption with one identity.
decrypt_stream_with_identities (reader: R, writer: W, identities: &[Box<dyn age::Identity>]) -> LibrageResponse<()> Streaming decryption with multiple identities.

EncryptOutput contains:

  • ciphertext: Zeroizing<Vec<u8>>
  • armored: bool
  • as_string() -> String – returns PEM if armoured, hex‑encoded otherwise.

DecryptOutput contains:

  • plaintext: Zeroizing<Vec<u8>>
  • as_string() -> String
  • as_bytes() -> &[u8]

Passphrase (scrypt)

Function Signature Description
encrypt_with_passphrase (plaintext: &[u8], passphrase: &str) -> LibrageResponse<EncryptOutput> Encrypts with a passphrase. Note: the &str is not zeroized.
decrypt_with_passphrase (ciphertext: &[u8], passphrase: &str) -> LibrageResponse<DecryptOutput> Decrypts with a passphrase. The &str is not zeroized.

SSH Keys

Function Signature Description
encrypt_with_ssh (plaintext: &[u8], ssh_public_key: &str) -> LibrageResponse<EncryptOutput> Encrypts to an SSH public key (RSA or Ed25519).
decrypt_with_ssh (ciphertext: &[u8], ssh_private_key: &str, passphrase: Option<&str>) -> LibrageResponse<DecryptOutput> Decrypts with an SSH private key. Supports encrypted keys via passphrase.

Tag & TagPQ Recipients

Function Signature Description
encrypt_with_tag (plaintext: &[u8], tag: &str) -> LibrageResponse<EncryptOutput> Encrypts to a tag recipient.
encrypt_with_tagpq (plaintext: &[u8], tagpq: &str) -> LibrageResponse<EncryptOutput> Encrypts to a tagpq recipient.

File Utilities

Function Signature Description
read_recipients_from_file (path: impl AsRef<Path>) -> Result<Vec<String>, LibrageError> Reads recipients (one per line, # comments allowed).
read_identities_from_file (path: impl AsRef<Path>) -> Result<Vec<Box<dyn age::Identity>>, LibrageError> Reads unencrypted identities (age and SSH) from a file.
read_identities_from_file_with_passphrase (path, passphrase: Option<&str>) -> Result<Vec<Box<dyn age::Identity>>, LibrageError> Reads identities, supporting encrypted SSH keys with the given passphrase.
read_identity_file (path: impl AsRef<Path>) -> Result<Vec<Box<dyn age::Identity>>, LibrageError> Alias for read_identities_from_file.
read_identity_file_with_passphrase (path, passphrase: Option<&str>) -> Result<Vec<Box<dyn age::Identity>>, LibrageError> Alias for read_identities_from_file_with_passphrase.

Error Handling

LibrageError is an enum with the following variants:

  • KeyGenFailed
  • SerializationFailed
  • InvalidPublicKey
  • InvalidSecretKey
  • InvalidSshKey
  • EncryptionFailed
  • DecryptionFailed
  • Io (transparently from std::io::Error)
  • InvalidInput
  • ArmorError
  • ExcessiveWork { required: u8, target: u8 }
  • NoMatchingKeys
  • MissingRecipients
  • MixedRecipientAndPassphrase
  • UnknownFormat

LibrageError implements std::error::Error and From<age::EncryptError>, From<age::DecryptError>, From<age::IdentityFileConvertError>.
It also implements Into<ErrorBody> for automatic conversion to a structured error.

ErrorBody contains:

  • code: String – machine‑readable error code (e.g., INVALID_PUBLIC_KEY)
  • message: String – human‑readable description

Example:

let res = encrypt(b"data", "bad_key");
assert!(!res.success);
let err = res.error.unwrap();
println!("Error code: {}, message: {}", err.code, err.message);

Security Notes

  • Secret key zeroization: KeyGenData::secret_key is wrapped in Zeroizing<String>. Once the value goes out of scope, it is overwritten with zeros.
  • Ciphertext/plaintext zeroization: Both EncryptOutput and DecryptOutput wrap their byte vectors in Zeroizing<Vec<u8>>.
  • Passphrase handling:
    • Functions that accept &str (e.g., encrypt_with_passphrase) do not zeroize the input. Use them only when the passphrase lifetime is short or the environment is trusted.
    • For long‑lived or sensitive applications, consider using age::secrecy::SecretString externally and then passing the passphrase as &str (the library will internally process it with SecretString where possible).
  • SSH private keys: When decrypting with an encrypted SSH key, the passphrase is processed through SecretString internally, but the original &str provided by the caller is not altered. Consider reading passphrases directly into SecretString if possible.

Running Tests

cargo fmt --all
cargo clippy -- -D warnings
cargo test

License

This project is licensed under the MIT License.

Contributing

Contributions are welcome. Please ensure your code passes the checks above before submitting a pull request.

About

A safe, simple, and complete Rust wrapper for the rage (age) encryption library.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors