use std::io::Write;
use std::process::{Command, Stdio};
use tracing::trace;
use super::Cipher;
use crate::error::{CipherError, Result};
#[cfg(feature = "gpg")]
#[allow(dead_code)]
pub struct Gpg;
#[cfg(feature = "gpg")]
impl Gpg {
#[allow(dead_code)]
fn check_gpg() -> Result<()> {
Command::new("gpg")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|_| {
CipherError::EncryptionFailed(
"gpg CLI not found. Install GnuPG from https://gnupg.org/download/".to_string(),
)
})?;
Ok(())
}
}
#[cfg(feature = "gpg")]
impl Cipher for Gpg {
type Recipient = String;
type Identity = ();
fn name(&self) -> &'static str {
"gpg"
}
fn encrypt(&self, plaintext: &str, recipients: &[String]) -> Result<String> {
trace!(
recipients = recipients.len(),
plaintext_len = plaintext.len(),
"encrypting with GPG"
);
Self::check_gpg()?;
if recipients.is_empty() {
return Err(CipherError::EncryptionFailed("no recipients provided".to_string()).into());
}
let mut cmd = Command::new("gpg");
cmd.args([
"--encrypt",
"--armor",
"--trust-model",
"always", "--batch", "--yes", ]);
for recipient in recipients {
cmd.args(["--recipient", recipient]);
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(|e| CipherError::EncryptionFailed(format!("failed to spawn gpg: {}", e)))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(plaintext.as_bytes()).map_err(|e| {
CipherError::EncryptionFailed(format!("failed to write plaintext: {}", e))
})?;
}
let output = child
.wait_with_output()
.map_err(|e| CipherError::EncryptionFailed(format!("gpg command failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(
CipherError::EncryptionFailed(format!("gpg encrypt failed: {}", stderr)).into(),
);
}
let ciphertext = String::from_utf8(output.stdout)
.map_err(|e| CipherError::EncryptionFailed(format!("UTF-8 error: {}", e)))?;
trace!(ciphertext_len = ciphertext.len(), "encrypted with GPG");
Ok(ciphertext)
}
fn decrypt(&self, ciphertext: &str, _identity: &()) -> Result<String> {
trace!(ciphertext_len = ciphertext.len(), "decrypting with GPG");
Self::check_gpg()?;
let mut cmd = Command::new("gpg");
cmd.args([
"--decrypt",
"--batch", "--yes", "--quiet", ]);
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(|e| CipherError::DecryptionFailed(format!("failed to spawn gpg: {}", e)))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(ciphertext.as_bytes()).map_err(|e| {
CipherError::DecryptionFailed(format!("failed to write ciphertext: {}", e))
})?;
}
let output = child
.wait_with_output()
.map_err(|e| CipherError::DecryptionFailed(format!("gpg command failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CipherError::DecryptionFailed(format!(
"gpg decrypt failed: {}. Ensure you have the private key in your keyring.",
stderr
))
.into());
}
let plaintext = String::from_utf8(output.stdout)
.map_err(|e| CipherError::DecryptionFailed(format!("UTF-8 error: {}", e)))?;
trace!(plaintext_len = plaintext.len(), "decrypted with GPG");
Ok(plaintext)
}
}
#[cfg(all(test, feature = "gpg"))]
mod tests {
use super::*;
#[test]
fn test_check_gpg() {
if Command::new("gpg")
.arg("--version")
.stdout(Stdio::null())
.status()
.is_err()
{
eprintln!("Skipping GPG tests - gpg not installed");
return;
}
assert!(Gpg::check_gpg().is_ok());
}
}