1use age_core::{
4 format::{FileKey, Stanza},
5 plugin::{self, BidirSend, Connection},
6 secrecy::{ExposeSecret, SecretString},
7};
8use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
9use bech32::FromBase32;
10
11use std::collections::HashMap;
12use std::convert::Infallible;
13use std::io;
14
15use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX};
16
17const ADD_IDENTITY: &str = "add-identity";
18const RECIPIENT_STANZA: &str = "recipient-stanza";
19
20pub trait IdentityPluginV1 {
26 fn add_identity(&mut self, index: usize, plugin_name: &str, bytes: &[u8]) -> Result<(), Error>;
32
33 fn unwrap_file_keys(
52 &mut self,
53 files: Vec<Vec<Stanza>>,
54 callbacks: impl Callbacks<Error>,
55 ) -> io::Result<HashMap<usize, Result<FileKey, Vec<Error>>>>;
56}
57
58impl IdentityPluginV1 for Infallible {
59 fn add_identity(&mut self, _: usize, _: &str, _: &[u8]) -> Result<(), Error> {
60 Ok(())
62 }
63
64 fn unwrap_file_keys(
65 &mut self,
66 _: Vec<Vec<Stanza>>,
67 _: impl Callbacks<Error>,
68 ) -> io::Result<HashMap<usize, Result<FileKey, Vec<Error>>>> {
69 Ok(HashMap::new())
71 }
72}
73
74struct BidirCallbacks<'a, 'b, R: io::Read, W: io::Write>(&'b mut BidirSend<'a, R, W>);
76
77impl<'a, 'b, R: io::Read, W: io::Write> Callbacks<Error> for BidirCallbacks<'a, 'b, R, W> {
78 fn message(&mut self, message: &str) -> plugin::Result<()> {
83 self.0
84 .send("msg", &[], message.as_bytes())
85 .map(|res| res.map(|_| ()))
86 }
87
88 fn confirm(
89 &mut self,
90 message: &str,
91 yes_string: &str,
92 no_string: Option<&str>,
93 ) -> age_core::plugin::Result<bool> {
94 let metadata: Vec<_> = Some(yes_string)
95 .into_iter()
96 .chain(no_string)
97 .map(|s| BASE64_STANDARD_NO_PAD.encode(s))
98 .collect();
99 let metadata: Vec<_> = metadata.iter().map(|s| s.as_str()).collect();
100
101 self.0
102 .send("confirm", &metadata, message.as_bytes())
103 .and_then(|res| match res {
104 Ok(s) => match &s.args[..] {
105 [x] if x == "yes" => Ok(Ok(true)),
106 [x] if x == "no" => Ok(Ok(false)),
107 _ => Err(io::Error::new(
108 io::ErrorKind::InvalidData,
109 "Invalid response to confirm command",
110 )),
111 },
112 Err(e) => Ok(Err(e)),
113 })
114 }
115
116 fn request_public(&mut self, message: &str) -> plugin::Result<String> {
117 self.0
118 .send("request-public", &[], message.as_bytes())
119 .and_then(|res| match res {
120 Ok(s) => String::from_utf8(s.body)
121 .map_err(|_| {
122 io::Error::new(io::ErrorKind::InvalidData, "response is not UTF-8")
123 })
124 .map(Ok),
125 Err(e) => Ok(Err(e)),
126 })
127 }
128
129 fn request_secret(&mut self, message: &str) -> plugin::Result<SecretString> {
133 self.0
134 .send("request-secret", &[], message.as_bytes())
135 .and_then(|res| match res {
136 Ok(s) => String::from_utf8(s.body)
137 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "secret is not UTF-8"))
138 .map(|s| Ok(SecretString::from(s))),
139 Err(e) => Ok(Err(e)),
140 })
141 }
142
143 fn error(&mut self, error: Error) -> plugin::Result<()> {
144 error.send(self.0).map(|()| Ok(()))
145 }
146}
147
148pub enum Error {
150 Identity {
152 index: usize,
154 message: String,
156 },
157 Internal {
159 message: String,
161 },
162 Stanza {
168 file_index: usize,
170 stanza_index: usize,
172 message: String,
174 },
175}
176
177impl Error {
178 fn kind(&self) -> &str {
179 match self {
180 Error::Identity { .. } => "identity",
181 Error::Internal { .. } => "internal",
182 Error::Stanza { .. } => "stanza",
183 }
184 }
185
186 fn message(&self) -> &str {
187 match self {
188 Error::Identity { message, .. } => message,
189 Error::Internal { message } => message,
190 Error::Stanza { message, .. } => message,
191 }
192 }
193
194 fn send<R: io::Read, W: io::Write>(self, phase: &mut BidirSend<R, W>) -> io::Result<()> {
195 let index = match self {
196 Error::Identity { index, .. } => Some((index.to_string(), None)),
197 Error::Internal { .. } => None,
198 Error::Stanza {
199 file_index,
200 stanza_index,
201 ..
202 } => Some((file_index.to_string(), Some(stanza_index.to_string()))),
203 };
204
205 let metadata = match &index {
206 Some((file_index, Some(stanza_index))) => vec![self.kind(), file_index, stanza_index],
207 Some((index, None)) => vec![self.kind(), index],
208 None => vec![self.kind()],
209 };
210
211 phase
212 .send("error", &metadata, self.message().as_bytes())?
213 .unwrap();
214
215 Ok(())
216 }
217}
218
219pub(crate) fn run_v1<P: IdentityPluginV1>(mut plugin: P) -> io::Result<()> {
221 let mut conn = Connection::accept();
222
223 let (identities, recipient_stanzas) = {
225 let (identities, stanzas, _, _) = conn.unidir_receive(
226 (ADD_IDENTITY, |s| match (&s.args[..], &s.body[..]) {
227 ([identity], []) => Ok(identity.clone()),
228 _ => Err(Error::Internal {
229 message: format!(
230 "{} command must have exactly one metadata argument and no data",
231 ADD_IDENTITY
232 ),
233 }),
234 }),
235 (RECIPIENT_STANZA, |mut s| {
236 if s.args.len() >= 2 {
237 let file_index = s.args.remove(0);
238 s.tag = s.args.remove(0);
239 file_index
240 .parse::<usize>()
241 .map(|i| (i, s))
242 .map_err(|_| Error::Internal {
243 message: format!(
244 "first metadata argument to {} must be an integer",
245 RECIPIENT_STANZA
246 ),
247 })
248 } else {
249 Err(Error::Internal {
250 message: format!(
251 "{} command must have at least two metadata arguments",
252 RECIPIENT_STANZA
253 ),
254 })
255 }
256 }),
257 (None, |_| Ok(())),
258 (None, |_| Ok(())),
259 )?;
260
261 let identities = identities.and_then(|items| {
264 let errors: Vec<_> = items
265 .into_iter()
266 .enumerate()
267 .map(|(index, item)| {
268 bech32::decode(&item)
269 .ok()
270 .and_then(|(hrp, data, variant)| {
271 if hrp.starts_with(PLUGIN_IDENTITY_PREFIX)
272 && hrp.ends_with('-')
273 && variant == bech32::Variant::Bech32
274 {
275 Vec::from_base32(&data).ok().map(|data| (hrp, data))
276 } else {
277 None
278 }
279 })
280 .ok_or_else(|| Error::Identity {
281 index,
282 message: "Invalid identity encoding".to_owned(),
283 })
284 .and_then(|(hrp, bytes)| {
285 plugin.add_identity(
286 index,
287 &hrp[PLUGIN_IDENTITY_PREFIX.len()..hrp.len() - 1],
288 &bytes,
289 )
290 })
291 })
292 .filter_map(|res| res.err())
293 .collect();
294
295 if errors.is_empty() {
296 Ok(())
297 } else {
298 Err(errors)
299 }
300 });
301
302 let stanzas = stanzas.and_then(|recipient_stanzas| {
303 let mut stanzas: Vec<Vec<Stanza>> = Vec::new();
304 let mut errors: Vec<Error> = vec![];
305 for (file_index, stanza) in recipient_stanzas {
306 if let Some(file) = stanzas.get_mut(file_index) {
307 file.push(stanza);
308 } else if stanzas.len() == file_index {
309 stanzas.push(vec![stanza]);
310 } else {
311 errors.push(Error::Internal {
312 message: format!(
313 "{} file indices are not ordered and monotonically increasing",
314 RECIPIENT_STANZA
315 ),
316 });
317 }
318 }
319 if errors.is_empty() {
320 Ok(stanzas)
321 } else {
322 Err(errors)
323 }
324 });
325
326 (identities, stanzas)
327 };
328
329 conn.bidir_send(|mut phase| {
331 let stanzas = match (identities, recipient_stanzas) {
332 (Ok(()), Ok(stanzas)) => stanzas,
333 (Err(errors1), Err(errors2)) => {
334 for error in errors1.into_iter().chain(errors2.into_iter()) {
335 error.send(&mut phase)?;
336 }
337 return Ok(());
338 }
339 (Err(errors), _) | (_, Err(errors)) => {
340 for error in errors {
341 error.send(&mut phase)?;
342 }
343 return Ok(());
344 }
345 };
346
347 let unwrapped = plugin.unwrap_file_keys(stanzas, BidirCallbacks(&mut phase))?;
348
349 for (file_index, file_key) in unwrapped {
350 match file_key {
351 Ok(file_key) => {
352 phase
353 .send(
354 "file-key",
355 &[&format!("{}", file_index)],
356 file_key.expose_secret(),
357 )?
358 .unwrap();
359 }
360 Err(errors) => {
361 for error in errors {
362 error.send(&mut phase)?;
363 }
364 }
365 }
366 }
367
368 Ok(())
369 })
370}