shadow_crypt_shell/decryption/
workflow.rs1use rayon::prelude::*;
2use shadow_crypt_core::{
3 memory::SecureString,
4 progress::ProgressCounter,
5 report::DecryptionReport,
6 vault::{MAX_HEADER_LEN, ParsedFile},
7};
8
9use crate::{
10 decryption::{
11 file::{DecryptionInput, DecryptionInputFile},
12 file_ops::stream_decrypt_file,
13 },
14 errors::{WorkflowError, WorkflowResult},
15 kdf::derive_untrusted_key,
16 ui::{display_decryption_success, display_error, display_progress},
17 utils::read_n_bytes_from_file,
18};
19
20pub fn run_workflow(input: DecryptionInput) -> WorkflowResult<()> {
21 let total = input.files.len();
22 let counter = ProgressCounter::new(total as u64);
23
24 let failures: Vec<WorkflowError> = input
26 .files
27 .par_iter()
28 .filter_map(|input_file| {
29 let result = process_file_decryption(
30 input_file.to_owned(),
31 &input.password,
32 &input.output_dir,
33 input.force,
34 )
35 .map_err(|e| WorkflowError::per_file(&input_file.filename, e));
36 counter.increment();
37 if !input.quiet {
38 display_progress(&counter);
39 }
40 match result {
41 Ok(report) => {
42 if !input.quiet {
43 display_decryption_success(&report);
44 }
45 None
46 }
47 Err(e) => {
48 display_error(&e);
49 Some(e)
50 }
51 }
52 })
53 .collect();
54
55 if !failures.is_empty() {
56 let message = format!("{} of {} file(s) failed to decrypt", failures.len(), total);
57 return Err(if failures.iter().all(|e| e.is_authentication_failure()) {
60 WorkflowError::Authentication(message)
61 } else {
62 WorkflowError::Decryption(message)
63 });
64 }
65
66 Ok(())
67}
68
69fn process_file_decryption(
70 file: DecryptionInputFile,
71 password: &SecureString,
72 output_dir: &std::path::Path,
73 force: bool,
74) -> WorkflowResult<DecryptionReport> {
75 let start_time = std::time::Instant::now();
76
77 let header_bytes = read_n_bytes_from_file(&file.path, MAX_HEADER_LEN)?;
82 let parsed = ParsedFile::parse(header_bytes.as_slice())?;
83 let key = derive_untrusted_key(&parsed, password)?;
84
85 let metadata = parsed.decrypt_metadata(&key)?;
88 let output_file = stream_decrypt_file(&file, &parsed, &key, &metadata, output_dir, force)?;
89
90 let duration = start_time.elapsed();
91
92 Ok(DecryptionReport::new(
93 file.filename,
94 output_file.filename,
95 duration,
96 parsed.algorithm(),
97 ))
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::kdf::MAX_KDF_MEMORY_KIB;
104 use shadow_crypt_core::{
105 file::PlaintextFile,
106 memory::{SecureBytes, SecureString},
107 profile::SecurityProfile,
108 v1, v2,
109 };
110
111 fn decrypt(bytes: &[u8], password: &SecureString) -> WorkflowResult<PlaintextFile> {
114 let parsed = ParsedFile::parse(bytes)?;
115 let key = derive_untrusted_key(&parsed, password)?;
116 Ok(parsed.decrypt(&key)?)
117 }
118
119 #[test]
120 fn test_v1_oversized_kdf_params_rejected_before_derivation() {
121 let kdf_params = v1::key::KeyDerivationParams::new(MAX_KDF_MEMORY_KIB + 1, 1, 1, 32);
124 let header =
125 v1::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3]);
126 let mut bytes = header.serialize();
127 bytes.extend_from_slice(b"ciphertext");
128
129 let password = SecureString::new("pw".to_string());
130 assert!(decrypt(&bytes, &password).is_err());
131 }
132
133 #[test]
134 fn test_v2_oversized_kdf_params_rejected_before_derivation() {
135 let kdf_params = v2::key::KeyDerivationParams::new(u32::MAX, 1, 1, 32);
136 let header =
137 v2::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3])
138 .unwrap();
139 let mut bytes = header.serialize();
140 bytes.extend_from_slice(b"ciphertext");
141
142 let password = SecureString::new("pw".to_string());
143 assert!(decrypt(&bytes, &password).is_err());
144 }
145
146 #[test]
147 fn test_v2_swapped_ciphertext_pairs_fail_decryption() {
148 let password = SecureString::new("testpassword".to_string());
152 let salt = [1u8; 16];
153 let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
154 let content_nonce = [2u8; 24];
155 let filename_nonce = [3u8; 24];
156
157 let (key, _) = kdf_params
158 .derive_key(password.as_str().as_bytes(), &salt)
159 .unwrap();
160 let binding =
161 v2::header::HeaderBinding::new(&salt, &kdf_params, &content_nonce, &filename_nonce);
162
163 let (filename_ct, _) = v2::crypt::encrypt_bytes(
164 b"name.txt",
165 key.as_bytes(),
166 &filename_nonce,
167 &binding.aad(v2::header::AadPurpose::Filename),
168 )
169 .unwrap();
170 let (content_ct, _) = v2::crypt::encrypt_bytes(
171 b"content",
172 key.as_bytes(),
173 &content_nonce,
174 &binding.aad(v2::header::AadPurpose::Content),
175 )
176 .unwrap();
177
178 let swapped_header = v2::header::FileHeader::new(
182 salt,
183 kdf_params,
184 filename_nonce, content_nonce, content_ct, )
188 .unwrap();
189 let mut bytes = swapped_header.serialize();
190 bytes.extend_from_slice(&filename_ct);
191
192 assert!(decrypt(&bytes, &password).is_err());
193 }
194
195 #[test]
196 fn test_v2_round_trip_via_raw_bytes() {
197 let password = SecureString::new("testpassword".to_string());
198 let salt = [1u8; 16];
199 let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
200
201 let (key, _) = kdf_params
202 .derive_key(password.as_str().as_bytes(), &salt)
203 .unwrap();
204
205 let plaintext_file = PlaintextFile::new(
206 SecureString::new("name.txt".to_string()),
207 SecureBytes::new(b"content".to_vec()),
208 );
209 let sealed = v2::file::EncryptedFile::seal(
210 &plaintext_file,
211 &key,
212 kdf_params,
213 salt,
214 [2u8; 24],
215 [3u8; 24],
216 )
217 .unwrap();
218
219 let decrypted = decrypt(&sealed.to_bytes(), &password).unwrap();
220 assert_eq!(decrypted.filename().as_str(), "name.txt");
221 assert_eq!(decrypted.content().as_slice(), b"content");
222 }
223}