pars_core/operation/
generate.rs

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