use crate::gpg::config::GpgConfig;
use crate::gpg::key::{GpgKey, KeyDetail, KeyType};
use anyhow::{anyhow, Result};
use gpgme::context::Keys;
use gpgme::{
Context, Data, ExportMode, Key, KeyListMode, PinentryMode, Protocol,
};
use serde::Serialize;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use tinytemplate::TinyTemplate;
#[derive(Serialize)]
struct ExportContext<'a> {
#[serde(rename = "type")]
pub type_: &'a str,
pub query: &'a str,
pub ext: &'a str,
}
#[derive(Debug)]
pub struct GpgContext {
inner: Context,
pub config: GpgConfig,
}
impl GpgContext {
pub fn new(config: GpgConfig) -> Result<Self> {
let mut context = Context::from_protocol(Protocol::OpenPgp)?;
context.set_key_list_mode(
KeyListMode::LOCAL | KeyListMode::SIGS | KeyListMode::SIG_NOTATIONS,
)?;
context.set_armor(config.armor);
context.set_offline(false);
context.set_pinentry_mode(PinentryMode::Ask)?;
Ok(Self {
inner: context,
config,
})
}
pub fn apply_config(&mut self) {
self.inner.set_armor(self.config.armor);
}
pub fn get_output_file(
&self,
key_type: KeyType,
patterns: Vec<String>,
) -> Result<PathBuf> {
let mut template = TinyTemplate::new();
template.add_template("export_template", &self.config.output_file)?;
let context = ExportContext {
type_: &key_type.to_string(),
query: if patterns.len() == 1 {
&patterns[0]
} else {
"out"
},
ext: if self.config.armor { "asc" } else { "pgp" },
};
let path = self
.config
.output_dir
.join(template.render("export_template", &context)?);
if !path.exists() {
fs::create_dir_all(path.parent().expect("path has no parent"))?;
}
Ok(path)
}
pub fn get_key(
&mut self,
key_type: KeyType,
key_id: String,
) -> Result<Key> {
match key_type {
KeyType::Public => Ok(self.inner.get_key(key_id)?),
KeyType::Secret => Ok(self.inner.get_secret_key(key_id)?),
}
}
fn get_keys_iter(
&mut self,
key_type: KeyType,
patterns: Option<Vec<String>>,
) -> Result<Keys<'_>> {
Ok(match key_type {
KeyType::Public => {
self.inner.find_keys(patterns.unwrap_or_default())?
}
KeyType::Secret => {
self.inner.find_secret_keys(patterns.unwrap_or_default())?
}
})
}
pub fn get_keys(
&mut self,
key_type: KeyType,
patterns: Option<Vec<String>>,
detail_level: KeyDetail,
) -> Result<Vec<GpgKey>> {
Ok(self
.get_keys_iter(key_type, patterns)?
.filter_map(|key| key.ok())
.map(|v| GpgKey::new(v, detail_level))
.collect())
}
pub fn get_all_keys(
&mut self,
detail_level: Option<KeyDetail>,
) -> Result<HashMap<KeyType, Vec<GpgKey>>> {
let mut keys = HashMap::new();
keys.insert(
KeyType::Public,
self.get_keys(
KeyType::Public,
None,
detail_level.unwrap_or_default(),
)?,
);
keys.insert(
KeyType::Secret,
self.get_keys(
KeyType::Secret,
None,
detail_level.unwrap_or_default(),
)?,
);
Ok(keys)
}
pub fn import_keys(
&mut self,
keys: Vec<String>,
read_from_file: bool,
) -> Result<u32> {
let mut imported_keys = 0;
for key in keys {
if read_from_file {
let input = File::open(key)?;
let mut data = Data::from_seekable_stream(input)?;
imported_keys += self.inner.import(&mut data)?.imported();
} else {
imported_keys += self.inner.import(key)?.imported();
}
}
Ok(imported_keys)
}
pub fn get_exported_keys(
&mut self,
key_type: KeyType,
patterns: Option<Vec<String>>,
) -> Result<Vec<u8>> {
let mut output = Vec::new();
let keys = self
.get_keys_iter(key_type, patterns)?
.filter_map(|key| key.ok())
.collect::<Vec<Key>>();
self.inner.export_keys(
&keys,
if key_type == KeyType::Secret {
ExportMode::SECRET
} else {
ExportMode::empty()
},
&mut output,
)?;
if output.is_empty() {
Err(anyhow!("nothing exported"))
} else {
Ok(output)
}
}
pub fn export_keys(
&mut self,
key_type: KeyType,
patterns: Option<Vec<String>>,
) -> Result<String> {
let output = self.get_exported_keys(key_type, patterns.clone())?;
let path =
self.get_output_file(key_type, patterns.unwrap_or_default())?;
File::create(&path)?.write_all(&output)?;
Ok(path.to_string_lossy().to_string())
}
pub fn send_key(&mut self, key_id: String) -> Result<String> {
let keys = self
.get_keys_iter(KeyType::Public, Some(vec![key_id]))?
.filter_map(|key| key.ok())
.collect::<Vec<Key>>();
if let Some(key) = &keys.first() {
self.inner
.export_keys_extern(vec![*key], ExportMode::EXTERN)
.map_err(|e| anyhow!("failed to send key(s): {:?}", e))?;
Ok(key.id().unwrap_or_default().to_string())
} else {
Err(anyhow!("key not found"))
}
}
pub fn delete_key(
&mut self,
key_type: KeyType,
key_id: String,
) -> Result<()> {
match self.get_key(key_type, key_id) {
Ok(key) => match key_type {
KeyType::Public => {
self.inner.delete_key(&key)?;
Ok(())
}
KeyType::Secret => {
self.inner.delete_secret_key(&key)?;
Ok(())
}
},
Err(e) => Err(e),
}
}
}
#[cfg(feature = "gpg-tests")]
#[cfg(test)]
mod tests {
use super::*;
use crate::args::Args;
use pretty_assertions::assert_eq;
use std::env;
use std::fs;
#[test]
fn test_gpg_context() -> Result<()> {
env::set_var(
"GNUPGHOME",
dirs_next::cache_dir()
.unwrap()
.join(env!("CARGO_PKG_NAME"))
.to_str()
.unwrap(),
);
let args = Args::default();
let config = GpgConfig::new(&args)?;
let mut context = GpgContext::new(config)?;
assert_eq!(false, context.config.armor);
context.config.armor = true;
context.apply_config();
assert_eq!(true, context.config.armor);
let keys = context.get_all_keys(None)?;
let key_count = keys.get(&KeyType::Public).unwrap().len();
assert!(context
.get_key(
KeyType::Secret,
keys.get(&KeyType::Public).unwrap()[0].get_id()
)
.is_ok());
let key_id = keys.get(&KeyType::Public).unwrap()[1].get_id();
assert!(context.get_key(KeyType::Public, key_id.clone()).is_ok());
context.config.output_file = String::from("{query}-{type}.{ext}");
assert_eq!(
context.config.output_dir.join(String::from("0x0-sec.asc")),
context
.get_output_file(KeyType::Secret, vec![String::from("0x0")])
.unwrap()
);
let output_file = context.export_keys(KeyType::Public, None)?;
context.delete_key(KeyType::Public, key_id)?;
assert_eq!(
key_count - 1,
context
.get_keys(KeyType::Public, None, KeyDetail::default())
.unwrap()
.len()
);
assert_eq!(
1,
context
.import_keys(vec![output_file.clone()], true)
.unwrap_or_default()
);
assert_eq!(
key_count,
context
.get_keys(KeyType::Public, None, KeyDetail::default())
.unwrap()
.len()
);
fs::remove_file(output_file)?;
Ok(())
}
}