prs_lib/crypto/backend/gnupg_bin/
context.rs

1//! Provides GnuPG binary context adapter.
2
3use anyhow::Result;
4use thiserror::Error;
5use version_compare::Version;
6
7use super::raw_cmd::gpg_stdout_ok;
8use super::{Config, raw};
9use crate::crypto::{Config as CryptoConfig, IsContext, Key, Proto, proto};
10use crate::{Ciphertext, Plaintext, Recipients};
11
12/// Binary name.
13#[cfg(not(windows))]
14const BIN_NAME: &str = "gpg";
15#[cfg(windows)]
16const BIN_NAME: &str = "gpg.exe";
17
18/// Minimum required version.
19const VERSION_MIN: &str = "2.0.0";
20
21/// Create GnuPG binary context.
22pub fn context(config: &CryptoConfig) -> Result<Context, Err> {
23    let mut gpg_config = find_gpg_bin().map_err(Err::Context)?;
24    gpg_config.gpg_tty = config.gpg_tty;
25    gpg_config.verbose = config.verbose;
26    Ok(Context::from(gpg_config))
27}
28
29/// GnuPG binary context.
30pub struct Context {
31    /// GPG config.
32    config: Config,
33}
34
35impl Context {
36    /// Construct context from GPG config.
37    fn from(config: Config) -> Self {
38        Self { config }
39    }
40}
41
42impl IsContext for Context {
43    fn encrypt(&mut self, recipients: &Recipients, plaintext: Plaintext) -> Result<Ciphertext> {
44        let fingerprints: Vec<String> = recipients
45            .keys()
46            .iter()
47            .map(|key| key.fingerprint(false))
48            .collect();
49        let fingerprints: Vec<&str> = fingerprints.iter().map(|fp| fp.as_str()).collect();
50        raw::encrypt(&self.config, &fingerprints, plaintext)
51    }
52
53    fn decrypt(&mut self, ciphertext: Ciphertext) -> Result<Plaintext> {
54        raw::decrypt(&self.config, ciphertext)
55    }
56
57    fn can_decrypt(&mut self, ciphertext: Ciphertext) -> Result<bool> {
58        raw::can_decrypt(&self.config, ciphertext)
59    }
60
61    fn keys_public(&mut self) -> Result<Vec<Key>> {
62        Ok(raw::public_keys(&self.config)?
63            .into_iter()
64            .map(|key| {
65                Key::Gpg(proto::gpg::Key {
66                    fingerprint: key.0,
67                    user_ids: key.1,
68                })
69            })
70            .collect())
71    }
72
73    fn keys_private(&mut self) -> Result<Vec<Key>> {
74        Ok(raw::private_keys(&self.config)?
75            .into_iter()
76            .map(|key| {
77                Key::Gpg(proto::gpg::Key {
78                    fingerprint: key.0,
79                    user_ids: key.1,
80                })
81            })
82            .collect())
83    }
84
85    fn import_key(&mut self, key: &[u8]) -> Result<()> {
86        raw::import_key(&self.config, key)
87    }
88
89    fn export_key(&mut self, key: Key) -> Result<Vec<u8>> {
90        raw::export_key(&self.config, &key.fingerprint(false))
91    }
92
93    fn supports_proto(&self, proto: Proto) -> bool {
94        proto == Proto::Gpg
95    }
96}
97
98/// Find the `gpg` binary, make GPG config.
99// TODO: also try default path at /usr/bin/gpg
100fn find_gpg_bin() -> Result<Config> {
101    let path = which::which(BIN_NAME).map_err(Err::Unavailable)?;
102    let config = Config::from(path);
103    test_gpg_compat(&config)?;
104    Ok(config)
105}
106
107/// Test gpg binary compatibility.
108fn test_gpg_compat(config: &Config) -> Result<()> {
109    // Strip stdout to just the version number
110    let stdout = gpg_stdout_ok(config, ["--version"])?;
111    let stdout = stdout
112        .trim_start()
113        .lines()
114        .next()
115        .and_then(|stdout| stdout.trim().strip_prefix("gpg (GnuPG) "))
116        .map(|stdout| stdout.trim())
117        .ok_or(Err::UnexpectedOutput)?;
118
119    // Assert minimum version number
120    let ver_min = Version::from(VERSION_MIN).unwrap();
121    let ver_gpg = Version::from(stdout).unwrap();
122    if ver_gpg < ver_min {
123        return Err(Err::UnsupportedVersion(ver_gpg.to_string()).into());
124    }
125
126    Ok(())
127}
128
129/// GnuPG binary context error.
130#[derive(Debug, Error)]
131pub enum Err {
132    #[error("failed to obtain GnuPG binary cryptography context")]
133    Context(#[source] anyhow::Error),
134
135    #[error("failed to find GnuPG gpg binary")]
136    Unavailable(#[source] which::Error),
137
138    #[error("failed to communicate with GnuPG gpg binary, got unexpected output")]
139    UnexpectedOutput,
140
141    #[error("failed to use GnuPG gpg binary, unsupported version: {}", _0)]
142    UnsupportedVersion(String),
143}