1use std::io::{BufRead, Read, Write};
2use std::path::{Path, PathBuf};
3use std::{fs, path};
4
5use anyhow::Result;
6use log::debug;
7use secrecy::ExposeSecret;
8
9use crate::pgp::PGPClient;
10use crate::util::fs_util::{
11 better_rename, copy_dir_recursive, get_dir_gpg_id_content, path_attack_check, path_to_str,
12};
13use crate::{IOErr, IOErrType};
14
15fn handle_overwrite_delete<I, O, E>(
17 path_to_overwrite: &Path,
18 force: bool,
19 stdin: &mut I,
20 stdout: &mut O,
21 _stderr: &mut E,
22) -> Result<bool>
23where
24 I: Read + BufRead,
25 O: Write,
26 E: Write,
27{
28 if !force {
29 stdout.write_fmt(format_args!(
30 "File {} already exists, overwrite? [y/N]: ",
31 path_to_overwrite.to_string_lossy()
32 ))?;
33 stdout.flush()?;
34 let mut input = String::new();
35 stdin.read_line(&mut input)?;
36 if !input.trim().to_lowercase().starts_with('y') {
37 stdout.write_all("Canceled\n".as_bytes())?;
38 return Ok(false);
39 }
40 }
41 if path_to_overwrite.is_file() {
42 fs::remove_file(path_to_overwrite)?;
43 } else if path_to_overwrite.is_dir() {
44 fs::remove_dir_all(path_to_overwrite)?;
45 }
46 Ok(true)
47}
48
49fn copy_rename_file<I, O, E>(
60 copy: bool,
61 from: &Path,
62 to: &Path,
63 extension: &str,
64 force: bool,
65 in_s: &mut I,
66 out_s: &mut O,
67 err_s: &mut E,
68) -> Result<()>
69where
70 I: Read + BufRead,
71 O: Write,
72 E: Write,
73{
74 let file_name =
75 from.file_name().ok_or_else(|| IOErr::new(IOErrType::CannotGetFileName, from))?;
76
77 if to.exists() {
79 return if to.is_dir() {
80 let sub_file = to.join(file_name);
81 if sub_file.exists() && !handle_overwrite_delete(&sub_file, force, in_s, out_s, err_s)?
82 {
83 return Ok(());
84 }
85 if copy {
86 fs::copy(from, sub_file)?;
87 } else {
88 better_rename(from.with_extension(extension), sub_file)?;
89 }
90 Ok(())
91 } else {
92 Err(IOErr::new(IOErrType::PathNotExist, to).into())
93 };
94 }
95
96 let to = PathBuf::from(format!("{}.{}", path_to_str(to)?, extension));
98 if to.exists() {
99 if to.is_file() {
100 if !handle_overwrite_delete(&to, force, in_s, out_s, err_s)? {
101 return Ok(());
102 }
103 } else {
104 return Err(IOErr::new(IOErrType::PathNotExist, &to).into());
105 }
106 }
107 if copy {
108 fs::copy(from, to)?;
109 } else {
110 better_rename(from.with_extension(extension), to)?;
111 }
112 Ok(())
113}
114
115fn copy_rename_dir<I, O, E>(
125 copy: bool,
126 from: &Path,
127 to: &Path,
128 force: bool,
129 in_s: &mut I,
130 out_s: &mut O,
131 err_s: &mut E,
132) -> Result<()>
133where
134 I: Read + BufRead,
135 O: Write,
136 E: Write,
137{
138 let file_name =
139 from.file_name().ok_or_else(|| IOErr::new(IOErrType::CannotGetFileName, from))?;
140
141 if to.exists() {
142 if to.is_dir() {
143 let sub_dir = to.join(file_name);
144 if sub_dir.exists() && !handle_overwrite_delete(&sub_dir, force, in_s, out_s, err_s)? {
145 return Ok(());
146 }
147 if copy {
148 copy_dir_recursive(from, sub_dir)?;
149 } else {
150 better_rename(from, sub_dir)?;
151 }
152 } else if to.is_file() {
153 if !handle_overwrite_delete(to, force, in_s, out_s, err_s)? {
154 return Ok(());
155 }
156 if copy {
157 copy_dir_recursive(from, to)?;
158 } else {
159 better_rename(from, to)?;
160 }
161 } else {
162 return Err(IOErr::new(IOErrType::InvalidFileType, to).into());
163 }
164 } else if copy {
165 copy_dir_recursive(from, to)?;
166 } else {
167 better_rename(from, to)?;
168 }
169 Ok(())
170}
171
172pub fn copy_rename_io<I, O, E>(
173 copy: bool,
174 root: &Path,
175 from: &str,
176 to: &str,
177 file_extension: &str,
178 force: bool,
179 stdin: &mut I,
180 stdout: &mut O,
181 stderr: &mut E,
182) -> Result<()>
183where
184 I: Read + BufRead,
185 O: Write,
186 E: Write,
187{
188 let mut from_path = root.join(from);
189 let to_path = root.join(to);
190 path_attack_check(root, &from_path)?;
191 path_attack_check(root, &to_path)?;
192
193 if !from_path.exists() {
194 let try_path = PathBuf::from(format!("{}.{}", path_to_str(&from_path)?, file_extension));
195 if !try_path.exists() {
196 return Err(IOErr::new(IOErrType::PathNotExist, &from_path).into());
197 }
198 from_path = try_path;
199 }
200 debug!("copy_rename_io: from_path: {}, to_path: {}", from_path.display(), to_path.display());
201
202 let to_is_dir = to.ends_with(path::MAIN_SEPARATOR);
203 if to_is_dir && (!to_path.exists() || !to_path.is_dir()) {
204 writeln!(
205 stderr,
206 "Cannot {} '{}' to '{}': No such directory",
207 if copy { "copy" } else { "rename" },
208 from,
209 to
210 )?;
211 return Err(IOErr::new(IOErrType::PathNotExist, &to_path).into());
212 }
213
214 let needs_reencryption =
216 if from_path.is_file() && from_path.extension().is_some_and(|ext| ext == file_extension) {
217 let from_dir = from_path.parent().unwrap_or(root);
218 let to_dir = if to_path.exists() && to_path.is_dir() {
219 &to_path
220 } else {
221 to_path.parent().unwrap_or(root)
222 };
223
224 match (get_dir_gpg_id_content(root, from_dir), get_dir_gpg_id_content(root, to_dir)) {
226 (Ok(from_keys), Ok(to_keys)) => {
227 let mut from_keys_sorted = from_keys.clone();
228 let mut to_keys_sorted = to_keys.clone();
229 from_keys_sorted.sort();
230 to_keys_sorted.sort();
231
232 from_keys_sorted != to_keys_sorted
234 }
235 _ => false, }
237 } else {
238 false
239 };
240
241 if needs_reencryption {
242 debug!("Different GPG IDs detected, re-encryption required");
243
244 if from_path.is_file() {
246 let target_dir = if to_path.exists() && to_path.is_dir() {
248 to_path.clone()
249 } else {
250 to_path.parent().unwrap_or(root).to_path_buf()
251 };
252
253 let target_file = if to_path.exists() && to_path.is_dir() {
255 let filename = from_path
256 .file_name()
257 .ok_or_else(|| IOErr::new(IOErrType::CannotGetFileName, &from_path))?;
258 to_path.join(filename)
259 } else {
260 PathBuf::from(format!("{}.{}", path_to_str(&to_path)?, file_extension))
261 };
262
263 if target_file.exists()
265 && !handle_overwrite_delete(&target_file, force, stdin, stdout, stderr)?
266 {
267 return Ok(());
268 }
269
270 let from_dir = from_path.parent().unwrap_or(root);
272 let from_keys = get_dir_gpg_id_content(root, from_dir)?;
273 let to_keys = get_dir_gpg_id_content(root, &target_dir)?;
274
275 let source_client = match PGPClient::new("gpg", &from_keys) {
277 Ok(client) => client,
278 Err(e) => {
279 writeln!(stderr, "Error creating PGP client for decryption: {}", e)?;
280 return Err(e);
281 }
282 };
283
284 let target_client = match PGPClient::new("gpg", &to_keys) {
286 Ok(client) => client,
287 Err(e) => {
288 writeln!(stderr, "Error creating PGP client for encryption: {}", e)?;
289 return Err(e);
290 }
291 };
292
293 let content = match source_client.decrypt_stdin(root, path_to_str(&from_path)?) {
295 Ok(content) => content,
296 Err(e) => {
297 writeln!(stderr, "Error decrypting file: {}", e)?;
298 return Err(e);
299 }
300 };
301
302 match target_client.encrypt(content.expose_secret(), path_to_str(&target_file)?) {
304 Ok(_) => {
305 if !copy {
306 if let Err(e) = fs::remove_file(&from_path) {
308 writeln!(
309 stderr,
310 "Warning: Failed to delete original file after move: {}",
311 e
312 )?;
313 }
314 }
315 return Ok(());
316 }
317 Err(e) => {
318 writeln!(stderr, "Error re-encrypting file: {}", e)?;
319 return Err(e);
320 }
321 }
322 }
323 }
324
325 if from_path.is_file() {
327 copy_rename_file(copy, &from_path, &to_path, file_extension, force, stdin, stdout, stderr)
328 } else if from_path.is_dir() {
329 copy_rename_dir(copy, &from_path, &to_path, force, stdin, stdout, stderr)
330 } else {
331 Err(IOErr::new(IOErrType::InvalidFileType, &from_path).into())
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use std::io::{self, BufReader};
338 use std::thread::{self, sleep};
339
340 use os_pipe::pipe;
341 use pretty_assertions::assert_eq;
342 use serial_test::serial;
343
344 use super::*;
345 use crate::pgp::key_management::key_gen_batch;
346 use crate::util::defer::cleanup;
347 use crate::util::test_util::{
348 clean_up_test_key, create_dir_structure, gen_unique_temp_dir, get_test_email,
349 get_test_executable, gpg_key_edit_example_batch, gpg_key_gen_example_batch, write_gpg_id,
350 };
351
352 #[test]
353 #[serial]
354 #[ignore = "need run interactively"]
355 fn rename_test() {
356 let (_tmp_dir, root) = gen_unique_temp_dir();
363 let structure: &[(Option<&str>, &[&str])] =
364 &[(None, &["a.gpg", "b.gpg"][..]), (Some("d_dir"), &[][..]), (Some("e_dir"), &[][..])];
365 create_dir_structure(&root, structure);
366
367 cleanup!(
368 {
369 let (stdin, mut stdin_w) = pipe().unwrap();
370 let mut stdin = BufReader::new(stdin);
371 let mut stdout = io::stdout().lock();
372 let mut stderr = io::stderr().lock();
373
374 copy_rename_io(
376 false,
377 &root,
378 "a",
379 "c",
380 "gpg",
381 false,
382 &mut stdin,
383 &mut stdout,
384 &mut stderr,
385 )
386 .unwrap();
387 assert_eq!(false, root.join("a.gpg").exists());
388 assert_eq!(true, root.join("c.gpg").exists());
389
390 thread::spawn(move || {
392 sleep(std::time::Duration::from_millis(100));
393 stdin_w.write_all(b"n\n").unwrap();
394 });
395 copy_rename_io(
396 false,
397 &root,
398 "b",
399 "c",
400 "gpg",
401 false,
402 &mut stdin,
403 &mut stdout,
404 &mut stderr,
405 )
406 .unwrap();
407 assert_eq!(true, root.join("b.gpg").exists());
408
409 copy_rename_io(
411 false,
412 &root,
413 "b",
414 "c",
415 "gpg",
416 true,
417 &mut stdin,
418 &mut stdout,
419 &mut stderr,
420 )
421 .unwrap();
422 assert_eq!(false, root.join("b.gpg").exists());
423 assert_eq!(true, root.join("c.gpg").exists());
424
425 copy_rename_io(
427 false,
428 &root,
429 "c",
430 &format!("d_dir{}", path::MAIN_SEPARATOR_STR),
431 "gpg",
432 false,
433 &mut stdin,
434 &mut stdout,
435 &mut stderr,
436 )
437 .unwrap();
438 assert_eq!(false, root.join("c.gpg").exists());
439 assert_eq!(true, root.join("d_dir").join("c.gpg").exists());
440
441 copy_rename_io(
443 false,
444 &root,
445 "d_dir",
446 "e_dir",
447 "gpg",
448 false,
449 &mut stdin,
450 &mut stdout,
451 &mut stderr,
452 )
453 .unwrap();
454 assert_eq!(false, root.join("d_dir").exists());
455 assert_eq!(true, root.join("e_dir").join("d_dir").exists());
456 },
457 {}
458 );
459 }
460
461 #[test]
462 #[serial]
463 #[ignore = "need run interactively"]
464 fn copy_test() {
465 let (_tmp_dir, root) = gen_unique_temp_dir();
472 let structure: &[(Option<&str>, &[&str])] =
473 &[(None, &["a.gpg", "b.gpg"][..]), (Some("d_dir"), &[][..]), (Some("e_dir"), &[][..])];
474 create_dir_structure(&root, structure);
475
476 cleanup!(
477 {
478 let (stdin, mut stdin_w) = pipe().unwrap();
479 let mut stdin = BufReader::new(stdin);
480 let mut stdout = io::stdout().lock();
481 let mut stderr = io::stderr().lock();
482
483 fs::write(root.join("a.gpg"), "foo_a").unwrap();
485 assert_eq!(false, root.join("c.gpg").exists());
486 copy_rename_io(
487 true,
488 &root,
489 "a",
490 "c",
491 "gpg",
492 false,
493 &mut stdin,
494 &mut stdout,
495 &mut stderr,
496 )
497 .unwrap();
498 assert_eq!(true, root.join("a.gpg").exists());
499 assert_eq!(true, root.join("c.gpg").exists());
500 assert_eq!("foo_a", fs::read_to_string(root.join("c.gpg")).unwrap());
501
502 fs::write(root.join("b.gpg"), "foo_b").unwrap();
504 thread::spawn(move || {
505 sleep(std::time::Duration::from_millis(100));
506 stdin_w.write_all(b"n\n").unwrap();
507 });
508
509 copy_rename_io(
510 true,
511 &root,
512 "b",
513 "c",
514 "gpg",
515 false,
516 &mut stdin,
517 &mut stdout,
518 &mut stderr,
519 )
520 .unwrap();
521 assert_ne!("foo_b", fs::read_to_string(root.join("c.gpg")).unwrap());
522
523 copy_rename_io(
525 true,
526 &root,
527 "b",
528 "c",
529 "gpg",
530 true,
531 &mut stdin,
532 &mut stdout,
533 &mut stderr,
534 )
535 .unwrap();
536 assert_eq!("foo_b", fs::read_to_string(root.join("c.gpg")).unwrap());
537
538 fs::write(root.join("c.gpg"), "foo_c").unwrap();
540 copy_rename_io(
541 true,
542 &root,
543 "c",
544 &format!("d_dir{}", path::MAIN_SEPARATOR_STR),
545 "gpg",
546 false,
547 &mut stdin,
548 &mut stdout,
549 &mut stderr,
550 )
551 .unwrap();
552 assert_eq!(true, root.join("c.gpg").exists());
553 assert_eq!("foo_c", fs::read_to_string(root.join("c.gpg")).unwrap());
554
555 copy_rename_io(
557 true,
558 &root,
559 "d_dir",
560 "e_dir",
561 "gpg",
562 false,
563 &mut stdin,
564 &mut stdout,
565 &mut stderr,
566 )
567 .unwrap();
568 assert_eq!(true, root.join("d_dir").exists());
569 assert_eq!(true, root.join("e_dir").join("d_dir").exists());
570 },
571 {}
572 );
573 }
574
575 #[test]
576 #[serial]
577 #[ignore = "need run interactively"]
578 fn path_attack_protection_test() {
580 let (_tmp_dir, root) = gen_unique_temp_dir();
584 let structure: &[(Option<&str>, &[&str])] = &[(None, &["a.gpg"][..])];
585 create_dir_structure(&root, structure);
586
587 cleanup!(
588 {
589 let mut stdin = io::stdin().lock();
590 let mut stdout = io::stdout().lock();
591 let mut stderr = io::stderr().lock();
592 if copy_rename_io(
593 false,
594 &root,
595 "../../a",
596 "c",
597 "gpg",
598 true,
599 &mut stdin,
600 &mut stdout,
601 &mut stderr,
602 )
603 .is_ok()
604 {
605 panic!(
606 "Should not be able to access parent directory: {}/../../a",
607 root.display()
608 );
609 }
610
611 if copy_rename_io(
612 true,
613 &root,
614 "a",
615 "../../c",
616 "gpg",
617 true,
618 &mut stdin,
619 &mut stdout,
620 &mut stderr,
621 )
622 .is_ok()
623 {
624 panic!(
625 "Should not be able to access parent directory: {}/../../c",
626 root.display()
627 );
628 }
629 },
630 {}
631 );
632 }
633
634 #[test]
635 #[serial]
636 #[ignore = "need run interactively"]
637 fn re_encrypt_case_test() {
638 let executable = get_test_executable();
645 let (_tmp_dir, root) = gen_unique_temp_dir();
646 let structure: &[(Option<&str>, &[&str])] = &[(None, &[][..]), (Some("subdir"), &[][..])];
647 create_dir_structure(&root, structure);
648
649 key_gen_batch(&executable, &gpg_key_gen_example_batch()).unwrap();
651 let email1 = get_test_email();
652 let client1 = PGPClient::new(executable.clone(), &[&email1]).unwrap();
653 client1.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
654 let key1_fpr = client1.get_keys_fpr();
655
656 let email2 = format!("sub-{}", email1);
659 let second_key_batch = format!(
660 r#"%echo Generating a second key
661Key-Type: RSA
662Key-Length: 2048
663Subkey-Type: RSA
664Subkey-Length: 2048
665Name-Real: Test User Sub
666Name-Email: {}
667Expire-Date: 0
668Passphrase: password
669%commit
670%echo Key generation complete
671"#,
672 email2
673 );
674
675 key_gen_batch(&executable, &second_key_batch).unwrap();
676 let client2 = PGPClient::new(executable.clone(), &[&email2]).unwrap();
677 client2.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
678 let key2_fpr = client2.get_keys_fpr();
679
680 write_gpg_id(&root, &key1_fpr);
682 write_gpg_id(&root.join("subdir"), &key2_fpr);
683
684 let (stdin, _stdin_w) = pipe().unwrap();
686 let mut stdin = BufReader::new(stdin);
687 let mut stdout = io::stdout().lock();
688 let mut stderr = io::stderr().lock();
689
690 cleanup!(
691 {
692 let test_content = "This is a secret message";
694 client1.encrypt(test_content, root.join("file1.gpg").to_str().unwrap()).unwrap();
695
696 copy_rename_io(
698 true,
699 &root,
700 "file1",
701 "subdir/file1",
702 "gpg",
703 false,
704 &mut stdin,
705 &mut stdout,
706 &mut stderr,
707 )
708 .unwrap();
709
710 assert!(root.join("file1.gpg").exists());
712 assert!(root.join("subdir").join("file1.gpg").exists());
713
714 let decrypted = client2.decrypt_stdin(&root, "subdir/file1.gpg").unwrap();
716 assert_eq!(decrypted.expose_secret(), test_content);
717
718 let test_content2 = "Another secret message for moving";
721 client1.encrypt(test_content2, root.join("file2.gpg").to_str().unwrap()).unwrap();
722
723 copy_rename_io(
725 false, &root,
727 "file2",
728 "subdir/file2",
729 "gpg",
730 false,
731 &mut stdin,
732 &mut stdout,
733 &mut stderr,
734 )
735 .unwrap();
736
737 assert!(!root.join("file2.gpg").exists());
739 assert!(root.join("subdir").join("file2.gpg").exists());
740
741 let decrypted = client2.decrypt_stdin(&root, "subdir/file2.gpg").unwrap();
743 assert_eq!(decrypted.expose_secret(), test_content2);
744 },
745 {
746 let emails = vec![email1.as_str(), email2.as_str()];
748 clean_up_test_key(&executable, &emails).unwrap();
749 }
750 );
751 }
752}