1use crate::gpg::config::GpgConfig;
2use crate::gpg::key::{GpgKey, KeyDetail, KeyType};
3use anyhow::{anyhow, Result};
4use gpgme::context::Keys;
5use gpgme::{
6 Context, Data, ExportMode, Key, KeyListMode, PinentryMode, Protocol,
7};
8use serde::Serialize;
9use std::collections::HashMap;
10use std::fs::{self, File};
11use std::io::Write;
12use std::path::PathBuf;
13use tinytemplate::TinyTemplate;
14
15#[derive(Serialize)]
17struct ExportContext<'a> {
18 #[serde(rename = "type")]
20 pub type_: &'a str,
21 pub query: &'a str,
23 pub ext: &'a str,
25}
26
27#[derive(Debug)]
29pub struct GpgContext {
30 inner: Context,
32 pub config: GpgConfig,
34}
35
36impl GpgContext {
37 pub fn new(config: GpgConfig) -> Result<Self> {
39 let mut context = Context::from_protocol(Protocol::OpenPgp)?;
40 context.set_key_list_mode(
41 KeyListMode::LOCAL | KeyListMode::SIGS | KeyListMode::SIG_NOTATIONS,
42 )?;
43 context.set_armor(config.armor);
44 context.set_offline(false);
45 context.set_pinentry_mode(PinentryMode::Ask)?;
46 Ok(Self {
47 inner: context,
48 config,
49 })
50 }
51
52 pub fn apply_config(&mut self) {
54 self.inner.set_armor(self.config.armor);
55 }
56
57 pub fn get_output_file(
63 &self,
64 key_type: KeyType,
65 patterns: Vec<String>,
66 ) -> Result<PathBuf> {
67 let mut template = TinyTemplate::new();
68 template.add_template("export_template", &self.config.output_file)?;
69 let context = ExportContext {
70 type_: &key_type.to_string(),
71 query: if patterns.len() == 1 {
72 &patterns[0]
73 } else {
74 "out"
75 },
76 ext: if self.config.armor { "asc" } else { "pgp" },
77 };
78 let path = self
79 .config
80 .output_dir
81 .join(template.render("export_template", &context)?);
82 if !path.exists() {
83 fs::create_dir_all(path.parent().expect("path has no parent"))?;
84 }
85 Ok(path)
86 }
87
88 pub fn get_key(
90 &mut self,
91 key_type: KeyType,
92 key_id: String,
93 ) -> Result<Key> {
94 match key_type {
95 KeyType::Public => Ok(self.inner.get_key(key_id)?),
96 KeyType::Secret => Ok(self.inner.get_secret_key(key_id)?),
97 }
98 }
99
100 fn get_keys_iter(
103 &mut self,
104 key_type: KeyType,
105 patterns: Option<Vec<String>>,
106 ) -> Result<Keys<'_>> {
107 Ok(match key_type {
108 KeyType::Public => {
109 self.inner.find_keys(patterns.unwrap_or_default())?
110 }
111 KeyType::Secret => {
112 self.inner.find_secret_keys(patterns.unwrap_or_default())?
113 }
114 })
115 }
116
117 pub fn get_keys(
120 &mut self,
121 key_type: KeyType,
122 patterns: Option<Vec<String>>,
123 detail_level: KeyDetail,
124 ) -> Result<Vec<GpgKey>> {
125 Ok(self
126 .get_keys_iter(key_type, patterns)?
127 .filter_map(|key| key.ok())
128 .map(|v| GpgKey::new(v, detail_level))
129 .collect())
130 }
131
132 pub fn get_all_keys(
134 &mut self,
135 detail_level: Option<KeyDetail>,
136 ) -> Result<HashMap<KeyType, Vec<GpgKey>>> {
137 let mut keys = HashMap::new();
138 keys.insert(
139 KeyType::Public,
140 self.get_keys(
141 KeyType::Public,
142 None,
143 detail_level.unwrap_or_default(),
144 )?,
145 );
146 keys.insert(
147 KeyType::Secret,
148 self.get_keys(
149 KeyType::Secret,
150 None,
151 detail_level.unwrap_or_default(),
152 )?,
153 );
154 Ok(keys)
155 }
156
157 pub fn import_keys(
159 &mut self,
160 keys: Vec<String>,
161 read_from_file: bool,
162 ) -> Result<u32> {
163 let mut imported_keys = 0;
164 for key in keys {
165 if read_from_file {
166 let input = File::open(key)?;
167 let mut data = Data::from_seekable_stream(input)?;
168 imported_keys += self.inner.import(&mut data)?.imported();
169 } else {
170 imported_keys += self.inner.import(key)?.imported();
171 }
172 }
173 Ok(imported_keys)
174 }
175
176 pub fn get_exported_keys(
179 &mut self,
180 key_type: KeyType,
181 patterns: Option<Vec<String>>,
182 ) -> Result<Vec<u8>> {
183 let mut output = Vec::new();
184 let keys = self
185 .get_keys_iter(key_type, patterns)?
186 .filter_map(|key| key.ok())
187 .collect::<Vec<Key>>();
188 self.inner.export_keys(
189 &keys,
190 if key_type == KeyType::Secret {
191 ExportMode::SECRET
192 } else {
193 ExportMode::empty()
194 },
195 &mut output,
196 )?;
197 if output.is_empty() {
198 Err(anyhow!("nothing exported"))
199 } else {
200 Ok(output)
201 }
202 }
203
204 pub fn export_keys(
206 &mut self,
207 key_type: KeyType,
208 patterns: Option<Vec<String>>,
209 ) -> Result<String> {
210 let output = self.get_exported_keys(key_type, patterns.clone())?;
211 let path =
212 self.get_output_file(key_type, patterns.unwrap_or_default())?;
213 File::create(&path)?.write_all(&output)?;
214 Ok(path.to_string_lossy().to_string())
215 }
216
217 pub fn send_key(&mut self, key_id: String) -> Result<String> {
219 let keys = self
220 .get_keys_iter(KeyType::Public, Some(vec![key_id]))?
221 .filter_map(|key| key.ok())
222 .collect::<Vec<Key>>();
223 if let Some(key) = &keys.first() {
224 self.inner
225 .export_keys_extern(vec![*key], ExportMode::EXTERN)
226 .map_err(|e| anyhow!("failed to send key(s): {:?}", e))?;
227 Ok(key.id().unwrap_or_default().to_string())
228 } else {
229 Err(anyhow!("key not found"))
230 }
231 }
232
233 pub fn delete_key(
238 &mut self,
239 key_type: KeyType,
240 key_id: String,
241 ) -> Result<()> {
242 match self.get_key(key_type, key_id) {
243 Ok(key) => match key_type {
244 KeyType::Public => {
245 self.inner.delete_key(&key)?;
246 Ok(())
247 }
248 KeyType::Secret => {
249 self.inner.delete_secret_key(&key)?;
250 Ok(())
251 }
252 },
253 Err(e) => Err(e),
254 }
255 }
256}
257
258#[cfg(feature = "gpg-tests")]
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::args::Args;
263 use pretty_assertions::assert_eq;
264 use std::env;
265 use std::fs;
266 #[test]
267 fn test_gpg_context() -> Result<()> {
268 env::set_var(
269 "GNUPGHOME",
270 dirs_next::cache_dir()
271 .unwrap()
272 .join(env!("CARGO_PKG_NAME"))
273 .to_str()
274 .unwrap(),
275 );
276 let args = Args::default();
277 let config = GpgConfig::new(&args)?;
278 let mut context = GpgContext::new(config)?;
279 assert_eq!(false, context.config.armor);
280 context.config.armor = true;
281 context.apply_config();
282 assert_eq!(true, context.config.armor);
283 let keys = context.get_all_keys(None)?;
284 let key_count = keys.get(&KeyType::Public).unwrap().len();
285 assert!(context
286 .get_key(
287 KeyType::Secret,
288 keys.get(&KeyType::Public).unwrap()[0].get_id()
289 )
290 .is_ok());
291 let key_id = keys.get(&KeyType::Public).unwrap()[1].get_id();
292 assert!(context.get_key(KeyType::Public, key_id.clone()).is_ok());
293 context.config.output_file = String::from("{query}-{type}.{ext}");
294 assert_eq!(
295 context.config.output_dir.join(String::from("0x0-sec.asc")),
296 context
297 .get_output_file(KeyType::Secret, vec![String::from("0x0")])
298 .unwrap()
299 );
300 let output_file = context.export_keys(KeyType::Public, None)?;
301 context.delete_key(KeyType::Public, key_id)?;
302 assert_eq!(
303 key_count - 1,
304 context
305 .get_keys(KeyType::Public, None, KeyDetail::default())
306 .unwrap()
307 .len()
308 );
309 assert_eq!(
310 1,
311 context
312 .import_keys(vec![output_file.clone()], true)
313 .unwrap_or_default()
314 );
315 assert_eq!(
316 key_count,
317 context
318 .get_keys(KeyType::Public, None, KeyDetail::default())
319 .unwrap()
320 .len()
321 );
322 fs::remove_file(output_file)?;
323 Ok(())
324 }
325}