shadow_crypt_shell/listing/
workflow.rs1use std::sync::Arc;
2
3use rayon::prelude::*;
4use shadow_crypt_core::{file::ContentKind, memory::SecureString, vault::ParsedFile};
5
6use crate::{
7 errors::WorkflowResult,
8 kdf::derive_untrusted_key,
9 listing::{
10 file::{FileInfoList, ListingInput, ShadowFile, ShadowFileInfo},
11 file_ops::{load_file_header_bytes, scan_directory_for_shadow_files},
12 },
13 ui,
14};
15
16pub fn run_workflow(input: ListingInput) -> WorkflowResult<()> {
17 let shadow_files: Vec<ShadowFile> = scan_directory_for_shadow_files(&input.work_dir)?;
18 let names_requested = input.password.is_some();
19 let password = Arc::new(input.password);
20
21 let file_infos: Vec<ShadowFileInfo> = shadow_files
24 .par_iter()
25 .map(|shadow_file| get_shadow_file_info(shadow_file, password.as_ref().as_ref()))
26 .filter_map(Result::ok)
27 .collect();
28
29 let info_list: FileInfoList = FileInfoList::new(file_infos);
30
31 if input.json {
32 ui::display_file_info_list_json(&info_list);
33 } else {
34 ui::display_file_info_list(&info_list, names_requested);
35 }
36 Ok(())
37}
38
39fn decipher_original_filename(
41 header_bytes: &[u8],
42 password: &SecureString,
43) -> Option<SecureString> {
44 let parsed = ParsedFile::parse(header_bytes).ok()?;
45 let key = derive_untrusted_key(&parsed, password).ok()?;
46 let metadata = parsed.decrypt_metadata(&key).ok()?;
47
48 let mut name = metadata.filename().as_str().to_string();
50 if metadata.kind() == ContentKind::Archive {
51 name.push('/');
52 }
53 Some(SecureString::new(name))
54}
55
56fn get_shadow_file_info(
57 shadow_file: &ShadowFile,
58 password: Option<&SecureString>,
59) -> WorkflowResult<ShadowFileInfo> {
60 let original_filename: Option<SecureString> = match password {
61 Some(password) => {
62 let header_bytes = load_file_header_bytes(shadow_file)?;
63 decipher_original_filename(header_bytes.as_slice(), password)
64 }
65 None => None,
66 };
67
68 Ok(ShadowFileInfo::new(
69 original_filename,
70 shadow_file.filename.clone(),
71 shadow_file.version,
72 shadow_file.size,
73 ))
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use shadow_crypt_core::{profile::SecurityProfile, v1, v2};
80
81 fn build_v1_header_bytes(password: &str, original_filename: &str) -> Vec<u8> {
82 let salt = [0u8; 16];
83 let kdf_params = v1::key::KeyDerivationParams::from(SecurityProfile::Test);
84 let filename_nonce = [0u8; 24];
85
86 let (key, _) = kdf_params.derive_key(password.as_bytes(), &salt).unwrap();
87 let (filename_ciphertext, _) = v1::crypt::encrypt_bytes(
88 original_filename.as_bytes(),
89 key.as_bytes(),
90 &filename_nonce,
91 )
92 .unwrap();
93
94 let header = v1::header::FileHeader::new(
95 salt,
96 kdf_params,
97 [0u8; 24], filename_nonce,
99 filename_ciphertext,
100 );
101 header.serialize()
102 }
103
104 fn build_v2_header_bytes(password: &str, original_filename: &str) -> Vec<u8> {
105 let salt = [0u8; 16];
106 let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
107 let content_nonce = [1u8; 24];
108 let filename_nonce = [0u8; 24];
109
110 let (key, _) = kdf_params.derive_key(password.as_bytes(), &salt).unwrap();
111 let binding =
112 v2::header::HeaderBinding::new(&salt, &kdf_params, &content_nonce, &filename_nonce);
113 let (filename_ciphertext, _) = v2::crypt::encrypt_bytes(
114 original_filename.as_bytes(),
115 key.as_bytes(),
116 &filename_nonce,
117 &binding.aad(v2::header::AadPurpose::Filename),
118 )
119 .unwrap();
120
121 let header = v2::header::FileHeader::new(
122 salt,
123 kdf_params,
124 content_nonce,
125 filename_nonce,
126 filename_ciphertext,
127 )
128 .unwrap();
129 header.serialize()
130 }
131
132 #[test]
133 fn test_decipher_original_filename_correct_password_v1() {
134 let header_bytes = build_v1_header_bytes("testpassword", "test.txt");
135 let password = SecureString::new("testpassword".to_string());
136
137 let result = decipher_original_filename(&header_bytes, &password);
138 assert_eq!(result.unwrap().as_str(), "test.txt");
139 }
140
141 #[test]
142 fn test_decipher_original_filename_wrong_password_v1() {
143 let header_bytes = build_v1_header_bytes("testpassword", "test.txt");
144 let password = SecureString::new("wrongpassword".to_string());
145
146 let result = decipher_original_filename(&header_bytes, &password);
147 assert!(result.is_none());
148 }
149
150 #[test]
151 fn test_decipher_original_filename_correct_password_v2() {
152 let header_bytes = build_v2_header_bytes("testpassword", "test.txt");
153 let password = SecureString::new("testpassword".to_string());
154
155 let result = decipher_original_filename(&header_bytes, &password);
156 assert_eq!(result.unwrap().as_str(), "test.txt");
157 }
158
159 #[test]
160 fn test_decipher_original_filename_wrong_password_v2() {
161 let header_bytes = build_v2_header_bytes("testpassword", "test.txt");
162 let password = SecureString::new("wrongpassword".to_string());
163
164 let result = decipher_original_filename(&header_bytes, &password);
165 assert!(result.is_none());
166 }
167
168 #[test]
169 fn test_decipher_rejects_oversized_kdf_params_without_deriving() {
170 let kdf_params = v1::key::KeyDerivationParams::new(u32::MAX, u32::MAX, u32::MAX, u8::MAX);
174 let header =
175 v1::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3]);
176 let header_bytes = header.serialize();
177
178 let password = SecureString::new("testpassword".to_string());
179 let result = decipher_original_filename(&header_bytes, &password);
180
181 assert!(result.is_none());
182 }
183}