shadow_crypt_shell/decryption/
workflow.rs1use rayon::prelude::*;
2use shadow_crypt_core::{
3 algorithm::Algorithm,
4 memory::{SecureBytes, SecureString},
5 progress::ProgressCounter,
6 report::DecryptionReport,
7 v1, v2,
8 version::{Version, read_file_version},
9};
10
11use crate::{
12 decryption::{
13 file::{DecryptionInput, DecryptionInputFile, DecryptionOutputFile},
14 file_ops::{load_file_bytes, store_plaintext_file},
15 },
16 errors::{WorkflowError, WorkflowResult},
17 kdf::derive_key_from_untrusted_params,
18 ui::{display_decryption_report, display_progress},
19 utils::parse_string_from_bytes,
20};
21
22pub fn run_workflow(input: DecryptionInput) -> WorkflowResult<()> {
23 let total = input.files.len();
24 let counter = ProgressCounter::new(total as u64);
25
26 let failures: usize = input
28 .files
29 .par_iter()
30 .map(|input_file| {
31 let result =
32 process_file_decryption(input_file.to_owned(), &input.password, &input.output_dir);
33 counter.increment();
34 display_progress(&counter);
35 let failed = result.is_err();
36 display_decryption_report(result);
37 usize::from(failed)
38 })
39 .sum();
40
41 if failures > 0 {
42 return Err(WorkflowError::Decryption(format!(
43 "{} of {} file(s) failed to decrypt",
44 failures, total
45 )));
46 }
47
48 Ok(())
49}
50
51fn process_file_decryption(
52 file: DecryptionInputFile,
53 password: &SecureString,
54 output_dir: &std::path::Path,
55) -> WorkflowResult<DecryptionReport> {
56 let start_time = std::time::Instant::now();
57
58 let bytes = load_file_bytes(&file)?;
59
60 let (filename, content, algorithm) = match read_file_version(&bytes)? {
63 Version::V1 => decrypt_v1(&bytes, password)?,
64 Version::V2 => decrypt_v2(&bytes, password)?,
65 };
66
67 let output_file: DecryptionOutputFile = store_plaintext_file(&filename, &content, output_dir)?;
68
69 let duration = start_time.elapsed();
70
71 Ok(DecryptionReport::new(
72 file.filename,
73 output_file.filename,
74 duration,
75 algorithm,
76 ))
77}
78
79fn decrypt_v1(
80 bytes: &[u8],
81 password: &SecureString,
82) -> WorkflowResult<(SecureString, SecureBytes, Algorithm)> {
83 let encrypted_file = v1::file_ops::get_encrypted_file_from_bytes(bytes)?;
84 let header = encrypted_file.header();
85
86 let kdf_params = v1::header_ops::get_kdf_params(header);
87 let key = derive_key_from_untrusted_params(
88 kdf_params.memory_cost,
89 kdf_params.time_cost,
90 kdf_params.parallelism,
91 kdf_params.key_size,
92 || v1::key_ops::derive_key(password.as_str().as_bytes(), &header.salt, &kdf_params),
93 )?;
94
95 let (filename_bytes, algorithm) = v1::crypt::decrypt_bytes(
96 &header.filename_ciphertext,
97 key.as_bytes(),
98 &header.filename_nonce,
99 )?;
100 let filename = parse_string_from_bytes(&filename_bytes)?;
101
102 let (content, _) = v1::crypt::decrypt_bytes(
103 encrypted_file.ciphertext(),
104 key.as_bytes(),
105 &header.content_nonce,
106 )?;
107
108 Ok((filename, content, algorithm))
109}
110
111fn decrypt_v2(
112 bytes: &[u8],
113 password: &SecureString,
114) -> WorkflowResult<(SecureString, SecureBytes, Algorithm)> {
115 let encrypted_file = v2::file_ops::get_encrypted_file_from_bytes(bytes)?;
116 let header = encrypted_file.header();
117
118 let kdf_params = v2::header_ops::get_kdf_params(header);
119 let key = derive_key_from_untrusted_params(
120 kdf_params.memory_cost,
121 kdf_params.time_cost,
122 kdf_params.parallelism,
123 kdf_params.key_size,
124 || v2::key_ops::derive_key(password.as_str().as_bytes(), &header.salt, &kdf_params),
125 )?;
126
127 let binding = header.binding();
130
131 let (filename_bytes, algorithm) = v2::crypt::decrypt_bytes(
132 &header.filename_ciphertext,
133 key.as_bytes(),
134 &header.filename_nonce,
135 &binding.aad(v2::header::AadPurpose::Filename),
136 )?;
137 let filename = parse_string_from_bytes(&filename_bytes)?;
138
139 let (content, _) = v2::crypt::decrypt_bytes(
140 encrypted_file.ciphertext(),
141 key.as_bytes(),
142 &header.content_nonce,
143 &binding.aad(v2::header::AadPurpose::Content),
144 )?;
145
146 Ok((filename, content, algorithm))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::kdf::MAX_KDF_MEMORY_KIB;
153 use shadow_crypt_core::profile::SecurityProfile;
154
155 #[test]
156 fn test_v1_oversized_kdf_params_rejected_before_derivation() {
157 let kdf_params = v1::key::KeyDerivationParams::new(MAX_KDF_MEMORY_KIB + 1, 1, 1, 32);
160 let header =
161 v1::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3]);
162 let mut bytes = v1::header_ops::serialize(&header);
163 bytes.extend_from_slice(b"ciphertext");
164
165 let password = SecureString::new("pw".to_string());
166 assert!(decrypt_v1(&bytes, &password).is_err());
167 }
168
169 #[test]
170 fn test_v2_oversized_kdf_params_rejected_before_derivation() {
171 let kdf_params = v2::key::KeyDerivationParams::new(u32::MAX, 1, 1, 32);
172 let header =
173 v2::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3])
174 .unwrap();
175 let mut bytes = v2::header_ops::serialize(&header);
176 bytes.extend_from_slice(b"ciphertext");
177
178 let password = SecureString::new("pw".to_string());
179 assert!(decrypt_v2(&bytes, &password).is_err());
180 }
181
182 #[test]
183 fn test_v2_swapped_ciphertext_pairs_fail_decryption() {
184 let password = SecureString::new("testpassword".to_string());
188 let salt = [1u8; 16];
189 let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
190 let content_nonce = [2u8; 24];
191 let filename_nonce = [3u8; 24];
192
193 let (key, _) =
194 v2::key_ops::derive_key(password.as_str().as_bytes(), &salt, &kdf_params).unwrap();
195 let binding =
196 v2::header::HeaderBinding::new(&salt, &kdf_params, &content_nonce, &filename_nonce);
197
198 let (filename_ct, _) = v2::crypt::encrypt_bytes(
199 b"name.txt",
200 key.as_bytes(),
201 &filename_nonce,
202 &binding.aad(v2::header::AadPurpose::Filename),
203 )
204 .unwrap();
205 let (content_ct, _) = v2::crypt::encrypt_bytes(
206 b"content",
207 key.as_bytes(),
208 &content_nonce,
209 &binding.aad(v2::header::AadPurpose::Content),
210 )
211 .unwrap();
212
213 let swapped_header = v2::header::FileHeader::new(
217 salt,
218 kdf_params,
219 filename_nonce, content_nonce, content_ct, )
223 .unwrap();
224 let mut bytes = v2::header_ops::serialize(&swapped_header);
225 bytes.extend_from_slice(&filename_ct);
226
227 assert!(decrypt_v2(&bytes, &password).is_err());
228 }
229
230 #[test]
231 fn test_v2_round_trip_via_raw_bytes() {
232 let password = SecureString::new("testpassword".to_string());
233 let salt = [1u8; 16];
234 let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
235 let content_nonce = [2u8; 24];
236 let filename_nonce = [3u8; 24];
237
238 let (key, _) =
239 v2::key_ops::derive_key(password.as_str().as_bytes(), &salt, &kdf_params).unwrap();
240 let binding =
241 v2::header::HeaderBinding::new(&salt, &kdf_params, &content_nonce, &filename_nonce);
242
243 let (filename_ct, _) = v2::crypt::encrypt_bytes(
244 b"name.txt",
245 key.as_bytes(),
246 &filename_nonce,
247 &binding.aad(v2::header::AadPurpose::Filename),
248 )
249 .unwrap();
250 let (content_ct, _) = v2::crypt::encrypt_bytes(
251 b"content",
252 key.as_bytes(),
253 &content_nonce,
254 &binding.aad(v2::header::AadPurpose::Content),
255 )
256 .unwrap();
257
258 let header = v2::header::FileHeader::new(
259 salt,
260 kdf_params,
261 content_nonce,
262 filename_nonce,
263 filename_ct,
264 )
265 .unwrap();
266 let mut bytes = v2::header_ops::serialize(&header);
267 bytes.extend_from_slice(&content_ct);
268
269 let (filename, content, _) = decrypt_v2(&bytes, &password).unwrap();
270 assert_eq!(filename.as_str(), "name.txt");
271 assert_eq!(content.as_slice(), b"content");
272 }
273}