shadow_crypt_shell/encryption/
file_ops.rs1use std::{
2 io::{Read, Write},
3 path::PathBuf,
4};
5
6use rand::rand_core::{OsRng, TryRngCore};
7use shadow_crypt_core::{
8 memory::{SecureBytes, SecureString},
9 v2::file::{EncryptedFile, PlaintextFile},
10};
11
12use crate::{
13 encryption::file::{EncryptionInputFile, EncryptionOutputFile},
14 errors::{WorkflowError, WorkflowResult},
15};
16
17pub fn store_encrypted_file(
18 encrypted_file: &EncryptedFile,
19 output_dir: &std::path::Path,
20) -> WorkflowResult<EncryptionOutputFile> {
21 let (mut f, output_file) = create_encryption_output_file(output_dir)?;
22 let serialized_header: Vec<u8> =
23 shadow_crypt_core::v2::header_ops::serialize(encrypted_file.header());
24 f.write_all(&serialized_header)?;
25 f.write_all(encrypted_file.ciphertext())?;
26
27 Ok(output_file)
28}
29
30pub fn load_plaintext_file(file: &EncryptionInputFile) -> WorkflowResult<PlaintextFile> {
31 let filename = SecureString::new(file.filename.clone());
32 let size: usize = file.size as usize;
33
34 let mut f = std::fs::File::open(&file.path)?;
35 let mut buffer: Vec<u8> = Vec::with_capacity(size);
36
37 f.read_to_end(&mut buffer)?;
38
39 let content = SecureBytes::new(buffer);
40
41 Ok(PlaintextFile::new(filename, content))
42}
43
44fn generate_output_filename() -> WorkflowResult<String> {
45 const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
46 const NAME_LENGTH: usize = 16;
47 const REJECTION_BOUND: u8 = (u8::MAX / CHARSET.len() as u8) * CHARSET.len() as u8;
50
51 let mut name = String::with_capacity(NAME_LENGTH);
52 let mut bytes = [0u8; 2 * NAME_LENGTH];
53 while name.len() < NAME_LENGTH {
54 OsRng.try_fill_bytes(&mut bytes).map_err(|e| {
55 WorkflowError::File(format!("Failed to generate output filename: {}", e))
56 })?;
57 for byte in bytes {
58 if byte < REJECTION_BOUND && name.len() < NAME_LENGTH {
59 name.push(CHARSET[byte as usize % CHARSET.len()] as char);
60 }
61 }
62 }
63 Ok(name)
64}
65
66fn create_encryption_output_file(
67 output_dir: &std::path::Path,
68) -> WorkflowResult<(std::fs::File, EncryptionOutputFile)> {
69 for _ in 0..1000 {
72 let mut path = PathBuf::from(generate_output_filename()?);
73 path.set_extension("shadow");
74
75 let full_path = output_dir.join(&path);
76
77 match std::fs::File::create_new(&full_path) {
78 Ok(f) => {
79 let filename_str = path
80 .to_str()
81 .ok_or_else(|| WorkflowError::File("Invalid output filename".to_string()))?
82 .to_string();
83
84 return Ok((
85 f,
86 EncryptionOutputFile {
87 path: full_path,
88 filename: filename_str,
89 },
90 ));
91 }
92 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
93 Err(e) => return Err(e.into()),
94 }
95 }
96
97 Err(WorkflowError::File(
98 "Unable to generate a unique output filename after 1000 attempts".to_string(),
99 ))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use shadow_crypt_core::{
106 profile::SecurityProfile,
107 v2::{file::EncryptedFile, header::FileHeader, key::KeyDerivationParams},
108 };
109 use std::fs;
110 use tempfile::TempDir;
111
112 fn create_test_header() -> FileHeader {
113 let salt = [1u8; 16];
114 let kdf_params = KeyDerivationParams::from(SecurityProfile::Test);
115 let content_nonce = [2u8; 24];
116 let filename_nonce = [3u8; 24];
117 let filename_ciphertext = vec![4, 5, 6, 7, 8];
118
119 FileHeader::new(
120 salt,
121 kdf_params,
122 content_nonce,
123 filename_nonce,
124 filename_ciphertext,
125 )
126 .unwrap()
127 }
128
129 fn create_test_encrypted_file() -> EncryptedFile {
130 let header = create_test_header();
131 let ciphertext = vec![10, 11, 12, 13, 14];
132 EncryptedFile::new(header, ciphertext)
133 }
134
135 #[test]
136 fn test_generate_output_filename() {
137 let filename = generate_output_filename().unwrap();
138 assert_eq!(filename.len(), 16);
139 assert!(filename.chars().all(|c| c.is_ascii_alphabetic()));
140 }
141
142 #[test]
143 fn test_create_encryption_output_file() {
144 let temp_dir = TempDir::new().unwrap();
145
146 let (_, first) = create_encryption_output_file(temp_dir.path()).unwrap();
147 let (_, second) = create_encryption_output_file(temp_dir.path()).unwrap();
148
149 assert!(first.path.exists());
150 assert!(second.path.exists());
151 assert_ne!(first.filename, second.filename);
152 assert!(first.filename.ends_with(".shadow"));
153 }
154
155 #[test]
156 fn test_load_file() {
157 let temp_dir = TempDir::new().unwrap();
158 let test_content = b"Hello, World!";
159 let test_filename = "test.txt";
160 let file_path = temp_dir.path().join(test_filename);
161
162 fs::write(&file_path, test_content).unwrap();
164
165 let input_file = EncryptionInputFile {
166 path: file_path.clone(),
167 filename: test_filename.to_string(),
168 size: test_content.len() as u64,
169 };
170
171 let plaintext_file = load_plaintext_file(&input_file).unwrap();
172
173 assert_eq!(plaintext_file.filename().as_str(), test_filename);
174 assert_eq!(plaintext_file.content().as_slice(), test_content);
175 }
176
177 #[test]
178 fn test_store_encrypted_file() {
179 let temp_dir = TempDir::new().unwrap();
181
182 let result = (|| -> Result<(), Box<dyn std::error::Error>> {
183 let encrypted_file = create_test_encrypted_file();
184 let output_file = store_encrypted_file(&encrypted_file, temp_dir.path())?;
185
186 assert!(output_file.path.exists());
188
189 let canonical_output = fs::canonicalize(&output_file.path)?;
191 let canonical_temp = fs::canonicalize(temp_dir.path())?;
192 assert!(canonical_output.starts_with(canonical_temp));
193
194 let written_content = fs::read(&output_file.path)?;
196 let expected_header =
197 shadow_crypt_core::v2::header_ops::serialize(encrypted_file.header());
198 let expected_content = [expected_header, encrypted_file.ciphertext().clone()].concat();
199
200 assert_eq!(written_content, expected_content);
201 Ok(())
202 })();
203
204 result.unwrap();
206 }
207}