use anyhow::{ensure, Context, Result};
use std::{
io::Write,
path::Path,
process::{Command, Output, Stdio},
};
use crate::openpgp::cert::CertType;
#[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<&[u8]>,
gpg_home: Option<impl AsRef<Path>>,
) -> Result<Output> {
let mut cmd = command("gpg");
let gpg = cmd.stdout(Stdio::piped()).stderr(Stdio::null());
if password.is_some() {
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 error_context = "'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().context(error_context)?;
let mut stdin = child.stdin.take().context("Failed to open stdin")?;
std::thread::scope(|s| {
s.spawn(|| -> Result<()> {
stdin.write_all(p)?;
stdin.write_all(b"\n")?;
Ok(())
});
});
Ok(child.wait_with_output()?)
} else {
gpg.stdin(Stdio::null()).output().context(error_context)
}
}
pub fn export_key(
identifier: &str,
cert_type: CertType,
password: Option<&[u8]>,
gpg_home: Option<impl AsRef<Path>>,
) -> Result<Vec<u8>> {
let gpg = run_gnupg_command(
vec![
"--armor",
match cert_type {
CertType::Public => "--export",
CertType::Secret => "--export-secret-key",
},
identifier,
],
password,
gpg_home,
)?;
ensure!(
!gpg.stdout.is_empty(),
"No {} key matching the identifier '{}' was found in the GnuPG keyring.{}",
cert_type,
identifier,
match cert_type {
CertType::Public => "",
CertType::Secret => " Please ensure that you use the correct password.",
},
);
Ok(gpg.stdout.to_vec())
}
pub fn list_keys(cert_type: CertType, gpg_home: Option<impl AsRef<Path>>) -> Result<Vec<CertInfo>> {
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);
}
}