Skip to main content

shadow_crypt_shell/encryption/
file_ops.rs

1use 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    // Largest multiple of CHARSET.len() that fits in a byte; rejection
48    // sampling below this bound keeps every character equally likely.
49    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    // create_new claims the filename atomically, so concurrent encryptions
70    // can never race each other (or an attacker) into overwriting a file.
71    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        // Create test file
163        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        // Create temp directory for isolated testing
180        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            // Check file was created in temp directory
187            assert!(output_file.path.exists());
188
189            // Canonicalize paths to handle macOS /private symlink
190            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            // Read back and verify content
195            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        // Propagate any test failure
205        result.unwrap();
206    }
207}