Skip to main content

gpg_tui/gpg/
context.rs

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/// Context to use for rendering the output template.
16#[derive(Serialize)]
17struct ExportContext<'a> {
18	/// Key type.
19	#[serde(rename = "type")]
20	pub type_: &'a str,
21	/// Export pattern.
22	pub query: &'a str,
23	/// File extension.
24	pub ext: &'a str,
25}
26
27/// A context for cryptographic operations.
28#[derive(Debug)]
29pub struct GpgContext {
30	/// GPGME context type.
31	inner: Context,
32	/// GPGME configuration manager.
33	pub config: GpgConfig,
34}
35
36impl GpgContext {
37	/// Constructs a new instance of `GpgContext`.
38	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	/// Applies the current configuration values to the context.
53	pub fn apply_config(&mut self) {
54		self.inner.set_armor(self.config.armor);
55	}
56
57	/// Returns the configured file path.
58	///
59	/// [`output_dir`] is used for output directory.
60	///
61	/// [`output_dir`]: GpgConfig::output_dir
62	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	/// Returns the public/secret key with the specified ID.
89	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	/// Returns an iterator over a list of all public/secret keys
101	/// matching one or more of the specified patterns.
102	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	/// Returns a list of all public/secret keys matching
118	/// one or more of the specified patterns.
119	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	/// Returns the all available keys and their types in a HashMap.
133	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	/// Adds the given keys to the keyring.
158	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	/// Returns the exported public/secret keys
177	/// matching one or more of the specified patterns.
178	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	/// Exports keys and saves them to the specified/default path.
205	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	/// Sends the given key to the default keyserver.
218	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	/// Deletes the specified public/secret key.
234	///
235	/// Searches the keyring for finding the specified
236	/// key ID for deleting it.
237	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}