use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
pub struct Gpg {
pub home: String,
}
pub fn gpg_available() -> bool {
Command::new("gpg")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
impl Gpg {
pub fn open_in(state_dir: &Path) -> Gpg {
let dir = state_dir.join("gnupg");
let _ = std::fs::create_dir_all(&dir);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
}
Gpg { home: dir.to_string_lossy().to_string() }
}
fn run(&self, args: &[&str], stdin_data: Option<&[u8]>) -> Result<(bool, Vec<u8>, String), String> {
let mut cmd = Command::new("gpg");
cmd.arg("--batch")
.arg("--yes")
.arg("--homedir")
.arg(&self.home)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| format!("gpg spawn failed: {}", e))?;
if let Some(data) = stdin_data {
let mut stdin = child.stdin.take().ok_or("gpg stdin unavailable")?;
stdin.write_all(data).map_err(|e| e.to_string())?;
}
let out = child.wait_with_output().map_err(|e| e.to_string())?;
Ok((
out.status.success(),
out.stdout,
String::from_utf8_lossy(&out.stderr).to_string(),
))
}
pub fn generate(&self, name: &str, kind_label: &str) -> Result<String, String> {
let spec = format!(
"Key-Type: eddsa\nKey-Curve: ed25519\nKey-Usage: sign\nName-Real: {}\nName-Comment: {}\nExpire-Date: 0\n%no-protection\n%commit\n",
name.replace('\n', " "),
kind_label.replace('\n', " ")
);
let (ok, _, err) = self.run(&["--gen-key"], Some(spec.as_bytes()))?;
if !ok {
return Err(format!("gpg --gen-key failed: {}", err.trim()));
}
let fps = self.list_fingerprints()?;
fps.last().cloned().ok_or_else(|| "no key after generation".to_string())
}
pub fn list_fingerprints(&self) -> Result<Vec<String>, String> {
let (ok, out, err) = self.run(&["--list-secret-keys", "--with-colons"], None)?;
if !ok {
return Err(format!("gpg list failed: {}", err.trim()));
}
let text = String::from_utf8_lossy(&out);
let mut fps = Vec::new();
let mut in_sec = false;
for line in text.lines() {
let fields: Vec<&str> = line.split(':').collect();
match fields.first().copied() {
Some("sec") => in_sec = true,
Some("fpr") if in_sec => {
if let Some(fp) = fields.get(9) {
fps.push(fp.to_string());
}
in_sec = false;
}
_ => {}
}
}
Ok(fps)
}
pub fn export_public(&self, fingerprint: &str) -> Result<String, String> {
let (ok, out, err) = self.run(&["--armor", "--export", fingerprint], None)?;
if !ok || out.is_empty() {
return Err(format!("gpg export failed: {}", err.trim()));
}
Ok(String::from_utf8_lossy(&out).to_string())
}
pub fn fingerprint_of(&self, armored: &str) -> Result<String, String> {
let (ok, out, err) = self.run(&["--show-keys", "--with-colons"], Some(armored.as_bytes()))?;
if !ok {
return Err(format!("gpg show-keys failed: {}", err.trim()));
}
let text = String::from_utf8_lossy(&out);
for line in text.lines() {
let fields: Vec<&str> = line.split(':').collect();
if fields.first().copied() == Some("fpr") {
if let Some(fp) = fields.get(9) {
return Ok(fp.to_string());
}
}
}
Err("no fingerprint in key material".to_string())
}
pub fn import_public(&self, armored: &str) -> Result<String, String> {
let (ok, out, err) = self.run(&["--import"], Some(armored.as_bytes()))?;
let text = format!("{}{}", String::from_utf8_lossy(&out), err);
if !ok {
return Err(format!("gpg import failed: {}", text.trim()));
}
Ok(text.trim().to_string())
}
pub fn sign_detached(&self, fingerprint: &str, data: &[u8]) -> Result<String, String> {
let (ok, out, err) = self.run(
&["--armor", "--detach-sign", "--local-user", fingerprint],
Some(data),
)?;
if !ok {
return Err(format!("gpg sign failed: {}", err.trim()));
}
Ok(String::from_utf8_lossy(&out).to_string())
}
pub fn verify_detached(&self, signature_armored: &str, data: &[u8]) -> Result<String, String> {
let tmp = std::path::Path::new(&self.home).join("verify-tmp.asc");
std::fs::write(&tmp, signature_armored).map_err(|e| e.to_string())?;
let (ok, out, err) = self.run(
&["--status-fd", "1", "--verify", tmp.to_str().unwrap_or("verify-tmp.asc"), "-"],
Some(data),
)?;
let _ = std::fs::remove_file(&tmp);
let stdout = String::from_utf8_lossy(&out).to_string();
if ok {
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 && parts[1] == "VALIDSIG" {
return Ok(parts[2].to_string());
}
}
return Err("signature valid but signer fingerprint not reported".into());
}
Err(format!("signature verification failed: {}", err.trim()))
}
pub fn encrypt_to(&self, recipients: &[&str], signer: Option<&str>, data: &[u8]) -> Result<String, String> {
let mut args: Vec<&str> = vec!["--armor", "--encrypt", "--trust-model", "always"];
for r in recipients {
args.push("--recipient");
args.push(r);
}
if let Some(s) = signer {
args.push("--sign");
args.push("--local-user");
args.push(s);
}
let (ok, out, err) = self.run(&args, Some(data))?;
if !ok {
return Err(format!("gpg encrypt failed: {}", err.trim()));
}
Ok(String::from_utf8_lossy(&out).to_string())
}
pub fn decrypt(&self, armored: &str) -> Result<Vec<u8>, String> {
let (ok, out, err) = self.run(&["--decrypt"], Some(armored.as_bytes()))?;
if !ok {
return Err(format!("gpg decrypt failed: {}", err.trim()));
}
Ok(out)
}
}