pars_core/operation/
insert.rs

1use std::fs::create_dir_all;
2use std::io::{BufRead, Read, Write};
3use std::path::Path;
4
5use anyhow::Result;
6use secrecy::{ExposeSecret, SecretString};
7use zeroize::Zeroize;
8
9use crate::pgp::PGPClient;
10use crate::util::fs_util::{
11    create_or_overwrite, get_dir_gpg_id_content, path_attack_check, prompt_overwrite,
12};
13use crate::{IOErr, IOErrType};
14
15pub struct PasswdInsertConfig {
16    pub echo: bool,
17    pub multiline: bool,
18    pub force: bool,
19    pub extension: String,
20    pub pgp_executable: String,
21}
22
23pub fn insert_io<I, O, E>(
24    root: &Path,
25    pass_name: &str,
26    insert_cfg: &PasswdInsertConfig,
27    in_s: &mut I,
28    out_s: &mut O,
29    err_s: &mut E,
30) -> Result<bool>
31where
32    I: Read + BufRead,
33    O: Write,
34    E: Write,
35{
36    let pass_path = root.join(format!("{}.{}", pass_name, insert_cfg.extension));
37    path_attack_check(root, &pass_path)?;
38
39    // Handle the case parent folder not exist
40    if let Some(parent) = pass_path.parent() {
41        if !parent.exists() {
42            create_dir_all(parent)?;
43        }
44    } else {
45        return Err(IOErr::new(IOErrType::InvalidPath, &pass_path).into());
46    }
47
48    if pass_path.exists() && !insert_cfg.force && !prompt_overwrite(in_s, err_s, pass_name)? {
49        return Ok(false);
50    }
51
52    if let Some(parent) = pass_path.parent() {
53        if !parent.exists() {
54            create_dir_all(parent)?;
55        }
56    }
57
58    write!(out_s, "Enter password for '{}': ", pass_name)?;
59    out_s.flush()?;
60
61    let password = if insert_cfg.multiline {
62        let mut buffer = String::new();
63        in_s.read_to_string(&mut buffer)?;
64        SecretString::new(buffer.into())
65    } else {
66        let mut buffer = String::new();
67        in_s.read_line(&mut buffer)?;
68        write!(out_s, "Confirm password for '{}': ", pass_name)?;
69        out_s.flush()?;
70        let mut confirm_buffer = String::new();
71        in_s.read_line(&mut confirm_buffer)?;
72        if buffer != confirm_buffer {
73            writeln!(out_s, "Passwords do not match.")?;
74            return Ok(false);
75        }
76        confirm_buffer.zeroize();
77        SecretString::new(buffer.trim().to_string().into())
78    };
79
80    if insert_cfg.echo {
81        writeln!(out_s, "{}", password.expose_secret())?;
82    }
83
84    // Get the appropriate key fingerprints for this path
85    let keys_fpr = get_dir_gpg_id_content(root, &pass_path)?;
86    let client = PGPClient::new(&insert_cfg.pgp_executable, &keys_fpr)?;
87
88    create_or_overwrite(&client, &pass_path, &password)?;
89    writeln!(out_s, "Password encrypted and saved.")?;
90    Ok(true)
91}
92
93#[cfg(test)]
94mod tests {
95    use std::io::BufReader;
96    use std::path::PathBuf;
97    use std::thread::{self, sleep};
98    use std::time::Duration;
99
100    use os_pipe::pipe;
101    use pretty_assertions::assert_eq;
102    use serial_test::serial;
103    use tempfile::TempDir;
104
105    use super::*;
106    use crate::pgp::key_management::key_gen_batch;
107    use crate::util::defer::cleanup;
108    use crate::util::test_util::*;
109
110    fn setup_test_environment() -> (String, String, PGPClient, TempDir, PathBuf) {
111        let executable = get_test_executable();
112        let email = get_test_email();
113        let (tmp_dir, root) = gen_unique_temp_dir();
114
115        key_gen_batch(&executable, &gpg_key_gen_example_batch()).unwrap();
116        let test_client = PGPClient::new(executable.clone(), &[&email]).unwrap();
117        test_client.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
118        write_gpg_id(&root, &test_client.get_keys_fpr());
119
120        (executable, email, test_client, tmp_dir, root)
121    }
122
123    #[test]
124    #[serial]
125    #[ignore = "need run interactively"]
126    fn basic_insert() {
127        let (executable, email, test_client, _tmp_dir, root) = setup_test_environment();
128
129        cleanup!(
130            {
131                let (mut stdin, mut stdin_w) = pipe().unwrap();
132                let mut stdout = Vec::new();
133                let mut stderr = Vec::new();
134
135                thread::spawn(move || {
136                    sleep(Duration::from_millis(100));
137                    stdin_w.write_all(b"password\n").unwrap();
138                    sleep(Duration::from_millis(100));
139                    stdin_w.write_all(b"password\n").unwrap();
140                });
141
142                let config = PasswdInsertConfig {
143                    echo: false,
144                    multiline: false,
145                    force: false,
146                    extension: "gpg".to_string(),
147                    pgp_executable: executable.clone(),
148                };
149
150                let res = insert_io(
151                    &root,
152                    "test1",
153                    &config,
154                    &mut BufReader::new(&mut stdin),
155                    &mut stdout,
156                    &mut stderr,
157                )
158                .unwrap();
159                assert_eq!(res, true);
160
161                let decrypted = test_client.decrypt_stdin(&root, "test1.gpg").unwrap();
162                assert_eq!(decrypted.expose_secret(), "password");
163            },
164            {
165                clean_up_test_key(&executable, &[&email]).unwrap();
166            }
167        );
168    }
169
170    #[test]
171    #[serial]
172    fn wrong_insert() {
173        let (executable, email, _test_client, _tmp_dir, root) = setup_test_environment();
174
175        cleanup!(
176            {
177                let (mut stdin, mut stdin_w) = pipe().unwrap();
178                let mut stdout = Vec::new();
179                let mut stderr = Vec::new();
180
181                thread::spawn(move || {
182                    sleep(Duration::from_millis(100));
183                    stdin_w.write_all(b"password\n").unwrap();
184                    sleep(Duration::from_millis(100));
185                    stdin_w.write_all(b"pd\n").unwrap();
186                });
187
188                let config = PasswdInsertConfig {
189                    echo: false,
190                    multiline: false,
191                    force: false,
192                    extension: "gpg".to_string(),
193                    pgp_executable: executable.clone(),
194                };
195
196                let res = insert_io(
197                    &root,
198                    "test1",
199                    &config,
200                    &mut BufReader::new(&mut stdin),
201                    &mut stdout,
202                    &mut stderr,
203                )
204                .unwrap();
205                assert_eq!(res, false);
206                assert_eq!(Path::new("test1.pgp").exists(), false);
207            },
208            {
209                clean_up_test_key(&executable, &[&email]).unwrap();
210            }
211        );
212    }
213
214    #[test]
215    #[serial]
216    #[ignore = "need run interactively"]
217    fn multiline_insert() {
218        let (executable, email, test_client, _tmp_dir, root) = setup_test_environment();
219
220        cleanup!(
221            {
222                let (mut stdin, mut stdin_w) = pipe().unwrap();
223                let mut stdout = Vec::new();
224                let mut stderr = Vec::new();
225
226                thread::spawn(move || {
227                    sleep(Duration::from_millis(100));
228                    stdin_w.write_all(b"line1\nline2\nline3").unwrap();
229                });
230
231                let config = PasswdInsertConfig {
232                    echo: false,
233                    multiline: true,
234                    force: false,
235                    extension: "gpg".into(),
236                    pgp_executable: executable.clone(),
237                };
238
239                let res = insert_io(
240                    &root,
241                    "test2",
242                    &config,
243                    &mut BufReader::new(&mut stdin),
244                    &mut stdout,
245                    &mut stderr,
246                )
247                .unwrap();
248                assert_eq!(res, true);
249
250                let decrypted = test_client.decrypt_stdin(&root, "test2.gpg").unwrap();
251                assert_eq!(decrypted.expose_secret(), "line1\nline2\nline3");
252            },
253            {
254                clean_up_test_key(&executable, &[&email]).unwrap();
255            }
256        );
257    }
258
259    #[test]
260    #[serial]
261    #[ignore = "need run interactively"]
262    fn force_overwrite() {
263        let (executable, email, test_client, _tmp_dir, root) = setup_test_environment();
264
265        cleanup!(
266            {
267                let (mut stdin, mut stdin_w) = pipe().unwrap();
268                let mut stdout = Vec::new();
269                let mut stderr = Vec::new();
270
271                // Create initial file
272                test_client
273                    .encrypt("old_password", root.join("test3.gpg").to_str().unwrap())
274                    .unwrap();
275
276                thread::spawn(move || {
277                    sleep(Duration::from_millis(100));
278                    stdin_w.write_all(b"N\n").unwrap();
279                });
280
281                let mut config = PasswdInsertConfig {
282                    echo: false,
283                    multiline: false,
284                    force: false,
285                    extension: "gpg".to_string(),
286                    pgp_executable: executable.clone(),
287                };
288
289                let res = insert_io(
290                    &root,
291                    "test3",
292                    &config,
293                    &mut BufReader::new(&mut stdin),
294                    &mut stdout,
295                    &mut stderr,
296                )
297                .unwrap();
298                // This insert should fail because prompt failed.
299                assert_eq!(res, false);
300
301                let (mut stdin, mut stdin_w) = pipe().unwrap();
302                thread::spawn(move || {
303                    sleep(Duration::from_millis(100));
304                    stdin_w.write_all(b"not\n").unwrap();
305                    sleep(Duration::from_millis(100));
306                    stdin_w.write_all(b"not\n").unwrap();
307                });
308                config.force = true;
309                let res = insert_io(
310                    &root,
311                    "test3",
312                    &config,
313                    &mut BufReader::new(&mut stdin),
314                    &mut stdout,
315                    &mut stderr,
316                )
317                .unwrap();
318                // This time insert should succeed.
319                assert_eq!(res, true);
320
321                // Now try to prompt
322                config.force = false;
323                let (mut stdin, mut stdin_w) = pipe().unwrap();
324                thread::spawn(move || {
325                    sleep(Duration::from_millis(100));
326                    stdin_w.write_all(b"y\n").unwrap();
327                    sleep(Duration::from_millis(100));
328                    stdin_w.write_all(b"new_password\n").unwrap();
329                    sleep(Duration::from_millis(100));
330                    stdin_w.write_all(b"new_password\n").unwrap();
331                });
332                let res = insert_io(
333                    &root,
334                    "test3",
335                    &config,
336                    &mut BufReader::new(&mut stdin),
337                    &mut stdout,
338                    &mut stderr,
339                )
340                .unwrap();
341                // This time insert should succeed.
342                assert_eq!(res, true);
343                let decrypted = test_client.decrypt_stdin(&root, "test3.gpg").unwrap();
344                assert_eq!(decrypted.expose_secret(), "new_password");
345            },
346            {
347                clean_up_test_key(&executable, &[&email]).unwrap();
348            }
349        );
350    }
351
352    #[test]
353    #[serial]
354    #[ignore = "need run interactively"]
355    fn invalid_path() {
356        let (executable, email, _test_client, _tmp_dir, root) = setup_test_environment();
357
358        cleanup!(
359            {
360                let (mut stdin, _) = pipe().unwrap();
361                let mut stdout = Vec::new();
362                let mut stderr = Vec::new();
363
364                let config = PasswdInsertConfig {
365                    echo: false,
366                    multiline: false,
367                    force: false,
368                    extension: "gpg".to_string(),
369                    pgp_executable: executable.clone(),
370                };
371
372                let result = insert_io(
373                    &root,
374                    "../outside",
375                    &config,
376                    &mut BufReader::new(&mut stdin),
377                    &mut stdout,
378                    &mut stderr,
379                );
380
381                assert!(result.is_err());
382            },
383            {
384                clean_up_test_key(&executable, &[&email]).unwrap();
385            }
386        );
387    }
388}