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 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 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 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 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 assert_eq!(res, true);
320
321 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 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}