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 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 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}