Skip to main content

shadow_crypt_shell/listing/
workflow.rs

1use std::sync::Arc;
2
3use rayon::prelude::*;
4use shadow_crypt_core::{memory::SecureString, v1, v2, version::Version};
5
6use crate::{
7    errors::WorkflowResult,
8    kdf::derive_key_from_untrusted_params,
9    listing::{
10        file::{FileInfoList, ListingInput, ShadowFile, ShadowFileInfo},
11        file_ops::{load_file_header_bytes, scan_directory_for_shadow_files},
12    },
13    ui,
14    utils::parse_string_from_bytes,
15};
16
17pub fn run_workflow(input: ListingInput) -> WorkflowResult<()> {
18    let shadow_files: Vec<ShadowFile> = scan_directory_for_shadow_files(&input.work_dir)?;
19    let password = Arc::new(input.password);
20
21    // Process files in parallel using rayon
22    let file_infos: Vec<ShadowFileInfo> = shadow_files
23        .par_iter()
24        .map(|shadow_file| get_shadow_file_info(shadow_file, &password))
25        .filter_map(Result::ok)
26        .collect();
27
28    let info_list: FileInfoList = FileInfoList::new(file_infos);
29
30    ui::display_file_info_list(&info_list);
31    Ok(())
32}
33
34fn decipher_original_filename(
35    header_bytes: &[u8],
36    version: Version,
37    password: &SecureString,
38) -> Option<SecureString> {
39    match version {
40        Version::V1 => decipher_v1(header_bytes, password),
41        Version::V2 => decipher_v2(header_bytes, password),
42    }
43}
44
45fn decipher_v1(header_bytes: &[u8], password: &SecureString) -> Option<SecureString> {
46    let header = v1::header_ops::try_deserialize(header_bytes).ok()?;
47    let kdf_params = v1::header_ops::get_kdf_params(&header);
48
49    let key = derive_key_from_untrusted_params(
50        kdf_params.memory_cost,
51        kdf_params.time_cost,
52        kdf_params.parallelism,
53        kdf_params.key_size,
54        || v1::key_ops::derive_key(password.as_str().as_bytes(), &header.salt, &kdf_params),
55    )
56    .ok()?;
57
58    let (filename_bytes, _) = v1::crypt::decrypt_bytes(
59        &header.filename_ciphertext,
60        key.as_bytes(),
61        &header.filename_nonce,
62    )
63    .ok()?;
64
65    parse_string_from_bytes(&filename_bytes).ok()
66}
67
68fn decipher_v2(header_bytes: &[u8], password: &SecureString) -> Option<SecureString> {
69    let header = v2::header_ops::try_deserialize(header_bytes).ok()?;
70    let kdf_params = v2::header_ops::get_kdf_params(&header);
71
72    let key = derive_key_from_untrusted_params(
73        kdf_params.memory_cost,
74        kdf_params.time_cost,
75        kdf_params.parallelism,
76        kdf_params.key_size,
77        || v2::key_ops::derive_key(password.as_str().as_bytes(), &header.salt, &kdf_params),
78    )
79    .ok()?;
80
81    let (filename_bytes, _) = v2::crypt::decrypt_bytes(
82        &header.filename_ciphertext,
83        key.as_bytes(),
84        &header.filename_nonce,
85        &header.binding().aad(v2::header::AadPurpose::Filename),
86    )
87    .ok()?;
88
89    parse_string_from_bytes(&filename_bytes).ok()
90}
91
92fn get_shadow_file_info(
93    shadow_file: &ShadowFile,
94    password: &SecureString,
95) -> WorkflowResult<ShadowFileInfo> {
96    let header_bytes = load_file_header_bytes(shadow_file)?;
97    let original_filename: Option<SecureString> =
98        decipher_original_filename(header_bytes.as_slice(), shadow_file.version, password);
99
100    Ok(ShadowFileInfo::new(
101        original_filename,
102        shadow_file.filename.clone(),
103        shadow_file.version,
104        shadow_file.size,
105    ))
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use shadow_crypt_core::profile::SecurityProfile;
112
113    fn build_v1_header_bytes(password: &str, original_filename: &str) -> Vec<u8> {
114        let salt = [0u8; 16];
115        let kdf_params = v1::key::KeyDerivationParams::from(SecurityProfile::Test);
116        let filename_nonce = [0u8; 24];
117
118        let (key, _) = v1::key_ops::derive_key(password.as_bytes(), &salt, &kdf_params).unwrap();
119        let (filename_ciphertext, _) = v1::crypt::encrypt_bytes(
120            original_filename.as_bytes(),
121            key.as_bytes(),
122            &filename_nonce,
123        )
124        .unwrap();
125
126        let header = v1::header::FileHeader::new(
127            salt,
128            kdf_params,
129            [0u8; 24], // content_nonce, not used
130            filename_nonce,
131            filename_ciphertext,
132        );
133        v1::header_ops::serialize(&header)
134    }
135
136    fn build_v2_header_bytes(password: &str, original_filename: &str) -> Vec<u8> {
137        let salt = [0u8; 16];
138        let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
139        let content_nonce = [1u8; 24];
140        let filename_nonce = [0u8; 24];
141
142        let (key, _) = v2::key_ops::derive_key(password.as_bytes(), &salt, &kdf_params).unwrap();
143        let binding =
144            v2::header::HeaderBinding::new(&salt, &kdf_params, &content_nonce, &filename_nonce);
145        let (filename_ciphertext, _) = v2::crypt::encrypt_bytes(
146            original_filename.as_bytes(),
147            key.as_bytes(),
148            &filename_nonce,
149            &binding.aad(v2::header::AadPurpose::Filename),
150        )
151        .unwrap();
152
153        let header = v2::header::FileHeader::new(
154            salt,
155            kdf_params,
156            content_nonce,
157            filename_nonce,
158            filename_ciphertext,
159        )
160        .unwrap();
161        v2::header_ops::serialize(&header)
162    }
163
164    #[test]
165    fn test_decipher_original_filename_correct_password_v1() {
166        let header_bytes = build_v1_header_bytes("testpassword", "test.txt");
167        let password = SecureString::new("testpassword".to_string());
168
169        let result = decipher_original_filename(&header_bytes, Version::V1, &password);
170        assert_eq!(result.unwrap().as_str(), "test.txt");
171    }
172
173    #[test]
174    fn test_decipher_original_filename_wrong_password_v1() {
175        let header_bytes = build_v1_header_bytes("testpassword", "test.txt");
176        let password = SecureString::new("wrongpassword".to_string());
177
178        let result = decipher_original_filename(&header_bytes, Version::V1, &password);
179        assert!(result.is_none());
180    }
181
182    #[test]
183    fn test_decipher_original_filename_correct_password_v2() {
184        let header_bytes = build_v2_header_bytes("testpassword", "test.txt");
185        let password = SecureString::new("testpassword".to_string());
186
187        let result = decipher_original_filename(&header_bytes, Version::V2, &password);
188        assert_eq!(result.unwrap().as_str(), "test.txt");
189    }
190
191    #[test]
192    fn test_decipher_original_filename_wrong_password_v2() {
193        let header_bytes = build_v2_header_bytes("testpassword", "test.txt");
194        let password = SecureString::new("wrongpassword".to_string());
195
196        let result = decipher_original_filename(&header_bytes, Version::V2, &password);
197        assert!(result.is_none());
198    }
199
200    #[test]
201    fn test_decipher_rejects_oversized_kdf_params_without_deriving() {
202        // A crafted header claiming an enormous memory cost must be rejected
203        // before any key derivation is attempted. If validation were missing,
204        // this test would attempt a multi-terabyte allocation.
205        let kdf_params = v1::key::KeyDerivationParams::new(u32::MAX, u32::MAX, u32::MAX, u8::MAX);
206        let header =
207            v1::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3]);
208        let header_bytes = v1::header_ops::serialize(&header);
209
210        let password = SecureString::new("testpassword".to_string());
211        let result = decipher_original_filename(&header_bytes, Version::V1, &password);
212
213        assert!(result.is_none());
214    }
215}