7 releases (breaking)

0.5.0 Feb 4, 2024
0.4.0 Oct 27, 2022
0.3.0 May 2, 2022
0.2.1 Dec 27, 2021
0.0.0 Feb 9, 2020

#1 in #decrypting

Download history 60/week @ 2023-10-30 48/week @ 2023-11-06 66/week @ 2023-11-13 73/week @ 2023-11-20 69/week @ 2023-11-27 47/week @ 2023-12-04 66/week @ 2023-12-11 51/week @ 2023-12-18 64/week @ 2023-12-25 49/week @ 2024-01-01 52/week @ 2024-01-08 49/week @ 2024-01-15 112/week @ 2024-01-22 122/week @ 2024-01-29 60/week @ 2024-02-05 83/week @ 2024-02-12

389 downloads per month
Used in 4 crates (3 directly)


1.5K SLoC

age-plugin Rust library

This crate provides an API for building age plugins.


The age file encryption format follows the "one well-oiled joint" design philosophy. The mechanism for extensibility (within a particular format version) is the recipient stanzas within the age header: file keys can be wrapped in any number of ways, and age clients are required to ignore stanzas that they do not understand.

The core APIs that exercise this mechanism are:

  • A recipient that wraps a file key and returns a stanza.
  • An identity that unwraps a stanza and returns a file key.

The age plugin system provides a mechanism for exposing these core APIs across process boundaries. It has two main components:

  • A map from recipients and identities to plugin binaries.
  • State machines for wrapping and unwrapping file keys.

With this composable design, you can implement a recipient or identity that you might use directly with the age library crate, and also deploy it as a plugin binary for use with clients like rage.

Mapping recipients and identities to plugin binaries

age plugins are identified by an arbitrary case-insensitive string NAME. This string is used in three places:

  • Plugin-compatible recipients are encoded using Bech32 with the HRP age1name (lowercase).
  • Plugin-compatible identities are encoded using Bech32 with the HRP AGE-PLUGIN-NAME- (uppercase).
  • Plugin binaries (to be started by age clients) are named age-plugin-name.

Users interact with age clients by providing either recipients for file encryption, or identities for file decryption. When a plugin recipient or identity is provided, the age client searches the PATH for a binary with the corresponding plugin name.

Recipient stanza types are not required to be correlated to specific plugin names. When decrypting, age clients will pass all recipient stanzas to every connected plugin. Plugins MUST ignore stanzas that they do not know about.

A plugin binary may handle multiple recipient or identity types by being present in the PATH under multiple names. This can be implemented with symlinks or aliases to the canonical binary.

Multiple plugin binaries can support the same recipient and identity types; the first binary found in the PATH will be used by age clients. Some Unix OSs support "alternatives", which plugin binaries should leverage if they provide support for a common recipient or identity type.

Note that the identity specified by a user doesn't need to point to a specific decryption key, or indeed contain any key material at all. It only needs to contain sufficient information for the plugin to locate the necessary key material.

Standard age keys

A plugin MAY support decrypting files encrypted to native age recipients, by including support for the x25519 recipient stanza. Such plugins will pick their own name, and users will use identity files containing identities that specify that plugin name.

Example plugin binary

The following example uses clap to parse CLI arguments, but any argument parsing logic will work as long as it can detect the --age-plugin=STATE_MACHINE flag.

use age_core::format::{FileKey, Stanza};
use age_plugin::{
    identity::{self, IdentityPluginV1},
    recipient::{self, RecipientPluginV1},
    Callbacks, run_state_machine,
use clap::Parser;

use std::collections::HashMap;
use std::io;

struct RecipientPlugin;

impl RecipientPluginV1 for RecipientPlugin {
    fn add_recipient(
        &mut self,
        index: usize,
        plugin_name: &str,
        bytes: &[u8],
    ) -> Result<(), recipient::Error> {

    fn add_identity(
        &mut self,
        index: usize,
        plugin_name: &str,
        bytes: &[u8]
    ) -> Result<(), recipient::Error> {

    fn wrap_file_keys(
        &mut self,
        file_keys: Vec<FileKey>,
        mut callbacks: impl Callbacks<recipient::Error>,
    ) -> io::Result<Result<Vec<Vec<Stanza>>, Vec<recipient::Error>>> {

struct IdentityPlugin;

impl IdentityPluginV1 for IdentityPlugin {
    fn add_identity(
        &mut self,
        index: usize,
        plugin_name: &str,
        bytes: &[u8]
    ) -> Result<(), identity::Error> {

    fn unwrap_file_keys(
        &mut self,
        files: Vec<Vec<Stanza>>,
        mut callbacks: impl Callbacks<identity::Error>,
    ) -> io::Result<HashMap<usize, Result<FileKey, Vec<identity::Error>>>> {

#[derive(Debug, Parser)]
struct PluginOptions {
    #[arg(help = "run the given age plugin state machine", long)]
    age_plugin: Option<String>,

fn main() -> io::Result<()> {
    let opts = PluginOptions::parse();

    if let Some(state_machine) = opts.age_plugin {
        // The plugin was started by an age client; run the state machine.
            Some(|| RecipientPlugin),
            Some(|| IdentityPlugin),
        return Ok(());

    // Here you can assume the binary is being run directly by a user,
    // and perform administrative tasks like generating keys.



Licensed under either of

at your option.


Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.


~183K SLoC