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