shadow_crypt_shell/decryption/
file_ops.rs1use std::io::{Read, Write};
2
3use shadow_crypt_core::memory::{SecureBytes, SecureString};
4
5use crate::{
6 decryption::file::{DecryptionInputFile, DecryptionOutputFile},
7 errors::{WorkflowError, WorkflowResult},
8};
9
10pub fn store_plaintext_file(
11 filename: &SecureString,
12 content: &SecureBytes,
13 output_dir: &std::path::Path,
14) -> WorkflowResult<DecryptionOutputFile> {
15 let safe_name = std::path::Path::new(filename.as_str())
18 .file_name()
19 .ok_or_else(|| {
20 WorkflowError::File("Decrypted filename contains invalid path components".to_string())
21 })?;
22 let safe_name_str = safe_name
23 .to_str()
24 .ok_or_else(|| WorkflowError::File("Decrypted filename is not valid UTF-8".to_string()))?
25 .to_string();
26
27 let output_file = DecryptionOutputFile {
28 path: output_dir.join(safe_name),
29 filename: safe_name_str,
30 };
31
32 let mut f = std::fs::File::create_new(output_file.path.as_path()).map_err(|e| {
36 if e.kind() == std::io::ErrorKind::AlreadyExists {
37 WorkflowError::File(format!(
38 "Output file '{}' already exists",
39 output_file.filename
40 ))
41 } else {
42 WorkflowError::Io(e)
43 }
44 })?;
45 f.write_all(content.as_slice())?;
46
47 Ok(output_file)
48}
49
50pub fn load_file_bytes(file: &DecryptionInputFile) -> WorkflowResult<Vec<u8>> {
53 let size: usize = file.size as usize;
54
55 let mut f = std::fs::File::open(&file.path)?;
56 let mut buffer: Vec<u8> = Vec::with_capacity(size);
57
58 f.read_to_end(&mut buffer)?;
59
60 Ok(buffer)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use std::fs;
67 use std::io::Write;
68 use tempfile::NamedTempFile;
69
70 #[test]
71 fn test_store_plaintext_file() {
72 let temp_dir = tempfile::TempDir::new().unwrap();
73 let filename = SecureString::new("test.txt".to_string());
74 let content = SecureBytes::new(b"test content".to_vec());
75
76 let output = store_plaintext_file(&filename, &content, temp_dir.path()).unwrap();
77 assert_eq!(output.filename, "test.txt");
78
79 let read_content = fs::read(&output.path).unwrap();
80 assert_eq!(read_content, b"test content");
81 }
82
83 #[test]
84 fn test_store_plaintext_file_path_traversal_rejected() {
85 let temp_dir = tempfile::TempDir::new().unwrap();
86
87 for malicious_name in &["../../etc/passwd", "../sibling", "/abs/path", ".."] {
88 let filename = SecureString::new(malicious_name.to_string());
89 let content = SecureBytes::new(b"evil".to_vec());
90 let result = store_plaintext_file(&filename, &content, temp_dir.path());
91
92 match malicious_name {
93 &".." => {
94 assert!(
95 result.is_err(),
96 "Expected error for filename '{malicious_name}'"
97 );
98 }
99 _ => {
100 if let Ok(output) = result {
103 assert!(
104 output.path.starts_with(temp_dir.path()),
105 "Output escaped temp_dir for '{malicious_name}'"
106 );
107 }
108 }
109 }
110 }
111 }
112
113 #[test]
114 fn test_store_plaintext_file_no_overwrite() {
115 let temp_dir = tempfile::TempDir::new().unwrap();
116
117 let filename = SecureString::new("test.txt".to_string());
118 let content = SecureBytes::new(b"new content".to_vec());
119
120 let output_path = temp_dir.path().join("test.txt");
122 let existing_content = b"existing content";
123 std::fs::write(&output_path, existing_content).unwrap();
124
125 let result = store_plaintext_file(&filename, &content, temp_dir.path());
126 assert!(result.is_err());
127 if let Err(WorkflowError::File(msg)) = result {
128 assert!(msg.contains("already exists"));
129 } else {
130 panic!("Expected File error");
131 }
132
133 let read_content = std::fs::read(&output_path).unwrap();
135 assert_eq!(read_content, existing_content);
136 }
137
138 #[test]
139 fn test_load_file_bytes() {
140 let mut temp_file = NamedTempFile::new().unwrap();
141 let data = b"raw file data";
142 temp_file.write_all(data).unwrap();
143 temp_file.flush().unwrap();
144
145 let input_file = DecryptionInputFile {
146 path: temp_file.path().to_path_buf(),
147 filename: "test.shadow".to_string(),
148 size: data.len() as u64,
149 };
150
151 let bytes = load_file_bytes(&input_file).unwrap();
152 assert_eq!(bytes, data);
153 }
154}