pars_core/pgp/
crypto.rs

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        //TODO: match each version
75        let prefix = vec![
76            "--batch",         // this is required after gnupg 2.0
77            "--pinentry-mode", //this is required after gnupg 2.1
78            "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]
156    // #[serial]
157    // #[ignore = "need run interactively"]
158    // fn decrypt_file_interact() {
159    //     let executable = &get_test_executable();
160    //     let email = &get_test_email();
161    //     let plaintext = "Hello, world!\nThis is a test message.\n";
162    //     let (_tmp_dir, root) = gen_unique_temp_dir();
163    //     let output_dest = "decrypt.gpg";
164    //
165    //     cleanup!(
166    //         {
167    //             let mut test_client = PGPClient::new(
168    //                 executable.to_string(),
169    //                 None,
170    //                 Some(get_test_username()),
171    //                 Some(email.to_string()),
172    //             );
173    //             test_client.key_gen_batch(&gpg_key_gen_example_batch()).unwrap();
174    //             test_client.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
175    //             test_client.update_info().unwrap();
176    //
177    //             test_client.encrypt(plaintext, output_dest).unwrap();
178    //             let decrypted = test_client.decrypt_stdin(&root, output_dest).unwrap();
179    //             assert_eq!(decrypted.expose_secret(), plaintext);
180    //             fs::remove_file(output_dest).unwrap();
181    //         },
182    //         {
183    //             clean_up_test_key(executable, email).unwrap();
184    //         }
185    //     )
186    // }
187
188    #[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}