1use age_core::{
4 format::{FileKey, Stanza},
5 plugin::{self, BidirSend, Connection},
6 primitives::bech32_decode,
7 secrecy::{ExposeSecret, SecretString},
8};
9use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
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(
269 &item,
270 |_| "invalid Bech32 encoding",
271 |hrp| {
272 (hrp.len() > PLUGIN_IDENTITY_PREFIX.len()
273 && hrp.as_str().starts_with(PLUGIN_IDENTITY_PREFIX)
274 && hrp.as_str().ends_with('-'))
275 .then_some(())
276 .ok_or("invalid HRP")
277 },
278 |hrp, bytes| Ok((hrp, bytes.collect::<Vec<_>>())),
279 )
280 .map_err(|message| Error::Identity {
281 index,
282 message: message.to_owned(),
283 })
284 .and_then(|(hrp, bytes)| {
285 let name = hrp
287 .as_str()
288 .split_at(PLUGIN_IDENTITY_PREFIX.len())
289 .1
290 .trim_end_matches('-')
291 .to_lowercase();
292 plugin.add_identity(index, &name, &bytes)
293 })
294 })
295 .filter_map(|res| res.err())
296 .collect();
297
298 if errors.is_empty() {
299 Ok(())
300 } else {
301 Err(errors)
302 }
303 });
304
305 let stanzas = stanzas.and_then(|recipient_stanzas| {
306 let mut stanzas: Vec<Vec<Stanza>> = Vec::new();
307 let mut errors: Vec<Error> = vec![];
308 for (file_index, stanza) in recipient_stanzas {
309 if let Some(file) = stanzas.get_mut(file_index) {
310 file.push(stanza);
311 } else if stanzas.len() == file_index {
312 stanzas.push(vec![stanza]);
313 } else {
314 errors.push(Error::Internal {
315 message: format!(
316 "{} file indices are not ordered and monotonically increasing",
317 RECIPIENT_STANZA
318 ),
319 });
320 }
321 }
322 if errors.is_empty() {
323 Ok(stanzas)
324 } else {
325 Err(errors)
326 }
327 });
328
329 (identities, stanzas)
330 };
331
332 conn.bidir_send(|mut phase| {
334 let stanzas = match (identities, recipient_stanzas) {
335 (Ok(()), Ok(stanzas)) => stanzas,
336 (Err(errors1), Err(errors2)) => {
337 for error in errors1.into_iter().chain(errors2.into_iter()) {
338 error.send(&mut phase)?;
339 }
340 return Ok(());
341 }
342 (Err(errors), _) | (_, Err(errors)) => {
343 for error in errors {
344 error.send(&mut phase)?;
345 }
346 return Ok(());
347 }
348 };
349
350 let unwrapped = plugin.unwrap_file_keys(stanzas, BidirCallbacks(&mut phase))?;
351
352 for (file_index, file_key) in unwrapped {
353 match file_key {
354 Ok(file_key) => {
355 phase
356 .send(
357 "file-key",
358 &[&format!("{}", file_index)],
359 file_key.expose_secret(),
360 )?
361 .unwrap();
362 }
363 Err(errors) => {
364 for error in errors {
365 error.send(&mut phase)?;
366 }
367 }
368 }
369 }
370
371 Ok(())
372 })
373}