use age_core::{
format::{FileKey, Stanza},
plugin::{self, BidirSend, Connection},
};
use bech32::FromBase32;
use secrecy::{ExposeSecret, SecretString};
use std::collections::HashMap;
use std::io;
use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX};
const ADD_IDENTITY: &str = "add-identity";
const RECIPIENT_STANZA: &str = "recipient-stanza";
pub trait IdentityPluginV1 {
fn add_identity(&mut self, index: usize, plugin_name: &str, bytes: &[u8]) -> Result<(), Error>;
fn unwrap_file_keys(
&mut self,
files: Vec<Vec<Stanza>>,
callbacks: impl Callbacks<Error>,
) -> io::Result<HashMap<usize, Result<FileKey, Vec<Error>>>>;
}
struct BidirCallbacks<'a, 'b, R: io::Read, W: io::Write>(&'b mut BidirSend<'a, R, W>);
impl<'a, 'b, R: io::Read, W: io::Write> Callbacks<Error> for BidirCallbacks<'a, 'b, R, W> {
fn message(&mut self, message: &str) -> plugin::Result<(), ()> {
self.0
.send("msg", &[], message.as_bytes())
.map(|res| res.map(|_| ()))
}
fn request_public(&mut self, message: &str) -> plugin::Result<String, ()> {
self.0
.send("request-public", &[], message.as_bytes())
.and_then(|res| match res {
Ok(s) => String::from_utf8(s.body)
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "response is not UTF-8")
})
.map(Ok),
Err(()) => Ok(Err(())),
})
}
fn request_secret(&mut self, message: &str) -> plugin::Result<SecretString, ()> {
self.0
.send("request-secret", &[], message.as_bytes())
.and_then(|res| match res {
Ok(s) => String::from_utf8(s.body)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "secret is not UTF-8"))
.map(|s| Ok(SecretString::new(s))),
Err(()) => Ok(Err(())),
})
}
fn error(&mut self, error: Error) -> plugin::Result<(), ()> {
error.send(self.0).map(|()| Ok(()))
}
}
pub enum Error {
Identity {
index: usize,
message: String,
},
Internal {
message: String,
},
Stanza {
file_index: usize,
stanza_index: usize,
message: String,
},
}
impl Error {
fn kind(&self) -> &str {
match self {
Error::Identity { .. } => "identity",
Error::Internal { .. } => "internal",
Error::Stanza { .. } => "stanza",
}
}
fn message(&self) -> &str {
match self {
Error::Identity { message, .. } => &message,
Error::Internal { message } => &message,
Error::Stanza { message, .. } => &message,
}
}
fn send<R: io::Read, W: io::Write>(self, phase: &mut BidirSend<R, W>) -> io::Result<()> {
let index = match self {
Error::Identity { index, .. } => Some((index.to_string(), None)),
Error::Internal { .. } => None,
Error::Stanza {
file_index,
stanza_index,
..
} => Some((file_index.to_string(), Some(stanza_index.to_string()))),
};
let metadata = match &index {
Some((file_index, Some(stanza_index))) => vec![self.kind(), &file_index, &stanza_index],
Some((index, None)) => vec![self.kind(), &index],
None => vec![self.kind()],
};
phase
.send("error", &metadata, self.message().as_bytes())?
.unwrap();
Ok(())
}
}
pub(crate) fn run_v1<P: IdentityPluginV1>(mut plugin: P) -> io::Result<()> {
let mut conn = Connection::accept();
let (identities, recipient_stanzas) = {
let (identities, stanzas, _) = conn.unidir_receive(
(ADD_IDENTITY, |s| match (&s.args[..], &s.body[..]) {
([identity], []) => Ok(identity.clone()),
_ => Err(Error::Internal {
message: format!(
"{} command must have exactly one metadata argument and no data",
ADD_IDENTITY
),
}),
}),
(RECIPIENT_STANZA, |mut s| {
if s.args.len() >= 2 {
let file_index = s.args.remove(0);
s.tag = s.args.remove(0);
file_index
.parse::<usize>()
.map(|i| (i, s))
.map_err(|_| Error::Internal {
message: format!(
"first metadata argument to {} must be an integer",
RECIPIENT_STANZA
),
})
} else {
Err(Error::Internal {
message: format!(
"{} command must have at least two metadata arguments",
RECIPIENT_STANZA
),
})
}
}),
(None, |_| Ok(())),
)?;
let identities = identities.and_then(|items| {
let errors: Vec<_> = items
.into_iter()
.enumerate()
.map(|(index, item)| {
bech32::decode(&item)
.ok()
.and_then(|(hrp, data, variant)| {
if hrp.starts_with(PLUGIN_IDENTITY_PREFIX)
&& hrp.ends_with('-')
&& variant == bech32::Variant::Bech32
{
Vec::from_base32(&data).ok().map(|data| (hrp, data))
} else {
None
}
})
.ok_or_else(|| Error::Identity {
index,
message: "Invalid identity encoding".to_owned(),
})
.and_then(|(hrp, bytes)| {
plugin.add_identity(
index,
&hrp[PLUGIN_IDENTITY_PREFIX.len()..hrp.len() - 1],
&bytes,
)
})
})
.filter_map(|res| res.err())
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
});
let stanzas = stanzas.and_then(|recipient_stanzas| {
let mut stanzas: Vec<Vec<Stanza>> = Vec::new();
let mut errors: Vec<Error> = vec![];
for (file_index, stanza) in recipient_stanzas {
if let Some(file) = stanzas.get_mut(file_index) {
file.push(stanza);
} else if stanzas.len() == file_index {
stanzas.push(vec![stanza]);
} else {
errors.push(Error::Internal {
message: format!(
"{} file indices are not ordered and monotonically increasing",
RECIPIENT_STANZA
),
});
}
}
if errors.is_empty() {
Ok(stanzas)
} else {
Err(errors)
}
});
(identities, stanzas)
};
conn.bidir_send(|mut phase| {
let stanzas = match (identities, recipient_stanzas) {
(Ok(()), Ok(stanzas)) => stanzas,
(Err(errors1), Err(errors2)) => {
for error in errors1.into_iter().chain(errors2.into_iter()) {
error.send(&mut phase)?;
}
return Ok(());
}
(Err(errors), _) | (_, Err(errors)) => {
for error in errors {
error.send(&mut phase)?;
}
return Ok(());
}
};
let unwrapped = plugin.unwrap_file_keys(stanzas, BidirCallbacks(&mut phase))?;
for (file_index, file_key) in unwrapped {
match file_key {
Ok(file_key) => {
phase
.send(
"file-key",
&[&format!("{}", file_index)],
file_key.expose_secret(),
)?
.unwrap();
}
Err(errors) => {
for error in errors {
error.send(&mut phase)?;
}
}
}
}
Ok(())
})
}