sequoia_gpg_agent/
gnupg.rs1#![warn(missing_docs)]
4
5use std::collections::BTreeMap;
6use std::ffi::OsStr;
7use std::path::{Path, PathBuf};
8
9use crate::Result;
10use crate::utils::new_background_command;
11
12#[derive(thiserror::Error, Debug)]
13#[non_exhaustive]
15pub enum Error {
16 #[error("component {0} is not installed")]
18 ComponentMissing(String),
19
20 #[error("gpgconf is not installed")]
22 GPGConfMissing,
23
24 #[error("gpgconf: {0}")]
26 GPGConf(String),
27}
28
29#[derive(Debug)]
31pub struct Context {
32 homedir: Option<PathBuf>,
33 sockets: BTreeMap<String, PathBuf>,
34 ephemeral: Option<tempfile::TempDir>,
35 #[cfg(windows)]
37 cygwin: bool,
38}
39
40impl Context {
41 pub fn new() -> Result<Self> {
43 Self::make(None, None)
44 }
45
46 pub fn with_homedir<P>(homedir: P) -> Result<Self>
48 where P: AsRef<Path>
49 {
50 Self::make(Some(homedir.as_ref()), None)
51 }
52
53 pub fn ephemeral() -> Result<Self> {
58 Self::make(None, Some(tempfile::tempdir()?))
59 }
60
61 fn make(homedir: Option<&Path>, ephemeral: Option<tempfile::TempDir>)
62 -> Result<Self> {
63 let mut sockets: BTreeMap<String, PathBuf> = Default::default();
64
65 let ephemeral_dir = ephemeral.as_ref().map(|tmp| tmp.path());
66 let homedir = ephemeral_dir.or(homedir);
67 let a_gpg_path = Self::gpgconf(&None, &["--list-dirs", "homedir"], 1)?;
70 let first_byte = a_gpg_path.get(0).and_then(|c| c.get(0)).and_then(|c| c.get(0));
71 let gpg_style = match first_byte {
72 Some(b'/') => Mode::Unix,
73 _ => Mode::native(),
74 };
75 let homedir = homedir.map(|dir|
76 convert_path(dir, gpg_style)
77 .unwrap_or_else(|_| PathBuf::from(dir))
78 );
79
80 for fields in Self::gpgconf(&homedir, &["--list-dirs"], 2)? {
81 let key = std::str::from_utf8(&fields[0])?;
82
83 let mut value = std::str::from_utf8(&fields[1])?.to_owned();
86 value = value.replace("%3a", ":");
89 let path = convert_path(&value, Mode::native())?;
91
92 if key == "homedir" && ! path.exists() {
94 return Err(crate::Error::GnuPGHomeMissing(path));
95 }
96
97 let socket = match key.strip_suffix("-socket") {
100 Some(socket) => socket,
101 _ => continue,
102 };
103
104 sockets.insert(socket.into(), path);
105 }
106
107 #[derive(Copy, Clone)]
109 #[allow(dead_code)]
110 enum Mode {
111 Windows,
112 Unix
113 }
114
115 impl Mode {
116 fn native() -> Self {
117 platform! {
118 unix => Mode::Unix,
119 windows => Mode::Windows,
120 }
121 }
122 }
123
124 #[cfg(not(windows))]
125 fn convert_path(path: impl AsRef<OsStr>, mode: Mode) -> Result<PathBuf> {
126 match mode {
127 Mode::Unix => Ok(PathBuf::from(path.as_ref())),
128 Mode::Windows => Err(anyhow::anyhow!(
129 "Converting to Windows-style paths is only supported on Windows"
130 ).into()),
131 }
132 }
133
134 #[cfg(windows)]
135 fn convert_path(path: impl AsRef<OsStr>, mode: Mode) -> Result<PathBuf> {
136 let conversion_type = match mode {
137 Mode::Windows => "--windows",
138 Mode::Unix => "--unix",
139 };
140 Ok(new_background_command("cygpath")
141 .arg(conversion_type)
142 .arg(path.as_ref())
143 .output()
144 .map_err(Into::into)
145 .and_then(|out|
146 if out.status.success() {
147 let output = std::str::from_utf8(&out.stdout)?.trim();
148 Ok(PathBuf::from(output))
149 } else {
150 Err(anyhow::anyhow!(
151 "Executing cygpath encountered error for path {}",
152 path.as_ref().to_string_lossy()
153 ))
154 }
155 )?)
156 }
157
158 Ok(Context {
159 homedir,
160 sockets,
161 ephemeral,
162 #[cfg(windows)]
163 cygwin: cfg!(windows) && matches!(gpg_style, Mode::Unix),
164 })
165 }
166
167 fn gpgconf(homedir: &Option<PathBuf>, arguments: &[&str], nfields: usize)
168 -> Result<Vec<Vec<Vec<u8>>>> {
169 let nl = |&c: &u8| c as char == '\n';
170 let colon = |&c: &u8| c as char == ':';
171
172 let mut gpgconf = new_background_command("gpgconf");
173 if let Some(homedir) = homedir {
174 gpgconf.arg("--homedir").arg(homedir);
175
176 gpgconf.env("GNUPGHOME", homedir);
178 }
179
180 gpgconf.args(arguments);
181
182 let output = gpgconf.output().map_err(|e| {
183 if e.kind() == std::io::ErrorKind::NotFound {
184 Error::GPGConfMissing
185 } else {
186 Error::GPGConf(e.to_string())
187 }
188 })?;
189
190 if output.status.success() {
191 let mut result = Vec::new();
192 for mut line in output.stdout.split(nl) {
193 if line.is_empty() {
194 break;
196 }
197
198 if line[line.len() - 1] == b'\r' {
200 line = &line[..line.len() - 1];
201 }
202
203 let fields =
204 line.splitn(nfields, colon).map(|f| f.to_vec())
205 .collect::<Vec<_>>();
206
207 if fields.len() != nfields {
208 return Err(Error::GPGConf(
209 format!("Malformed response, expected {} fields, \
210 on line: {:?}", nfields, line)).into());
211 }
212
213 result.push(fields);
214 }
215 Ok(result)
216 } else {
217 Err(Error::GPGConf(String::from_utf8_lossy(
218 &output.stderr).into_owned()).into())
219 }
220 }
221
222 pub fn homedir(&self) -> Option<&Path> {
229 self.homedir.as_deref()
230 }
231
232 pub fn socket<C>(&self, socket: C) -> Result<&Path>
234 where C: AsRef<str>
235 {
236 self.sockets.get(socket.as_ref())
237 .map(|p| p.as_path())
238 .ok_or_else(|| {
239 Error::GPGConf(format!("No such socket {:?}",
240 socket.as_ref())).into()
241 })
242 }
243
244 pub fn create_socket_dir(&self) -> Result<()> {
246 #[cfg(windows)]
254 if self.cygwin {
255 return Ok(());
256 }
257
258 Self::gpgconf(&self.homedir, &["--create-socketdir"], 1)?;
259 Ok(())
260 }
261
262 pub fn remove_socket_dir(&self) -> Result<()> {
267 Self::gpgconf(&self.homedir, &["--remove-socketdir"], 1)?;
268 Ok(())
269 }
270
271 pub fn start(&self, component: &str) -> Result<()> {
273 let _ = self.create_socket_dir(); Self::gpgconf(&self.homedir, &["--launch", component], 1)
275 .map_err(|e| {
276 if let crate::Error::GnuPG(Error::GPGConf(msg)) = &e {
277 if msg.contains("probably not installed") {
278 return crate::Error::GnuPG(Error::ComponentMissing(
279 component.into()));
280 }
281 }
282 e
283 })?;
284 Ok(())
285 }
286
287 pub fn stop(&self, component: &str) -> Result<()> {
289 Self::gpgconf(&self.homedir, &["--kill", component], 1)?;
290 Ok(())
291 }
292
293 pub fn stop_all(&self) -> Result<()> {
295 self.stop("all")
296 }
297}
298
299impl Drop for Context {
300 fn drop(&mut self) {
301 if self.ephemeral.is_some() {
302 let _ = self.stop_all();
303 let _ = self.remove_socket_dir();
304 }
305 }
306}