shadow_crypt_shell/listing/
workflow.rs1use 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 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], 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 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}