prs_lib/crypto/backend/gnupg_bin/
context.rs1use 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#[cfg(not(windows))]
14const BIN_NAME: &str = "gpg";
15#[cfg(windows)]
16const BIN_NAME: &str = "gpg.exe";
17
18const VERSION_MIN: &str = "2.0.0";
20
21pub 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
29pub struct Context {
31 config: Config,
33}
34
35impl Context {
36 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
98fn 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
107fn test_gpg_compat(config: &Config) -> Result<()> {
109 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 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#[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}