sequoia_gpg_agent/
gnupg.rs

1//! GnuPG RPC support.
2
3#![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/// Errors used in this module.
14#[non_exhaustive]
15pub enum Error {
16    /// The requested component is not installed.
17    #[error("component {0} is not installed")]
18    ComponentMissing(String),
19
20    /// gpgconf is not installed.
21    #[error("gpgconf is not installed")]
22    GPGConfMissing,
23
24    /// Errors related to `gpgconf`.
25    #[error("gpgconf: {0}")]
26    GPGConf(String),
27}
28
29/// A GnuPG context.
30#[derive(Debug)]
31pub struct Context {
32    homedir: Option<PathBuf>,
33    sockets: BTreeMap<String, PathBuf>,
34    ephemeral: Option<tempfile::TempDir>,
35    // XXX: Remove me once hack for Cygwin won't be necessary.
36    #[cfg(windows)]
37    cygwin: bool,
38}
39
40impl Context {
41    /// Creates a new context for the default GnuPG home directory.
42    pub fn new() -> Result<Self> {
43        Self::make(None, None)
44    }
45
46    /// Creates a new context for the given GnuPG home directory.
47    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    /// Creates a new ephemeral context.
54    ///
55    /// The created home directory will be deleted once this object is
56    /// dropped.
57    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        // Guess if we're dealing with Unix/Cygwin or native Windows variant
68        // We need to do that in order to pass paths in correct style to gpgconf
69        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            // NOTE: Directories and socket paths are percent-encoded if no
84            // argument to "--list-dirs" is given
85            let mut value = std::str::from_utf8(&fields[1])?.to_owned();
86            // FIXME: Percent-decode everything, but for now at least decode
87            // colons to support Windows drive letters
88            value = value.replace("%3a", ":");
89            // Store paths in native format, following the least surprise rule.
90            let path = convert_path(&value, Mode::native())?;
91
92            // Check that the homedir exists.
93            if key == "homedir" && ! path.exists() {
94                return Err(crate::Error::GnuPGHomeMissing(path));
95            }
96
97            // For now, we're only interested in collecting socket
98            // paths.
99            let socket = match key.strip_suffix("-socket") {
100                Some(socket) => socket,
101                _ => continue,
102            };
103
104            sockets.insert(socket.into(), path);
105        }
106
107        /// Whether we're dealing with gpg that expects Windows or Unix-style paths.
108        #[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            // https://dev.gnupg.org/T4496
177            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                    // EOF.
195                    break;
196                }
197
198                // Make sure to also skip \r on Windows
199                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    /// Returns the path to `homedir` directory.
223    ///
224    /// The path returned will be in a local format, i. e. one accepted by
225    /// available `gpgconf` or `gpg` tools.
226    ///
227    ///
228    pub fn homedir(&self) -> Option<&Path> {
229        self.homedir.as_deref()
230    }
231
232    /// Returns the path to a GnuPG socket.
233    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    /// Creates directories for RPC communication.
245    pub fn create_socket_dir(&self) -> Result<()> {
246        // FIXME: GnuPG as packaged by MinGW fails to create socketdir because
247        // it follows upstream Unix logic, which expects Unix-like `/var/run`
248        // sockets to work. Additionally, GnuPG expects to work with and set
249        // correct POSIX permissions that MinGW does not even support/emulate,
250        // so this fails loudly.
251        // Instead, don't do anything and rely on on homedir being treated
252        // (correctly) as a fallback here.
253        #[cfg(windows)]
254        if self.cygwin {
255            return Ok(());
256        }
257
258        Self::gpgconf(&self.homedir, &["--create-socketdir"], 1)?;
259        Ok(())
260    }
261
262    /// Removes directories for RPC communication.
263    ///
264    /// Note: This will stop all servers once they note that their
265    /// socket is gone.
266    pub fn remove_socket_dir(&self) -> Result<()> {
267        Self::gpgconf(&self.homedir, &["--remove-socketdir"], 1)?;
268        Ok(())
269    }
270
271    /// Starts a GnuPG component.
272    pub fn start(&self, component: &str) -> Result<()> {
273        let _ = self.create_socket_dir(); // Best effort.
274        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    /// Stops a GnuPG component.
288    pub fn stop(&self, component: &str) -> Result<()> {
289        Self::gpgconf(&self.homedir, &["--kill", component], 1)?;
290        Ok(())
291    }
292
293    /// Stops all GnuPG components.
294    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}