age/cli_common/
recipients.rs

1use std::io::{self, BufReader};
2
3use super::StdinGuard;
4use super::{identities::parse_identity_files, ReadError};
5use crate::identity::RecipientsAccumulator;
6use crate::{x25519, Recipient};
7
8#[cfg(feature = "plugin")]
9use crate::{cli_common::UiCallbacks, plugin};
10
11#[cfg(not(feature = "plugin"))]
12use std::convert::Infallible;
13
14#[cfg(feature = "ssh")]
15use crate::ssh;
16
17#[cfg(any(feature = "armor", feature = "plugin"))]
18use crate::EncryptError;
19
20/// Handles error mapping for the given SSH recipient parser.
21///
22/// Returns `Ok(None)` if the parser finds a parseable value that should be ignored. This
23/// case is for handling SSH recipient types that may occur in files we want to be able to
24/// parse, but that we do not directly support.
25#[cfg(feature = "ssh")]
26fn parse_ssh_recipient<F, G>(
27    parser: F,
28    invalid: G,
29    filename: &str,
30) -> Result<Option<Box<dyn Recipient + Send>>, ReadError>
31where
32    F: FnOnce() -> Result<ssh::Recipient, ssh::ParseRecipientKeyError>,
33    G: FnOnce() -> Result<Option<Box<dyn Recipient + Send>>, ReadError>,
34{
35    use ssh::{ParseRecipientKeyError, UnsupportedKey};
36
37    match parser() {
38        Ok(pk) => Ok(Some(Box::new(pk))),
39        Err(e) => match e {
40            ParseRecipientKeyError::Ignore => Ok(None),
41            ParseRecipientKeyError::Invalid(_) => invalid(),
42            ParseRecipientKeyError::RsaModulusTooLarge => Err(ReadError::RsaModulusTooLarge),
43            ParseRecipientKeyError::RsaModulusTooSmall => Err(ReadError::RsaModulusTooSmall),
44            ParseRecipientKeyError::Unsupported(key_type) => Err(ReadError::UnsupportedKey(
45                filename.to_string(),
46                UnsupportedKey::from_key_type(key_type),
47            )),
48        },
49    }
50}
51
52/// Parses a recipient from a string.
53fn parse_recipient(
54    _filename: &str,
55    s: String,
56    recipients: &mut RecipientsAccumulator,
57) -> Result<(), ReadError> {
58    if let Ok(pk) = s.parse::<x25519::Recipient>() {
59        recipients.push(Box::new(pk));
60    } else if let Some(pk) = {
61        #[cfg(feature = "ssh")]
62        {
63            parse_ssh_recipient(|| s.parse::<ssh::Recipient>(), || Ok(None), _filename)?
64        }
65
66        #[cfg(not(feature = "ssh"))]
67        None
68    } {
69        recipients.push(pk);
70    } else if let Some(_recipient) = {
71        #[cfg(feature = "plugin")]
72        {
73            // TODO Do something with the error?
74            s.parse::<plugin::Recipient>().ok()
75        }
76
77        #[cfg(not(feature = "plugin"))]
78        None::<Infallible>
79    } {
80        #[cfg(feature = "plugin")]
81        recipients.push_plugin(_recipient);
82    } else {
83        return Err(ReadError::InvalidRecipient(s));
84    }
85
86    Ok(())
87}
88
89/// Reads file contents as a list of recipients
90fn read_recipients_list<R: io::BufRead>(
91    filename: &str,
92    buf: R,
93    recipients: &mut RecipientsAccumulator,
94) -> Result<(), ReadError> {
95    for (line_number, line) in buf.lines().enumerate() {
96        let line = line?;
97
98        // Skip empty lines and comments
99        if line.is_empty() || line.find('#') == Some(0) {
100            continue;
101        } else if let Err(_e) = parse_recipient(filename, line, recipients) {
102            #[cfg(feature = "ssh")]
103            match _e {
104                ReadError::RsaModulusTooLarge
105                | ReadError::RsaModulusTooSmall
106                | ReadError::UnsupportedKey(_, _) => {
107                    return Err(io::Error::new(io::ErrorKind::InvalidData, _e.to_string()).into());
108                }
109                _ => (),
110            }
111
112            // Return a line number in place of the line, so we don't leak the file
113            // contents in error messages.
114            return Err(ReadError::InvalidRecipientsFile {
115                filename: filename.to_owned(),
116                line_number: line_number + 1,
117            });
118        }
119    }
120
121    Ok(())
122}
123
124/// Reads recipients from the provided arguments.
125///
126/// `recipients_file_strings` and `identity_strings` may collectively contain at most one
127/// entry of `"-"`, which will be interpreted as reading from standard input. An error
128/// will be returned if `stdin_guard` is guarding an existing usage of standard input.
129pub fn read_recipients(
130    recipient_strings: Vec<String>,
131    recipients_file_strings: Vec<String>,
132    identity_strings: Vec<String>,
133    max_work_factor: Option<u8>,
134    stdin_guard: &mut StdinGuard,
135) -> Result<Vec<Box<dyn Recipient + Send>>, ReadError> {
136    let mut recipients = RecipientsAccumulator::new();
137
138    for arg in recipient_strings {
139        parse_recipient("", arg, &mut recipients)?;
140    }
141
142    for arg in recipients_file_strings {
143        let f = stdin_guard.open(arg.clone()).map_err(|e| match e {
144            ReadError::Io(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {
145                ReadError::MissingRecipientsFile(arg.clone())
146            }
147            _ => e,
148        })?;
149        let buf = BufReader::new(f);
150        read_recipients_list(&arg, buf, &mut recipients)?;
151    }
152
153    parse_identity_files::<_, ReadError>(
154        identity_strings,
155        max_work_factor,
156        stdin_guard,
157        &mut recipients,
158        #[cfg(feature = "armor")]
159        |recipients, identity| {
160            recipients.extend(identity.recipients().map_err(|e| {
161                // Only one error can occur here.
162                if let EncryptError::EncryptedIdentities(e) = e {
163                    ReadError::EncryptedIdentities(e)
164                } else {
165                    unreachable!()
166                }
167            })?);
168            Ok(())
169        },
170        #[cfg(feature = "ssh")]
171        |recipients, filename, identity| {
172            let recipient = parse_ssh_recipient(
173                || ssh::Recipient::try_from(identity),
174                || Err(ReadError::InvalidRecipient(filename.to_owned())),
175                filename,
176            )?
177            .expect("unsupported identities were already handled");
178            recipients.push(recipient);
179            Ok(())
180        },
181        |recipients, identity_file| {
182            recipients.with_identities(identity_file);
183            Ok(())
184        },
185    )?;
186
187    recipients
188        .build(
189            #[cfg(feature = "plugin")]
190            UiCallbacks,
191        )
192        .map_err(|_e| {
193            // Only one error can occur here.
194            #[cfg(feature = "plugin")]
195            {
196                if let EncryptError::MissingPlugin { binary_name } = _e {
197                    ReadError::MissingPlugin { binary_name }
198                } else {
199                    unreachable!()
200                }
201            }
202
203            #[cfg(not(feature = "plugin"))]
204            unreachable!()
205        })
206}