Skip to main content

age_plugin/
recipient.rs

1//! Recipient plugin helpers.
2
3use age_core::{
4    format::{is_arbitrary_string, FileKey, Stanza},
5    plugin::{self, BidirSend, Connection},
6    primitives::bech32_decode,
7    secrecy::SecretString,
8};
9use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
10
11use std::collections::HashSet;
12use std::convert::Infallible;
13use std::io;
14
15use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX, PLUGIN_RECIPIENT_PREFIX};
16
17const ADD_RECIPIENT: &str = "add-recipient";
18const ADD_IDENTITY: &str = "add-identity";
19const WRAP_FILE_KEY: &str = "wrap-file-key";
20const EXTENSION_LABELS: &str = "extension-labels";
21const RECIPIENT_STANZA: &str = "recipient-stanza";
22const LABELS: &str = "labels";
23
24/// The interface that age implementations will use to interact with an age plugin.
25///
26/// Implementations of this trait will be used within the [`recipient-v1`] state machine.
27///
28/// The trait methods are always called in this order:
29/// - [`Self::add_recipient`] / [`Self::add_identity`] (in any order, including
30///   potentially interleaved).
31/// - [`Self::labels`] (once all recipients and identities have been added).
32/// - [`Self::wrap_file_keys`]
33///
34/// [`recipient-v1`]: https://c2sp.org/age-plugin#wrapping-with-recipient-v1
35pub trait RecipientPluginV1 {
36    /// Stores a recipient that the user would like to encrypt age files to.
37    ///
38    /// `plugin_name` is the name of the binary that resolved to this plugin.
39    ///
40    /// Returns an error if the recipient is unknown or invalid.
41    fn add_recipient(&mut self, index: usize, plugin_name: &str, bytes: &[u8])
42        -> Result<(), Error>;
43
44    /// Stores an identity that the user would like to encrypt age files to.
45    ///
46    /// `plugin_name` is the name of the binary that resolved to this plugin.
47    ///
48    /// Returns an error if the identity is unknown or invalid.
49    fn add_identity(&mut self, index: usize, plugin_name: &str, bytes: &[u8]) -> Result<(), Error>;
50
51    /// Returns labels that constrain how the stanzas produced by [`Self::wrap_file_keys`]
52    /// may be combined with those from other recipients.
53    ///
54    /// Encryption will succeed only if every recipient returns the same set of labels.
55    /// Subsets or partial overlapping sets are not allowed; all sets must be identical.
56    /// Labels are compared exactly, and are case-sensitive.
57    ///
58    /// Label sets can be used to ensure a recipient is only encrypted to alongside other
59    /// recipients with equivalent properties, or to ensure a recipient is always used
60    /// alone. A recipient with no particular properties to enforce should return an empty
61    /// label set.
62    ///
63    /// Labels can have any value that is a valid arbitrary string (`1*VCHAR` in ABNF),
64    /// but usually take one of several forms:
65    ///   - *Common public label* - used by multiple recipients to permit their stanzas to
66    ///     be used only together. Examples include:
67    ///     - `postquantum` - indicates that the recipient stanzas being generated are
68    ///       postquantum-secure, and that they can only be combined with other stanzas
69    ///       that are also postquantum-secure.
70    ///   - *Common private label* - used by recipients created by the same private entity
71    ///     to permit their recipient stanzas to be used only together. For example,
72    ///     private recipients used in a corporate environment could all send the same
73    ///     private label in order to prevent compliant age clients from simultaneously
74    ///     wrapping file keys with other recipients.
75    ///   - *Random label* - used by recipients that want to ensure their stanzas are not
76    ///     used with any other recipient stanzas. This can be used to produce a file key
77    ///     that is only encrypted to a single recipient stanza, for example to preserve
78    ///     its authentication properties.
79    fn labels(&mut self) -> HashSet<String>;
80
81    /// Wraps each `file_key` to all recipients and identities previously added via
82    /// `add_recipient` and `add_identity`.
83    ///
84    /// Returns a set of stanzas per file key that wrap it to each recipient and identity.
85    /// Plugins may return more than one stanza per "actual recipient", e.g. to support
86    /// multiple formats, to build group aliases, or to act as a proxy.
87    ///
88    /// If one or more recipients or identities could not be wrapped to, `Err(_)` **MUST**
89    /// be returned.
90    ///
91    /// `callbacks` can be used to interact with the user, to have them take some physical
92    /// action or request a secret value.
93    fn wrap_file_keys(
94        &mut self,
95        file_keys: Vec<FileKey>,
96        callbacks: impl Callbacks<Error>,
97    ) -> io::Result<Result<Vec<Vec<Stanza>>, Vec<Error>>>;
98}
99
100impl RecipientPluginV1 for Infallible {
101    fn add_recipient(&mut self, _: usize, _: &str, _: &[u8]) -> Result<(), Error> {
102        // This is never executed.
103        Ok(())
104    }
105
106    fn add_identity(&mut self, _: usize, _: &str, _: &[u8]) -> Result<(), Error> {
107        // This is never executed.
108        Ok(())
109    }
110
111    fn labels(&mut self) -> HashSet<String> {
112        // This is never executed.
113        HashSet::new()
114    }
115
116    fn wrap_file_keys(
117        &mut self,
118        _: Vec<FileKey>,
119        _: impl Callbacks<Error>,
120    ) -> io::Result<Result<Vec<Vec<Stanza>>, Vec<Error>>> {
121        // This is never executed.
122        Ok(Ok(vec![]))
123    }
124}
125
126/// The interface that age plugins can use to interact with an age implementation.
127struct BidirCallbacks<'a, 'b, R: io::Read, W: io::Write>(&'b mut BidirSend<'a, R, W>);
128
129impl<'a, 'b, R: io::Read, W: io::Write> Callbacks<Error> for BidirCallbacks<'a, 'b, R, W> {
130    /// Shows a message to the user.
131    ///
132    /// This can be used to prompt the user to take some physical action, such as
133    /// inserting a hardware key.
134    fn message(&mut self, message: &str) -> plugin::Result<()> {
135        self.0
136            .send("msg", &[], message.as_bytes())
137            .map(|res| res.map(|_| ()))
138    }
139
140    fn confirm(
141        &mut self,
142        message: &str,
143        yes_string: &str,
144        no_string: Option<&str>,
145    ) -> age_core::plugin::Result<bool> {
146        let metadata: Vec<_> = Some(yes_string)
147            .into_iter()
148            .chain(no_string)
149            .map(|s| BASE64_STANDARD_NO_PAD.encode(s))
150            .collect();
151        let metadata: Vec<_> = metadata.iter().map(|s| s.as_str()).collect();
152
153        self.0
154            .send("confirm", &metadata, message.as_bytes())
155            .and_then(|res| match res {
156                Ok(s) => match &s.args[..] {
157                    [x] if x == "yes" => Ok(Ok(true)),
158                    [x] if x == "no" => Ok(Ok(false)),
159                    _ => Err(io::Error::new(
160                        io::ErrorKind::InvalidData,
161                        "Invalid response to confirm command",
162                    )),
163                },
164                Err(e) => Ok(Err(e)),
165            })
166    }
167
168    fn request_public(&mut self, message: &str) -> plugin::Result<String> {
169        self.0
170            .send("request-public", &[], message.as_bytes())
171            .and_then(|res| match res {
172                Ok(s) => String::from_utf8(s.body)
173                    .map_err(|_| {
174                        io::Error::new(io::ErrorKind::InvalidData, "response is not UTF-8")
175                    })
176                    .map(Ok),
177                Err(e) => Ok(Err(e)),
178            })
179    }
180
181    /// Requests a secret value from the user, such as a passphrase.
182    ///
183    /// `message` will be displayed to the user, providing context for the request.
184    fn request_secret(&mut self, message: &str) -> plugin::Result<SecretString> {
185        self.0
186            .send("request-secret", &[], message.as_bytes())
187            .and_then(|res| match res {
188                Ok(s) => String::from_utf8(s.body)
189                    .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "secret is not UTF-8"))
190                    .map(|s| Ok(SecretString::from(s))),
191                Err(e) => Ok(Err(e)),
192            })
193    }
194
195    fn error(&mut self, error: Error) -> plugin::Result<()> {
196        error.send(self.0).map(|()| Ok(()))
197    }
198}
199
200/// The kinds of errors that can occur within the recipient plugin state machine.
201pub enum Error {
202    /// An error caused by a specific recipient.
203    Recipient {
204        /// The index of the recipient.
205        index: usize,
206        /// The error message.
207        message: String,
208    },
209    /// An error caused by a specific identity.
210    Identity {
211        /// The index of the identity.
212        index: usize,
213        /// The error message.
214        message: String,
215    },
216    /// A general error that occured inside the state machine.
217    Internal {
218        /// The error message.
219        message: String,
220    },
221}
222
223impl Error {
224    fn kind(&self) -> &str {
225        match self {
226            Error::Recipient { .. } => "recipient",
227            Error::Identity { .. } => "identity",
228            Error::Internal { .. } => "internal",
229        }
230    }
231
232    fn message(&self) -> &str {
233        match self {
234            Error::Recipient { message, .. } => message,
235            Error::Identity { message, .. } => message,
236            Error::Internal { message } => message,
237        }
238    }
239
240    fn send<R: io::Read, W: io::Write>(self, phase: &mut BidirSend<R, W>) -> io::Result<()> {
241        let index = match self {
242            Error::Recipient { index, .. } | Error::Identity { index, .. } => {
243                Some(index.to_string())
244            }
245            Error::Internal { .. } => None,
246        };
247
248        let metadata = match &index {
249            Some(index) => vec![self.kind(), index],
250            None => vec![self.kind()],
251        };
252
253        phase
254            .send("error", &metadata, self.message().as_bytes())?
255            .unwrap();
256
257        Ok(())
258    }
259}
260
261/// Runs the recipient plugin v1 protocol.
262pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
263    let mut conn = Connection::accept();
264
265    // Phase 1: collect recipients, and file keys to be wrapped
266    let ((recipients, identities), file_keys, labels_supported) = {
267        let (recipients, identities, file_keys, labels_supported) = conn.unidir_receive(
268            (ADD_RECIPIENT, |s| match (&s.args[..], &s.body[..]) {
269                ([recipient], []) => Ok(recipient.clone()),
270                _ => Err(Error::Internal {
271                    message: format!(
272                        "{} command must have exactly one metadata argument and no data",
273                        ADD_RECIPIENT
274                    ),
275                }),
276            }),
277            (ADD_IDENTITY, |s| match (&s.args[..], &s.body[..]) {
278                ([identity], []) => Ok(identity.clone()),
279                _ => Err(Error::Internal {
280                    message: format!(
281                        "{} command must have exactly one metadata argument and no data",
282                        ADD_IDENTITY
283                    ),
284                }),
285            }),
286            (Some(WRAP_FILE_KEY), |s| {
287                // TODO: Should we ignore file key commands with unexpected metadata args?
288                FileKey::try_init_with_mut(|file_key| {
289                    if s.body.len() == file_key.len() {
290                        file_key.copy_from_slice(&s.body);
291                        Ok(())
292                    } else {
293                        Err(Error::Internal {
294                            message: "invalid file key length".to_owned(),
295                        })
296                    }
297                })
298            }),
299            (Some(EXTENSION_LABELS), |_| Ok(())),
300        )?;
301        (
302            match (recipients, identities) {
303                (Ok(r), Ok(i)) if r.is_empty() && i.is_empty() => (
304                    Err(vec![Error::Internal {
305                        message: format!(
306                            "Need at least one {} or {} command",
307                            ADD_RECIPIENT, ADD_IDENTITY
308                        ),
309                    }]),
310                    Err(vec![]),
311                ),
312                r => r,
313            },
314            match file_keys.unwrap() {
315                Ok(f) if f.is_empty() => Err(vec![Error::Internal {
316                    message: format!("Need at least one {} command", WRAP_FILE_KEY),
317                }]),
318                r => r,
319            },
320            match &labels_supported.unwrap() {
321                Ok(v) if v.is_empty() => Ok(false),
322                Ok(v) if v.len() == 1 => Ok(true),
323                _ => Err(vec![Error::Internal {
324                    message: format!("Received more than one {} command", EXTENSION_LABELS),
325                }]),
326            },
327        )
328    };
329
330    // Now that we have the full list of recipients and identities, parse them as Bech32
331    // and add them to the plugin.
332    fn parse_and_add(
333        items: Result<Vec<String>, Vec<Error>>,
334        plugin_name: impl Fn(&str) -> Option<String>,
335        error: impl Fn(usize) -> Error,
336        mut adder: impl FnMut(usize, &str, Vec<u8>) -> Result<(), Error>,
337    ) -> Result<usize, Vec<Error>> {
338        items.and_then(|items| {
339            let count = items.len();
340            let errors: Vec<_> = items
341                .into_iter()
342                .enumerate()
343                .map(|(index, item)| {
344                    bech32_decode(
345                        &item,
346                        |_| (),
347                        |_| Ok(()),
348                        |hrp, bytes| {
349                            plugin_name(hrp.as_str())
350                                .map(|plugin_name| (plugin_name, bytes.collect()))
351                                .ok_or(())
352                        },
353                    )
354                    .map_err(|()| error(index))
355                    .and_then(|(plugin_name, bytes)| adder(index, &plugin_name, bytes))
356                })
357                .filter_map(|res| res.err())
358                .collect();
359
360            if errors.is_empty() {
361                Ok(count)
362            } else {
363                Err(errors)
364            }
365        })
366    }
367    let recipients = parse_and_add(
368        recipients,
369        |hrp| {
370            hrp.strip_prefix(PLUGIN_RECIPIENT_PREFIX)
371                .map(|s| s.to_owned())
372        },
373        |index| Error::Recipient {
374            index,
375            message: "Invalid recipient encoding".to_owned(),
376        },
377        |index, plugin_name, bytes| plugin.add_recipient(index, plugin_name, &bytes),
378    );
379    let identities = parse_and_add(
380        identities,
381        |hrp| {
382            if hrp.len() > PLUGIN_IDENTITY_PREFIX.len()
383                && hrp.starts_with(PLUGIN_IDENTITY_PREFIX)
384                && hrp.ends_with('-')
385            {
386                // TODO: Decide whether to allow plugin names to end in -
387                let name = hrp
388                    .split_at(PLUGIN_IDENTITY_PREFIX.len())
389                    .1
390                    .trim_end_matches('-')
391                    .to_lowercase();
392                Some(name)
393            } else {
394                None
395            }
396        },
397        |index| Error::Identity {
398            index,
399            message: "Invalid identity encoding".to_owned(),
400        },
401        |index, plugin_name, bytes| plugin.add_identity(index, plugin_name, &bytes),
402    );
403
404    let required_labels = plugin.labels();
405
406    let labels = match (labels_supported, required_labels.is_empty()) {
407        (Ok(true), _) | (Ok(false), true) => {
408            if required_labels.contains("") {
409                Err(vec![Error::Internal {
410                    message: "Plugin tried to use the empty string as a label".into(),
411                }])
412            } else if required_labels.iter().all(is_arbitrary_string) {
413                Ok(required_labels)
414            } else {
415                Err(vec![Error::Internal {
416                    message: "Plugin tried to use a label containing an invalid character".into(),
417                }])
418            }
419        }
420        (Ok(false), false) => Err(vec![Error::Internal {
421            message: "Plugin requires labels but client does not support them".into(),
422        }]),
423        (Err(errors), true) => Err(errors),
424        (Err(mut errors), false) => {
425            errors.push(Error::Internal {
426                message: "Plugin requires labels but client does not support them".into(),
427            });
428            Err(errors)
429        }
430    };
431
432    // Phase 2: wrap the file keys or return errors
433    conn.bidir_send(|mut phase| {
434        let (expected_stanzas, file_keys, labels) =
435            match (recipients, identities, file_keys, labels) {
436                (Ok(recipients), Ok(identities), Ok(file_keys), Ok(labels)) => {
437                    (recipients + identities, file_keys, labels)
438                }
439                (recipients, identities, file_keys, labels) => {
440                    for error in recipients
441                        .err()
442                        .into_iter()
443                        .chain(identities.err())
444                        .chain(file_keys.err())
445                        .chain(labels.err())
446                        .flatten()
447                    {
448                        error.send(&mut phase)?;
449                    }
450                    return Ok(());
451                }
452            };
453
454        let labels = labels.iter().map(|s| s.as_str()).collect::<Vec<_>>();
455        // We confirmed above that if `labels` is non-empty, the client supports labels.
456        // So we can unconditionally send this, and will only get an `unsupported`
457        // response if `labels` is empty (where it does not matter).
458        let _ = phase.send(LABELS, &labels, &[])?;
459
460        match plugin.wrap_file_keys(file_keys, BidirCallbacks(&mut phase))? {
461            Ok(files) => {
462                for (file_index, stanzas) in files.into_iter().enumerate() {
463                    // The plugin MUST generate an error if one or more recipients or
464                    // identities cannot be wrapped to.
465                    assert!(stanzas.len() >= expected_stanzas);
466
467                    for stanza in stanzas {
468                        phase
469                            .send_stanza(RECIPIENT_STANZA, &[&file_index.to_string()], &stanza)?
470                            .unwrap();
471                    }
472                }
473            }
474            Err(errors) => {
475                for error in errors {
476                    error.send(&mut phase)?;
477                }
478            }
479        }
480
481        Ok(())
482    })
483}