1use std::io::{Read, Write};
2use std::path::Path;
3use std::process::{Command, Stdio};
4
5use anyhow::{anyhow, Result};
6use log::debug;
7use secrecy::{ExposeSecret, SecretString};
8use zeroize::Zeroize;
9
10use super::{PGPClient, PGPErr};
11impl PGPClient {
12 pub fn encrypt(&self, plaintext: &str, output_path: &str) -> Result<()> {
13 let fprs = self.get_keys_fpr();
14 let prefix = vec!["--batch", "--encrypt"];
15 let mut args = Vec::with_capacity(prefix.len() + fprs.len() * 2 + 2);
16 args.extend(prefix);
17 fprs.into_iter().for_each(|fpr| {
18 args.push("--recipient");
19 args.push(fpr);
20 });
21 args.push("--output");
22 args.push(output_path);
23 let mut child = Command::new(&self.executable)
24 .args(&args)
25 .stdin(Stdio::piped())
26 .stdout(Stdio::piped())
27 .stderr(Stdio::piped())
28 .spawn()?;
29
30 if let Some(mut stdin) = child.stdin.take() {
31 stdin.write_all(plaintext.as_bytes())?;
32 }
33
34 let status = child.wait()?;
35 if status.success() {
36 debug!("File encrypted successfully: {}", output_path);
37 Ok(())
38 } else {
39 let mut buffer = String::new();
40 let err_msg = match child.stderr.take() {
41 Some(mut err) => {
42 let _ = err.read_to_string(&mut buffer);
43 buffer
44 }
45 None => String::new(),
46 };
47 Err(anyhow!(format!("PGP encryption failed: {}", err_msg)))
48 }
49 }
50
51 pub fn decrypt_stdin(&self, work_dir: &Path, file_path: &str) -> Result<SecretString> {
52 let mut args = Vec::with_capacity(1 + 2 * self.keys.len() + 1);
53 args.push("--decrypt");
54 for key in &self.keys {
55 args.push("--recipient");
56 args.push(&key.key_fpr);
57 }
58 args.push(file_path);
59 let output = Command::new(&self.executable).current_dir(work_dir).args(&args).output()?;
60
61 if output.status.success() {
62 Ok(String::from_utf8(output.stdout)?.into())
63 } else {
64 let error_message = String::from_utf8_lossy(&output.stderr);
65 Err(anyhow!(format!("PGP decryption failed: {}", error_message)))
66 }
67 }
68
69 pub fn decrypt_with_password(
70 &self,
71 file_path: &str,
72 mut passwd: SecretString,
73 ) -> Result<SecretString> {
74 let prefix = vec![
76 "--batch", "--pinentry-mode", "loopback",
79 "--decrypt",
80 "--passphrase-fd",
81 "0",
82 ];
83 let mut args = Vec::with_capacity(prefix.len() + 2 * self.keys.len() + 1);
84 args.extend(prefix);
85 for key in &self.keys {
86 args.push("--recipient");
87 args.push(&key.key_fpr);
88 }
89 args.push(file_path);
90 let mut cmd = Command::new(&self.executable)
91 .args(&args)
92 .stdin(Stdio::piped())
93 .stdout(Stdio::piped())
94 .stderr(Stdio::piped())
95 .spawn()?;
96
97 if let Some(mut input) = cmd.stdin.take() {
98 input.write_all(passwd.expose_secret().as_bytes())?;
99 input.flush()?;
100 passwd.zeroize();
101 } else {
102 return Err(PGPErr::CannotTakeStdin.into());
103 }
104 let output = cmd.wait_with_output()?;
105
106 if output.status.success() {
107 Ok(String::from_utf8(output.stdout)?.into())
108 } else {
109 let error_message = String::from_utf8_lossy(&output.stderr);
110 Err(anyhow!(format!("PGP decryption failed: {}", error_message)))
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use std::fs;
118 use std::path::Path;
119
120 use pretty_assertions::assert_eq;
121 use serial_test::serial;
122
123 use super::*;
124 use crate::pgp::key_management::key_gen_batch;
125 use crate::util::defer::cleanup;
126 use crate::util::test_util::{
127 clean_up_test_key, get_test_email, get_test_executable, get_test_password,
128 gpg_key_edit_example_batch, gpg_key_gen_example_batch,
129 };
130
131 #[test]
132 #[serial]
133 fn encrypt_with_key() {
134 let executable = &get_test_executable();
135 let email = &get_test_email();
136 let plaintext = "Hello, world!\nThis is a test message.";
137 let output_dest = "encrypt.gpg";
138 cleanup!(
139 {
140 key_gen_batch(executable, &gpg_key_gen_example_batch()).unwrap();
141 let test_client = PGPClient::new(executable, &[email]).unwrap();
142 test_client.encrypt(plaintext, output_dest).unwrap();
143
144 if !Path::new(output_dest).exists() {
145 panic!("Encrypted file not found");
146 }
147 },
148 {
149 let _ = fs::remove_file(output_dest);
150 clean_up_test_key(executable, &[email]).unwrap();
151 }
152 );
153 }
154
155 #[test]
189 #[serial]
190 fn decrypt_file() {
191 let plaintext = "Hello, world!\nThis is a test message.\n";
192 let output_dest = "decrypt.gpg";
193
194 let _ = fs::remove_file(output_dest);
195
196 cleanup!(
197 {
198 key_gen_batch(&get_test_executable(), &gpg_key_gen_example_batch()).unwrap();
199 let test_client =
200 PGPClient::new(get_test_executable(), &[&get_test_email()]).unwrap();
201 test_client.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
202 test_client.encrypt(plaintext, output_dest).unwrap();
203 let decrypted = test_client
204 .decrypt_with_password(output_dest, get_test_password().into())
205 .unwrap();
206 assert_eq!(decrypted.expose_secret(), plaintext);
207 },
208 {
209 fs::remove_file(output_dest).unwrap();
210 clean_up_test_key(&get_test_executable(), &[&get_test_email()]).unwrap();
211 }
212 )
213 }
214}