use std::{
io::Write,
path::Path,
process::{Command, Output, Stdio},
};
use crate::{openpgp::cert::CertType, secret::Secret};
#[derive(Clone, Debug)]
pub struct CertInfo {
pub fingerprint: String,
pub userid: Option<String>,
pub cert_type: CertType,
}
fn run_gnupg_command(
args: Vec<&str>,
password: Option<&Secret>,
gpg_home: Option<impl AsRef<Path>>,
) -> Result<Output, std::io::Error> {
let mut cmd = command("gpg");
let gpg = cmd.stdout(Stdio::piped()).stderr(Stdio::null());
if password.is_some() {
gpg.arg("--batch");
gpg.arg("--pinentry-mode").arg("loopback");
gpg.arg("--passphrase-fd").arg("0");
}
if let Some(gpg_home) = gpg_home {
gpg.arg("--homedir").arg(gpg_home.as_ref());
}
for arg in args {
gpg.arg(arg);
}
let gpg_not_found = |e: std::io::Error| {
std::io::Error::new(
e.kind(),
"'gpg' failed to start. Ensure that 'gpg' is installed AND in your PATH",
)
};
if let Some(p) = password {
let mut child = gpg.stdin(Stdio::piped()).spawn().map_err(gpg_not_found)?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| std::io::Error::other("Failed to open stdin"))?;
std::thread::scope(|s| {
s.spawn(|| {
stdin.write_all(
p.reveal("GnuPG key password")
.map_err(std::io::Error::other)?
.as_bytes(),
)?;
stdin.write_all(b"\n")
});
});
Ok(child.wait_with_output()?)
} else {
gpg.stdin(Stdio::null()).output().map_err(gpg_not_found)
}
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
KeyNotFound {
id: String,
cert_type: CertType,
},
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::KeyNotFound { id, cert_type } => {
write!(
f,
"No {cert_type} key matching the identifier '{id}' was found in the GnuPG keyring."
)?;
if cert_type == &CertType::Secret {
write!(f, " Please ensure that you use the correct password.")?;
}
}
Self::Io(err) => {
err.fmt(f)?;
}
}
Ok(())
}
}
impl core::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
pub fn export_key(
identifier: &str,
cert_type: CertType,
password: Option<&Secret>,
gpg_home: Option<impl AsRef<Path>>,
) -> Result<Vec<u8>, Error> {
let gpg = run_gnupg_command(
vec![
"--armor",
match cert_type {
CertType::Public => "--export",
CertType::Secret => "--export-secret-key",
},
identifier,
],
password,
gpg_home,
)?;
if gpg.stdout.is_empty() {
return Err(Error::KeyNotFound {
id: identifier.to_string(),
cert_type,
});
}
Ok(gpg.stdout.to_vec())
}
pub fn list_keys(
cert_type: CertType,
gpg_home: Option<impl AsRef<Path>>,
) -> Result<Vec<CertInfo>, std::io::Error> {
let gpg = run_gnupg_command(
vec![
"--with-colons",
match cert_type {
CertType::Public => "--list-keys",
CertType::Secret => "--list-secret-keys",
},
],
None,
gpg_home,
)?;
Ok(parse_key_info(String::from_utf8_lossy(&gpg.stdout).trim()))
}
fn parse_key_info(gpg_output: &str) -> Vec<CertInfo> {
let lines = gpg_output.lines();
let mut keys = Vec::new();
let mut fingerprint: Option<String> = None;
let mut userid: Option<String> = None;
let mut cert_type = CertType::Public;
for line in lines.into_iter() {
let tokens = line.split(':').collect::<Vec<_>>();
match tokens[0] {
"fpr" => {
if fingerprint.is_none() {
fingerprint = Some(tokens[9].into())
}
}
"uid" => {
if userid.is_none() {
userid = Some(tokens[9].into())
}
}
"sec" | "pub" => {
cert_type = match tokens[0] {
"sec" => CertType::Secret,
"pub" => CertType::Public,
_ => unreachable!(),
};
if let Some(fp) = fingerprint {
keys.push(CertInfo {
fingerprint: fp,
userid,
cert_type: CertType::Public,
});
fingerprint = None;
userid = None;
}
}
_ => (),
}
}
if let Some(fp) = fingerprint {
keys.push(CertInfo {
fingerprint: fp,
userid,
cert_type,
});
}
keys
}
#[allow(clippy::let_and_return)]
fn command<S>(program: S) -> Command
where
S: AsRef<std::ffi::OsStr>,
{
let command = Command::new(program);
#[cfg(windows)]
let command = {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let mut command = command;
command.creation_flags(CREATE_NO_WINDOW);
command
};
command
}
#[cfg(test)]
mod tests {
use super::*;
const SEC_CERT: &str = "sec:u:4096:1:D59C9655C1ACE95E:1692170831:::u:::scESC:::+:::23::0:
fpr:::::::::F7EE982159CFE60063D373FDD59C9655C1ACE95E:
grp:::::::::62A6FDDB0DFB1A63141DAF4F49A26BF8E9561D7D:
uid:u::::1692170831::9464D2BD8DF8ECFE2012547819A68FB41DF70C26::Another test <test@test.ch>::::::::::0:
ssb:u:4096:1:2E12C85A59B7B933:1692170831::::::e:::+:::23:
fpr:::::::::FC5964D80D9E05CBF429B6A32E12C85A59B7B933:
grp:::::::::F87137FCF6AD55C2D5707028152B3CC252B1D77D:
";
const PUB_CERT: &str = "tru::1:1692170835:0:3:1:5
pub:u:4096:1:D59C9655C1ACE95E:1692170831:::u:::scESC::::::23::0:
fpr:::::::::F7EE982159CFE60063D373FDD59C9655C1ACE95E:
uid:u::::1692170831::9464D2BD8DF8ECFE2012547819A68FB41DF70C26::Another test <test@test.ch>::::::::::0:
sub:u:4096:1:2E12C85A59B7B933:1692170831::::::e::::::23:
fpr:::::::::FC5964D80D9E05CBF429B6A32E12C85A59B7B933:
";
#[test]
fn test_from_secret_keys() {
let certs = parse_key_info(SEC_CERT);
assert_eq!(certs.len(), 1);
assert_eq!(
certs[0].fingerprint,
"F7EE982159CFE60063D373FDD59C9655C1ACE95E"
);
assert_eq!(
certs[0].userid.as_deref(),
Some("Another test <test@test.ch>")
);
assert_eq!(certs[0].cert_type, CertType::Secret);
}
#[test]
fn test_from_public_keys() {
let certs = parse_key_info(PUB_CERT);
assert_eq!(certs.len(), 1);
assert_eq!(
certs[0].fingerprint,
"F7EE982159CFE60063D373FDD59C9655C1ACE95E"
);
assert_eq!(
certs[0].userid.as_deref(),
Some("Another test <test@test.ch>")
);
assert_eq!(certs[0].cert_type, CertType::Public);
}
}