age_plugin/lib.rs
1//! This crate provides an API for building age plugins.
2//!
3//! # Introduction
4//!
5//! The [age file encryption format] follows the "one well-oiled joint" design philosophy.
6//! The mechanism for extensibility (within a particular format version) is the recipient
7//! stanzas within the age header: file keys can be wrapped in any number of ways, and age
8//! clients are required to ignore stanzas that they do not understand.
9//!
10//! The core APIs that exercise this mechanism are:
11//! - A recipient that wraps a file key and returns a stanza.
12//! - An identity that unwraps a stanza and returns a file key.
13//!
14//! The age plugin system provides a mechanism for exposing these core APIs across process
15//! boundaries. It has two main components:
16//!
17//! - A map from recipients and identities to plugin binaries.
18//! - State machines for wrapping and unwrapping file keys.
19//!
20//! With this composable design, you can implement a recipient or identity that you might
21//! use directly with the [`age`] library crate, and also deploy it as a plugin binary for
22//! use with clients like [`rage`].
23//!
24//! [age file encryption format]: https://age-encryption.org/v1
25//! [`age`]: https://crates.io/crates/age
26//! [`rage`]: https://crates.io/crates/rage
27//!
28//! # Mapping recipients and identities to plugin binaries
29//!
30//! age plugins are identified by an arbitrary case-insensitive string `NAME`. This string
31//! is used in three places:
32//!
33//! - Plugin-compatible recipients are encoded using Bech32 with the HRP `age1name`
34//! (lowercase).
35//! - Plugin-compatible identities are encoded using Bech32 with the HRP
36//! `AGE-PLUGIN-NAME-` (uppercase).
37//! - Plugin binaries (to be started by age clients) are named `age-plugin-name`.
38//!
39//! Users interact with age clients by providing either recipients for file encryption, or
40//! identities for file decryption. When a plugin recipient or identity is provided, the
41//! age client searches the `PATH` for a binary with the corresponding plugin name.
42//!
43//! Recipient stanza types are not required to be correlated to specific plugin names.
44//! When decrypting, age clients will pass all recipient stanzas to every connected
45//! plugin. Plugins MUST ignore stanzas that they do not know about.
46//!
47//! A plugin binary may handle multiple recipient or identity types by being present in
48//! the `PATH` under multiple names. This can be implemented with symlinks or aliases to
49//! the canonical binary.
50//!
51//! Multiple plugin binaries can support the same recipient and identity types; the first
52//! binary found in the `PATH` will be used by age clients. Some Unix OSs support
53//! "alternatives", which plugin binaries should leverage if they provide support for a
54//! common recipient or identity type.
55//!
56//! Note that the identity specified by a user doesn't need to point to a specific
57//! decryption key, or indeed contain any key material at all. It only needs to contain
58//! sufficient information for the plugin to locate the necessary key material.
59//!
60//! ## Standard age keys
61//!
62//! A plugin MAY support decrypting files encrypted to native age recipients, by including
63//! support for the `x25519` recipient stanza. Such plugins will pick their own name, and
64//! users will use identity files containing identities that specify that plugin name.
65//!
66//! # Example plugin binary
67//!
68//! The following example uses `clap` to parse CLI arguments, but any argument parsing
69//! logic will work as long as it can detect the `--age-plugin=STATE_MACHINE` flag.
70//!
71//! ```
72//! use age_core::format::{FileKey, Stanza};
73//! use age_plugin::{
74//! identity::{self, IdentityPluginV1},
75//! print_new_identity,
76//! recipient::{self, RecipientPluginV1},
77//! Callbacks, PluginHandler, run_state_machine,
78//! };
79//! use clap::Parser;
80//!
81//! use std::collections::{HashMap, HashSet};
82//! use std::io;
83//!
84//! struct Handler;
85//!
86//! impl PluginHandler for Handler {
87//! type RecipientV1 = RecipientPlugin;
88//! type IdentityV1 = IdentityPlugin;
89//!
90//! fn recipient_v1(self) -> io::Result<Self::RecipientV1> {
91//! Ok(RecipientPlugin)
92//! }
93//!
94//! fn identity_v1(self) -> io::Result<Self::IdentityV1> {
95//! Ok(IdentityPlugin)
96//! }
97//! }
98//!
99//! struct RecipientPlugin;
100//!
101//! impl RecipientPluginV1 for RecipientPlugin {
102//! fn add_recipient(
103//! &mut self,
104//! index: usize,
105//! plugin_name: &str,
106//! bytes: &[u8],
107//! ) -> Result<(), recipient::Error> {
108//! todo!()
109//! }
110//!
111//! fn add_identity(
112//! &mut self,
113//! index: usize,
114//! plugin_name: &str,
115//! bytes: &[u8]
116//! ) -> Result<(), recipient::Error> {
117//! todo!()
118//! }
119//!
120//! fn labels(&mut self) -> HashSet<String> {
121//! todo!()
122//! }
123//!
124//! fn wrap_file_keys(
125//! &mut self,
126//! file_keys: Vec<FileKey>,
127//! mut callbacks: impl Callbacks<recipient::Error>,
128//! ) -> io::Result<Result<Vec<Vec<Stanza>>, Vec<recipient::Error>>> {
129//! todo!()
130//! }
131//! }
132//!
133//! struct IdentityPlugin;
134//!
135//! impl IdentityPluginV1 for IdentityPlugin {
136//! fn add_identity(
137//! &mut self,
138//! index: usize,
139//! plugin_name: &str,
140//! bytes: &[u8]
141//! ) -> Result<(), identity::Error> {
142//! todo!()
143//! }
144//!
145//! fn unwrap_file_keys(
146//! &mut self,
147//! files: Vec<Vec<Stanza>>,
148//! mut callbacks: impl Callbacks<identity::Error>,
149//! ) -> io::Result<HashMap<usize, Result<FileKey, Vec<identity::Error>>>> {
150//! todo!()
151//! }
152//! }
153//!
154//! #[derive(Debug, Parser)]
155//! struct PluginOptions {
156//! #[arg(help = "run the given age plugin state machine", long)]
157//! age_plugin: Option<String>,
158//! }
159//!
160//! fn main() -> io::Result<()> {
161//! let opts = PluginOptions::parse();
162//!
163//! if let Some(state_machine) = opts.age_plugin {
164//! // The plugin was started by an age client; run the state machine.
165//! run_state_machine(&state_machine, Handler)?;
166//! return Ok(());
167//! }
168//!
169//! // Here you can assume the binary is being run directly by a user,
170//! // and perform administrative tasks like generating keys.
171//!
172//! Ok(())
173//! }
174//! ```
175
176#![forbid(unsafe_code)]
177// Catch documentation errors caused by code changes.
178#![deny(rustdoc::broken_intra_doc_links)]
179#![deny(missing_docs)]
180
181use age_core::secrecy::SecretString;
182use bech32::Variant;
183use std::io;
184
185pub mod identity;
186pub mod recipient;
187
188// Plugin HRPs are age1[name] and AGE-PLUGIN-[NAME]-
189const PLUGIN_RECIPIENT_PREFIX: &str = "age1";
190const PLUGIN_IDENTITY_PREFIX: &str = "age-plugin-";
191
192/// Prints the newly-created identity and corresponding recipient to standard out.
193///
194/// A "created" time is included in the output, set to the current local time.
195pub fn print_new_identity(plugin_name: &str, identity: &[u8], recipient: &[u8]) {
196 use bech32::ToBase32;
197
198 println!(
199 "# created: {}",
200 chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
201 );
202 println!(
203 "# recipient: {}",
204 bech32::encode(
205 &format!("{}{}", PLUGIN_RECIPIENT_PREFIX, plugin_name),
206 recipient.to_base32(),
207 Variant::Bech32
208 )
209 .expect("HRP is valid")
210 );
211 println!(
212 "{}",
213 bech32::encode(
214 &format!("{}{}-", PLUGIN_IDENTITY_PREFIX, plugin_name),
215 identity.to_base32(),
216 Variant::Bech32,
217 )
218 .expect("HRP is valid")
219 .to_uppercase()
220 );
221}
222
223/// Runs the plugin state machine defined by `state_machine`.
224///
225/// This should be triggered if the `--age-plugin=state_machine` flag is provided as an
226/// argument when starting the plugin.
227///
228/// # Panics
229///
230/// The state machine will panic if the `PluginHandler` implementation violates any
231/// **MUST** requirements of the [age plugin specification]. Examples include:
232/// - Returning fewer stanzas from [`RecipientPluginV1::wrap_file_keys`] than the number
233/// of recipients and identities that were provided (instead of returning an error if
234/// any could not be encrypted to).
235/// - Note that this currently prohibits plugins from automatically deduplicating
236/// provided recipients and identities; either duplicate stanzas must be produced, or
237/// an error returned. This prohibition might be lifted in a future release.
238///
239/// [age plugin specification]: https://c2sp.org/age-plugin
240/// [`RecipientPluginV1::wrap_file_keys`]: crate::recipient::RecipientPluginV1::wrap_file_keys
241pub fn run_state_machine(state_machine: &str, handler: impl PluginHandler) -> io::Result<()> {
242 use age_core::plugin::{IDENTITY_V1, RECIPIENT_V1};
243
244 match state_machine {
245 RECIPIENT_V1 => recipient::run_v1(handler.recipient_v1()?),
246 IDENTITY_V1 => identity::run_v1(handler.identity_v1()?),
247 _ => Err(io::Error::new(
248 io::ErrorKind::InvalidInput,
249 "unknown plugin state machine",
250 )),
251 }
252}
253
254/// The interfaces that age implementations will use to interact with an age plugin.
255///
256/// This trait exists to encapsulate the set of arguments to [`run_state_machine`] that
257/// different plugins may want to provide.
258///
259/// # How to implement this trait
260///
261/// ## Full plugins
262///
263/// - Set all associated types to your plugin's implementations.
264/// - Override all default methods of the trait.
265///
266/// ## Recipient-only plugins
267///
268/// - Set [`PluginHandler::RecipientV1`] to your plugin's implementation.
269/// - Override [`PluginHandler::recipient_v1`] to return an instance of your type.
270/// - Set [`PluginHandler::IdentityV1`] to [`std::convert::Infallible`].
271/// - Don't override [`PluginHandler::identity_v1`].
272///
273/// ## Identity-only plugins
274///
275/// - Set [`PluginHandler::RecipientV1`] to [`std::convert::Infallible`].
276/// - Don't override [`PluginHandler::recipient_v1`].
277/// - Set [`PluginHandler::IdentityV1`] to your plugin's implementation.
278/// - Override [`PluginHandler::identity_v1`] to return an instance of your type.
279pub trait PluginHandler: Sized {
280 /// The plugin's [`recipient-v1`] implementation.
281 ///
282 /// [`recipient-v1`]: https://c2sp.org/age-plugin#wrapping-with-recipient-v1
283 type RecipientV1: recipient::RecipientPluginV1;
284
285 /// The plugin's [`identity-v1`] implementation.
286 ///
287 /// [`identity-v1`]: https://c2sp.org/age-plugin#unwrapping-with-identity-v1
288 type IdentityV1: identity::IdentityPluginV1;
289
290 /// Returns an instance of the plugin's [`recipient-v1`] implementation.
291 ///
292 /// [`recipient-v1`]: https://c2sp.org/age-plugin#wrapping-with-recipient-v1
293 fn recipient_v1(self) -> io::Result<Self::RecipientV1> {
294 Err(io::Error::new(
295 io::ErrorKind::InvalidInput,
296 "plugin doesn't support recipient-v1 state machine",
297 ))
298 }
299
300 /// Returns an instance of the plugin's [`identity-v1`] implementation.
301 ///
302 /// [`identity-v1`]: https://c2sp.org/age-plugin#unwrapping-with-identity-v1
303 fn identity_v1(self) -> io::Result<Self::IdentityV1> {
304 Err(io::Error::new(
305 io::ErrorKind::InvalidInput,
306 "plugin doesn't support identity-v1 state machine",
307 ))
308 }
309}
310
311/// The interface that age plugins can use to interact with an age implementation.
312pub trait Callbacks<E> {
313 /// Shows a message to the user.
314 ///
315 /// This can be used to prompt the user to take some physical action, such as
316 /// inserting a hardware key.
317 fn message(&mut self, message: &str) -> age_core::plugin::Result<()>;
318
319 /// Requests that the user provides confirmation for some action.
320 ///
321 /// This can be used to, for example, request that a hardware key the plugin wants to
322 /// try either be plugged in, or skipped.
323 ///
324 /// - `message` is the request or call-to-action to be displayed to the user.
325 /// - `yes_string` and (optionally) `no_string` will be displayed on buttons or next
326 /// to selection options in the user's UI.
327 ///
328 /// Returns:
329 /// - `Ok(true)` if the user selected the option marked with `yes_string`.
330 /// - `Ok(false)` if the user selected the option marked with `no_string` (or the
331 /// default negative confirmation label).
332 /// - `Err(Error::Fail)` if the confirmation request could not be given to the user
333 /// (for example, if there is no UI for displaying messages).
334 /// - `Err(Error::Unsupported)` if the user's client does not support this callback.
335 fn confirm(
336 &mut self,
337 message: &str,
338 yes_string: &str,
339 no_string: Option<&str>,
340 ) -> age_core::plugin::Result<bool>;
341
342 /// Requests a non-secret value from the user.
343 ///
344 /// `message` will be displayed to the user, providing context for the request.
345 ///
346 /// To request secrets, use [`Callbacks::request_secret`].
347 fn request_public(&mut self, message: &str) -> age_core::plugin::Result<String>;
348
349 /// Requests a secret value from the user, such as a passphrase.
350 ///
351 /// `message` will be displayed to the user, providing context for the request.
352 fn request_secret(&mut self, message: &str) -> age_core::plugin::Result<SecretString>;
353
354 /// Sends an error.
355 ///
356 /// Note: This API may be removed in a subsequent API refactor, after we've figured
357 /// out how errors should be handled overall, and how to distinguish between hard and
358 /// soft errors.
359 fn error(&mut self, error: E) -> age_core::plugin::Result<()>;
360}